bhcli

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

main.rs (435870B)


      1 mod account_management;
      2 mod ai_service;
      3 mod bhc;
      4 mod bot_client;
      5 // mod bot_integration;
      6 mod bot_system;
      7 mod chatops;
      8 // mod enhanced_bot_commands;
      9 // mod enhanced_bot_system;
     10 mod harm;
     11 mod lechatphp;
     12 mod util;
     13 
     14 use crate::account_management::{AccountManager, AccountRelationshipStatus, parse_enhanced_command};
     15 use crate::ai_service::AIService;
     16 use crate::bot_client::BotManager;
     17 
     18 use crate::chatops::{ChatOpsRouter, UserRole};
     19 use crate::lechatphp::LoginErr;
     20 use anyhow::{anyhow, Context};
     21 use async_openai::{
     22    config::OpenAIConfig,
     23    types::{
     24        ChatCompletionRequestAssistantMessage, ChatCompletionRequestAssistantMessageContent,
     25        ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage,
     26        ChatCompletionRequestSystemMessageContent, ChatCompletionRequestUserMessage,
     27        ChatCompletionRequestUserMessageContent, CreateChatCompletionRequestArgs,
     28    },
     29    Client as OpenAIClient,
     30 };
     31 use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
     32 use clap::Parser;
     33 use clipboard::ClipboardContext;
     34 use clipboard::ClipboardProvider;
     35 use colors_transform::{Color, Rgb};
     36 use crossbeam_channel::{self, after, select};
     37 use crossterm::event;
     38 use crossterm::event::Event as CEvent;
     39 use crossterm::event::{MouseEvent, MouseEventKind};
     40 use crossterm::{
     41    event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
     42    execute,
     43    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     44 };
     45 use harm::{action_from_score, score_message, Action};
     46 use lazy_static::lazy_static;
     47 use linkify::LinkFinder;
     48 
     49 use log::LevelFilter;
     50 use log4rs::append::file::FileAppender;
     51 use log4rs::encode::pattern::PatternEncoder;
     52 use rand::distributions::Alphanumeric;
     53 use rand::{thread_rng, Rng};
     54 use regex::Regex;
     55 use reqwest::blocking::multipart;
     56 use reqwest::blocking::Client;
     57 use reqwest::redirect::Policy;
     58 #[cfg(feature = "audio")]
     59 use rodio::{source::Source, Decoder, OutputStream};
     60 use select::document::Document;
     61 use select::predicate::{Attr, Name};
     62 use serde_derive::{Deserialize, Serialize};
     63 use std::collections::HashMap;
     64 use std::fs::OpenOptions;
     65 use std::io::Cursor;
     66 use std::io::{self, Write};
     67 use std::process::Command;
     68 use std::sync::Mutex;
     69 use std::sync::{Arc, MutexGuard};
     70 use std::thread;
     71 use std::time::Duration;
     72 use std::time::Instant;
     73 use tokio::runtime::Runtime;
     74 use tui::layout::Rect;
     75 use tui::style::Color as tuiColor;
     76 use tui::{
     77    backend::CrosstermBackend,
     78    layout::{Constraint, Direction, Layout},
     79    style::{Modifier, Style},
     80    text::{Span, Spans, Text},
     81    widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
     82    Frame, Terminal,
     83 };
     84 use unicode_width::UnicodeWidthStr;
     85 use util::StatefulList;
     86 
     87 const LANG: &str = "en";
     88 const SEND_TO_ALL: &str = "s *";
     89 const SEND_TO_MEMBERS: &str = "s ?";
     90 const SEND_TO_STAFFS: &str = "s %";
     91 const SEND_TO_ADMINS: &str = "s _";
     92 const SOUND1: &[u8] = include_bytes!("sound1.mp3");
     93 const DKF_URL: &str = "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion";
     94 const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down";
     95 const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down";
     96 const KICKED_ERR: &str = "You have been kicked";
     97 const REG_ERR: &str = "This nickname is a registered member";
     98 const NICKNAME_ERR: &str = "Invalid nickname";
     99 const CAPTCHA_WG_ERR: &str = "Wrong Captcha";
    100 const CAPTCHA_FAILED_SOLVE_ERR: &str = "Failed solve captcha";
    101 const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out";
    102 const UNKNOWN_ERR: &str = "Unknown error";
    103 const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion";
    104 
    105 lazy_static! {
    106    static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap();
    107    static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap();
    108    static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap();
    109    static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap();
    110    static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap();
    111    static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap();
    112    static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap();
    113    static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap();
    114    static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap();
    115    static ref DELETE_RGX: Regex = Regex::new(r#"^/delete (\d+)"#).unwrap();
    116    static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap();
    117    static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap();
    118    static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap();
    119    static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap();
    120 }
    121 
    122 fn default_empty_str() -> String {
    123    "".to_string()
    124 }
    125 
    126 fn default_true() -> bool {
    127    true
    128 }
    129 
    130 #[derive(Debug, Serialize, Deserialize)]
    131 struct Profile {
    132    username: String,
    133    password: String,
    134    #[serde(default = "default_empty_str")]
    135    url: String,
    136    #[serde(default = "default_empty_str")]
    137    date_format: String,
    138    #[serde(default = "default_empty_str")]
    139    page_php: String,
    140    #[serde(default = "default_empty_str")]
    141    members_tag: String,
    142    #[serde(default = "default_empty_str")]
    143    keepalive_send_to: String,
    144    #[serde(default)]
    145    alt_account: Option<String>,
    146    #[serde(default)]
    147    master_account: Option<String>,
    148    #[serde(default = "default_empty_str")]
    149    system_intel: String,
    150    #[serde(default)]
    151    ai_enabled: bool,
    152    #[serde(default = "default_ai_mode")]
    153    ai_mode: String,
    154    #[serde(default = "default_moderation_strictness")]
    155    moderation_strictness: String, // "strict", "balanced", "lenient"
    156    #[serde(default = "default_true")]
    157    mod_logs_enabled: bool,
    158    #[serde(default)]
    159    identities: HashMap<String, Vec<String>>, // command -> [nickname, color, incognito?, member?, staff?]
    160 }
    161 
    162 fn default_ai_mode() -> String {
    163    "off".to_string()
    164 }
    165 
    166 fn default_moderation_strictness() -> String {
    167    "balanced".to_string()
    168 }
    169 
    170 #[derive(Default, Debug, Serialize, Deserialize)]
    171 struct MyConfig {
    172    dkf_api_key: Option<String>,
    173    #[serde(default)]
    174    alt_account: Option<String>,
    175    #[serde(default)]
    176    master_account: Option<String>,
    177    #[serde(default = "default_true")]
    178    alt_forwarding_enabled: bool,
    179    #[serde(default)]
    180    bad_usernames: Vec<String>,
    181    #[serde(default)]
    182    bad_exact_usernames: Vec<String>,
    183    #[serde(default)]
    184    bad_messages: Vec<String>,
    185    #[serde(default)]
    186    allowlist: Vec<String>,
    187    #[serde(default)]
    188    commands: HashMap<String, String>,
    189    profiles: HashMap<String, Profile>,
    190 }
    191 
    192 #[derive(Parser)]
    193 #[command(name = "bhcli")]
    194 #[command(author = "Dasho <o_o@dasho.dev>")]
    195 #[command(version = "0.1.0")]
    196 struct Opts {
    197    #[arg(long, env = "DKF_API_KEY")]
    198    dkf_api_key: Option<String>,
    199    #[arg(short, long, env = "BHC_USERNAME")]
    200    username: Option<String>,
    201    #[arg(short, long, env = "BHC_PASSWORD")]
    202    password: Option<String>,
    203    #[arg(short, long, env = "BHC_MANUAL_CAPTCHA")]
    204    manual_captcha: bool,
    205    #[arg(short, long, env = "BHC_GUEST_COLOR")]
    206    guest_color: Option<String>,
    207    #[arg(short, long, env = "BHC_REFRESH_RATE", default_value = "1")]
    208    refresh_rate: u64,
    209    #[arg(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")]
    210    max_login_retry: isize,
    211    #[arg(long)]
    212    url: Option<String>,
    213    #[arg(long)]
    214    page_php: Option<String>,
    215    #[arg(long)]
    216    datetime_fmt: Option<String>,
    217    #[arg(long)]
    218    members_tag: Option<String>,
    219    #[arg(short, long)]
    220    dan: bool,
    221    #[arg(
    222        short,
    223        long,
    224        env = "BHC_PROXY_URL",
    225        default_value = "socks5h://127.0.0.1:9050"
    226    )]
    227    socks_proxy_url: String,
    228    #[arg(long)]
    229    no_proxy: bool,
    230    #[arg(long, env = "DNMX_USERNAME")]
    231    dnmx_username: Option<String>,
    232    #[arg(long, env = "DNMX_PASSWORD")]
    233    dnmx_password: Option<String>,
    234    #[arg(short = 'c', long, default_value = "default")]
    235    profile: String,
    236 
    237    //Strange
    238    #[arg(long, default_value = "0")]
    239    keepalive_send_to: Option<String>,
    240 
    241    #[arg(long)]
    242    session: Option<String>,
    243 
    244    #[arg(long)]
    245    sxiv: bool,
    246 
    247    #[arg(skip)]
    248    bad_usernames: Option<Vec<String>>,
    249    #[arg(skip)]
    250    bad_exact_usernames: Option<Vec<String>>,
    251    #[arg(skip)]
    252    bad_messages: Option<Vec<String>>,
    253    #[arg(skip)]
    254    allowlist: Option<Vec<String>>,
    255 
    256    // Bot system parameters
    257    #[arg(long)]
    258    bot: Option<String>,
    259    #[arg(long)]
    260    bot_admins: Vec<String>,
    261    #[arg(long)]
    262    bot_data_dir: Option<String>,
    263 
    264    // Use 404 chatroom profile
    265    #[arg(long = "404")]
    266    use_404: bool,
    267 }
    268 
    269 struct LeChatPHPConfig {
    270    url: String,
    271    datetime_fmt: String,
    272    page_php: String,
    273    keepalive_send_to: String,
    274    members_tag: String,
    275    staffs_tag: String,
    276 }
    277 
    278 impl LeChatPHPConfig {
    279    fn new_black_hat_chat_config() -> Self {
    280        Self {
    281            url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(),
    282            datetime_fmt: "%m-%d %H:%M:%S".to_owned(),
    283            page_php: "chat.php".to_owned(),
    284            keepalive_send_to: "0".to_owned(),
    285            members_tag: "[M] ".to_owned(),
    286            staffs_tag: "[Staff] ".to_owned(),
    287        }
    288    }
    289 
    290    fn new_404_chatroom_not_found_config() -> Self {
    291        Self {
    292            url: "http://4o4o4hn4hsujpnbsso7tqigujuokafxys62thulbk2k3mf46vq22qfqd.onion/chat/min".to_owned(),
    293            datetime_fmt: "%Y-%m-%d %H:%M:%S".to_owned(),
    294            page_php: "index.php".to_owned(),
    295            keepalive_send_to: "0".to_owned(),
    296            members_tag: "[M] ".to_owned(),
    297            staffs_tag: "[Staff] ".to_owned(),
    298        }
    299    }
    300 }
    301 
    302 struct BaseClient {
    303    username: String,
    304    password: String,
    305 }
    306 
    307 struct LeChatPHPClient {
    308    base_client: BaseClient,
    309    guest_color: String,
    310    client: Client,
    311    session: Option<String>,
    312    config: LeChatPHPConfig,
    313    last_key_event: Option<KeyCode>,
    314    manual_captcha: bool,
    315    sxiv: bool,
    316    refresh_rate: u64,
    317    max_login_retry: isize,
    318 
    319    is_muted: Arc<Mutex<bool>>,
    320    show_sys: bool,
    321    display_guest_view: bool,
    322    display_member_view: bool,
    323    display_hidden_msgs: bool,
    324    tx: crossbeam_channel::Sender<PostType>,
    325    rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>,
    326 
    327    color_tx: crossbeam_channel::Sender<()>,
    328    color_rx: Arc<Mutex<crossbeam_channel::Receiver<()>>>,
    329 
    330    bad_username_filters: Arc<Mutex<Vec<String>>>,
    331    bad_exact_username_filters: Arc<Mutex<Vec<String>>>,
    332    bad_message_filters: Arc<Mutex<Vec<String>>>,
    333    allowlist: Arc<Mutex<Vec<String>>>,
    334 
    335    account_manager: AccountManager,
    336    profile: String,
    337    display_pm_only: bool,
    338    display_staff_view: bool,
    339    display_master_pm_view: bool,
    340    clean_mode: bool,
    341    inbox_mode: bool,
    342    alt_forwarding_enabled: Arc<Mutex<bool>>,
    343 
    344    // Store current active identity for restoration
    345    current_username: String,
    346    current_color: String,
    347 
    348    // AI fields
    349    ai_enabled: Arc<Mutex<bool>>,
    350    ai_mode: Arc<Mutex<String>>,
    351    system_intel: String,
    352    moderation_strictness: String,
    353    mod_logs_enabled: Arc<Mutex<bool>>,
    354    openai_client: Option<async_openai::Client<async_openai::config::OpenAIConfig>>,
    355    ai_conversation_memory: Arc<Mutex<std::collections::HashMap<String, Vec<(String, String)>>>>, // user -> (role, message) history
    356 
    357    // Warning tracking for alt mode moderation
    358    user_warnings: Arc<Mutex<std::collections::HashMap<String, u32>>>, // user -> warning count
    359 
    360    // Identity configurations from profile
    361    identities: HashMap<String, Vec<String>>, // command -> [nickname, color, incognito?, member?, staff?]
    362 
    363    // ChatOps system
    364    chatops_router: ChatOpsRouter,
    365 
    366    // Enhanced AI service
    367    ai_service: Arc<AIService>,
    368    #[allow(dead_code)]
    369    runtime: Arc<Runtime>,
    370 
    371    // Bot system manager
    372    bot_manager: Option<Arc<Mutex<BotManager>>>,
    373 }
    374 
    375 impl LeChatPHPClient {
    376    fn run_forever(&mut self) {
    377        let max_retry = self.max_login_retry;
    378        let mut attempt = 0;
    379        loop {
    380            match self.login() {
    381                Err(e) => match e {
    382                    LoginErr::KickedErr
    383                    | LoginErr::RegErr
    384                    | LoginErr::NicknameErr
    385                    | LoginErr::UnknownErr => {
    386                        log::error!("{}", e);
    387                        println!("Login error: {}", e); // Print error message
    388                        break;
    389                    }
    390                    LoginErr::CaptchaFailedSolveErr => {
    391                        log::error!("{}", e);
    392                        println!("Captcha failed to solve: {}", e); // Print error message
    393                        continue;
    394                    }
    395                    LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {}
    396                    LoginErr::ServerDownErr | LoginErr::ServerDown500Err => {
    397                        log::error!("{}", e);
    398                        println!("Server is down: {}", e); // Print error message
    399                    }
    400                    LoginErr::Reqwest(err) => {
    401                        if err.is_connect() {
    402                            log::error!("{}\nIs tor proxy enabled ?", err);
    403                            println!("Connection error: {}\nIs tor proxy enabled ?", err); // Print error message
    404                            break;
    405                        } else if err.is_timeout() {
    406                            log::error!("timeout: {}", err);
    407                            println!("Timeout error: {}", err); // Print error message
    408                        } else {
    409                            log::error!("{}", err);
    410                            println!("Reqwest error: {}", err); // Print error message
    411                        }
    412                    }
    413                },
    414 
    415                Ok(()) => {
    416                    attempt = 0;
    417                    match self.get_msgs() {
    418                        Ok(ExitSignal::NeedLogin) => {}
    419                        Ok(ExitSignal::Terminate) => return,
    420                        Err(e) => log::error!("{:?}", e),
    421                    }
    422                }
    423            }
    424            attempt += 1;
    425            if max_retry > 0 && attempt > max_retry {
    426                break;
    427            }
    428            self.session = None;
    429            let retry_in = Duration::from_secs(2);
    430            let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt);
    431            if max_retry > 0 {
    432                msg += &format!("/{}", max_retry);
    433            }
    434            println!("{}", msg);
    435            thread::sleep(retry_in);
    436        }
    437    }
    438 
    439    fn start_keepalive_thread(
    440        &self,
    441        exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    442        last_post_rx: crossbeam_channel::Receiver<()>,
    443        users: &Arc<Mutex<Users>>,
    444        username: &str,
    445    ) -> thread::JoinHandle<()> {
    446        let tx = self.tx.clone();
    447        let send_to = self.config.keepalive_send_to.clone();
    448        let users_clone = Arc::clone(users);
    449        let username_clone = username.to_string();
    450        thread::spawn(move || loop {
    451            // Check if user is a guest
    452            let is_guest = {
    453                let users_guard = users_clone.lock().unwrap();
    454                let is_member_or_staff = users_guard
    455                    .members
    456                    .iter()
    457                    .any(|(_, n)| n == &username_clone)
    458                    || users_guard.staff.iter().any(|(_, n)| n == &username_clone)
    459                    || users_guard.admin.iter().any(|(_, n)| n == &username_clone);
    460                !is_member_or_staff
    461            };
    462 
    463            let clb = || {
    464                // For guests, send keepalive to @0, otherwise use configured target
    465                let target = if is_guest {
    466                    "0".to_string()
    467                } else {
    468                    send_to.clone()
    469                };
    470                let _ = tx.send(PostType::KeepAlive(target));
    471            };
    472 
    473            // For guests: 25 minutes, for others: 55 minutes
    474            let timeout_minutes = if is_guest { 25 } else { 55 };
    475            let timeout = after(Duration::from_secs(60 * timeout_minutes));
    476            select! {
    477                // Whenever we send a message to chat server,
    478                // we will receive a message on this channel
    479                // and reset the timer for next keepalive.
    480                recv(&last_post_rx) -> _ => {},
    481                recv(&exit_rx) -> _ => return,
    482                recv(&timeout) -> _ => clb(),
    483            }
    484        })
    485    }
    486 
    487    // Thread that POST to chat server
    488    fn start_post_msg_thread(
    489        &self,
    490        exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    491        last_post_tx: crossbeam_channel::Sender<()>,
    492    ) -> thread::JoinHandle<()> {
    493        let client = self.client.clone();
    494        let rx = Arc::clone(&self.rx);
    495        let full_url = format!("{}/{}", &self.config.url, &self.config.page_php);
    496        let session = self.session.clone().unwrap();
    497        let url = format!("{}?action=post&session={}", &full_url, &session);
    498        thread::spawn(move || loop {
    499            // Each message gets its own thread to avoid race conditions
    500            let rx = rx.lock().unwrap();
    501            select! {
    502                recv(&exit_rx) -> _ => return,
    503                recv(&rx) -> v => {
    504                    if let Ok(post_type_recv) = v {
    505                        // Clone necessary data for the new thread
    506                        let client_clone = client.clone();
    507                        let full_url_clone = full_url.clone();
    508                        let session_clone = session.clone();
    509                        let url_clone = url.clone();
    510                        let last_post_tx_clone = last_post_tx.clone();
    511 
    512                        // Spawn a new thread for each message to prevent race conditions
    513                        thread::spawn(move || {
    514                            post_msg(
    515                                &client_clone,
    516                                post_type_recv,
    517                                &full_url_clone,
    518                                session_clone,
    519                                &url_clone,
    520                                &last_post_tx_clone,
    521                            );
    522                        });
    523                    } else {
    524                        return;
    525                    }
    526                },
    527            }
    528        })
    529    }
    530 
    531    // Thread that update messages every "refresh_rate"
    532    fn start_get_msgs_thread(
    533        &self,
    534        sig: &Arc<Mutex<Sig>>,
    535        messages: &Arc<Mutex<Vec<Message>>>,
    536        users: &Arc<Mutex<Users>>,
    537        messages_updated_tx: crossbeam_channel::Sender<()>,
    538    ) -> thread::JoinHandle<()> {
    539        let client = self.client.clone();
    540        let messages = Arc::clone(messages);
    541        let users = Arc::clone(users);
    542        let session = self.session.clone().unwrap();
    543        let username = self.base_client.username.clone();
    544        let refresh_rate = self.refresh_rate;
    545        let base_url = self.config.url.clone();
    546        let page_php = self.config.page_php.clone();
    547        let datetime_fmt = self.config.datetime_fmt.clone();
    548        let is_muted = Arc::clone(&self.is_muted);
    549        let exit_rx = sig.lock().unwrap().clone();
    550        let sig = Arc::clone(sig);
    551        let members_tag = self.config.members_tag.clone();
    552        let staffs_tag = self.config.staffs_tag.clone();
    553        let tx = self.tx.clone();
    554        let bad_usernames = Arc::clone(&self.bad_username_filters);
    555        let bad_exact_usernames = Arc::clone(&self.bad_exact_username_filters);
    556        let bad_messages = Arc::clone(&self.bad_message_filters);
    557        let allowlist = Arc::clone(&self.allowlist);
    558        let alt_account = self.account_manager.alt_account.clone();
    559        let master_account = self.account_manager.master_account.clone();
    560        let alt_forwarding_enabled = Arc::clone(&self.alt_forwarding_enabled);
    561        let ai_enabled = Arc::clone(&self.ai_enabled);
    562        let ai_mode = Arc::clone(&self.ai_mode);
    563        let openai_client = self.openai_client.clone();
    564        let system_intel = self.system_intel.clone();
    565        let moderation_strictness = self.moderation_strictness.clone();
    566        let mod_logs_enabled = Arc::clone(&self.mod_logs_enabled);
    567        let ai_conversation_memory = Arc::clone(&self.ai_conversation_memory);
    568        let user_warnings = Arc::clone(&self.user_warnings);
    569        let ai_service = Arc::clone(&self.ai_service);
    570        let bot_manager = self.bot_manager.clone();
    571        thread::spawn(move || {
    572            #[cfg(feature = "audio")]
    573            let audio_output = OutputStream::try_default().ok();
    574            #[cfg(feature = "audio")]
    575            let stream_handle = audio_output.as_ref().map(|(_, handle)| handle);
    576 
    577            loop {
    578                let mut should_notify = false;
    579 
    580                if let Err(err) = get_msgs(
    581                    &client,
    582                    &base_url,
    583                    &page_php,
    584                    &session,
    585                    &username,
    586                    &users,
    587                    &sig,
    588                    &messages_updated_tx,
    589                    &members_tag,
    590                    &staffs_tag,
    591                    &datetime_fmt,
    592                    &messages,
    593                    &mut should_notify,
    594                    &tx,
    595                    &bad_usernames,
    596                    &bad_exact_usernames,
    597                    &bad_messages,
    598                    &allowlist,
    599                    alt_account.as_deref(),
    600                    master_account.as_deref(),
    601                    &alt_forwarding_enabled,
    602                    &ai_enabled,
    603                    &ai_mode,
    604                    &openai_client,
    605                    &system_intel,
    606                    &moderation_strictness,
    607                    &mod_logs_enabled,
    608                    &ai_conversation_memory,
    609                    &user_warnings,
    610                    &ai_service,
    611                    &bot_manager,
    612                ) {
    613                    log::error!("{}", err);
    614                };
    615 
    616                let muted = { *is_muted.lock().unwrap() };
    617                if should_notify && !muted {
    618                    #[cfg(feature = "audio")]
    619                    if let Some(handle) = &stream_handle {
    620                        if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) {
    621                            if let Err(err) = handle.play_raw(source.convert_samples()) {
    622                                log::error!("Audio playback error: {}", err);
    623                            }
    624                        }
    625                    }
    626                }
    627 
    628                let timeout = after(Duration::from_secs(refresh_rate));
    629                select! {
    630                    recv(&exit_rx) -> _ => return,
    631                    recv(&timeout) -> _ => {},
    632                }
    633            }
    634        })
    635    }
    636 
    637    fn get_msgs(&mut self) -> anyhow::Result<ExitSignal> {
    638        let terminate_signal: ExitSignal;
    639 
    640        let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new()));
    641        let users: Arc<Mutex<Users>> = Arc::new(Mutex::new(Users::default()));
    642 
    643        // Create default app state
    644        let mut app = App::default();
    645 
    646        // Each threads gets a clone of the receiver.
    647        // When someone calls ".signal", all threads receive it,
    648        // and knows that they have to terminate.
    649        let sig = Arc::new(Mutex::new(Sig::new()));
    650 
    651        let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded();
    652        let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded();
    653 
    654        let h1 = self.start_keepalive_thread(
    655            sig.lock().unwrap().clone(),
    656            last_post_rx,
    657            &users,
    658            &self.base_client.username,
    659        );
    660        let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx);
    661        let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx);
    662 
    663        // Terminal initialization
    664        let mut stdout = io::stdout();
    665        enable_raw_mode().unwrap();
    666        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    667        let backend = CrosstermBackend::new(stdout);
    668        let mut terminal = Terminal::new(backend)?;
    669 
    670        // Setup event handlers
    671        let (events, h4) = Events::with_config(Config {
    672            messages_updated_rx,
    673            exit_rx: sig.lock().unwrap().clone(),
    674            // Increased from 250ms to 500ms to reduce CPU usage significantly
    675            tick_rate: Duration::from_millis(500),
    676        });
    677 
    678        loop {
    679            app.is_muted = *self.is_muted.lock().unwrap();
    680            app.show_sys = self.show_sys;
    681            app.display_guest_view = self.display_guest_view;
    682            app.display_member_view = self.display_member_view;
    683            app.display_hidden_msgs = self.display_hidden_msgs;
    684            app.display_pm_only = self.display_pm_only;
    685            app.display_staff_view = self.display_staff_view;
    686            app.display_master_pm_view = self.display_master_pm_view;
    687            app.clean_mode = self.clean_mode;
    688            app.inbox_mode = self.inbox_mode;
    689            // Account relationships are now managed by the account_manager
    690            app.members_tag = self.config.members_tag.clone();
    691            app.staffs_tag = self.config.staffs_tag.clone();
    692 
    693            // process()
    694            // Draw UI
    695            terminal.draw(|f| {
    696                draw_terminal_frame(f, &mut app, &messages, &users, &self.base_client.username);
    697            })?;
    698 
    699            // Handle input
    700            match self.handle_input(&events, &mut app, &messages, &users) {
    701                Err(ExitSignal::Terminate) => {
    702                    terminate_signal = ExitSignal::Terminate;
    703                    sig.lock().unwrap().signal(&terminate_signal);
    704                    break;
    705                }
    706                Err(ExitSignal::NeedLogin) => {
    707                    terminate_signal = ExitSignal::NeedLogin;
    708                    sig.lock().unwrap().signal(&terminate_signal);
    709                    break;
    710                }
    711                Ok(_) => continue,
    712            };
    713        }
    714 
    715        // Cleanup before leaving
    716        disable_raw_mode()?;
    717        execute!(
    718            terminal.backend_mut(),
    719            LeaveAlternateScreen,
    720            DisableMouseCapture
    721        )?;
    722        terminal.show_cursor()?;
    723        terminal.clear()?;
    724        terminal.set_cursor(0, 0)?;
    725 
    726        if let Err(e) = h1.join() {
    727            log::error!("keepalive thread panicked: {:?}", e);
    728        }
    729        if let Err(e) = h2.join() {
    730            log::error!("post_msg thread panicked: {:?}", e);
    731        }
    732        if let Err(e) = h3.join() {
    733            log::error!("get_msgs thread panicked: {:?}", e);
    734        }
    735        if let Err(e) = h4.join() {
    736            log::error!("events thread panicked: {:?}", e);
    737        }
    738 
    739        Ok(terminate_signal)
    740    }
    741 
    742    fn post_msg(&self, post_type: PostType) -> anyhow::Result<()> {
    743        self.tx.send(post_type)?;
    744        Ok(())
    745    }
    746 
    747    fn clear_all_inbox_messages(&self, app: &mut App) -> anyhow::Result<()> {
    748        if let Some(session) = &self.session {
    749            let url = format!("{}?action=inbox&session={}", &self.config.url, session);
    750 
    751            // Collect all message IDs
    752            let message_ids: Vec<String> =
    753                app.inbox_items.items.iter().map(|m| m.id.clone()).collect();
    754 
    755            if message_ids.is_empty() {
    756                return Ok(());
    757            }
    758 
    759            let mut form = reqwest::blocking::multipart::Form::new()
    760                .text("lang", "en")
    761                .text("action", "inbox")
    762                .text("session", session.clone())
    763                .text("do", "clean");
    764 
    765            // Add all message IDs as checkboxes
    766            for mid in &message_ids {
    767                form = form.text("mid[]", mid.clone());
    768            }
    769 
    770            let response = self.client.post(&url).multipart(form).send()?;
    771 
    772            if response.status().is_success() {
    773                // Clear local inbox
    774                app.inbox_items.items.clear();
    775                app.inbox_items.state.select(None);
    776            } else {
    777                return Err(anyhow::anyhow!(
    778                    "Failed to clear inbox: {}",
    779                    response.status()
    780                ));
    781            }
    782        }
    783        Ok(())
    784    }
    785 
    786    fn login(&mut self) -> Result<(), LoginErr> {
    787        // If we provided a session, skip login process
    788        if self.session.is_some() {
    789            // println!("Session in params: {:?}", self.session);
    790            return Ok(());
    791        }
    792        // println!("self.session is not Some");
    793        // println!("self.sxiv = {:?}", self.sxiv);
    794        self.session = Some(lechatphp::login(
    795            &self.client,
    796            &self.config.url,
    797            &self.config.page_php,
    798            &self.base_client.username,
    799            &self.base_client.password,
    800            &self.guest_color,
    801            self.manual_captcha,
    802            self.sxiv,
    803        )?);
    804        Ok(())
    805    }
    806 
    807    fn logout(&mut self) -> anyhow::Result<()> {
    808        if let Some(session) = &self.session {
    809            lechatphp::logout(
    810                &self.client,
    811                &self.config.url,
    812                &self.config.page_php,
    813                session,
    814            )?;
    815            self.session = None;
    816        }
    817        Ok(())
    818    }
    819 
    820    fn start_cycle(&self, color_only: bool) {
    821        let username = self.base_client.username.clone();
    822        let tx = self.tx.clone();
    823        let color_rx = Arc::clone(&self.color_rx);
    824        thread::spawn(move || {
    825            let mut idx = 0;
    826            let colors = [
    827                "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF",
    828                "#6633FF", "#CC33FF", "#efefef",
    829            ];
    830            loop {
    831                let color_rx = color_rx.lock().unwrap();
    832                let timeout = after(Duration::from_millis(5200));
    833                select! {
    834                    recv(&color_rx) -> _ => break,
    835                    recv(&timeout) -> _ => {}
    836                }
    837                idx = (idx + 1) % colors.len();
    838                let color = colors[idx].to_owned();
    839                if !color_only {
    840                    let name = format!("{}{}", username, random_string(14));
    841                    log::error!("New name : {}", name);
    842                    let _ = tx.send(PostType::Profile(color, name, true, true, true));
    843                } else {
    844                    let _ = tx.send(PostType::NewColor(color));
    845                }
    846                // tx.send(PostType::Post("!up".to_owned(), Some(username.clone())))
    847                //     .unwrap();
    848                // tx.send(PostType::DeleteLast).unwrap();
    849            }
    850            let msg = PostType::Profile("#90ee90".to_owned(), username, true, true, true);
    851            let _ = tx.send(msg);
    852        });
    853    }
    854 
    855    fn save_filters(&self) {
    856        if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
    857            cfg.bad_usernames = self.bad_username_filters.lock().unwrap().clone();
    858            cfg.bad_exact_usernames = self.bad_exact_username_filters.lock().unwrap().clone();
    859            cfg.bad_messages = self.bad_message_filters.lock().unwrap().clone();
    860            cfg.allowlist = self.allowlist.lock().unwrap().clone();
    861            if let Err(e) = confy::store("bhcli", None, cfg) {
    862                log::error!("failed to store config: {}", e);
    863            }
    864        }
    865    }
    866 
    867    fn save_alt_forwarding_config(&self) {
    868        if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
    869            cfg.alt_forwarding_enabled = *self.alt_forwarding_enabled.lock().unwrap();
    870            if let Err(e) = confy::store("bhcli", None, cfg) {
    871                log::error!("failed to store config: {}", e);
    872            }
    873        }
    874    }
    875 
    876    fn save_ai_config(&self) {
    877        if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
    878            if let Some(profile_cfg) = cfg.profiles.get_mut(&self.profile) {
    879                profile_cfg.ai_enabled = *self.ai_enabled.lock().unwrap();
    880                profile_cfg.ai_mode = self.ai_mode.lock().unwrap().clone();
    881                profile_cfg.moderation_strictness = self.moderation_strictness.clone();
    882                profile_cfg.mod_logs_enabled = *self.mod_logs_enabled.lock().unwrap();
    883                if let Err(e) = confy::store("bhcli", None, cfg) {
    884                    log::error!("failed to store AI config: {}", e);
    885                }
    886            }
    887        }
    888    }
    889 
    890    fn set_account(&mut self, which: &str, username: String) {
    891        if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
    892            if let Some(profile_cfg) = cfg.profiles.get_mut(&self.profile) {
    893                match which {
    894                    "alt" => {
    895                        profile_cfg.alt_account = Some(username.clone());
    896                        self.account_manager.set_alt_account(username.clone());
    897                    }
    898                    "master" => {
    899                        profile_cfg.master_account = Some(username.clone());
    900                        self.account_manager.set_master_account(username.clone());
    901                    }
    902                    _ => return,
    903                }
    904                if let Err(e) = confy::store("bhcli", None, cfg) {
    905                    log::error!("failed to store config: {}", e);
    906                }
    907            }
    908        }
    909    }
    910 
    911    fn list_filters(&self, usernames: bool) -> String {
    912        let list = if usernames {
    913            self.bad_username_filters.lock().unwrap().clone()
    914        } else {
    915            self.bad_message_filters.lock().unwrap().clone()
    916        };
    917        if list.is_empty() {
    918            String::from("(empty)")
    919        } else {
    920            list.join(", ")
    921        }
    922    }
    923 
    924    fn list_exact_filters(&self) -> String {
    925        let list = self.bad_exact_username_filters.lock().unwrap().clone();
    926        if list.is_empty() {
    927            String::from("(empty)")
    928        } else {
    929            list.join(", ")
    930        }
    931    }
    932 
    933    fn remove_filter(&self, term: &str, usernames: bool) -> bool {
    934        if usernames {
    935            {
    936                let mut filters = self.bad_username_filters.lock().unwrap();
    937                if let Some(pos) = filters.iter().position(|x| x == term) {
    938                    filters.remove(pos);
    939                    return true;
    940                }
    941            }
    942            {
    943                let mut filters = self.bad_exact_username_filters.lock().unwrap();
    944                if let Some(pos) = filters.iter().position(|x| x == term) {
    945                    filters.remove(pos);
    946                    return true;
    947                }
    948            }
    949            false
    950        } else {
    951            let mut filters = self.bad_message_filters.lock().unwrap();
    952            if let Some(pos) = filters.iter().position(|x| x == term) {
    953                filters.remove(pos);
    954                true
    955            } else {
    956                false
    957            }
    958        }
    959    }
    960 
    961    fn apply_ban_filters(&self, users: &Arc<Mutex<Users>>) {
    962        let users = users.lock().unwrap();
    963        let name_filters = self.bad_username_filters.lock().unwrap().clone();
    964        let exact_filters = self.bad_exact_username_filters.lock().unwrap().clone();
    965        for (_, name) in &users.guests {
    966            if exact_filters.iter().any(|f| f == name)
    967                || name_filters
    968                    .iter()
    969                    .any(|f| name.to_lowercase().contains(&f.to_lowercase()))
    970            {
    971                let _ = self.tx.send(PostType::Kick(String::new(), name.clone()));
    972            }
    973        }
    974    }
    975 
    976    /// Determine user role based on current client state
    977    fn determine_user_role(&self) -> UserRole {
    978        // This is a simplified role determination - in a real implementation,
    979        // you'd check the user's actual permissions from the server
    980        if self.account_manager.master_account.is_some() {
    981            UserRole::Admin
    982        } else if self.account_manager.alt_account.is_some() {
    983            UserRole::Staff
    984        } else if !self.display_guest_view {
    985            UserRole::Member
    986        } else {
    987            UserRole::Guest
    988        }
    989    }
    990 
    991    fn handle_identity_command(
    992        &mut self,
    993        command: &str,
    994        message: &str,
    995        app: &mut App,
    996        target: Option<String>,
    997    ) -> bool {
    998        if let Some(identity_config) = self.identities.get(command) {
    999            if identity_config.len() < 2 {
   1000                return false; // Invalid config, need at least nickname and color
   1001            }
   1002 
   1003            let nickname = identity_config[0].clone();
   1004            let color = identity_config[1].clone();
   1005            // Trim quotes from color if present (for backwards compatibility)
   1006            let color = color.trim_matches('"').trim_matches('\'');
   1007            let incognito = identity_config.get(2).map(|s| s == "true").unwrap_or(false);
   1008            let bold = identity_config.get(3).map(|s| s == "true").unwrap_or(false);
   1009            let italic = identity_config.get(4).map(|s| s == "true").unwrap_or(false);
   1010 
   1011            // Store current user info for restoration
   1012            let current_username = self.current_username.clone();
   1013            let current_color = self.current_color.clone();
   1014 
   1015            if !message.is_empty() {
   1016                // First set profile to the configured identity
   1017                self.post_msg(PostType::Profile(
   1018                    color.to_string(),
   1019                    nickname,
   1020                    incognito,
   1021                    bold,
   1022                    italic,
   1023                ))
   1024                .unwrap();
   1025 
   1026                // Check if this is a kick command
   1027                if let Some(captures) = KICK_RGX.captures(message) {
   1028                    // Handle kick command
   1029                    let username = captures[1].to_owned();
   1030                    let kick_msg = captures[2].to_owned();
   1031                    let tx = self.tx.clone();
   1032                    thread::spawn(move || {
   1033                        thread::sleep(Duration::from_millis(2000)); // Increased delay to 2 seconds
   1034                        let _ = tx.send(PostType::Kick(kick_msg, username));
   1035 
   1036                        // Add another delay before restoring profile
   1037                        thread::sleep(Duration::from_millis(1000));
   1038                        let _ = tx.send(PostType::Profile(
   1039                            current_color,
   1040                            current_username,
   1041                            true,
   1042                            true,
   1043                            true,
   1044                        ));
   1045                    });
   1046                } else {
   1047                    // Handle regular message
   1048                    let tx = self.tx.clone();
   1049                    let message_clone = message.to_owned();
   1050                    let target_clone = target.clone();
   1051                    thread::spawn(move || {
   1052                        thread::sleep(Duration::from_millis(2000)); // Increased delay to 2 seconds
   1053                        let _ = tx.send(PostType::Post(message_clone, target_clone));
   1054 
   1055                        // Add another delay before restoring profile
   1056                        thread::sleep(Duration::from_millis(1000));
   1057                        let _ = tx.send(PostType::Profile(
   1058                            current_color,
   1059                            current_username,
   1060                            true,
   1061                            true,
   1062                            true,
   1063                        ));
   1064                    });
   1065                }
   1066            }
   1067            app.input = format!("/{} ", command);
   1068            app.input_idx = app.input.width();
   1069            true
   1070        } else {
   1071            false
   1072        }
   1073    }
   1074 
   1075    fn ensure_default_identities(&mut self) {
   1076        // Add default identities if they don't exist
   1077        let defaults = vec![
   1078            (
   1079                "admin",
   1080                vec![
   1081                    "Administrator".to_string(),
   1082                    "#FF4444".to_string(),
   1083                    "false".to_string(),
   1084                    "true".to_string(),
   1085                    "false".to_string(),
   1086                ],
   1087            ),
   1088            (
   1089                "mod",
   1090                vec![
   1091                    "Moderator".to_string(),
   1092                    "#FFAA00".to_string(),
   1093                    "false".to_string(),
   1094                    "true".to_string(),
   1095                    "false".to_string(),
   1096                ],
   1097            ),
   1098            (
   1099                "john",
   1100                vec![
   1101                    "JohnDoe".to_string(),
   1102                    "#FC129E".to_string(),
   1103                    "false".to_string(),
   1104                    "false".to_string(),
   1105                    "false".to_string(),
   1106                ],
   1107            ),
   1108            (
   1109                "intel",
   1110                vec![
   1111                    "intelroker".to_string(),
   1112                    "#FF1212".to_string(),
   1113                    "false".to_string(),
   1114                    "true".to_string(),
   1115                    "false".to_string(),
   1116                ],
   1117            ),
   1118            (
   1119                "op",
   1120                vec![
   1121                    "Operator".to_string(),
   1122                    "#00FF88".to_string(),
   1123                    "false".to_string(),
   1124                    "true".to_string(),
   1125                    "false".to_string(),
   1126                ],
   1127            ),
   1128            (
   1129                "shadow",
   1130                vec![
   1131                    "ShadowUser".to_string(),
   1132                    "#2C2C2C".to_string(),
   1133                    "false".to_string(),
   1134                    "false".to_string(),
   1135                    "true".to_string(),
   1136                ],
   1137            ),
   1138            (
   1139                "ghost",
   1140                vec![
   1141                    "Ghost".to_string(),
   1142                    "#CCCCCC".to_string(),
   1143                    "false".to_string(),
   1144                    "false".to_string(),
   1145                    "false".to_string(),
   1146                ],
   1147            ),
   1148            (
   1149                "cyber",
   1150                vec![
   1151                    "CyberNinja".to_string(),
   1152                    "#00FFFF".to_string(),
   1153                    "false".to_string(),
   1154                    "true".to_string(),
   1155                    "true".to_string(),
   1156                ],
   1157            ),
   1158            (
   1159                "viper",
   1160                vec![
   1161                    "ViperX".to_string(),
   1162                    "#00FF00".to_string(),
   1163                    "false".to_string(),
   1164                    "true".to_string(),
   1165                    "false".to_string(),
   1166                ],
   1167            ),
   1168            (
   1169                "phoenix",
   1170                vec![
   1171                    "PhoenixRise".to_string(),
   1172                    "#FF8C00".to_string(),
   1173                    "false".to_string(),
   1174                    "false".to_string(),
   1175                    "true".to_string(),
   1176                ],
   1177            ),
   1178        ];
   1179 
   1180        for (cmd, config) in defaults {
   1181            if !self.identities.contains_key(cmd) {
   1182                self.identities.insert(cmd.to_string(), config);
   1183            }
   1184        }
   1185    }
   1186 
   1187    fn switch_to_identity(&mut self, command: &str) -> Result<(), String> {
   1188        if let Some(identity_config) = self.identities.get(command) {
   1189            if identity_config.len() >= 2 {
   1190                let nickname = identity_config[0].clone();
   1191                let color = identity_config[1].clone();
   1192                // Trim quotes from color if present (for backwards compatibility)
   1193                let color = color.trim_matches('"').trim_matches('\'');
   1194                let incognito = identity_config.get(2).map(|s| s == "true").unwrap_or(false);
   1195                let bold = identity_config.get(3).map(|s| s == "true").unwrap_or(false);
   1196                let italic = identity_config.get(4).map(|s| s == "true").unwrap_or(false);
   1197 
   1198                // Update current identity tracking
   1199                self.current_username = nickname.clone();
   1200                self.current_color = color.to_string();
   1201 
   1202                // Update the base client username for login purposes
   1203                self.base_client.username = nickname.clone();
   1204 
   1205                // Save username to config file for future logins
   1206                if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
   1207                    if let Some(profile_cfg) = cfg.profiles.get_mut(&self.profile) {
   1208                        profile_cfg.username = nickname.clone();
   1209                        if let Err(e) = confy::store("bhcli", None, cfg) {
   1210                            log::error!("Failed to save username to config: {}", e);
   1211                        }
   1212                    }
   1213                }
   1214 
   1215                // Permanently switch to the identity
   1216                self.post_msg(PostType::Profile(
   1217                    color.to_string(),
   1218                    nickname.clone(),
   1219                    incognito,
   1220                    bold,
   1221                    italic,
   1222                ))
   1223                .unwrap();
   1224 
   1225                // Send confirmation message to @0
   1226                let confirmation_msg = format!("You are now @{}", nickname);
   1227                self.post_msg(PostType::Post(confirmation_msg, Some("0".to_owned())))
   1228                    .unwrap();
   1229 
   1230                Ok(())
   1231            } else {
   1232                Err(format!("Invalid identity configuration for /{}", command))
   1233            }
   1234        } else {
   1235            Err(format!("Identity /{} not found", command))
   1236        }
   1237    }
   1238 
   1239    fn process_command_with_target(
   1240        &mut self,
   1241        input: &str,
   1242        app: &mut App,
   1243        users: &Arc<Mutex<Users>>,
   1244        target: Option<String>,
   1245    ) -> bool {
   1246        // First, check for enhanced command processing (master/alt delegation)
   1247        if let Some(enhanced_command) = parse_enhanced_command(input, &self.account_manager) {
   1248            if enhanced_command != input {
   1249                // Command was transformed, process the enhanced version recursively
   1250                return self.process_command_with_target(&enhanced_command, app, users, target);
   1251            }
   1252        }
   1253 
   1254        // Check if account relationship is active for status display
   1255        let relationship_status = self.account_manager.get_relationship_status(users);
   1256        if matches!(relationship_status, AccountRelationshipStatus::MasterOffline | AccountRelationshipStatus::AltOffline) {
   1257            // Optionally show status warning (could be toggled via config)
   1258            if input.trim() == "/status" || input.trim() == "/account" {
   1259                let status_message = self.account_manager.format_status_message(&relationship_status);
   1260                self.post_msg(PostType::Post(status_message, target.clone())).unwrap();
   1261                return true;
   1262            }
   1263        }
   1264 
   1265        // Try ChatOps commands
   1266        let user_role = self.determine_user_role();
   1267        if let Some(chatops_result) =
   1268            self.chatops_router
   1269                .process_command(input, &self.base_client.username, user_role)
   1270        {
   1271            // Convert ChatOps result to chat messages
   1272            let messages = chatops_result.to_messages();
   1273            for message in messages {
   1274                // Special case: /help command should always go to @0 (user 0)
   1275                let message_target = if input.trim() == "/help" {
   1276                    Some("0".to_owned())
   1277                } else {
   1278                    // Use the provided target, or None for main chat
   1279                    target.clone()
   1280                };
   1281                self.post_msg(PostType::Post(message, message_target))
   1282                    .unwrap();
   1283            }
   1284            return true;
   1285        }
   1286 
   1287        // Continue with existing commands
   1288        if input == "/dl" {
   1289            self.post_msg(PostType::DeleteLast).unwrap();
   1290        } else if input.starts_with("/m ") {
   1291            // Send message to members
   1292            let msg = input.trim_start_matches("/m ").to_owned();
   1293            let to = Some(SEND_TO_MEMBERS.to_owned());
   1294            self.post_msg(PostType::Post(msg, to)).unwrap();
   1295            return true;
   1296        } else if input.starts_with("/s ") {
   1297            // Send message to staff
   1298            let msg = input.trim_start_matches("/s ").to_owned();
   1299            let to = Some(SEND_TO_STAFFS.to_owned());
   1300            self.post_msg(PostType::Post(msg, to)).unwrap();
   1301            return true;
   1302        } else if let Some(captures) = DLX_RGX.captures(input) {
   1303            let x: usize = captures.get(1).unwrap().as_str().parse().unwrap();
   1304            for _ in 0..x {
   1305                self.post_msg(PostType::DeleteLast).unwrap();
   1306            }
   1307        } else if input == "/dall" {
   1308            self.post_msg(PostType::DeleteAll).unwrap();
   1309        } else if let Some(captures) = DELETE_RGX.captures(input) {
   1310            let msg_id = captures.get(1).unwrap().as_str().to_owned();
   1311            self.post_msg(PostType::Delete(msg_id)).unwrap();
   1312        } else if input == "/cycles" {
   1313            self.color_tx.send(()).unwrap();
   1314        } else if input == "/cycle1" {
   1315            self.start_cycle(true);
   1316        } else if input == "/cycle2" {
   1317            self.start_cycle(false);
   1318        } else if input == "/kall" {
   1319            let username = "s _".to_owned();
   1320            let msg = "".to_owned();
   1321            self.post_msg(PostType::Kick(msg, username)).unwrap();
   1322        } else if let Some(captures) = PM_RGX.captures(input) {
   1323            let username = &captures[1];
   1324            let msg = captures[2].to_owned();
   1325            let to = Some(username.to_owned());
   1326            self.post_msg(PostType::Post(msg, to)).unwrap();
   1327            app.input = format!("/pm {} ", username);
   1328            app.input_idx = app.input.width();
   1329        } else if let Some(captures) = NEW_NICKNAME_RGX.captures(input) {
   1330            let new_nickname = captures[1].to_owned();
   1331            self.post_msg(PostType::NewNickname(new_nickname)).unwrap();
   1332        } else if let Some(captures) = NEW_COLOR_RGX.captures(input) {
   1333            let new_color = captures[1].to_owned();
   1334            self.post_msg(PostType::NewColor(new_color)).unwrap();
   1335        } else if let Some(captures) = KICK_RGX.captures(input) {
   1336            let username = captures[1].to_owned();
   1337            let msg = captures[2].to_owned();
   1338 
   1339            // Protect Dasho from being kicked
   1340            if username.to_lowercase() == "dasho" {
   1341                let protection_msg = "❌ Cannot kick Dasho - protected user".to_string();
   1342                self.post_msg(PostType::Post(protection_msg, Some("0".to_owned())))
   1343                    .unwrap();
   1344            } else {
   1345                self.post_msg(PostType::Kick(msg, username)).unwrap();
   1346            }
   1347        } else if input.starts_with("/banname ") || input.starts_with("/ban ") {
   1348            let mut name = if input.starts_with("/banname ") {
   1349                remove_prefix(input, "/banname ")
   1350            } else {
   1351                remove_prefix(input, "/ban ")
   1352            };
   1353            let exact = name.starts_with('"') && name.ends_with('"') && name.len() >= 2;
   1354            if exact {
   1355                name = &name[1..name.len() - 1];
   1356            }
   1357            let name = name.to_owned();
   1358 
   1359            // Protect Dasho from being banned
   1360            if name.to_lowercase().contains("dasho") {
   1361                let protection_msg = "❌ Cannot ban Dasho - protected user".to_string();
   1362                self.post_msg(PostType::Post(protection_msg, Some("0".to_owned())))
   1363                    .unwrap();
   1364            } else {
   1365                if exact {
   1366                    let mut f = self.bad_exact_username_filters.lock().unwrap();
   1367                    f.push(name.clone());
   1368                } else {
   1369                    let mut f = self.bad_username_filters.lock().unwrap();
   1370                    f.push(name.clone());
   1371                }
   1372                self.save_filters();
   1373                self.post_msg(PostType::Kick(String::new(), name.clone()))
   1374                    .unwrap();
   1375                self.apply_ban_filters(users);
   1376                let msg = if exact {
   1377                    format!("Banned exact user \"{}\"", name)
   1378                } else {
   1379                    format!("Banned userfilter \"{}\"", name)
   1380                };
   1381                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1382                    .unwrap();
   1383            }
   1384        } else if input.starts_with("/banmsg ") || input.starts_with("/filter ") {
   1385            let term = if input.starts_with("/banmsg ") {
   1386                remove_prefix(input, "/banmsg ")
   1387            } else {
   1388                remove_prefix(input, "/filter ")
   1389            };
   1390            let term = term.to_owned();
   1391            {
   1392                let mut f = self.bad_message_filters.lock().unwrap();
   1393                f.push(term.clone());
   1394            }
   1395            self.save_filters();
   1396            let msg = format!("Filtering messages including \"{}\"", term);
   1397            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1398                .unwrap();
   1399        } else if input == "/banlist" {
   1400            let list = self.list_filters(true);
   1401            let list_exact = self.list_exact_filters();
   1402            let msg = format!("Banned names: {}", list)
   1403                + &if list_exact.is_empty() {
   1404                    String::new()
   1405                } else {
   1406                    format!("\nBanned exact names: {}", list_exact)
   1407                };
   1408            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1409                .unwrap();
   1410        } else if input == "/filterlist" {
   1411            let list = self.list_filters(false);
   1412            let msg = format!("Filtered messages: {}", list);
   1413            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1414                .unwrap();
   1415        } else if input.starts_with("/unban ") {
   1416            let mut name = remove_prefix(input, "/unban ");
   1417            if name.starts_with('"') && name.ends_with('"') && name.len() >= 2 {
   1418                name = &name[1..name.len() - 1];
   1419            }
   1420            if self.remove_filter(name, true) {
   1421                self.save_filters();
   1422                let msg = format!("Unbanned {}", name);
   1423                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1424                    .unwrap();
   1425            }
   1426        } else if input.starts_with("/unfilter ") {
   1427            let term = remove_prefix(input, "/unfilter ");
   1428            if self.remove_filter(term, false) {
   1429                self.save_filters();
   1430                let msg = format!("Unfiltered \"{}\"", term);
   1431                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1432                    .unwrap();
   1433            }
   1434        } else if input.starts_with("/set ") {
   1435            let rest = remove_prefix(input, "/set ");
   1436            if let Some(username) = rest.strip_prefix("alt ") {
   1437                let user = username.to_owned();
   1438                self.set_account("alt", user.clone());
   1439                let msg = format!("ALT account set to {}", user);
   1440                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1441                    .unwrap();
   1442            } else if let Some(username) = rest.strip_prefix("master ") {
   1443                let user = username.to_owned();
   1444                self.set_account("master", user.clone());
   1445                let msg = format!("MASTER account set to {}", user);
   1446                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1447                    .unwrap();
   1448            } else {
   1449                return false;
   1450            }
   1451        } else if input == "/alt on" {
   1452            *self.alt_forwarding_enabled.lock().unwrap() = true;
   1453            self.save_alt_forwarding_config();
   1454            let msg = "ALT message forwarding enabled".to_string();
   1455            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1456                .unwrap();
   1457        } else if input == "/alt off" {
   1458            *self.alt_forwarding_enabled.lock().unwrap() = false;
   1459            self.save_alt_forwarding_config();
   1460            let msg = "ALT message forwarding disabled".to_string();
   1461            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1462                .unwrap();
   1463        } else if input.starts_with("/allow ") {
   1464            let user = remove_prefix(input, "/allow ").to_owned();
   1465            {
   1466                let mut list = self.allowlist.lock().unwrap();
   1467                if !list.contains(&user) {
   1468                    list.push(user.clone());
   1469                }
   1470            }
   1471            self.save_filters();
   1472            let msg = format!("Allowed {}", user);
   1473            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1474                .unwrap();
   1475        } else if input.starts_with("/revoke ") {
   1476            let user = remove_prefix(input, "/revoke ").to_owned();
   1477            {
   1478                let mut list = self.allowlist.lock().unwrap();
   1479                if let Some(pos) = list.iter().position(|u| u == &user) {
   1480                    list.remove(pos);
   1481                }
   1482            }
   1483            self.save_filters();
   1484            let msg = format!("Revoked {}", user);
   1485            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1486                .unwrap();
   1487        } else if input == "/allowlist" {
   1488            let list = self.allowlist.lock().unwrap().clone();
   1489            let out = if list.is_empty() {
   1490                String::from("(empty)")
   1491            } else {
   1492                list.join(", ")
   1493            };
   1494            let msg = format!("Allowlist: {}", out);
   1495            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1496                .unwrap();
   1497        } else if input == "/ai on" {
   1498            *self.ai_enabled.lock().unwrap() = true;
   1499            *self.ai_mode.lock().unwrap() = "mod_only".to_string();
   1500            self.save_ai_config();
   1501            let msg = "AI enabled in moderation only mode".to_string();
   1502            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1503                .unwrap();
   1504        } else if input == "/ai mod" {
   1505            *self.ai_enabled.lock().unwrap() = true;
   1506            *self.ai_mode.lock().unwrap() = "mod_only".to_string();
   1507            self.save_ai_config();
   1508            let msg = "AI set to moderation only mode (kicks/bans harmful messages, no replies)"
   1509                .to_string();
   1510            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1511                .unwrap();
   1512        } else if input == "/ai reply all" {
   1513            *self.ai_enabled.lock().unwrap() = true;
   1514            *self.ai_mode.lock().unwrap() = "reply_all".to_string();
   1515            self.save_ai_config();
   1516            let msg =
   1517                "AI set to reply all mode (responds to all appropriate messages + moderation)"
   1518                    .to_string();
   1519            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1520                .unwrap();
   1521        } else if input == "/ai reply ping" {
   1522            *self.ai_enabled.lock().unwrap() = true;
   1523            *self.ai_mode.lock().unwrap() = "reply_ping".to_string();
   1524            self.save_ai_config();
   1525            let msg =
   1526                "AI set to reply ping mode (responds only when tagged/mentioned + moderation)"
   1527                    .to_string();
   1528            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1529                .unwrap();
   1530        } else if input == "/ai off" {
   1531            *self.ai_enabled.lock().unwrap() = false; // Completely disable AI
   1532            *self.ai_mode.lock().unwrap() = "off".to_string(); // Completely off
   1533            self.save_ai_config();
   1534            let msg = "AI completely disabled (no moderation, no replies)".to_string();
   1535            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1536                .unwrap();
   1537        } else if input == "/ai strict" {
   1538            self.moderation_strictness = "strict".to_string();
   1539            self.save_ai_config();
   1540            let msg = "AI moderation set to STRICT mode (very strict, moderates anything potentially harmful)".to_string();
   1541            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1542                .unwrap();
   1543        } else if input == "/ai balanced" {
   1544            self.moderation_strictness = "balanced".to_string();
   1545            self.save_ai_config();
   1546            let msg = "AI moderation set to BALANCED mode (moderate clear violations, preserve free speech)".to_string();
   1547            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1548                .unwrap();
   1549        } else if input == "/ai lenient" {
   1550            self.moderation_strictness = "lenient".to_string();
   1551            self.save_ai_config();
   1552            let msg = "AI moderation set to LENIENT mode (very lenient, only moderate obvious violations)".to_string();
   1553            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1554                .unwrap();
   1555        } else if input == "/check ai" {
   1556            let ai_enabled = *self.ai_enabled.lock().unwrap();
   1557            let ai_mode = self.ai_mode.lock().unwrap().clone();
   1558            let has_openai = self.openai_client.is_some();
   1559 
   1560            let status_msg = format!(
   1561                "AI Status Check:\n- AI Enabled: {}\n- AI Mode: {}\n- OpenAI Client: {}\n- Moderation Strictness: {}",
   1562                if ai_enabled { "YES" } else { "NO" },
   1563                ai_mode,
   1564                if has_openai { "CONNECTED" } else { "NOT AVAILABLE (check OPENAI_API_KEY)" },
   1565                self.moderation_strictness
   1566            );
   1567 
   1568            self.post_msg(PostType::Post(status_msg, Some("0".to_owned())))
   1569                .unwrap();
   1570 
   1571            // Test quick moderation patterns
   1572            let test_messages = vec!["young boy", "hello world", "cheese pizza"];
   1573            for test_msg in test_messages {
   1574                let quick_result = if let Some(should_moderate) = quick_moderation_check(test_msg) {
   1575                    if should_moderate {
   1576                        "BLOCK"
   1577                    } else {
   1578                        "FLAG"
   1579                    }
   1580                } else {
   1581                    "ALLOW"
   1582                };
   1583                let test_result = format!("Quick test '{}': {}", test_msg, quick_result);
   1584                self.post_msg(PostType::Post(test_result, Some("0".to_owned())))
   1585                    .unwrap();
   1586            }
   1587        } else if input.starts_with("/check mod ") {
   1588            let test_message = input.trim_start_matches("/check mod ").trim();
   1589            if test_message.is_empty() {
   1590                let msg = "Usage: /check mod <message> - Test AI moderation response for a message"
   1591                    .to_string();
   1592                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1593                    .unwrap();
   1594            } else {
   1595                let ai_enabled = *self.ai_enabled.lock().unwrap();
   1596                let has_openai = self.openai_client.is_some();
   1597 
   1598                if !ai_enabled {
   1599                    let msg = "AI is currently disabled. Enable with /ai mod first.".to_string();
   1600                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1601                        .unwrap();
   1602                } else if !has_openai {
   1603                    let msg =
   1604                        "OpenAI client not available. Check OPENAI_API_KEY environment variable."
   1605                            .to_string();
   1606                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1607                        .unwrap();
   1608                } else {
   1609                    // First test quick moderation
   1610                    let quick_result =
   1611                        if let Some(should_moderate) = quick_moderation_check(test_message) {
   1612                            if should_moderate {
   1613                                "YES (Quick Pattern Match)"
   1614                            } else {
   1615                                "NO (Quick Pattern False)"
   1616                            }
   1617                        } else {
   1618                            "INCONCLUSIVE (Needs AI Analysis)"
   1619                        };
   1620 
   1621                    let quick_msg = format!("Quick Check: '{}' -> {}", test_message, quick_result);
   1622                    self.post_msg(PostType::Post(quick_msg, Some("0".to_owned())))
   1623                        .unwrap();
   1624 
   1625                    // If quick check didn't catch it, test AI moderation
   1626                    if quick_result == "INCONCLUSIVE (Needs AI Analysis)" {
   1627                        let openai_client = self.openai_client.as_ref().unwrap().clone();
   1628                        let moderation_strictness = self.moderation_strictness.clone();
   1629                        let test_msg = test_message.to_string();
   1630                        let tx = self.tx.clone();
   1631 
   1632                        // Show that we're starting AI analysis
   1633                        let start_msg = format!("Starting AI analysis for: '{}'...", test_msg);
   1634                        self.post_msg(PostType::Post(start_msg, Some("0".to_owned())))
   1635                            .unwrap();
   1636 
   1637                        // Use same pattern as process_ai_message - create runtime and spawn thread
   1638                        thread::spawn(move || {
   1639                            let rt = Runtime::new().unwrap();
   1640                            rt.block_on(async move {
   1641                                match check_ai_moderation(&openai_client, &test_msg, &moderation_strictness).await {
   1642                                    Some(true) => {
   1643                                        let ai_msg = format!("AI Check: '{}' -> YES (AI recommends kick)", test_msg);
   1644                                        let _ = tx.send(PostType::Post(ai_msg, Some("0".to_owned())));
   1645                                    }
   1646                                    Some(false) => {
   1647                                        let ai_msg = format!("AI Check: '{}' -> NO (AI allows message)", test_msg);
   1648                                        let _ = tx.send(PostType::Post(ai_msg, Some("0".to_owned())));
   1649                                    }
   1650                                    None => {
   1651                                        let ai_msg = format!("AI Check: '{}' -> ERROR (AI request failed - check logs)", test_msg);
   1652                                        let _ = tx.send(PostType::Post(ai_msg, Some("0".to_owned())));
   1653                                    }
   1654                                }
   1655                            });
   1656                        });
   1657                    }
   1658                }
   1659            }
   1660        } else if input == "/modlog on" {
   1661            *self.mod_logs_enabled.lock().unwrap() = true;
   1662            self.save_ai_config();
   1663            let msg =
   1664                "Moderation logging ENABLED - MOD LOG messages will be sent to @0".to_string();
   1665            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1666                .unwrap();
   1667        } else if input == "/modlog off" {
   1668            *self.mod_logs_enabled.lock().unwrap() = false;
   1669            self.save_ai_config();
   1670            let msg = "Moderation logging DISABLED - MOD LOG messages are now muted".to_string();
   1671            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1672                .unwrap();
   1673        } else if input == "/warnings" {
   1674            let warnings = self.user_warnings.lock().unwrap();
   1675            if warnings.is_empty() {
   1676                let msg = "No active warnings".to_string();
   1677                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1678                    .unwrap();
   1679            } else {
   1680                let mut msg = "Current warnings:\n".to_string();
   1681                for (user, count) in warnings.iter() {
   1682                    msg.push_str(&format!("- {}: {}/3 warnings\n", user, count));
   1683                }
   1684                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1685                    .unwrap();
   1686            }
   1687        } else if input.starts_with("/clearwarn ") {
   1688            let user = input.trim_start_matches("/clearwarn ").trim();
   1689            if !user.is_empty() {
   1690                let mut warnings = self.user_warnings.lock().unwrap();
   1691                if warnings.remove(user).is_some() {
   1692                    let msg = format!("Cleared warnings for {}", user);
   1693                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1694                        .unwrap();
   1695                } else {
   1696                    let msg = format!("No warnings found for {}", user);
   1697                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1698                        .unwrap();
   1699                }
   1700            }
   1701        } else if input == "/clearwarn all" {
   1702            let mut warnings = self.user_warnings.lock().unwrap();
   1703            let count = warnings.len();
   1704            warnings.clear();
   1705            let msg = format!("Cleared all warnings ({} users)", count);
   1706            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1707                .unwrap();
   1708        } else if input == "/clearinbox" {
   1709            if self.inbox_mode {
   1710                match self.clear_all_inbox_messages(app) {
   1711                    Ok(()) => {
   1712                        let msg = "All inbox messages cleared".to_string();
   1713                        self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1714                            .unwrap();
   1715                    }
   1716                    Err(e) => {
   1717                        let msg = format!("Failed to clear inbox: {}", e);
   1718                        self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1719                            .unwrap();
   1720                    }
   1721                }
   1722            } else {
   1723                let msg = "Command only available in inbox mode (Shift+O)".to_string();
   1724                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1725                    .unwrap();
   1726            }
   1727        } else if let Some(captures) = IGNORE_RGX.captures(input) {
   1728            let username = captures[1].to_owned();
   1729            self.post_msg(PostType::Ignore(username)).unwrap();
   1730        } else if let Some(captures) = UNIGNORE_RGX.captures(input) {
   1731            let username = captures[1].to_owned();
   1732            self.post_msg(PostType::Unignore(username)).unwrap();
   1733        } else if let Some(captures) = UPLOAD_RGX.captures(input) {
   1734            let file_path = captures[1].to_owned();
   1735            let send_to = match captures.get(2) {
   1736                Some(to_match) => match to_match.as_str() {
   1737                    "members" => SEND_TO_MEMBERS,
   1738                    "staffs" => SEND_TO_STAFFS,
   1739                    "admins" => SEND_TO_ADMINS,
   1740                    _ => SEND_TO_ALL,
   1741                },
   1742                None => SEND_TO_ALL,
   1743            }
   1744            .to_owned();
   1745            let msg = match captures.get(3) {
   1746                Some(msg_match) => msg_match.as_str().to_owned(),
   1747                None => "".to_owned(),
   1748            };
   1749            self.post_msg(PostType::Upload(file_path, send_to, msg))
   1750                .unwrap();
   1751        } else if input.starts_with("/hide on") {
   1752            // Toggle incognito mode on
   1753            self.post_msg(PostType::SetIncognito(true)).unwrap();
   1754            let msg = "Incognito mode ENABLED - you will be hidden from the guest list".to_string();
   1755            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1756                .unwrap();
   1757        } else if input.starts_with("/hide off") {
   1758            // Toggle incognito mode off
   1759            self.post_msg(PostType::SetIncognito(false)).unwrap();
   1760            let msg = "Incognito mode DISABLED - you will be visible on the guest list".to_string();
   1761            self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1762                .unwrap();
   1763        } else if input.starts_with("/switch ") {
   1764            // Alias for /identity switch
   1765            let command = input.trim_start_matches("/switch ");
   1766            match self.switch_to_identity(command) {
   1767                Ok(()) => {} // Success message already sent by helper
   1768                Err(msg) => {
   1769                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1770                        .unwrap();
   1771                }
   1772            }
   1773        } else if input.starts_with("/identity ") {
   1774            let rest = input.trim_start_matches("/identity ");
   1775            if rest == "list" {
   1776                if self.identities.is_empty() {
   1777                    let msg = "No custom identities configured".to_string();
   1778                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1779                        .unwrap();
   1780                } else {
   1781                    let mut msg = "Configured identities:\n".to_string();
   1782                    for (cmd, config) in &self.identities {
   1783                        let nickname = config.get(0).cloned().unwrap_or_else(|| "?".to_string());
   1784                        let color = config.get(1).cloned().unwrap_or_else(|| "?".to_string());
   1785                        msg.push_str(&format!("/{}: {} ({})\n", cmd, nickname, color));
   1786                    }
   1787                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1788                        .unwrap();
   1789                }
   1790            } else if rest.starts_with("add ") {
   1791                let parts: Vec<&str> = rest.splitn(4, ' ').collect();
   1792                if parts.len() >= 4 {
   1793                    let command = parts[1];
   1794                    let nickname = parts[2];
   1795                    let color = parts[3];
   1796                    // Trim quotes from color if present
   1797                    let color = color.trim_matches('"').trim_matches('\'');
   1798                    // Create a complete config: [nickname, color, incognito, bold, italic]
   1799                    let config = vec![
   1800                        nickname.to_string(),
   1801                        color.to_string(),
   1802                        "false".to_string(), // incognito
   1803                        "false".to_string(), // bold
   1804                        "false".to_string(), // italic
   1805                    ];
   1806 
   1807                    // Update in memory
   1808                    self.identities.insert(command.to_string(), config);
   1809 
   1810                    // Save to config file
   1811                    if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
   1812                        if let Some(profile_cfg) = cfg.profiles.get_mut(&self.profile) {
   1813                            profile_cfg
   1814                                .identities
   1815                                .insert(command.to_string(), self.identities[command].clone());
   1816                            if let Err(e) = confy::store("bhcli", None, cfg) {
   1817                                log::error!("failed to store config: {}", e);
   1818                            } else {
   1819                                let msg = format!(
   1820                                    "Added identity /{}: {} ({})",
   1821                                    command, nickname, color
   1822                                );
   1823                                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1824                                    .unwrap();
   1825                            }
   1826                        }
   1827                    }
   1828                } else {
   1829                    let msg = "Usage: /identity add <command> <nickname> <color>".to_string();
   1830                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1831                        .unwrap();
   1832                }
   1833            } else if rest.starts_with("remove ") {
   1834                let command = rest.trim_start_matches("remove ");
   1835                if self.identities.remove(command).is_some() {
   1836                    // Save to config file
   1837                    if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
   1838                        if let Some(profile_cfg) = cfg.profiles.get_mut(&self.profile) {
   1839                            profile_cfg.identities.remove(command);
   1840                            if let Err(e) = confy::store("bhcli", None, cfg) {
   1841                                log::error!("failed to store config: {}", e);
   1842                            } else {
   1843                                let msg = format!("Removed identity /{}", command);
   1844                                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1845                                    .unwrap();
   1846                            }
   1847                        }
   1848                    }
   1849                } else {
   1850                    let msg = format!("Identity /{} not found", command);
   1851                    self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1852                        .unwrap();
   1853                }
   1854            } else if rest.starts_with("switch ") {
   1855                let command = rest.trim_start_matches("switch ");
   1856                match self.switch_to_identity(command) {
   1857                    Ok(()) => {} // Success message already sent by helper
   1858                    Err(msg) => {
   1859                        self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1860                            .unwrap();
   1861                    }
   1862                }
   1863            } else {
   1864                let msg = "Usage: /identity list | add <command> <nickname> <color> | remove <command> | switch <command>".to_string();
   1865                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   1866                    .unwrap();
   1867            }
   1868        } else if input.starts_with("/me ") {
   1869            // Handle /me commands - send as regular messages including the /me prefix
   1870            self.post_msg(PostType::Post(input.to_string(), target))
   1871                .unwrap();
   1872            return true;
   1873        } else if input.starts_with('/') && input.contains(' ') {
   1874            // Check for any unknown slash command that might be a custom identity
   1875            if let Some(space_pos) = input.find(' ') {
   1876                let command = &input[1..space_pos]; // Remove leading '/' and get command name
   1877                let message = input[space_pos + 1..].trim();
   1878                if self.handle_identity_command(command, message, app, target.clone()) {
   1879                    return true;
   1880                }
   1881            }
   1882        } else if input.starts_with("!warn") {
   1883            let msg = input.trim_start_matches("!warn").trim();
   1884            let msg = if msg.starts_with('@') {
   1885                msg.to_owned()
   1886            } else if msg.is_empty() {
   1887                String::new()
   1888            } else {
   1889                format!("@{}", msg)
   1890            };
   1891            let end_msg = format!(
   1892                "This is your warning - {}, will be kicked next. Please read the !-rules.",
   1893                msg
   1894            );
   1895            self.post_msg(PostType::Post(end_msg, None)).unwrap();
   1896        } else if input == "/help" {
   1897            let help_text = r#"Available Commands:
   1898 
   1899 Chat Commands:
   1900 /pm [user] [message] - Send private message to user
   1901 /m [message] - Send message to members only
   1902 /s [message] - Send message to staff only
   1903 
   1904 Identity Commands (send as different users, then restore to original user):
   1905 /john [message] - Send as JohnDoe with pink color, then restore
   1906 /intel [message] - Send as intelroker with red color, then restore
   1907 /op [message] - Send as Operator with white color, then restore
   1908 /shadow [message] - Send as ShadowUser with dark gray color, then restore
   1909 /ghost [message] - Send as Ghost with light gray color, then restore
   1910 /cyber [message] - Send as CyberNinja with electric blue color, then restore
   1911 /viper [message] - Send as ViperX with green color, then restore
   1912 /phoenix [message] - Send as PhoenixRise with orange color, then restore
   1913 (All identity commands support targeting: /m /john, /s /intel, /pm user /op)
   1914 (Custom identities can be configured - see Identity Configuration below)
   1915 
   1916 Identity Configuration:
   1917 /identity list - List all configured custom identities
   1918 /identity add [cmd] [nick] [color] - Add custom identity (/cmd nickname #color)
   1919 /identity remove [cmd] - Remove custom identity command
   1920 /identity switch [cmd] - Permanently switch to the specified identity
   1921 /switch [cmd] - Alias for /identity switch (quick identity switching)
   1922 
   1923 ChatOps Developer Commands (30+ tools available):
   1924 /man [command] - Manual pages for system commands
   1925 /doc [lang] [term] - Language-specific documentation
   1926 /github [user/repo] - GitHub repository information
   1927 /crates [crate] - Rust crate information from crates.io
   1928 /npm [package] - NPM package information
   1929 /hash [algo] [text] - Generate cryptographic hashes
   1930 /uuid - Generate UUID v4
   1931 /base64 [encode|decode] - Base64 encoding/decoding
   1932 /regex [pattern] [text] - Test regular expressions
   1933 /whois [domain] - Domain WHOIS lookup
   1934 /dig [domain] - DNS record lookup
   1935 /ping [host] - Test network connectivity
   1936 /time - Current timestamp info
   1937 /explain [concept] - AI explanations of concepts
   1938 /translate [lang] [text] - Translate text between languages
   1939 ... and 15+ more tools
   1940 
   1941 Use '/commands' to see all ChatOps commands for your role.
   1942 Use '/help [command]' for detailed help on ChatOps commands.
   1943 
   1944 ChatOps Command Prefixes:
   1945 /pm [user] /command - Send ChatOps result as PM to user
   1946 /m /command - Send ChatOps result to members channel
   1947 /s /command - Send ChatOps result to staff channel
   1948 (no prefix) - Send ChatOps result to main chat
   1949 
   1950 AI Commands:
   1951 /ai off - Completely disable AI (no moderation, no replies)
   1952 /ai mod - Enable moderation only (kicks/bans harmful messages)
   1953 /ai reply all - Enable replies to all messages + moderation
   1954 /ai reply ping - Enable replies only when tagged + moderation
   1955 /ai strict - Set AI moderation to strict mode (very strict)
   1956 /ai balanced - Set AI moderation to balanced mode (default)
   1957 /ai lenient - Set AI moderation to lenient mode (very lenient)
   1958 /check ai - Check AI system status and OpenAI connection
   1959 /check mod [message] - Test AI moderation response for a message
   1960 /modlog on/off - Enable/disable moderation logging to @0
   1961 /warnings - Show current warning counts for users
   1962 /clearwarn [user] - Clear warnings for specific user
   1963 /clearwarn all - Clear all user warnings
   1964 
   1965 Inbox Commands:
   1966 Shift+O - Toggle inbox view (view offline PMs)
   1967 x (in inbox) - Delete selected inbox message
   1968 /clearinbox - Clear all inbox messages (only in inbox mode)
   1969 
   1970 Moderation Commands:
   1971 /kick [user] [reason] - Kick user with reason
   1972 /ban [username] - Ban username (partial match)
   1973 /ban "[exact]" - Ban exact username (use quotes)
   1974 /unban [username] - Remove username ban
   1975 /unfilter [text] - Remove message filter
   1976 /filter [text] - Filter messages containing text (same as /banmsg)
   1977 /allow [user] - Add user to allowlist (bypass filters)
   1978 /revoke [user] - Remove user from allowlist
   1979 /banlist - Show banned usernames
   1980 /filterlist - Show filtered message terms
   1981 /allowlist - Show allowlisted users
   1982 !warn [@user] - Send warning message
   1983 
   1984 Message Management:
   1985 /dl - Delete last message
   1986 /dl[number] - Delete last N messages (e.g., /dl5)
   1987 /dall - Delete all messages
   1988 /delete [msg_id] - Delete specific message by ID
   1989 
   1990 Account Management:
   1991 /set alt [username] - Set alt account for forwarding
   1992 /set master [username] - Set master account for PMs
   1993 /alt on/off - Enable/disable alt message forwarding
   1994 
   1995 File Upload:
   1996 /upload [path] [to] [msg] - Upload file (to: members/staffs/admins/all)
   1997 
   1998 Visual/Color:
   1999 /nick [nickname] - Change nickname
   2000 /color [hex] - Change color (#ff0000)
   2001 /cycle1 - Start color cycling
   2002 /cycle2 - Start name + color cycling
   2003 /cycles - Stop cycling
   2004 
   2005 Utility:
   2006 /status - Show current settings and status
   2007 /help - Show this help message
   2008 
   2009 Note: Some commands require appropriate permissions."#;
   2010 
   2011            self.post_msg(PostType::Post(help_text.to_string(), Some("0".to_owned())))
   2012                .unwrap();
   2013        } else if input == "/commands" {
   2014            // List all ChatOps commands available to user
   2015            let user_role = self.determine_user_role();
   2016            if let Some(chatops_result) =
   2017                self.chatops_router
   2018                    .process_command("/list", &self.base_client.username, user_role)
   2019            {
   2020                let messages = chatops_result.to_messages();
   2021                for message in messages {
   2022                    self.post_msg(PostType::Post(message, Some("0".to_owned())))
   2023                        .unwrap();
   2024                }
   2025            } else {
   2026                let msg = "ChatOps commands not available.".to_string();
   2027                self.post_msg(PostType::Post(msg, Some("0".to_owned())))
   2028                    .unwrap();
   2029            }
   2030        } else if input == "/status" {
   2031            let ai_enabled = *self.ai_enabled.lock().unwrap();
   2032            let ai_mode = self.ai_mode.lock().unwrap().clone();
   2033            let alt_forwarding = *self.alt_forwarding_enabled.lock().unwrap();
   2034 
   2035            let alt_account = self
   2036                .account_manager.alt_account
   2037                .as_ref()
   2038                .map(|a| a.as_str())
   2039                .unwrap_or("(not set)");
   2040            let master_account = self
   2041                .account_manager.master_account
   2042                .as_ref()
   2043                .map(|m| m.as_str())
   2044                .unwrap_or("(not set)");
   2045 
   2046            let bad_usernames = self.bad_username_filters.lock().unwrap();
   2047            let bad_exact_usernames = self.bad_exact_username_filters.lock().unwrap();
   2048            let bad_messages = self.bad_message_filters.lock().unwrap();
   2049            let allowlist = self.allowlist.lock().unwrap();
   2050 
   2051            let status_text = format!(
   2052                r#"Current Status:
   2053 
   2054 Account Settings:
   2055 - Username: {}
   2056 - ALT Account: {}
   2057 - Master Account: {}
   2058 - ALT Forwarding: {}
   2059 
   2060 AI Settings:
   2061 - AI Enabled: {}
   2062 - AI Mode: {}
   2063 - System Intel: {}
   2064 - Moderation Strictness: {}
   2065 - Mod Logs Enabled: {}
   2066 
   2067 Display Settings:
   2068 - Show System Messages: {}
   2069 - Guest View: {}
   2070 - Member View: {}
   2071 - Staff View: {}
   2072 - Master PM View: {}
   2073 - PM Only Mode: {}
   2074 - Hidden Messages: {}
   2075 - Clean Mode: {}
   2076 - Sound Muted: {}
   2077 
   2078 Filters & Moderation:
   2079 - Banned Usernames ({}): {}
   2080 - Banned Exact Names ({}): {}
   2081 - Filtered Messages ({}): {}
   2082 - Allowlisted Users ({}): {}
   2083 
   2084 Connection:
   2085 - Profile: {}
   2086 - Session Active: {}
   2087 - Refresh Rate: {}s"#,
   2088                self.base_client.username,
   2089                alt_account,
   2090                master_account,
   2091                if alt_forwarding { "ON" } else { "OFF" },
   2092                if ai_enabled { "YES" } else { "NO" },
   2093                ai_mode,
   2094                if self.system_intel.len() > 50 {
   2095                    format!("{}...", &self.system_intel[..50])
   2096                } else {
   2097                    self.system_intel.clone()
   2098                },
   2099                self.moderation_strictness,
   2100                if *self.mod_logs_enabled.lock().unwrap() {
   2101                    "ON"
   2102                } else {
   2103                    "OFF"
   2104                },
   2105                if self.show_sys { "ON" } else { "OFF" },
   2106                if self.display_guest_view { "ON" } else { "OFF" },
   2107                if self.display_member_view {
   2108                    "ON"
   2109                } else {
   2110                    "OFF"
   2111                },
   2112                if self.display_staff_view { "ON" } else { "OFF" },
   2113                if self.display_master_pm_view {
   2114                    "ON"
   2115                } else {
   2116                    "OFF"
   2117                },
   2118                if self.display_pm_only { "ON" } else { "OFF" },
   2119                if self.display_hidden_msgs {
   2120                    "ON"
   2121                } else {
   2122                    "OFF"
   2123                },
   2124                if self.clean_mode { "ON" } else { "OFF" },
   2125                if *self.is_muted.lock().unwrap() {
   2126                    "YES"
   2127                } else {
   2128                    "NO"
   2129                },
   2130                bad_usernames.len(),
   2131                if bad_usernames.is_empty() {
   2132                    "(none)".to_string()
   2133                } else {
   2134                    bad_usernames.join(", ")
   2135                },
   2136                bad_exact_usernames.len(),
   2137                if bad_exact_usernames.is_empty() {
   2138                    "(none)".to_string()
   2139                } else {
   2140                    bad_exact_usernames.join(", ")
   2141                },
   2142                bad_messages.len(),
   2143                if bad_messages.is_empty() {
   2144                    "(none)".to_string()
   2145                } else {
   2146                    bad_messages.join(", ")
   2147                },
   2148                allowlist.len(),
   2149                if allowlist.is_empty() {
   2150                    "(none)".to_string()
   2151                } else {
   2152                    allowlist.join(", ")
   2153                },
   2154                self.profile,
   2155                if self.session.is_some() { "YES" } else { "NO" },
   2156                self.refresh_rate
   2157            );
   2158 
   2159            self.post_msg(PostType::Post(status_text, Some("0".to_owned())))
   2160                .unwrap();
   2161        } else {
   2162            return false;
   2163        }
   2164        true
   2165    }
   2166 
   2167    fn handle_input(
   2168        &mut self,
   2169        events: &Events,
   2170        app: &mut App,
   2171        messages: &Arc<Mutex<Vec<Message>>>,
   2172        users: &Arc<Mutex<Users>>,
   2173    ) -> Result<(), ExitSignal> {
   2174        match events.next() {
   2175            Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin),
   2176            Ok(Event::Terminate) => return Err(ExitSignal::Terminate),
   2177            Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt),
   2178            _ => Ok(()),
   2179        }
   2180    }
   2181 
   2182    fn handle_event(
   2183        &mut self,
   2184        app: &mut App,
   2185        messages: &Arc<Mutex<Vec<Message>>>,
   2186        users: &Arc<Mutex<Users>>,
   2187        event: event::Event,
   2188    ) -> Result<(), ExitSignal> {
   2189        match event {
   2190            event::Event::Resize(_cols, _rows) => Ok(()),
   2191            event::Event::FocusGained => Ok(()),
   2192            event::Event::FocusLost => Ok(()),
   2193            event::Event::Paste(_) => Ok(()),
   2194            event::Event::Key(key_event) => self.handle_key_event(app, messages, users, key_event),
   2195            event::Event::Mouse(mouse_event) => {
   2196                // Ignore mouse events when external editor is active
   2197                if app.external_editor_active {
   2198                    Ok(())
   2199                } else {
   2200                    self.handle_mouse_event(app, mouse_event)
   2201                }
   2202            }
   2203        }
   2204    }
   2205 
   2206    fn handle_key_event(
   2207        &mut self,
   2208        app: &mut App,
   2209        messages: &Arc<Mutex<Vec<Message>>>,
   2210        users: &Arc<Mutex<Users>>,
   2211        key_event: KeyEvent,
   2212    ) -> Result<(), ExitSignal> {
   2213        if app.input_mode != InputMode::Normal {
   2214            self.last_key_event = None;
   2215        }
   2216        match app.input_mode {
   2217            InputMode::LongMessage => {
   2218                self.handle_long_message_mode_key_event(app, key_event, messages)
   2219            }
   2220            InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages),
   2221            InputMode::Editing | InputMode::EditingErr => {
   2222                self.handle_editing_mode_key_event(app, key_event, users)
   2223            }
   2224            InputMode::MultilineEditing => {
   2225                self.handle_multiline_editing_mode_key_event(app, key_event, users)
   2226            }
   2227            InputMode::Notes => {
   2228                self.handle_notes_mode_key_event(app, key_event)
   2229            }
   2230            InputMode::MessageEditor => {
   2231                self.handle_message_editor_key_event(app, key_event, users)
   2232            }
   2233        }
   2234    }
   2235 
   2236    fn handle_long_message_mode_key_event(
   2237        &mut self,
   2238        app: &mut App,
   2239        key_event: KeyEvent,
   2240        messages: &Arc<Mutex<Vec<Message>>>,
   2241    ) -> Result<(), ExitSignal> {
   2242        match key_event {
   2243            KeyEvent {
   2244                code: KeyCode::Enter,
   2245                modifiers: KeyModifiers::NONE,
   2246                ..
   2247            }
   2248            | KeyEvent {
   2249                code: KeyCode::Esc,
   2250                modifiers: KeyModifiers::NONE,
   2251                ..
   2252            } => self.handle_long_message_mode_key_event_esc(app),
   2253            KeyEvent {
   2254                code: KeyCode::Char('d'),
   2255                modifiers: KeyModifiers::CONTROL,
   2256                ..
   2257            } => self.handle_long_message_mode_key_event_ctrl_d(app, messages),
   2258            KeyEvent {
   2259                code: KeyCode::Char('j'),
   2260                modifiers: KeyModifiers::NONE,
   2261                ..
   2262            }
   2263            | KeyEvent {
   2264                code: KeyCode::Down,
   2265                modifiers: KeyModifiers::NONE,
   2266                ..
   2267            } => {
   2268                // Scroll down
   2269                app.long_message_scroll_offset = app.long_message_scroll_offset.saturating_add(1);
   2270            }
   2271            KeyEvent {
   2272                code: KeyCode::Char('k'),
   2273                modifiers: KeyModifiers::NONE,
   2274                ..
   2275            }
   2276            | KeyEvent {
   2277                code: KeyCode::Up,
   2278                modifiers: KeyModifiers::NONE,
   2279                ..
   2280            } => {
   2281                // Scroll up
   2282                app.long_message_scroll_offset = app.long_message_scroll_offset.saturating_sub(1);
   2283            }
   2284            KeyEvent {
   2285                code: KeyCode::PageUp,
   2286                modifiers: KeyModifiers::NONE,
   2287                ..
   2288            } => {
   2289                // Scroll up by 10 lines
   2290                app.long_message_scroll_offset = app.long_message_scroll_offset.saturating_sub(10);
   2291            }
   2292            KeyEvent {
   2293                code: KeyCode::PageDown,
   2294                modifiers: KeyModifiers::NONE,
   2295                ..
   2296            } => {
   2297                // Scroll down by 10 lines
   2298                app.long_message_scroll_offset = app.long_message_scroll_offset.saturating_add(10);
   2299            }
   2300            _ => {}
   2301        }
   2302        Ok(())
   2303    }
   2304 
   2305    fn handle_normal_mode_key_event(
   2306        &mut self,
   2307        app: &mut App,
   2308        key_event: KeyEvent,
   2309        messages: &Arc<Mutex<Vec<Message>>>,
   2310    ) -> Result<(), ExitSignal> {
   2311        match key_event {
   2312            KeyEvent {
   2313                code: KeyCode::Char('/'),
   2314                modifiers: KeyModifiers::NONE,
   2315                ..
   2316            } => self.handle_normal_mode_key_event_slash(app),
   2317            KeyEvent {
   2318                code: KeyCode::Char('j'),
   2319                modifiers: KeyModifiers::NONE,
   2320                ..
   2321            }
   2322            | KeyEvent {
   2323                code: KeyCode::Down,
   2324                modifiers: KeyModifiers::NONE,
   2325                ..
   2326            } => self.handle_normal_mode_key_event_down(app),
   2327            KeyEvent {
   2328                code: KeyCode::Char('J'),
   2329                modifiers: KeyModifiers::SHIFT,
   2330                ..
   2331            } => self.handle_normal_mode_key_event_j(app, 5),
   2332            KeyEvent {
   2333                code: KeyCode::Char('k'),
   2334                modifiers: KeyModifiers::NONE,
   2335                ..
   2336            }
   2337            | KeyEvent {
   2338                code: KeyCode::Up,
   2339                modifiers: KeyModifiers::NONE,
   2340                ..
   2341            } => self.handle_normal_mode_key_event_up(app),
   2342            KeyEvent {
   2343                code: KeyCode::Char('K'),
   2344                modifiers: KeyModifiers::SHIFT,
   2345                ..
   2346            } => self.handle_normal_mode_key_event_k(app, 5),
   2347            KeyEvent {
   2348                code: KeyCode::Enter,
   2349                modifiers,
   2350                ..
   2351            } if modifiers.contains(KeyModifiers::CONTROL) => {
   2352                self.handle_normal_mode_key_event_member_pm(app)
   2353            }
   2354            KeyEvent {
   2355                code: KeyCode::Enter,
   2356                modifiers: KeyModifiers::NONE,
   2357                ..
   2358            } => self.handle_normal_mode_key_event_enter(app, messages),
   2359            KeyEvent {
   2360                code: KeyCode::Backspace,
   2361                modifiers: KeyModifiers::NONE,
   2362                ..
   2363            } => self.handle_normal_mode_key_event_backspace(app, messages),
   2364            KeyEvent {
   2365                code: KeyCode::Char('y'),
   2366                modifiers: KeyModifiers::NONE,
   2367                ..
   2368            }
   2369            | KeyEvent {
   2370                code: KeyCode::Char('c'),
   2371                modifiers: KeyModifiers::CONTROL,
   2372                ..
   2373            } => self.handle_normal_mode_key_event_yank(app),
   2374            KeyEvent {
   2375                code: KeyCode::Char('Y'),
   2376                modifiers: KeyModifiers::SHIFT,
   2377                ..
   2378            } => self.handle_normal_mode_key_event_yank_link(app),
   2379 
   2380            //Strange
   2381            KeyEvent {
   2382                code: KeyCode::Char('D'),
   2383                modifiers: KeyModifiers::SHIFT,
   2384                ..
   2385            } => self.handle_normal_mode_key_event_download_link(app),
   2386 
   2387            //Strange
   2388            KeyEvent {
   2389                code: KeyCode::Char('d'),
   2390                modifiers: KeyModifiers::NONE,
   2391                ..
   2392            } => self.handle_normal_mode_key_event_download_and_view(app),
   2393 
   2394            // KeyEvent {
   2395            //     code: KeyCode::Char('d'),
   2396            //     modifiers: KeyModifiers::NONE,
   2397            //     ..
   2398            // } => self.handle_normal_mode_key_event_debug(app),
   2399            // KeyEvent {
   2400            //     code: KeyCode::Char('D'),
   2401            //     modifiers: KeyModifiers::SHIFT,
   2402            //     ..
   2403            // } => self.handle_normal_mode_key_event_debug2(app),
   2404            KeyEvent {
   2405                code: KeyCode::Char('m'),
   2406                modifiers: KeyModifiers::NONE,
   2407                ..
   2408            } => self.handle_normal_mode_key_event_toggle_mute(),
   2409            KeyEvent {
   2410                code: KeyCode::Char('S'),
   2411                modifiers: KeyModifiers::SHIFT,
   2412                ..
   2413            } => self.handle_normal_mode_key_event_toggle_sys(),
   2414            KeyEvent {
   2415                code: KeyCode::Char('M'),
   2416                modifiers: KeyModifiers::SHIFT,
   2417                ..
   2418            } => self.handle_normal_mode_key_event_toggle_member_view(),
   2419            KeyEvent {
   2420                code: KeyCode::Char('G'),
   2421                modifiers: KeyModifiers::SHIFT,
   2422                ..
   2423            } => self.handle_normal_mode_key_event_toggle_guest_view(),
   2424            KeyEvent {
   2425                code: KeyCode::Char('P'),
   2426                modifiers: KeyModifiers::SHIFT,
   2427                ..
   2428            } => self.handle_normal_mode_key_event_toggle_pm_only(),
   2429            KeyEvent {
   2430                code: KeyCode::Char('V'),
   2431                modifiers: KeyModifiers::SHIFT,
   2432                ..
   2433            } => self.handle_normal_mode_key_event_toggle_v_view(),
   2434            KeyEvent {
   2435                code: KeyCode::Char('C'),
   2436                modifiers: KeyModifiers::SHIFT,
   2437                ..
   2438            } => self.handle_normal_mode_key_event_shift_c(app),
   2439            KeyEvent {
   2440                code: KeyCode::Char('O'),
   2441                modifiers: KeyModifiers::SHIFT,
   2442                ..
   2443            } => self.handle_normal_mode_key_event_shift_o(app),
   2444            KeyEvent {
   2445                code: KeyCode::Char('H'),
   2446                modifiers: KeyModifiers::SHIFT,
   2447                ..
   2448            } => self.handle_normal_mode_key_event_toggle_hidden(),
   2449            KeyEvent {
   2450                code: KeyCode::Char('i'),
   2451                modifiers: KeyModifiers::NONE,
   2452                ..
   2453            } => self.handle_normal_mode_key_event_input_mode(app),
   2454            KeyEvent {
   2455                code: KeyCode::Char('Q'),
   2456                modifiers: KeyModifiers::SHIFT,
   2457                ..
   2458            } => self.handle_normal_mode_key_event_logout()?,
   2459            KeyEvent {
   2460                code: KeyCode::Char('q'),
   2461                modifiers: KeyModifiers::NONE,
   2462                ..
   2463            } => self.handle_normal_mode_key_event_exit()?,
   2464            KeyEvent {
   2465                code: KeyCode::Char('t'),
   2466                modifiers: KeyModifiers::NONE,
   2467                ..
   2468            } => self.handle_normal_mode_key_event_tag(app),
   2469            KeyEvent {
   2470                code: KeyCode::Char('p'),
   2471                modifiers: KeyModifiers::NONE,
   2472                ..
   2473            } => self.handle_normal_mode_key_event_pm(app),
   2474            KeyEvent {
   2475                code: KeyCode::Char('a'),
   2476                modifiers: KeyModifiers::CONTROL,
   2477                ..
   2478            } => self.handle_normal_mode_key_event_member_pm(app),
   2479            KeyEvent {
   2480                code: KeyCode::Char('k'),
   2481                modifiers: KeyModifiers::CONTROL,
   2482                ..
   2483            } => self.handle_normal_mode_key_event_kick(app),
   2484            KeyEvent {
   2485                code: KeyCode::Char('b'),
   2486                modifiers: KeyModifiers::CONTROL,
   2487                ..
   2488            } => self.handle_normal_mode_key_event_ban(app),
   2489            KeyEvent {
   2490                code: KeyCode::Char('B'),
   2491                modifiers,
   2492                ..
   2493            } if modifiers.contains(KeyModifiers::CONTROL) => {
   2494                self.handle_normal_mode_key_event_ban_exact(app)
   2495            }
   2496            KeyEvent {
   2497                code: KeyCode::Char('w'),
   2498                modifiers: KeyModifiers::CONTROL,
   2499                ..
   2500            } => self.handle_normal_mode_key_event_warn(app),
   2501            KeyEvent {
   2502                code: KeyCode::Char(' '),
   2503                modifiers: KeyModifiers::NONE,
   2504                ..
   2505            } => self.handle_normal_mode_key_event_space(app),
   2506            KeyEvent {
   2507                code: KeyCode::Char('x'),
   2508                modifiers: KeyModifiers::NONE,
   2509                ..
   2510            } => self.handle_normal_mode_key_event_delete(app, messages),
   2511            KeyEvent {
   2512                code: KeyCode::Char('T'),
   2513                modifiers: KeyModifiers::SHIFT,
   2514                ..
   2515            } => {
   2516                self.handle_normal_mode_key_event_translate(app, messages);
   2517            }
   2518            KeyEvent {
   2519                code: KeyCode::Char('N'),
   2520                modifiers: KeyModifiers::SHIFT,
   2521                ..
   2522            } => {
   2523                app.enter_notes_mode(self);
   2524            }
   2525            KeyEvent {
   2526                code: KeyCode::Char('u'),
   2527                modifiers: KeyModifiers::CONTROL,
   2528                ..
   2529            }
   2530            | KeyEvent {
   2531                code: KeyCode::PageUp,
   2532                modifiers: KeyModifiers::NONE,
   2533                ..
   2534            } => self.handle_normal_mode_key_event_page_up(app),
   2535            KeyEvent {
   2536                code: KeyCode::Char('d'),
   2537                modifiers: KeyModifiers::CONTROL,
   2538                ..
   2539            }
   2540            | KeyEvent {
   2541                code: KeyCode::PageDown,
   2542                modifiers: KeyModifiers::NONE,
   2543                ..
   2544            } => self.handle_normal_mode_key_event_page_down(app),
   2545            KeyEvent {
   2546                code: KeyCode::Esc,
   2547                modifiers: KeyModifiers::NONE,
   2548                ..
   2549            } => self.handle_normal_mode_key_event_esc(app),
   2550            KeyEvent {
   2551                code: KeyCode::Char('u'),
   2552                modifiers: KeyModifiers::SHIFT,
   2553                ..
   2554            } => self.handle_normal_mode_key_event_shift_u(app),
   2555            KeyEvent {
   2556                code: KeyCode::Char('g'),
   2557                modifiers: KeyModifiers::NONE,
   2558                ..
   2559            } => self.handle_normal_mode_key_event_g(app),
   2560            _ => {}
   2561        }
   2562        self.last_key_event = Some(key_event.code);
   2563        Ok(())
   2564    }
   2565 
   2566    fn handle_editing_mode_key_event(
   2567        &mut self,
   2568        app: &mut App,
   2569        key_event: KeyEvent,
   2570        users: &Arc<Mutex<Users>>,
   2571    ) -> Result<(), ExitSignal> {
   2572        app.input_mode = InputMode::Editing;
   2573        match key_event {
   2574            KeyEvent {
   2575                code: KeyCode::Enter,
   2576                modifiers,
   2577                ..
   2578            } if modifiers.contains(KeyModifiers::SHIFT)
   2579                || modifiers.contains(KeyModifiers::CONTROL) =>
   2580            {
   2581                self.handle_editing_mode_key_event_newline(app)
   2582            }
   2583            KeyEvent {
   2584                code: KeyCode::Enter,
   2585                modifiers: KeyModifiers::NONE,
   2586                ..
   2587            } => self.handle_editing_mode_key_event_enter(app, users)?,
   2588            KeyEvent {
   2589                code: KeyCode::Tab,
   2590                modifiers: KeyModifiers::NONE,
   2591                ..
   2592            } => self.handle_editing_mode_key_event_tab(app, users),
   2593            KeyEvent {
   2594                code: KeyCode::Char('c'),
   2595                modifiers: KeyModifiers::CONTROL,
   2596                ..
   2597            } => self.handle_editing_mode_key_event_ctrl_c(app),
   2598            KeyEvent {
   2599                code: KeyCode::Char('a'),
   2600                modifiers: KeyModifiers::CONTROL,
   2601                ..
   2602            } => self.handle_editing_mode_key_event_ctrl_a(app),
   2603            KeyEvent {
   2604                code: KeyCode::Char('e'),
   2605                modifiers: KeyModifiers::CONTROL,
   2606                ..
   2607            } => self.handle_editing_mode_key_event_ctrl_e(app),
   2608            KeyEvent {
   2609                code: KeyCode::Char('f'),
   2610                modifiers: KeyModifiers::CONTROL,
   2611                ..
   2612            } => self.handle_editing_mode_key_event_ctrl_f(app),
   2613            KeyEvent {
   2614                code: KeyCode::Char('b'),
   2615                modifiers: KeyModifiers::CONTROL,
   2616                ..
   2617            } => self.handle_editing_mode_key_event_ctrl_b(app),
   2618            KeyEvent {
   2619                code: KeyCode::Char('v'),
   2620                modifiers: KeyModifiers::CONTROL,
   2621                ..
   2622            } => self.handle_editing_mode_key_event_ctrl_v(app),
   2623            KeyEvent {
   2624                code: KeyCode::Char('x'),
   2625                modifiers: KeyModifiers::CONTROL,
   2626                ..
   2627            } => {
   2628                app.enter_message_editor_mode();
   2629            }
   2630            KeyEvent {
   2631                code: KeyCode::Char('l'),
   2632                modifiers: KeyModifiers::CONTROL,
   2633                ..
   2634            } => self.handle_editing_mode_key_event_toggle_multiline(app),
   2635            KeyEvent {
   2636                code: KeyCode::Left,
   2637                modifiers: KeyModifiers::NONE,
   2638                ..
   2639            } => self.handle_editing_mode_key_event_left(app),
   2640            KeyEvent {
   2641                code: KeyCode::Right,
   2642                modifiers: KeyModifiers::NONE,
   2643                ..
   2644            } => self.handle_editing_mode_key_event_right(app),
   2645            KeyEvent {
   2646                code: KeyCode::Down,
   2647                modifiers: KeyModifiers::NONE,
   2648                ..
   2649            } => self.handle_editing_mode_key_event_down(app),
   2650            KeyEvent {
   2651                code: KeyCode::Up,
   2652                modifiers: KeyModifiers::NONE,
   2653                ..
   2654            } => self.handle_editing_mode_key_event_up(app),
   2655            KeyEvent {
   2656                code: KeyCode::Char(c),
   2657                modifiers: KeyModifiers::NONE,
   2658                ..
   2659            }
   2660            | KeyEvent {
   2661                code: KeyCode::Char(c),
   2662                modifiers: KeyModifiers::SHIFT,
   2663                ..
   2664            } => self.handle_editing_mode_key_event_shift_c(app, c),
   2665            KeyEvent {
   2666                code: KeyCode::Backspace,
   2667                modifiers: KeyModifiers::NONE,
   2668                ..
   2669            } => self.handle_editing_mode_key_event_backspace(app),
   2670            KeyEvent {
   2671                code: KeyCode::Delete,
   2672                modifiers: KeyModifiers::NONE,
   2673                ..
   2674            } => self.handle_editing_mode_key_event_delete(app),
   2675            KeyEvent {
   2676                code: KeyCode::Esc,
   2677                modifiers: KeyModifiers::NONE,
   2678                ..
   2679            } => self.handle_editing_mode_key_event_esc(app),
   2680            _ => {}
   2681        }
   2682        Ok(())
   2683    }
   2684 
   2685    fn handle_long_message_mode_key_event_esc(&mut self, app: &mut App) {
   2686        app.long_message = None;
   2687        app.input_mode = InputMode::Normal;
   2688    }
   2689 
   2690    fn handle_long_message_mode_key_event_ctrl_d(
   2691        &mut self,
   2692        app: &mut App,
   2693        messages: &Arc<Mutex<Vec<Message>>>,
   2694    ) {
   2695        if let Some(idx) = app.items.state.selected() {
   2696            if let Some(item) = app.items.items.get(idx) {
   2697                self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text()))
   2698                    .unwrap();
   2699                let mut messages = messages.lock().unwrap();
   2700                if let Some(pos) = messages
   2701                    .iter()
   2702                    .position(|m| m.date == item.date && m.text == item.text)
   2703                {
   2704                    messages[pos].hide = !messages[pos].hide;
   2705                }
   2706                app.long_message = None;
   2707                app.input_mode = InputMode::Normal;
   2708            }
   2709        }
   2710    }
   2711 
   2712    fn handle_normal_mode_key_event_up(&mut self, app: &mut App) {
   2713        if app.inbox_mode {
   2714            app.inbox_items.previous();
   2715        } else if app.clean_mode {
   2716            app.clean_items.previous();
   2717        } else {
   2718            app.items.previous();
   2719        }
   2720    }
   2721 
   2722    fn handle_normal_mode_key_event_down(&mut self, app: &mut App) {
   2723        if app.inbox_mode {
   2724            app.inbox_items.next();
   2725        } else if app.clean_mode {
   2726            app.clean_items.next();
   2727        } else {
   2728            app.items.next();
   2729        }
   2730    }
   2731 
   2732    fn handle_normal_mode_key_event_space(&mut self, app: &mut App) {
   2733        if app.inbox_mode {
   2734            // Toggle checkbox for selected inbox message
   2735            if let Some(idx) = app.inbox_items.state.selected() {
   2736                if let Some(message) = app.inbox_items.items.get_mut(idx) {
   2737                    message.selected = !message.selected;
   2738                }
   2739            }
   2740        } else if app.clean_mode {
   2741            // Toggle checkbox for selected clean message
   2742            if let Some(idx) = app.clean_items.state.selected() {
   2743                if let Some(message) = app.clean_items.items.get_mut(idx) {
   2744                    message.selected = !message.selected;
   2745                }
   2746            }
   2747        }
   2748    }
   2749 
   2750    fn handle_normal_mode_key_event_j(&mut self, app: &mut App, lines: usize) {
   2751        for _ in 0..lines {
   2752            if app.inbox_mode {
   2753                app.inbox_items.next();
   2754            } else if app.clean_mode {
   2755                app.clean_items.next();
   2756            } else {
   2757                app.items.next();
   2758            }
   2759        }
   2760    }
   2761 
   2762    fn handle_normal_mode_key_event_k(&mut self, app: &mut App, lines: usize) {
   2763        for _ in 0..lines {
   2764            if app.inbox_mode {
   2765                app.inbox_items.previous();
   2766            } else if app.clean_mode {
   2767                app.clean_items.previous();
   2768            } else {
   2769                app.items.previous();
   2770            }
   2771        }
   2772    }
   2773 
   2774    fn handle_normal_mode_key_event_slash(&mut self, app: &mut App) {
   2775        app.items.unselect();
   2776        app.input = "/".to_owned();
   2777        app.input_idx = app.input.width();
   2778        app.input_mode = InputMode::Editing;
   2779    }
   2780 
   2781    fn handle_normal_mode_key_event_enter(
   2782        &mut self,
   2783        app: &mut App,
   2784        messages: &Arc<Mutex<Vec<Message>>>,
   2785    ) {
   2786        if let Some(idx) = app.items.state.selected() {
   2787            if let Some(item) = app.items.items.get(idx) {
   2788                // If we have a filter, <enter> will "jump" to the message
   2789                if !app.filter.is_empty() {
   2790                    let idx = messages
   2791                        .lock()
   2792                        .unwrap()
   2793                        .iter()
   2794                        .enumerate()
   2795                        .find(|(_, e)| e.date == item.date)
   2796                        .map(|(i, _)| i);
   2797                    app.clear_filter();
   2798                    app.items.state.select(idx);
   2799                    return;
   2800                }
   2801                app.long_message = Some(item.clone());
   2802                app.long_message_scroll_offset = 0;
   2803                app.input_mode = InputMode::LongMessage;
   2804            }
   2805        }
   2806    }
   2807 
   2808    fn handle_normal_mode_key_event_backspace(
   2809        &mut self,
   2810        app: &mut App,
   2811        messages: &Arc<Mutex<Vec<Message>>>,
   2812    ) {
   2813        if let Some(idx) = app.items.state.selected() {
   2814            if let Some(item) = app.items.items.get(idx) {
   2815                let mut messages = messages.lock().unwrap();
   2816                if let Some(pos) = messages
   2817                    .iter()
   2818                    .position(|m| m.date == item.date && m.text == item.text)
   2819                {
   2820                    if item.deleted {
   2821                        messages.remove(pos);
   2822                    } else {
   2823                        messages[pos].hide = !messages[pos].hide;
   2824                    }
   2825                }
   2826            }
   2827        }
   2828    }
   2829 
   2830    fn handle_normal_mode_key_event_yank(&mut self, app: &mut App) {
   2831        if let Some(idx) = app.items.state.selected() {
   2832            if let Some(item) = app.items.items.get(idx) {
   2833                if let Some(upload_link) = &item.upload_link {
   2834                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   2835                    let mut out = format!("{}{}", self.config.url, upload_link);
   2836                    if let Some((_, _, msg, _)) = get_message(
   2837                        &item.text,
   2838                        &self.config.members_tag,
   2839                        &self.config.staffs_tag,
   2840                    ) {
   2841                        out = format!("{} {}", msg, out);
   2842                    }
   2843                    ctx.set_contents(out).unwrap();
   2844                } else if let Some((_, _, msg, _)) = get_message(
   2845                    &item.text,
   2846                    &self.config.members_tag,
   2847                    &self.config.staffs_tag,
   2848                ) {
   2849                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   2850                    ctx.set_contents(msg).unwrap();
   2851                }
   2852            }
   2853        }
   2854    }
   2855 
   2856    fn handle_normal_mode_key_event_yank_link(&mut self, app: &mut App) {
   2857        if let Some(idx) = app.items.state.selected() {
   2858            if let Some(item) = app.items.items.get(idx) {
   2859                if let Some(upload_link) = &item.upload_link {
   2860                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   2861                    let out = format!("{}{}", self.config.url, upload_link);
   2862                    ctx.set_contents(out).unwrap();
   2863                } else if let Some((_, _, msg, _)) = get_message(
   2864                    &item.text,
   2865                    &self.config.members_tag,
   2866                    &self.config.staffs_tag,
   2867                ) {
   2868                    let finder = LinkFinder::new();
   2869                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   2870                    if let Some(link) = links.get(0) {
   2871                        let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   2872                        ctx.set_contents(link.as_str().to_owned()).unwrap();
   2873                    }
   2874                }
   2875            }
   2876        }
   2877    }
   2878 
   2879    //Strange
   2880    fn handle_normal_mode_key_event_download_link(&mut self, app: &mut App) {
   2881        if let Some(idx) = app.items.state.selected() {
   2882            if let Some(item) = app.items.items.get(idx) {
   2883                if let Some(upload_link) = &item.upload_link {
   2884                    let url = format!("{}{}", self.config.url, upload_link);
   2885                    let _ = Command::new("curl")
   2886                        .args([
   2887                            "--socks5",
   2888                            "localhost:9050",
   2889                            "--socks5-hostname",
   2890                            "localhost:9050",
   2891                            &url,
   2892                        ])
   2893                        .arg("-o")
   2894                        .arg("download.img")
   2895                        .output()
   2896                        .expect("Failed to execute curl command");
   2897                } else if let Some((_, _, msg, _)) = get_message(
   2898                    &item.text,
   2899                    &self.config.members_tag,
   2900                    &self.config.staffs_tag,
   2901                ) {
   2902                    let finder = LinkFinder::new();
   2903                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   2904                    if let Some(link) = links.first() {
   2905                        let url = link.as_str();
   2906                        let _ = Command::new("curl")
   2907                            .args([
   2908                                "--socks5",
   2909                                "localhost:9050",
   2910                                "--socks5-hostname",
   2911                                "localhost:9050",
   2912                                url,
   2913                            ])
   2914                            .arg("-o")
   2915                            .arg("download.img")
   2916                            .output()
   2917                            .expect("Failed to execute curl command");
   2918                    }
   2919                }
   2920            }
   2921        }
   2922    }
   2923 
   2924    //strageEdit
   2925    fn handle_normal_mode_key_event_download_and_view(&mut self, app: &mut App) {
   2926        if let Some(idx) = app.items.state.selected() {
   2927            if let Some(item) = app.items.items.get(idx) {
   2928                if let Some(upload_link) = &item.upload_link {
   2929                    let url = format!("{}{}", self.config.url, upload_link);
   2930                    let _ = Command::new("curl")
   2931                        .args([
   2932                            "--socks5",
   2933                            "localhost:9050",
   2934                            "--socks5-hostname",
   2935                            "localhost:9050",
   2936                            &url,
   2937                        ])
   2938                        .arg("-o")
   2939                        .arg("download.img")
   2940                        .output()
   2941                        .expect("Failed to execute curl command");
   2942 
   2943                    let _ = Command::new("xdg-open")
   2944                        .arg("./download.img")
   2945                        .output()
   2946                        .expect("Failed to execute sxiv command");
   2947                } else if let Some((_, _, msg, _)) = get_message(
   2948                    &item.text,
   2949                    &self.config.members_tag,
   2950                    &self.config.staffs_tag,
   2951                ) {
   2952                    let finder = LinkFinder::new();
   2953                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   2954                    if let Some(link) = links.first() {
   2955                        let url = link.as_str();
   2956                        let _ = Command::new("curl")
   2957                            .args([
   2958                                "--socks5",
   2959                                "localhost:9050",
   2960                                "--socks5-hostname",
   2961                                "localhost:9050",
   2962                                url,
   2963                            ])
   2964                            .arg("-o")
   2965                            .arg("download.img")
   2966                            .output()
   2967                            .expect("Failed to execute curl command");
   2968 
   2969                        let _ = Command::new("sxiv")
   2970                            .arg("./download.img")
   2971                            .output()
   2972                            .expect("Failed to execute sxiv command");
   2973                    }
   2974                }
   2975            }
   2976        }
   2977    }
   2978 
   2979    fn handle_normal_mode_key_event_toggle_mute(&mut self) {
   2980        let mut is_muted = self.is_muted.lock().unwrap();
   2981        *is_muted = !*is_muted;
   2982    }
   2983 
   2984    fn handle_normal_mode_key_event_toggle_sys(&mut self) {
   2985        self.show_sys = !self.show_sys;
   2986    }
   2987 
   2988    fn handle_normal_mode_key_event_toggle_guest_view(&mut self) {
   2989        self.display_guest_view = !self.display_guest_view;
   2990    }
   2991 
   2992    fn handle_normal_mode_key_event_toggle_member_view(&mut self) {
   2993        self.display_member_view = !self.display_member_view;
   2994    }
   2995 
   2996    fn handle_normal_mode_key_event_toggle_pm_only(&mut self) {
   2997        self.display_pm_only = !self.display_pm_only;
   2998    }
   2999 
   3000    fn handle_normal_mode_key_event_toggle_v_view(&mut self) {
   3001        if self.account_manager.master_account.is_some() {
   3002            self.display_master_pm_view = !self.display_master_pm_view;
   3003        } else {
   3004            self.display_staff_view = !self.display_staff_view;
   3005        }
   3006    }
   3007 
   3008    fn handle_normal_mode_key_event_shift_c(&mut self, app: &mut App) {
   3009        if self.clean_mode {
   3010            self.clean_mode = false;
   3011            return;
   3012        }
   3013        if let Some(session) = &self.session {
   3014            match fetch_clean_messages(
   3015                &self.client,
   3016                &self.config.url,
   3017                &self.config.page_php,
   3018                session,
   3019            ) {
   3020                Ok(msgs) => {
   3021                    app.clean_items.items = msgs;
   3022                    app.clean_items.state.select(None);
   3023                    self.clean_mode = true;
   3024                }
   3025                Err(e) => log::error!("failed to load clean view: {}", e),
   3026            }
   3027        }
   3028    }
   3029 
   3030    fn handle_normal_mode_key_event_shift_o(&mut self, app: &mut App) {
   3031        if self.inbox_mode {
   3032            self.inbox_mode = false;
   3033            return;
   3034        }
   3035        if let Some(session) = &self.session {
   3036            match fetch_inbox_messages(&self.client, &self.config.url, session) {
   3037                Ok(msgs) => {
   3038                    app.inbox_items.items = msgs;
   3039                    app.inbox_items.state.select(None);
   3040                    self.inbox_mode = true;
   3041                }
   3042                Err(e) => log::error!("failed to load inbox view: {}", e),
   3043            }
   3044        }
   3045    }
   3046 
   3047    fn handle_normal_mode_key_event_g(&mut self, app: &mut App) {
   3048        // Handle "gg" key combination
   3049        if self.last_key_event == Some(KeyCode::Char('g')) {
   3050            app.items.select_top();
   3051            self.last_key_event = None;
   3052        }
   3053    }
   3054 
   3055    fn handle_normal_mode_key_event_toggle_hidden(&mut self) {
   3056        self.display_hidden_msgs = !self.display_hidden_msgs;
   3057    }
   3058 
   3059    fn handle_normal_mode_key_event_input_mode(&mut self, app: &mut App) {
   3060        app.input_mode = InputMode::Editing;
   3061        app.items.unselect();
   3062    }
   3063 
   3064    fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> {
   3065        self.logout().unwrap();
   3066        return Err(ExitSignal::Terminate);
   3067    }
   3068 
   3069    fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> {
   3070        return Err(ExitSignal::Terminate);
   3071    }
   3072 
   3073    fn handle_normal_mode_key_event_tag(&mut self, app: &mut App) {
   3074        if let Some(idx) = app.items.state.selected() {
   3075            let text = &app.items.items.get(idx).unwrap().text;
   3076            if let Some(username) = get_username(
   3077                &self.base_client.username,
   3078                &text,
   3079                &self.config.members_tag,
   3080                &self.config.staffs_tag,
   3081            ) {
   3082                let txt = text.text();
   3083                if let Some(master) = &self.account_manager.master_account {
   3084                    if let Some((cmd, original)) =
   3085                        parse_forwarded_username(&txt, &app.members_tag, &app.staffs_tag)
   3086                    {
   3087                        app.input = format!("/pm {} {} @{} ", master, cmd, original);
   3088                        app.input_idx = app.input.width();
   3089                        app.input_mode = InputMode::Editing;
   3090                        app.items.unselect();
   3091                        return;
   3092                    }
   3093                }
   3094 
   3095                if txt.starts_with(&app.staffs_tag) {
   3096                    app.input = format!("/s @{} ", username);
   3097                } else if txt.starts_with(&app.members_tag) {
   3098                    app.input = format!("/m @{} ", username);
   3099                } else {
   3100                    app.input = format!("@{} ", username);
   3101                }
   3102                app.input_idx = app.input.width();
   3103                app.input_mode = InputMode::Editing;
   3104                app.items.unselect();
   3105            }
   3106        }
   3107    }
   3108 
   3109    fn handle_normal_mode_key_event_pm(&mut self, app: &mut App) {
   3110        if let Some(idx) = app.items.state.selected() {
   3111            if let Some(username) = get_username(
   3112                &self.base_client.username,
   3113                &app.items.items.get(idx).unwrap().text,
   3114                &self.config.members_tag,
   3115                &self.config.staffs_tag,
   3116            ) {
   3117                app.input = format!("/pm {} ", username);
   3118                app.input_idx = app.input.width();
   3119                app.input_mode = InputMode::Editing;
   3120                app.items.unselect();
   3121            }
   3122        }
   3123    }
   3124 
   3125    fn handle_normal_mode_key_event_member_pm(&mut self, app: &mut App) {
   3126        if let Some(master) = &self.account_manager.master_account {
   3127            app.input = format!("/pm {} /m ", master);
   3128        } else {
   3129            app.input = "/m ".to_owned();
   3130        }
   3131        app.input_idx = app.input.width();
   3132        app.input_mode = InputMode::Editing;
   3133        app.items.unselect();
   3134    }
   3135 
   3136    fn handle_normal_mode_key_event_kick(&mut self, app: &mut App) {
   3137        if let Some(idx) = app.items.state.selected() {
   3138            if let Some(username) = get_username(
   3139                &self.base_client.username,
   3140                &app.items.items.get(idx).unwrap().text,
   3141                &self.config.members_tag,
   3142                &self.config.staffs_tag,
   3143            ) {
   3144                if let Some(master) = &self.account_manager.master_account {
   3145                    app.input = format!("/pm {} #kick {} ", master, username);
   3146                } else {
   3147                    app.input = format!("/kick {} ", username);
   3148                }
   3149                app.input_idx = app.input.width();
   3150                app.input_mode = InputMode::Editing;
   3151                app.items.unselect();
   3152            }
   3153        }
   3154    }
   3155 
   3156    fn handle_normal_mode_key_event_ban(&mut self, app: &mut App) {
   3157        if let Some(idx) = app.items.state.selected() {
   3158            if let Some(username) = get_username(
   3159                &self.base_client.username,
   3160                &app.items.items.get(idx).unwrap().text,
   3161                &self.config.members_tag,
   3162                &self.config.staffs_tag,
   3163            ) {
   3164                if let Some(master) = &self.account_manager.master_account {
   3165                    app.input = format!("/pm {} #ban {} ", master, username);
   3166                } else {
   3167                    app.input = format!("/ban {} ", username);
   3168                }
   3169                app.input_idx = app.input.width();
   3170                app.input_mode = InputMode::Editing;
   3171                app.items.unselect();
   3172            }
   3173        }
   3174    }
   3175 
   3176    fn handle_normal_mode_key_event_ban_exact(&mut self, app: &mut App) {
   3177        if let Some(idx) = app.items.state.selected() {
   3178            if let Some(username) = get_username(
   3179                &self.base_client.username,
   3180                &app.items.items.get(idx).unwrap().text,
   3181                &self.config.members_tag,
   3182                &self.config.staffs_tag,
   3183            ) {
   3184                app.input = format!(r#"/ban "{}" "#, username);
   3185                app.input_idx = app.input.width();
   3186                app.input_mode = InputMode::Editing;
   3187                app.items.unselect();
   3188            }
   3189        }
   3190    }
   3191 
   3192    //Strange
   3193    fn handle_normal_mode_key_event_translate(
   3194        &mut self,
   3195        app: &mut App,
   3196        messages: &Arc<Mutex<Vec<Message>>>,
   3197    ) {
   3198        log::error!("translate running");
   3199        if let Some(idx) = app.items.state.selected() {
   3200            log::error!("1353");
   3201            let mut message_lock = messages.lock().unwrap();
   3202            if let Some(message) = message_lock.get_mut(idx) {
   3203                log::error!("1356");
   3204                let original_text = &mut message.text;
   3205                let output = Command::new("trans")
   3206                    .arg("-b")
   3207                    .arg(&original_text.text())
   3208                    .output()
   3209                    .expect("Failed to execute translation command");
   3210 
   3211                if output.status.success() {
   3212                    if let Ok(new_text) = String::from_utf8(output.stdout) {
   3213                        *original_text = StyledText::Text(new_text.trim().to_owned());
   3214                        log::error!("Translation successful: {}", new_text);
   3215                    } else {
   3216                        log::error!("Failed to decode translation output as UTF-8");
   3217                    }
   3218                } else {
   3219                    log::error!("Translation command failed with error: {:?}", output.status);
   3220                }
   3221            }
   3222        }
   3223    }
   3224 
   3225    //Strange
   3226    fn handle_normal_mode_key_event_warn(&mut self, app: &mut App) {
   3227        if let Some(idx) = app.items.state.selected() {
   3228            if let Some(username) = get_username(
   3229                &self.base_client.username,
   3230                &app.items.items.get(idx).unwrap().text,
   3231                &self.config.members_tag,
   3232                &self.config.staffs_tag,
   3233            ) {
   3234                app.input = format!("!warn @{} ", username);
   3235                app.input_idx = app.input.width();
   3236                app.input_mode = InputMode::Editing;
   3237                app.items.unselect();
   3238            }
   3239        }
   3240    }
   3241 
   3242    fn handle_normal_mode_key_event_delete(
   3243        &mut self,
   3244        app: &mut App,
   3245        messages: &Arc<Mutex<Vec<Message>>>,
   3246    ) {
   3247        if app.inbox_mode {
   3248            // Handle deletion in inbox mode - delete all checked messages
   3249            let mut indices_to_remove = Vec::new();
   3250            let mut message_ids_to_delete = Vec::new();
   3251 
   3252            for (idx, message) in app.inbox_items.items.iter().enumerate() {
   3253                if message.selected {
   3254                    let message_id = message.id.clone();
   3255                    message_ids_to_delete.push(message_id);
   3256                    indices_to_remove.push(idx);
   3257                }
   3258            }
   3259 
   3260            // Remove messages from UI immediately
   3261            for &idx in indices_to_remove.iter().rev() {
   3262                app.inbox_items.items.remove(idx);
   3263            }
   3264 
   3265            // Adjust selection
   3266            if app.inbox_items.items.is_empty() {
   3267                app.inbox_items.state.select(None);
   3268            } else if let Some(selected) = app.inbox_items.state.selected() {
   3269                if selected >= app.inbox_items.items.len() {
   3270                    app.inbox_items
   3271                        .state
   3272                        .select(Some(app.inbox_items.items.len() - 1));
   3273                }
   3274            }
   3275 
   3276            // Send delete requests in background thread
   3277            if !message_ids_to_delete.is_empty() {
   3278                let client = self.client.clone();
   3279                let session = self.session.clone();
   3280                let url = self.config.url.clone();
   3281                thread::spawn(move || {
   3282                    if let Some(session) = session {
   3283                        for message_id in message_ids_to_delete {
   3284                            let delete_url = format!("{}?action=inbox&session={}", url, session);
   3285                            let form = reqwest::blocking::multipart::Form::new()
   3286                                .text("lang", "en")
   3287                                .text("action", "inbox")
   3288                                .text("session", session.clone())
   3289                                .text("do", "delete")
   3290                                .text("mid[]", message_id.clone());
   3291 
   3292                            if let Err(e) = client.post(&delete_url).multipart(form).send() {
   3293                                log::error!("Failed to delete inbox message {}: {}", message_id, e);
   3294                            }
   3295                        }
   3296                    }
   3297                });
   3298            }
   3299            return;
   3300        }
   3301 
   3302        if app.clean_mode {
   3303            // Handle deletion in clean mode - delete all checked messages
   3304            let mut indices_to_remove = Vec::new();
   3305            let mut message_ids_to_delete = Vec::new();
   3306 
   3307            for (idx, message) in app.clean_items.items.iter().enumerate() {
   3308                if message.selected {
   3309                    let message_id = message.id.clone();
   3310                    message_ids_to_delete.push(message_id);
   3311                    indices_to_remove.push(idx);
   3312                }
   3313            }
   3314 
   3315            // Remove messages from UI immediately
   3316            for &idx in indices_to_remove.iter().rev() {
   3317                app.clean_items.items.remove(idx);
   3318            }
   3319 
   3320            // Adjust selection
   3321            if app.clean_items.items.is_empty() {
   3322                app.clean_items.state.select(None);
   3323            } else if let Some(selected) = app.clean_items.state.selected() {
   3324                if selected >= app.clean_items.items.len() {
   3325                    app.clean_items
   3326                        .state
   3327                        .select(Some(app.clean_items.items.len() - 1));
   3328                }
   3329            }
   3330 
   3331            // Send delete requests in background thread
   3332            if !message_ids_to_delete.is_empty() {
   3333                let tx = self.tx.clone();
   3334                thread::spawn(move || {
   3335                    for message_id in message_ids_to_delete {
   3336                        let message_id_for_log = message_id.clone();
   3337                        if let Err(e) = tx.send(PostType::Delete(message_id)) {
   3338                            log::error!(
   3339                                "Failed to send delete request for message {}: {}",
   3340                                message_id_for_log,
   3341                                e
   3342                            );
   3343                        }
   3344                    }
   3345                });
   3346            }
   3347            return;
   3348        }
   3349 
   3350        // Regular message deletion
   3351        if let Some(idx) = app.items.state.selected() {
   3352            if let Some(id) = app.items.items.get(idx).and_then(|m| m.id) {
   3353                if self.clean_mode {
   3354                    self.post_msg(PostType::Delete(id.to_string())).unwrap();
   3355                    if let Ok(mut msgs) = messages.lock() {
   3356                        msgs.retain(|m| m.id != Some(id));
   3357                    }
   3358                    app.items.unselect();
   3359                } else {
   3360                    app.input = format!("/delete {}", id);
   3361                    app.input_idx = app.input.width();
   3362                    app.input_mode = InputMode::Editing;
   3363                    app.items.unselect();
   3364                }
   3365            }
   3366        }
   3367    }
   3368    fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) {
   3369        if let Some(idx) = app.items.state.selected() {
   3370            app.items.state.select(idx.checked_sub(10).or(Some(0)));
   3371        } else {
   3372            app.items.next();
   3373        }
   3374    }
   3375 
   3376    fn handle_normal_mode_key_event_page_down(&mut self, app: &mut App) {
   3377        if let Some(idx) = app.items.state.selected() {
   3378            let wanted_idx = idx + 10;
   3379            let max_idx = app.items.items.len() - 1;
   3380            let new_idx = std::cmp::min(wanted_idx, max_idx);
   3381            app.items.state.select(Some(new_idx));
   3382        } else {
   3383            app.items.next();
   3384        }
   3385    }
   3386 
   3387    fn handle_normal_mode_key_event_esc(&mut self, app: &mut App) {
   3388        app.items.unselect();
   3389    }
   3390 
   3391    fn handle_normal_mode_key_event_shift_u(&mut self, app: &mut App) {
   3392        app.items.state.select(Some(0));
   3393    }
   3394 
   3395    fn handle_editing_mode_key_event_enter(
   3396        &mut self,
   3397        app: &mut App,
   3398        users: &Arc<Mutex<Users>>,
   3399    ) -> Result<(), ExitSignal> {
   3400        if FIND_RGX.is_match(&app.input) {
   3401            return Ok(());
   3402        }
   3403 
   3404        let mut input: String = app.input.drain(..).collect();
   3405        input = replace_newline_escape(&input);
   3406        app.input_idx = 0;
   3407 
   3408        // Add to history if not empty
   3409        if !input.trim().is_empty() {
   3410            app.add_to_history(input.clone());
   3411        }
   3412 
   3413        // Iterate over commands and execute associated actions
   3414        for (command, action) in &app.commands.commands {
   3415            // log::error!("command :{} action :{}", command, action);
   3416            let expected_input = format!("!{}", command);
   3417            if input == expected_input {
   3418                // Execute the action by posting a message
   3419                self.post_msg(PostType::Post(action.clone(), None)).unwrap();
   3420                // Return Ok(()) if the action is executed successfully
   3421                return Ok(());
   3422            }
   3423        }
   3424 
   3425        let mut cmd_input = input.clone();
   3426        let mut members_prefix = false;
   3427        let mut staffs_prefix = false;
   3428        let mut pm_target: Option<String> = None;
   3429 
   3430        // Check for /pm prefix first
   3431        if let Some(captures) = PM_RGX.captures(&cmd_input) {
   3432            let username = captures[1].to_string();
   3433            let remaining = captures[2].to_string();
   3434            if remaining.starts_with('/') {
   3435                // This is a ChatOps command with PM target
   3436                pm_target = Some(username);
   3437                cmd_input = remaining;
   3438            }
   3439        } else if cmd_input.starts_with("/m ") {
   3440            members_prefix = true;
   3441            if remove_prefix(&cmd_input, "/m ").starts_with('/') {
   3442                cmd_input = remove_prefix(&cmd_input, "/m ").to_owned();
   3443            }
   3444        } else if cmd_input.starts_with("/s ") {
   3445            staffs_prefix = true;
   3446            if remove_prefix(&cmd_input, "/s ").starts_with('/') {
   3447                cmd_input = remove_prefix(&cmd_input, "/s ").to_owned();
   3448            }
   3449        }
   3450 
   3451        // Determine target for ChatOps commands
   3452        let chatops_target = if let Some(user) = pm_target.clone() {
   3453            Some(user)
   3454        } else if members_prefix {
   3455            Some(SEND_TO_MEMBERS.to_owned())
   3456        } else if staffs_prefix {
   3457            Some(SEND_TO_STAFFS.to_owned())
   3458        } else {
   3459            None
   3460        };
   3461 
   3462        if self.process_command_with_target(&cmd_input, app, users, chatops_target) {
   3463            if members_prefix {
   3464                app.input = "/m ".to_owned();
   3465                app.input_idx = app.input.width();
   3466            } else if staffs_prefix {
   3467                app.input = "/s ".to_owned();
   3468                app.input_idx = app.input.width();
   3469            } else if pm_target.is_some() {
   3470                // Don't reset input for PM - let user continue the conversation
   3471            }
   3472            return Ok(());
   3473        }
   3474 
   3475        if members_prefix {
   3476            let msg = remove_prefix(&input, "/m ").to_owned();
   3477            let to = Some(SEND_TO_MEMBERS.to_owned());
   3478            self.post_msg(PostType::Post(msg, to)).unwrap();
   3479            app.input = "/m ".to_owned();
   3480            app.input_idx = app.input.width();
   3481        } else if staffs_prefix {
   3482            let msg = remove_prefix(&input, "/s ").to_owned();
   3483            let to = Some(SEND_TO_STAFFS.to_owned());
   3484            self.post_msg(PostType::Post(msg, to)).unwrap();
   3485            app.input = "/s ".to_owned();
   3486            app.input_idx = app.input.width();
   3487        } else if let Some(user) = pm_target {
   3488            // Handle PM that wasn't a ChatOps command
   3489            let msg = if let Some(captures) = PM_RGX.captures(&input) {
   3490                captures[2].to_string()
   3491            } else {
   3492                input.clone()
   3493            };
   3494            let to = Some(user.clone());
   3495            self.post_msg(PostType::Post(msg, to)).unwrap();
   3496            app.input = format!("/pm {} ", user);
   3497            app.input_idx = app.input.width();
   3498        } else if input.starts_with("/a ") {
   3499            let msg = remove_prefix(&input, "/a ").to_owned();
   3500            let to = Some(SEND_TO_ADMINS.to_owned());
   3501            self.post_msg(PostType::Post(msg, to)).unwrap();
   3502            app.input = "/a ".to_owned();
   3503            app.input_idx = app.input.width();
   3504        } else if input.starts_with("/s ") {
   3505            let msg = remove_prefix(&input, "/s ").to_owned();
   3506            let to = Some(SEND_TO_STAFFS.to_owned());
   3507            self.post_msg(PostType::Post(msg, to)).unwrap();
   3508            app.input = "/s ".to_owned();
   3509            app.input_idx = app.input.width();
   3510        } else {
   3511            if input.starts_with("/") && !input.starts_with("/me ") {
   3512                app.input_idx = input.len();
   3513                app.input = input;
   3514                app.input_mode = InputMode::EditingErr;
   3515            } else {
   3516                self.post_msg(PostType::Post(input, None)).unwrap();
   3517                // Reset input mode to Normal after sending message
   3518                app.input_mode = InputMode::Normal;
   3519            }
   3520        }
   3521        Ok(())
   3522    }
   3523 
   3524    fn handle_editing_mode_key_event_tab(&mut self, app: &mut App, users: &Arc<Mutex<Users>>) {
   3525        let (p1, p2) = app.input.split_at(app.input_idx);
   3526        if p2 == "" || p2.chars().nth(0) == Some(' ') {
   3527            let mut parts: Vec<&str> = p1.split(" ").collect();
   3528            if let Some(user_prefix) = parts.pop() {
   3529                let mut should_autocomplete = false;
   3530                let mut prefix = "";
   3531                if parts.len() == 1
   3532                    && ((parts[0] == "/kick" || parts[0] == "/k")
   3533                        || parts[0] == "/pm"
   3534                        || parts[0] == "/ignore"
   3535                        || parts[0] == "/unignore"
   3536                        || parts[0] == "/ban")
   3537                {
   3538                    should_autocomplete = true;
   3539                } else if user_prefix.starts_with("@") {
   3540                    should_autocomplete = true;
   3541                    prefix = "@";
   3542                }
   3543                if should_autocomplete {
   3544                    let user_prefix_norm = remove_prefix(user_prefix, prefix);
   3545                    let user_prefix_norm_len = user_prefix_norm.len();
   3546                    if let Some(name) = autocomplete_username(users, user_prefix_norm) {
   3547                        let complete_name = format!("{}{}", prefix, name);
   3548                        parts.push(complete_name.as_str());
   3549                        let p2 = p2.trim_start();
   3550                        if p2 != "" {
   3551                            parts.push(p2);
   3552                        }
   3553                        app.input = parts.join(" ");
   3554                        app.input_idx += name.len() - user_prefix_norm_len;
   3555                    }
   3556                }
   3557            }
   3558        }
   3559    }
   3560 
   3561    fn handle_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) {
   3562        app.clear_filter();
   3563        app.input = "".to_owned();
   3564        app.input_idx = 0;
   3565        app.input_mode = InputMode::Normal;
   3566    }
   3567 
   3568    fn handle_editing_mode_key_event_ctrl_a(&mut self, app: &mut App) {
   3569        app.input_idx = 0;
   3570    }
   3571 
   3572    fn handle_editing_mode_key_event_ctrl_e(&mut self, app: &mut App) {
   3573        app.input_idx = app.input.width();
   3574    }
   3575 
   3576    fn handle_editing_mode_key_event_ctrl_f(&mut self, app: &mut App) {
   3577        if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') {
   3578            app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width());
   3579        } else {
   3580            app.input_idx = app.input.width();
   3581        }
   3582    }
   3583 
   3584    fn handle_editing_mode_key_event_ctrl_b(&mut self, app: &mut App) {
   3585        if let Some(idx) = app.input_idx.checked_sub(2) {
   3586            let tmp = app
   3587                .input
   3588                .chars()
   3589                .take(idx)
   3590                .collect::<String>()
   3591                .chars()
   3592                .rev()
   3593                .collect::<String>();
   3594            if let Some(idx) = tmp.chars().position(|c| c == ' ') {
   3595                app.input_idx = std::cmp::max(tmp.width() - idx, 0);
   3596            } else {
   3597                app.input_idx = 0;
   3598            }
   3599        }
   3600    }
   3601 
   3602    fn handle_editing_mode_key_event_ctrl_v(&mut self, app: &mut App) {
   3603        let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   3604        if let Ok(clipboard) = ctx.get_contents() {
   3605            let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   3606            app.input.insert_str(byte_position, &clipboard);
   3607            app.input_idx += clipboard.chars().count();
   3608        }
   3609    }
   3610 
   3611    fn handle_editing_mode_key_event_newline(&mut self, app: &mut App) {
   3612        let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   3613        app.input.insert(byte_position, '\n');
   3614        app.input_idx += 1;
   3615    }
   3616 
   3617    fn handle_editing_mode_key_event_left(&mut self, app: &mut App) {
   3618        if app.input_idx > 0 {
   3619            app.input_idx -= 1;
   3620        }
   3621    }
   3622 
   3623    fn handle_editing_mode_key_event_right(&mut self, app: &mut App) {
   3624        if app.input_idx < app.input.width() {
   3625            app.input_idx += 1;
   3626        }
   3627    }
   3628 
   3629    fn handle_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) {
   3630        app.reset_history_navigation();
   3631        let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   3632        app.input.insert(byte_position, c);
   3633 
   3634        app.input_idx += 1;
   3635        app.update_filter();
   3636    }
   3637 
   3638    fn handle_editing_mode_key_event_backspace(&mut self, app: &mut App) {
   3639        app.reset_history_navigation();
   3640        if app.input_idx > 0 {
   3641            app.input_idx -= 1;
   3642            app.input = remove_at(&app.input, app.input_idx);
   3643            app.update_filter();
   3644        }
   3645    }
   3646 
   3647    fn handle_editing_mode_key_event_delete(&mut self, app: &mut App) {
   3648        app.reset_history_navigation();
   3649        if app.input_idx > 0 && app.input_idx == app.input.width() {
   3650            app.input_idx -= 1;
   3651        }
   3652        app.input = remove_at(&app.input, app.input_idx);
   3653        app.update_filter();
   3654    }
   3655 
   3656    fn handle_editing_mode_key_event_esc(&mut self, app: &mut App) {
   3657        app.input_mode = InputMode::Normal;
   3658        app.reset_history_navigation();
   3659    }
   3660 
   3661    fn handle_editing_mode_key_event_up(&mut self, app: &mut App) {
   3662        // In multiline mode, handle cursor navigation first
   3663        if app.input_mode == InputMode::MultilineEditing {
   3664            let input = &app.input;
   3665            let lines: Vec<&str> = input.split('\n').collect();
   3666 
   3667            // Calculate which line the cursor is on
   3668            let mut current_pos = 0;
   3669            let mut cursor_line = 0;
   3670            let mut chars_in_line = 0;
   3671 
   3672            for (line_idx, line) in lines.iter().enumerate() {
   3673                let line_len = line.chars().count();
   3674                if current_pos + line_len >= app.input_idx {
   3675                    cursor_line = line_idx;
   3676                    chars_in_line = app.input_idx - current_pos;
   3677                    break;
   3678                }
   3679                current_pos += line_len + 1; // +1 for newline
   3680            }
   3681 
   3682            // Try to move cursor to previous line
   3683            if cursor_line > 0 {
   3684                let prev_line = lines[cursor_line - 1];
   3685                let prev_line_len = prev_line.chars().count();
   3686                let new_pos_in_line = chars_in_line.min(prev_line_len);
   3687 
   3688                // Calculate new cursor position
   3689                let mut new_cursor_pos = 0;
   3690                for i in 0..(cursor_line - 1) {
   3691                    new_cursor_pos += lines[i].chars().count();
   3692                    if i < cursor_line - 1 {
   3693                        new_cursor_pos += 1; // for newline
   3694                    }
   3695                }
   3696                if cursor_line > 1 {
   3697                    new_cursor_pos += 1; // for newline before previous line
   3698                }
   3699                new_cursor_pos += new_pos_in_line;
   3700 
   3701                app.input_idx = new_cursor_pos;
   3702            } else {
   3703                // At first line, try history navigation
   3704                app.navigate_history_up();
   3705            }
   3706        } else {
   3707            // Regular single-line mode, use history navigation
   3708            app.navigate_history_up();
   3709        }
   3710    }
   3711 
   3712    fn handle_editing_mode_key_event_down(&mut self, app: &mut App) {
   3713        app.navigate_history_down();
   3714    }
   3715 
   3716    fn handle_editing_mode_key_event_toggle_multiline(&mut self, app: &mut App) {
   3717        match app.input_mode {
   3718            InputMode::Editing | InputMode::EditingErr => {
   3719                app.input_mode = InputMode::MultilineEditing;
   3720            }
   3721            InputMode::MultilineEditing => {
   3722                app.input_mode = InputMode::Editing;
   3723            }
   3724            _ => {}
   3725        }
   3726    }
   3727 
   3728    fn handle_multiline_editing_mode_key_event(
   3729        &mut self,
   3730        app: &mut App,
   3731        key_event: KeyEvent,
   3732        users: &Arc<Mutex<Users>>,
   3733    ) -> Result<(), ExitSignal> {
   3734        match key_event {
   3735            // Send message on Ctrl+Enter in multiline mode
   3736            KeyEvent {
   3737                code: KeyCode::Enter,
   3738                modifiers: KeyModifiers::CONTROL,
   3739                ..
   3740            } => self.handle_multiline_editing_mode_key_event_send(app, users)?,
   3741            // Add newline on Enter in multiline mode
   3742            KeyEvent {
   3743                code: KeyCode::Enter,
   3744                modifiers: KeyModifiers::NONE,
   3745                ..
   3746            } => self.handle_editing_mode_key_event_newline(app),
   3747            // Send message with Ctrl+L in multiline mode
   3748            KeyEvent {
   3749                code: KeyCode::Char('l'),
   3750                modifiers: KeyModifiers::CONTROL,
   3751                ..
   3752            } => self.handle_multiline_editing_mode_key_event_ctrl_l(app, users)?,
   3753            // History navigation
   3754            KeyEvent {
   3755                code: KeyCode::Up,
   3756                modifiers: KeyModifiers::NONE,
   3757                ..
   3758            } => self.handle_editing_mode_key_event_up(app),
   3759            KeyEvent {
   3760                code: KeyCode::Down,
   3761                modifiers: KeyModifiers::NONE,
   3762                ..
   3763            } => self.handle_multiline_editing_mode_key_event_down(app),
   3764            // All other editing keys work the same
   3765            KeyEvent {
   3766                code: KeyCode::Tab,
   3767                modifiers: KeyModifiers::NONE,
   3768                ..
   3769            } => self.handle_editing_mode_key_event_tab(app, users),
   3770            KeyEvent {
   3771                code: KeyCode::Char('c'),
   3772                modifiers: KeyModifiers::CONTROL,
   3773                ..
   3774            } => self.handle_multiline_editing_mode_key_event_ctrl_c(app),
   3775            KeyEvent {
   3776                code: KeyCode::Char('a'),
   3777                modifiers: KeyModifiers::CONTROL,
   3778                ..
   3779            } => self.handle_editing_mode_key_event_ctrl_a(app),
   3780            KeyEvent {
   3781                code: KeyCode::Char('e'),
   3782                modifiers: KeyModifiers::CONTROL,
   3783                ..
   3784            } => self.handle_editing_mode_key_event_ctrl_e(app),
   3785            KeyEvent {
   3786                code: KeyCode::Char('f'),
   3787                modifiers: KeyModifiers::CONTROL,
   3788                ..
   3789            } => self.handle_editing_mode_key_event_ctrl_f(app),
   3790            KeyEvent {
   3791                code: KeyCode::Char('b'),
   3792                modifiers: KeyModifiers::CONTROL,
   3793                ..
   3794            } => self.handle_editing_mode_key_event_ctrl_b(app),
   3795            KeyEvent {
   3796                code: KeyCode::Char('v'),
   3797                modifiers: KeyModifiers::CONTROL,
   3798                ..
   3799            } => self.handle_editing_mode_key_event_ctrl_v(app),
   3800            KeyEvent {
   3801                code: KeyCode::Char('x'),
   3802                modifiers: KeyModifiers::CONTROL,
   3803                ..
   3804            } => {
   3805                app.enter_message_editor_mode();
   3806            }
   3807            KeyEvent {
   3808                code: KeyCode::Left,
   3809                modifiers: KeyModifiers::NONE,
   3810                ..
   3811            } => self.handle_editing_mode_key_event_left(app),
   3812            KeyEvent {
   3813                code: KeyCode::Right,
   3814                modifiers: KeyModifiers::NONE,
   3815                ..
   3816            } => self.handle_editing_mode_key_event_right(app),
   3817            KeyEvent {
   3818                code: KeyCode::Char(c),
   3819                modifiers: KeyModifiers::NONE,
   3820                ..
   3821            }
   3822            | KeyEvent {
   3823                code: KeyCode::Char(c),
   3824                modifiers: KeyModifiers::SHIFT,
   3825                ..
   3826            } => self.handle_multiline_editing_mode_key_event_shift_c(app, c),
   3827            KeyEvent {
   3828                code: KeyCode::Backspace,
   3829                modifiers: KeyModifiers::NONE,
   3830                ..
   3831            } => self.handle_editing_mode_key_event_backspace(app),
   3832            KeyEvent {
   3833                code: KeyCode::Delete,
   3834                modifiers: KeyModifiers::NONE,
   3835                ..
   3836            } => self.handle_editing_mode_key_event_delete(app),
   3837            KeyEvent {
   3838                code: KeyCode::Esc,
   3839                modifiers: KeyModifiers::NONE,
   3840                ..
   3841            } => self.handle_multiline_editing_mode_key_event_esc(app),
   3842            _ => {}
   3843        }
   3844        Ok(())
   3845    }
   3846 
   3847    fn handle_multiline_editing_mode_key_event_send(
   3848        &mut self,
   3849        app: &mut App,
   3850        users: &Arc<Mutex<Users>>,
   3851    ) -> Result<(), ExitSignal> {
   3852        // Same logic as regular enter, but add to history first
   3853        if !app.input.trim().is_empty() {
   3854            app.add_to_history(app.input.clone());
   3855        }
   3856        self.handle_editing_mode_key_event_enter(app, users)
   3857    }
   3858 
   3859    fn handle_multiline_editing_mode_key_event_ctrl_l(
   3860        &mut self,
   3861        app: &mut App,
   3862        users: &Arc<Mutex<Users>>,
   3863    ) -> Result<(), ExitSignal> {
   3864        // In multiline mode, Ctrl+L sends the message (like Ctrl+Enter)
   3865        self.handle_multiline_editing_mode_key_event_send(app, users)
   3866    }
   3867 
   3868    fn handle_multiline_editing_mode_key_event_down(&mut self, app: &mut App) {
   3869        // Handle cursor navigation in multiline content
   3870        let input = &app.input;
   3871        let lines: Vec<&str> = input.split('\n').collect();
   3872 
   3873        // Calculate which line the cursor is on
   3874        let mut current_pos = 0;
   3875        let mut cursor_line = 0;
   3876        let mut chars_in_line = 0;
   3877 
   3878        for (line_idx, line) in lines.iter().enumerate() {
   3879            let line_len = line.chars().count();
   3880            if current_pos + line_len >= app.input_idx {
   3881                cursor_line = line_idx;
   3882                chars_in_line = app.input_idx - current_pos;
   3883                break;
   3884            }
   3885            current_pos += line_len + 1; // +1 for newline
   3886        }
   3887 
   3888        // Try to move cursor to next line
   3889        if cursor_line + 1 < lines.len() {
   3890            let next_line = lines[cursor_line + 1];
   3891            let next_line_len = next_line.chars().count();
   3892            let new_pos_in_line = chars_in_line.min(next_line_len);
   3893 
   3894            // Calculate new cursor position
   3895            let mut new_cursor_pos = 0;
   3896            for i in 0..=cursor_line {
   3897                new_cursor_pos += lines[i].chars().count();
   3898                if i < cursor_line {
   3899                    new_cursor_pos += 1; // for newline
   3900                }
   3901            }
   3902            new_cursor_pos += 1; // for the newline between current and next line
   3903            new_cursor_pos += new_pos_in_line;
   3904 
   3905            app.input_idx = new_cursor_pos.min(input.chars().count());
   3906        } else {
   3907            // At last line, try history navigation
   3908            app.navigate_history_down();
   3909        }
   3910    }
   3911 
   3912    fn handle_multiline_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) {
   3913        app.reset_history_navigation();
   3914        self.handle_editing_mode_key_event_shift_c(app, c);
   3915    }
   3916 
   3917    fn handle_multiline_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) {
   3918        app.reset_history_navigation();
   3919        app.input_mode = InputMode::Normal;
   3920        app.clear_filter();
   3921        app.input = "".to_owned();
   3922        app.input_idx = 0;
   3923    }
   3924 
   3925    fn handle_multiline_editing_mode_key_event_esc(&mut self, app: &mut App) {
   3926        app.input_mode = InputMode::Normal;
   3927        app.reset_history_navigation();
   3928    }
   3929 
   3930    fn handle_notes_mode_key_event(
   3931        &mut self,
   3932        app: &mut App,
   3933        key_event: KeyEvent,
   3934    ) -> Result<(), ExitSignal> {
   3935        use crossterm::event::{KeyCode, KeyModifiers};
   3936 
   3937        match key_event {
   3938            KeyEvent {
   3939                code: KeyCode::Tab,
   3940                modifiers: KeyModifiers::NONE,
   3941                ..
   3942            } => {
   3943                app.cycle_notes_type(self);
   3944                Ok(())
   3945            }
   3946            KeyEvent {
   3947                code: KeyCode::Char(c),
   3948                modifiers: KeyModifiers::NONE,
   3949                ..
   3950            } => {
   3951                if app.handle_notes_vim_key(c, self) {
   3952                    Ok(())
   3953                } else {
   3954                    Ok(())
   3955                }
   3956            }
   3957            KeyEvent {
   3958                code: KeyCode::Char(c),
   3959                modifiers: KeyModifiers::SHIFT,
   3960                ..
   3961            } => {
   3962                // Handle capital letters and shifted characters
   3963                if app.handle_notes_vim_key(c, self) {
   3964                    Ok(())
   3965                } else {
   3966                    Ok(())
   3967                }
   3968            }
   3969            KeyEvent {
   3970                code: KeyCode::Char('r'),
   3971                modifiers: KeyModifiers::CONTROL,
   3972                ..
   3973            } => {
   3974                // Ctrl+r - redo
   3975                app.notes_redo();
   3976                Ok(())
   3977            }
   3978            KeyEvent {
   3979                code: KeyCode::Backspace,
   3980                modifiers: KeyModifiers::NONE,
   3981                ..
   3982            } => {
   3983                app.handle_notes_vim_key('\x08', self);
   3984                Ok(())
   3985            }
   3986            KeyEvent {
   3987                code: KeyCode::Delete,
   3988                modifiers: KeyModifiers::NONE,
   3989                ..
   3990            } => {
   3991                app.handle_notes_vim_key('\x7f', self);
   3992                Ok(())
   3993            }
   3994            KeyEvent {
   3995                code: KeyCode::Enter,
   3996                modifiers: KeyModifiers::NONE,
   3997                ..
   3998            } => {
   3999                app.handle_notes_vim_key('\n', self);
   4000                Ok(())
   4001            }
   4002            KeyEvent {
   4003                code: KeyCode::Esc,
   4004                modifiers: KeyModifiers::NONE,
   4005                ..
   4006            } => {
   4007                app.handle_notes_vim_key('\x1b', self);
   4008                Ok(())
   4009            }
   4010            // Arrow keys for insert mode
   4011            KeyEvent {
   4012                code: KeyCode::Left,
   4013                modifiers: KeyModifiers::NONE,
   4014                ..
   4015            } => {
   4016                if app.notes_vim_mode == VimMode::Insert {
   4017                    if app.notes_cursor_pos.1 > 0 {
   4018                        app.notes_cursor_pos.1 -= 1;
   4019                    }
   4020                }
   4021                Ok(())
   4022            }
   4023            KeyEvent {
   4024                code: KeyCode::Right,
   4025                modifiers: KeyModifiers::NONE,
   4026                ..
   4027            } => {
   4028                if app.notes_vim_mode == VimMode::Insert {
   4029                    let line_len = app.notes_content[app.notes_cursor_pos.0].len();
   4030                    if app.notes_cursor_pos.1 < line_len {
   4031                        app.notes_cursor_pos.1 += 1;
   4032                    }
   4033                }
   4034                Ok(())
   4035            }
   4036            KeyEvent {
   4037                code: KeyCode::Up,
   4038                modifiers: KeyModifiers::NONE,
   4039                ..
   4040            } => {
   4041                if app.notes_vim_mode == VimMode::Insert && app.notes_cursor_pos.0 > 0 {
   4042                    app.notes_cursor_pos.0 -= 1;
   4043                    let line_len = app.notes_content[app.notes_cursor_pos.0].len();
   4044                    if app.notes_cursor_pos.1 > line_len {
   4045                        app.notes_cursor_pos.1 = line_len;
   4046                    }
   4047                }
   4048                Ok(())
   4049            }
   4050            KeyEvent {
   4051                code: KeyCode::Down,
   4052                modifiers: KeyModifiers::NONE,
   4053                ..
   4054            } => {
   4055                if app.notes_vim_mode == VimMode::Insert && app.notes_cursor_pos.0 < app.notes_content.len() - 1 {
   4056                    app.notes_cursor_pos.0 += 1;
   4057                    let line_len = app.notes_content[app.notes_cursor_pos.0].len();
   4058                    if app.notes_cursor_pos.1 > line_len {
   4059                        app.notes_cursor_pos.1 = line_len;
   4060                    }
   4061                }
   4062                Ok(())
   4063            }
   4064            _ => Ok(()),
   4065        }
   4066    }
   4067 
   4068    fn handle_mouse_event(
   4069        &mut self,
   4070        app: &mut App,
   4071        mouse_event: MouseEvent,
   4072    ) -> Result<(), ExitSignal> {
   4073        match mouse_event.kind {
   4074            MouseEventKind::ScrollDown => app.items.next(),
   4075            MouseEventKind::ScrollUp => app.items.previous(),
   4076            _ => {}
   4077        }
   4078        Ok(())
   4079    }
   4080 
   4081    // Notes functionality
   4082    fn fetch_notes(&self, note_type: &str) -> Result<(Vec<String>, Option<String>), Box<dyn std::error::Error>> {
   4083        let session = self.session.as_ref().ok_or("Not logged in")?;
   4084        let full_url = format!("{}/{}", self.config.url, self.config.page_php);
   4085        
   4086        let mut params = vec![
   4087            ("action", "notes"),
   4088            ("session", session),
   4089            ("lang", LANG),
   4090        ];
   4091 
   4092        if !note_type.is_empty() && note_type != "personal" {
   4093            params.push(("do", note_type));
   4094        }
   4095 
   4096        let response = self.client.post(&full_url).form(&params).send()?;
   4097        let body = response.text()?;
   4098 
   4099        // Parse HTML to extract textarea content and last edited info
   4100        let doc = select::document::Document::from(body.as_str());
   4101        
   4102        let content = if let Some(textarea) = doc.find(select::predicate::Name("textarea")).next() {
   4103            let content = textarea.text();
   4104            let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
   4105            if lines.is_empty() {
   4106                vec!["".to_string()]
   4107            } else {
   4108                lines
   4109            }
   4110        } else {
   4111            vec!["Access denied or no notes found".to_string()]
   4112        };
   4113 
   4114        // Extract last edited information from the paragraph before the form
   4115        let last_edited = doc
   4116            .find(select::predicate::Name("p"))
   4117            .filter_map(|node| {
   4118                let text = node.text();
   4119                // Look for text containing "Last edited by" pattern
   4120                if text.contains("Last edited by") || text.contains("at ") {
   4121                    Some(text.trim().to_string())
   4122                } else {
   4123                    None
   4124                }
   4125            })
   4126            .next();
   4127 
   4128        Ok((content, last_edited))
   4129    }
   4130 
   4131    fn save_notes(&self, note_type: &str, content: &[String]) -> Result<(), Box<dyn std::error::Error>> {
   4132        let session = self.session.as_ref().ok_or("Not logged in")?;
   4133        let full_url = format!("{}/{}", self.config.url, self.config.page_php);
   4134        
   4135        let text = content.join("\n");
   4136        let mut params = vec![
   4137            ("action", "notes"),
   4138            ("session", session),
   4139            ("lang", LANG),
   4140            ("text", text.as_str()),
   4141        ];
   4142 
   4143        if !note_type.is_empty() && note_type != "personal" {
   4144            params.push(("do", note_type));
   4145        }
   4146 
   4147        let response = self.client.post(&full_url).form(&params).send()?;
   4148        let body = response.text()?;
   4149 
   4150        // Check if save was successful
   4151        if body.contains("Notes saved!") || body.contains("saved") {
   4152            Ok(())
   4153        } else {
   4154            Err("Failed to save notes".into())
   4155        }
   4156    }
   4157 }
   4158 
   4159 // Give a char index, return the byte position
   4160 fn byte_pos(v: &str, idx: usize) -> Option<usize> {
   4161    let mut b = 0;
   4162    let mut chars = v.chars();
   4163    for _ in 0..idx {
   4164        if let Some(c) = chars.next() {
   4165            b += c.len_utf8();
   4166        } else {
   4167            return None;
   4168        }
   4169    }
   4170    Some(b)
   4171 }
   4172 
   4173 // Remove the character at idx (utf-8 aware)
   4174 fn remove_at(v: &str, idx: usize) -> String {
   4175    v.chars()
   4176        .enumerate()
   4177        .flat_map(|(i, c)| {
   4178            if i == idx {
   4179                return None;
   4180            }
   4181            Some(c)
   4182        })
   4183        .collect::<String>()
   4184 }
   4185 
   4186 // Autocomplete any username
   4187 fn autocomplete_username(users: &Arc<Mutex<Users>>, prefix: &str) -> Option<String> {
   4188    let users = users.lock().unwrap();
   4189    let all_users = users.all();
   4190    let prefix_lower = prefix.to_lowercase();
   4191    let filtered = all_users
   4192        .iter()
   4193        .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower));
   4194    Some(filtered?.1.to_owned())
   4195 }
   4196 
   4197 fn set_profile_base_info(
   4198    client: &Client,
   4199    full_url: &str,
   4200    params: &mut Vec<(&str, String)>,
   4201 ) -> anyhow::Result<()> {
   4202    params.extend(vec![("action", "profile".to_owned())]);
   4203    let profile_resp = client.post(full_url).form(&params).send()?;
   4204    let profile_resp_txt = profile_resp.text().unwrap();
   4205    let doc = Document::from(profile_resp_txt.as_str());
   4206    let bold = doc.find(Attr("id", "bold")).next().unwrap();
   4207    let italic = doc.find(Attr("id", "italic")).next().unwrap();
   4208    let small = doc.find(Attr("id", "small")).next().unwrap();
   4209    if bold.attr("checked").is_some() {
   4210        params.push(("bold", "on".to_owned()));
   4211    }
   4212    if italic.attr("checked").is_some() {
   4213        params.push(("italic", "on".to_owned()));
   4214    }
   4215    if small.attr("checked").is_some() {
   4216        params.push(("small", "on".to_owned()));
   4217    }
   4218    let font_select = doc.find(Attr("name", "font")).next().unwrap();
   4219    let font = font_select.find(Name("option")).find_map(|el| {
   4220        if el.attr("selected").is_some() {
   4221            return Some(el.attr("value").unwrap());
   4222        }
   4223        None
   4224    });
   4225    params.push(("font", font.unwrap_or("").to_owned()));
   4226    Ok(())
   4227 }
   4228 
   4229 enum RetryErr {
   4230    Retry,
   4231    Exit,
   4232 }
   4233 
   4234 fn retry_fn<F>(mut clb: F)
   4235 where
   4236    F: FnMut() -> anyhow::Result<RetryErr>,
   4237 {
   4238    loop {
   4239        match clb() {
   4240            Ok(RetryErr::Retry) => continue,
   4241            Ok(RetryErr::Exit) => return,
   4242            Err(err) => {
   4243                log::error!("{}", err);
   4244                continue;
   4245            }
   4246        }
   4247    }
   4248 }
   4249 
   4250 fn post_msg(
   4251    client: &Client,
   4252    post_type_recv: PostType,
   4253    full_url: &str,
   4254    session: String,
   4255    url: &str,
   4256    last_post_tx: &crossbeam_channel::Sender<()>,
   4257 ) {
   4258    let mut should_reset_keepalive_timer = false;
   4259    let mut delete_after = false;
   4260    retry_fn(|| -> anyhow::Result<RetryErr> {
   4261        let post_type = post_type_recv.clone();
   4262        let resp_text = client.get(url).send()?.text()?;
   4263        let doc = Document::from(resp_text.as_str());
   4264        let nc = doc
   4265            .find(Attr("name", "nc"))
   4266            .next()
   4267            .context("nc not found")?;
   4268        let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   4269        let postid = doc
   4270            .find(Attr("name", "postid"))
   4271            .next()
   4272            .context("failed to get postid")?;
   4273        let postid_value = postid
   4274            .attr("value")
   4275            .context("failed to get postid value")?
   4276            .to_owned();
   4277        let mut params: Vec<(&str, String)> = vec![
   4278            ("lang", LANG.to_owned()),
   4279            ("nc", nc_value.to_owned()),
   4280            ("session", session.clone()),
   4281        ];
   4282 
   4283        if let PostType::Clean(date, text) = post_type {
   4284            if let Err(e) = delete_message(&client, full_url, &mut params, date, text) {
   4285                log::error!("failed to delete message: {:?}", e);
   4286                return Ok(RetryErr::Retry);
   4287            }
   4288            return Ok(RetryErr::Exit);
   4289        }
   4290 
   4291        let mut req = client.post(full_url);
   4292        let mut form: Option<multipart::Form> = None;
   4293 
   4294        match post_type {
   4295            PostType::Post(msg, send_to) => {
   4296                should_reset_keepalive_timer = true;
   4297                params.extend(vec![
   4298                    ("action", "post".to_owned()),
   4299                    ("postid", postid_value.to_owned()),
   4300                    ("multi", "on".to_owned()),
   4301                    ("message", msg),
   4302                    ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())),
   4303                ]);
   4304            }
   4305            PostType::PM(to, msg) => {
   4306                should_reset_keepalive_timer = true;
   4307                params.extend(vec![
   4308                    ("action", "post".to_owned()),
   4309                    ("postid", postid_value.to_owned()),
   4310                    ("multi", "on".to_owned()),
   4311                    ("message", format!("/pm {} {}", to, msg)),
   4312                    ("sendto", SEND_TO_ALL.to_owned()),
   4313                ]);
   4314            }
   4315            PostType::KeepAlive(send_to) => {
   4316                should_reset_keepalive_timer = true;
   4317                delete_after = true;
   4318                params.extend(vec![
   4319                    ("action", "post".to_owned()),
   4320                    ("postid", postid_value.to_owned()),
   4321                    ("multi", "on".to_owned()),
   4322                    ("message", "keep alive".to_owned()),
   4323                    ("sendto", send_to),
   4324                ]);
   4325            }
   4326            PostType::NewNickname(new_nickname) => {
   4327                set_profile_base_info(client, full_url, &mut params)?;
   4328                params.extend(vec![
   4329                    ("do", "save".to_owned()),
   4330                    ("timestamps", "on".to_owned()),
   4331                    ("newnickname", new_nickname),
   4332                ]);
   4333            }
   4334            PostType::NewColor(new_color) => {
   4335                set_profile_base_info(client, full_url, &mut params)?;
   4336                params.extend(vec![
   4337                    ("do", "save".to_owned()),
   4338                    ("timestamps", "on".to_owned()),
   4339                    ("colour", new_color),
   4340                ]);
   4341            }
   4342            PostType::Ignore(username) => {
   4343                set_profile_base_info(client, full_url, &mut params)?;
   4344                params.extend(vec![
   4345                    ("do", "save".to_owned()),
   4346                    ("timestamps", "on".to_owned()),
   4347                    ("ignore", username),
   4348                ]);
   4349            }
   4350            PostType::Unignore(username) => {
   4351                set_profile_base_info(client, full_url, &mut params)?;
   4352                params.extend(vec![
   4353                    ("do", "save".to_owned()),
   4354                    ("timestamps", "on".to_owned()),
   4355                    ("unignore", username),
   4356                ]);
   4357            }
   4358            PostType::Profile(new_color, new_nickname, incognito_on, bold, italic) => {
   4359                set_profile_base_info(client, full_url, &mut params)?;
   4360                params.extend(vec![
   4361                    ("do", "save".to_owned()),
   4362                    ("timestamps", "on".to_owned()),
   4363                    ("colour", new_color),
   4364                    ("newnickname", new_nickname),
   4365                    (
   4366                        "incognito",
   4367                        if incognito_on { "on" } else { "off" }.to_owned(),
   4368                    ),
   4369                    ("bold", if bold { "on" } else { "off" }.to_owned()),
   4370                    ("italic", if italic { "on" } else { "off" }.to_owned()),
   4371                ]);
   4372            }
   4373            PostType::SetIncognito(incognito_on) => {
   4374                set_profile_base_info(client, full_url, &mut params)?;
   4375                params.extend(vec![
   4376                    ("do", "save".to_owned()),
   4377                    ("timestamps", "on".to_owned()),
   4378                ]);
   4379                if incognito_on {
   4380                    params.push(("incognito", "on".to_owned()));
   4381                } else {
   4382                    params.push(("incognito", "off".to_owned()));
   4383                }
   4384            }
   4385            PostType::Kick(msg, send_to) => {
   4386                params.extend(vec![
   4387                    ("action", "post".to_owned()),
   4388                    ("postid", postid_value.to_owned()),
   4389                    ("message", msg),
   4390                    ("sendto", send_to),
   4391                    ("kick", "kick".to_owned()),
   4392                    ("what", "purge".to_owned()),
   4393                ]);
   4394            }
   4395            PostType::DeleteLast | PostType::DeleteAll => {
   4396                params.extend(vec![("action", "delete".to_owned())]);
   4397                if let PostType::DeleteAll = post_type {
   4398                    params.extend(vec![
   4399                        ("sendto", SEND_TO_ALL.to_owned()),
   4400                        ("confirm", "yes".to_owned()),
   4401                        ("what", "all".to_owned()),
   4402                    ]);
   4403                } else {
   4404                    params.extend(vec![("sendto", "".to_owned()), ("what", "last".to_owned())]);
   4405                }
   4406            }
   4407            PostType::Delete(msg) => {
   4408                params.extend(vec![
   4409                    ("action", "admin".to_owned()),
   4410                    ("do", "clean".to_owned()),
   4411                    ("what", "selected".to_owned()),
   4412                    ("mid[]", msg.to_owned()),
   4413                    ("sendto", SEND_TO_ALL.to_owned()),
   4414                ]);
   4415            }
   4416            PostType::Upload(file_path, send_to, msg) => {
   4417                form = Some(
   4418                    match multipart::Form::new()
   4419                        .text("lang", LANG.to_owned())
   4420                        .text("nc", nc_value.to_owned())
   4421                        .text("session", session.clone())
   4422                        .text("action", "post".to_owned())
   4423                        .text("postid", postid_value.to_owned())
   4424                        .text("message", msg)
   4425                        .text("sendto", send_to.to_owned())
   4426                        .text("what", "purge".to_owned())
   4427                        .file("file", file_path)
   4428                    {
   4429                        Ok(f) => f,
   4430                        Err(e) => {
   4431                            log::error!("{:?}", e);
   4432                            return Ok(RetryErr::Exit);
   4433                        }
   4434                    },
   4435                );
   4436            }
   4437            PostType::Clean(_, _) => {}
   4438        }
   4439 
   4440        if let Some(form_content) = form {
   4441            req = req.multipart(form_content);
   4442        } else {
   4443            req = req.form(&params);
   4444        }
   4445        match req.send() {
   4446            Ok(resp) => {
   4447                if let Err(err) = resp.error_for_status_ref() {
   4448                    log::error!("HTTP error: {:?}", err);
   4449                    return Ok(RetryErr::Retry);
   4450                }
   4451            }
   4452            Err(err) => {
   4453                log::error!("{:?}", err.to_string());
   4454                if err.is_timeout() {
   4455                    return Ok(RetryErr::Retry);
   4456                }
   4457                return Ok(RetryErr::Retry);
   4458            }
   4459        }
   4460 
   4461        if delete_after {
   4462            let params = vec![
   4463                ("lang", LANG.to_owned()),
   4464                ("nc", nc_value.to_owned()),
   4465                ("session", session.clone()),
   4466                ("action", "delete".to_owned()),
   4467                ("sendto", "".to_owned()),
   4468                ("what", "last".to_owned()),
   4469            ];
   4470            if let Err(err) = client.post(full_url).form(&params).send() {
   4471                log::error!("{:?}", err.to_string());
   4472                if err.is_timeout() {
   4473                    return Ok(RetryErr::Retry);
   4474                }
   4475            }
   4476        }
   4477        return Ok(RetryErr::Exit);
   4478    });
   4479    if should_reset_keepalive_timer {
   4480        last_post_tx.send(()).unwrap();
   4481    }
   4482 }
   4483 
   4484 impl LeChatPHPClient {
   4485    fn handle_message_editor_key_event(
   4486        &mut self,
   4487        app: &mut App,
   4488        key_event: KeyEvent,
   4489        users: &Arc<Mutex<Users>>,
   4490    ) -> Result<(), ExitSignal> {
   4491        let command = match key_event {
   4492            KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::CONTROL, .. } => {
   4493                // Ctrl+r - redo
   4494                app.msg_editor_redo();
   4495                EditorCommand::None
   4496            }
   4497            KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, .. } => {
   4498                app.handle_msg_editor_vim_key(c)
   4499            }
   4500            KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::SHIFT, .. } => {
   4501                // Handle capital letters and shifted characters
   4502                app.handle_msg_editor_vim_key(c)
   4503            }
   4504            KeyEvent { code: KeyCode::Esc, .. } => app.handle_msg_editor_vim_key('\x1b'),
   4505            KeyEvent { code: KeyCode::Enter, .. } => app.handle_msg_editor_vim_key('\r'),
   4506            KeyEvent { code: KeyCode::Backspace, .. } => app.handle_msg_editor_vim_key('\x08'),
   4507            KeyEvent { code: KeyCode::Tab, .. } => app.handle_msg_editor_vim_key('\t'),
   4508            KeyEvent { code: KeyCode::Left, .. } => app.handle_msg_editor_vim_key('h'),
   4509            KeyEvent { code: KeyCode::Right, .. } => app.handle_msg_editor_vim_key('l'),
   4510            KeyEvent { code: KeyCode::Up, .. } => app.handle_msg_editor_vim_key('k'),
   4511            KeyEvent { code: KeyCode::Down, .. } => app.handle_msg_editor_vim_key('j'),
   4512            _ => EditorCommand::None,
   4513        };
   4514        
   4515        // Handle the command
   4516        match command {
   4517            EditorCommand::Send(content) => {
   4518                if !content.trim().is_empty() {
   4519                    // Process commands like /m, /s, /pm, and !commands
   4520                    self.process_message_editor_content(content, app, users)?;
   4521                }
   4522                // Reset input mode to Normal after sending message
   4523                app.input_mode = InputMode::Normal;
   4524                app.input.clear();
   4525                app.input_idx = 0;
   4526                Ok(())
   4527            }
   4528            EditorCommand::Quit => {
   4529                // Already handled by exit_message_editor_mode
   4530                Ok(())
   4531            }
   4532            EditorCommand::None => Ok(()),
   4533        }
   4534    }
   4535 
   4536    fn process_message_editor_content(
   4537        &mut self,
   4538        content: String,
   4539        app: &mut App,
   4540        users: &Arc<Mutex<Users>>,
   4541    ) -> Result<(), ExitSignal> {
   4542        // Check for !commands first
   4543        for (command, action) in &app.commands.commands {
   4544            let expected_input = format!("!{}", command);
   4545            if content.trim() == expected_input {
   4546                if let Err(e) = self.post_msg(PostType::Post(action.clone(), None)) {
   4547                    log::error!("Failed to send command from message editor: {}", e);
   4548                }
   4549                return Ok(());
   4550            }
   4551        }
   4552 
   4553        let mut processed_content = content;
   4554        let mut members_prefix = false;
   4555        let mut staffs_prefix = false;
   4556        let mut admin_prefix = false;
   4557        let mut pm_target: Option<String> = None;
   4558 
   4559        // Check for /pm prefix first
   4560        if let Some(captures) = PM_RGX.captures(&processed_content) {
   4561            pm_target = Some(captures[1].to_string());
   4562            processed_content = captures[2].to_string();
   4563        } else if processed_content.starts_with("/m ") {
   4564            members_prefix = true;
   4565            processed_content = processed_content.strip_prefix("/m ").unwrap().to_string();
   4566        } else if processed_content.starts_with("/s ") {
   4567            staffs_prefix = true;
   4568            processed_content = processed_content.strip_prefix("/s ").unwrap().to_string();
   4569        } else if processed_content.starts_with("/a ") {
   4570            admin_prefix = true;
   4571            processed_content = processed_content.strip_prefix("/a ").unwrap().to_string();
   4572        }
   4573 
   4574        // Determine target for ChatOps commands
   4575        let chatops_target = if let Some(user) = pm_target.clone() {
   4576            Some(user)
   4577        } else if members_prefix {
   4578            Some(SEND_TO_MEMBERS.to_owned())
   4579        } else if staffs_prefix {
   4580            Some(SEND_TO_STAFFS.to_owned())
   4581        } else {
   4582            None
   4583        };
   4584 
   4585        // Check if it's a ChatOps command
   4586        if processed_content.starts_with("/") && !processed_content.starts_with("/me ") {
   4587            if self.process_command_with_target(&processed_content, app, users, chatops_target) {
   4588                // Command was processed successfully
   4589                if let Some(user) = pm_target {
   4590                    app.input = format!("/pm {} ", user);
   4591                    app.input_idx = app.input.width();
   4592                } else if members_prefix {
   4593                    app.input = "/m ".to_owned();
   4594                    app.input_idx = app.input.width();
   4595                } else if staffs_prefix {
   4596                    app.input = "/s ".to_owned();
   4597                    app.input_idx = app.input.width();
   4598                } else if admin_prefix {
   4599                    app.input = "/a ".to_owned();
   4600                    app.input_idx = app.input.width();
   4601                }
   4602                return Ok(());
   4603            }
   4604        }
   4605 
   4606        // Send regular message with appropriate target
   4607        if let Some(user) = pm_target {
   4608            if let Err(e) = self.post_msg(PostType::Post(processed_content, Some(user.clone()))) {
   4609                log::error!("Failed to send PM from message editor: {}", e);
   4610            }
   4611            app.input = format!("/pm {} ", user);
   4612            app.input_idx = app.input.width();
   4613        } else if members_prefix {
   4614            if let Err(e) = self.post_msg(PostType::Post(
   4615                processed_content,
   4616                Some(SEND_TO_MEMBERS.to_owned()),
   4617            )) {
   4618                log::error!("Failed to send message to members from message editor: {}", e);
   4619            }
   4620            app.input = "/m ".to_owned();
   4621            app.input_idx = app.input.width();
   4622        } else if staffs_prefix {
   4623            if let Err(e) = self.post_msg(PostType::Post(
   4624                processed_content,
   4625                Some(SEND_TO_STAFFS.to_owned()),
   4626            )) {
   4627                log::error!("Failed to send message to staff from message editor: {}", e);
   4628            }
   4629            app.input = "/s ".to_owned();
   4630            app.input_idx = app.input.width();
   4631        } else if admin_prefix {
   4632            if let Err(e) = self.post_msg(PostType::Post(
   4633                processed_content,
   4634                Some(SEND_TO_ADMINS.to_owned()),
   4635            )) {
   4636                log::error!("Failed to send message to admins from message editor: {}", e);
   4637            }
   4638            app.input = "/a ".to_owned();
   4639            app.input_idx = app.input.width();
   4640        } else {
   4641            // Regular message to main chat
   4642            if processed_content.starts_with("/") && !processed_content.starts_with("/me ") {
   4643                // Invalid command - just send as regular message for now
   4644                if let Err(e) = self.post_msg(PostType::Post(processed_content, None)) {
   4645                    log::error!("Failed to send message from message editor: {}", e);
   4646                }
   4647            } else {
   4648                // Send as regular message
   4649                if let Err(e) = self.post_msg(PostType::Post(processed_content, None)) {
   4650                    log::error!("Failed to send message from message editor: {}", e);
   4651                }
   4652            }
   4653        }
   4654 
   4655        Ok(())
   4656    }
   4657 }
   4658 
   4659 fn parse_date(date: &str, datetime_fmt: &str) -> NaiveDateTime {
   4660    let now = Utc::now();
   4661    let date_fmt = format!("%Y-{}", datetime_fmt);
   4662    NaiveDateTime::parse_from_str(
   4663        format!("{}-{}", now.year(), date).as_str(),
   4664        date_fmt.as_str(),
   4665    )
   4666    .unwrap()
   4667 }
   4668 
   4669 fn get_msgs(
   4670    client: &Client,
   4671    base_url: &str,
   4672    page_php: &str,
   4673    session: &str,
   4674    username: &str,
   4675    users: &Arc<Mutex<Users>>,
   4676    sig: &Arc<Mutex<Sig>>,
   4677    messages_updated_tx: &crossbeam_channel::Sender<()>,
   4678    members_tag: &str,
   4679    staffs_tag: &str,
   4680    datetime_fmt: &str,
   4681    messages: &Arc<Mutex<Vec<Message>>>,
   4682    should_notify: &mut bool,
   4683    tx: &crossbeam_channel::Sender<PostType>,
   4684    bad_usernames: &Arc<Mutex<Vec<String>>>,
   4685    bad_exact_usernames: &Arc<Mutex<Vec<String>>>,
   4686    bad_messages: &Arc<Mutex<Vec<String>>>,
   4687    allowlist: &Arc<Mutex<Vec<String>>>,
   4688    alt_account: Option<&str>,
   4689    master_account: Option<&str>,
   4690    alt_forwarding_enabled: &Arc<Mutex<bool>>,
   4691    ai_enabled: &Arc<Mutex<bool>>,
   4692    ai_mode: &Arc<Mutex<String>>,
   4693    openai_client: &Option<OpenAIClient<OpenAIConfig>>,
   4694    system_intel: &str,
   4695    moderation_strictness: &str,
   4696    mod_logs_enabled: &Arc<Mutex<bool>>,
   4697    ai_conversation_memory: &Arc<Mutex<std::collections::HashMap<String, Vec<(String, String)>>>>,
   4698    user_warnings: &Arc<Mutex<std::collections::HashMap<String, u32>>>,
   4699    ai_service: &Arc<AIService>,
   4700    bot_manager: &Option<Arc<Mutex<BotManager>>>,
   4701 ) -> anyhow::Result<()> {
   4702    let url = format!(
   4703        "{}/{}?action=view&session={}&lang={}",
   4704        base_url, page_php, session, LANG
   4705    );
   4706    let resp_text = client.get(url).send()?.text()?;
   4707    let resp_text = resp_text.replace("<br>", "\n");
   4708    let doc = Document::from(resp_text.as_str());
   4709    let new_messages = match extract_messages(&doc) {
   4710        Ok(messages) => messages,
   4711        Err(_) => {
   4712            // Failed to get messages, probably need re-login
   4713            sig.lock().unwrap().signal(&ExitSignal::NeedLogin);
   4714            return Ok(());
   4715        }
   4716    };
   4717    let current_users = extract_users(&doc);
   4718    {
   4719        let previous = users.lock().unwrap();
   4720        let filters = bad_usernames.lock().unwrap();
   4721        let exact_filters = bad_exact_usernames.lock().unwrap();
   4722        for (_, name) in &current_users.guests {
   4723            if !previous.guests.iter().any(|(_, n)| n == name) {
   4724                if exact_filters.iter().any(|f| f == name)
   4725                    || filters
   4726                        .iter()
   4727                        .any(|f| name.to_lowercase().contains(&f.to_lowercase()))
   4728                {
   4729                    let _ = tx.send(PostType::Kick(String::new(), name.clone()));
   4730                }
   4731            }
   4732        }
   4733    }
   4734    {
   4735        let messages = messages.lock().unwrap();
   4736        process_new_messages(
   4737            &new_messages,
   4738            &messages,
   4739            datetime_fmt,
   4740            members_tag,
   4741            staffs_tag,
   4742            username,
   4743            should_notify,
   4744            &current_users,
   4745            tx,
   4746            bad_usernames,
   4747            bad_exact_usernames,
   4748            bad_messages,
   4749            allowlist,
   4750            alt_account,
   4751            alt_forwarding_enabled,
   4752            ai_enabled,
   4753            ai_mode,
   4754            openai_client,
   4755            system_intel,
   4756            moderation_strictness,
   4757            mod_logs_enabled,
   4758            ai_conversation_memory,
   4759            user_warnings,
   4760            master_account,
   4761            ai_service,
   4762            bot_manager,
   4763        );
   4764        // Build messages vector. Tag deleted messages.
   4765        update_messages(
   4766            new_messages,
   4767            messages,
   4768            datetime_fmt,
   4769            members_tag,
   4770            staffs_tag,
   4771            alt_account,
   4772            master_account,
   4773        );
   4774        // Notify new messages has arrived.
   4775        // This ensure that we redraw the messages on the screen right away.
   4776        // Otherwise, the screen would not redraw until a keyboard event occurs.
   4777        messages_updated_tx.send(()).unwrap();
   4778    }
   4779    {
   4780        let mut u = users.lock().unwrap();
   4781        *u = current_users;
   4782    }
   4783    Ok(())
   4784 }
   4785 
   4786 fn process_new_messages(
   4787    new_messages: &Vec<Message>,
   4788    messages: &MutexGuard<Vec<Message>>,
   4789    datetime_fmt: &str,
   4790    members_tag: &str,
   4791    staffs_tag: &str,
   4792    username: &str,
   4793    should_notify: &mut bool,
   4794    users: &Users,
   4795    tx: &crossbeam_channel::Sender<PostType>,
   4796    bad_usernames: &Arc<Mutex<Vec<String>>>,
   4797    bad_exact_usernames: &Arc<Mutex<Vec<String>>>,
   4798    bad_messages: &Arc<Mutex<Vec<String>>>,
   4799    allowlist: &Arc<Mutex<Vec<String>>>,
   4800    alt_account: Option<&str>,
   4801    alt_forwarding_enabled: &Arc<Mutex<bool>>,
   4802    ai_enabled: &Arc<Mutex<bool>>,
   4803    ai_mode: &Arc<Mutex<String>>,
   4804    openai_client: &Option<OpenAIClient<OpenAIConfig>>,
   4805    system_intel: &str,
   4806    moderation_strictness: &str,
   4807    mod_logs_enabled: &Arc<Mutex<bool>>,
   4808    ai_conversation_memory: &Arc<Mutex<std::collections::HashMap<String, Vec<(String, String)>>>>,
   4809    user_warnings: &Arc<Mutex<std::collections::HashMap<String, u32>>>,
   4810    master_account: Option<&str>,
   4811    ai_service: &Arc<AIService>,
   4812    bot_manager: &Option<Arc<Mutex<BotManager>>>,
   4813 ) {
   4814    if let Some(last_known_msg) = messages.first() {
   4815        let last_known_msg_parsed_dt = parse_date(&last_known_msg.date, datetime_fmt);
   4816        let filtered = new_messages.iter().filter(|new_msg| {
   4817            last_known_msg_parsed_dt <= parse_date(&new_msg.date, datetime_fmt)
   4818                && !(new_msg.date == last_known_msg.date && last_known_msg.text == new_msg.text)
   4819        });
   4820        for new_msg in filtered {
   4821            log_chat_message(new_msg, username);
   4822            if let Some((from, to_opt, msg, channel_info)) = get_message(&new_msg.text, members_tag, staffs_tag) {
   4823                // Track message in AI service for summarization and analysis
   4824                let chat_message = crate::ai_service::ChatMessage {
   4825                    author: from.clone(),
   4826                    content: msg.clone(),
   4827                    is_pm: to_opt.is_some(),
   4828                };
   4829                ai_service.add_message(chat_message);
   4830 
   4831                // Process message through bot system if available
   4832                if let Some(bot_mgr) = bot_manager {
   4833                    if let Ok(manager) = bot_mgr.lock() {
   4834                        let _is_private = to_opt.is_some();
   4835 
   4836                        // FIXED: Use actual channel information from message parsing
   4837                        let (channel_context, is_member) = if to_opt.is_some() {
   4838                            // Private message
   4839                            log::info!("Bot: Processing PM from {}", from);
   4840                            ("private", users.members.iter().any(|(_, name)| name == &from))
   4841                        } else {
   4842                            // Use the channel info parsed from the message structure
   4843                            let is_member = users.members.iter().any(|(_, name)| name == &from);
   4844                            let channel = channel_info.as_deref().unwrap_or("public");
   4845                            log::info!("Bot: Processing message from {} in channel: '{}' (member: {})", 
   4846                                from, channel, is_member);
   4847                            (channel, is_member)
   4848                        };
   4849                        
   4850                        if let Err(e) = manager.process_message_for_all_bots(
   4851                            &from,
   4852                            &msg,
   4853                            crate::bot_system::MessageType::Normal,
   4854                            new_msg.id.map(|id| id as u64),
   4855                            if channel_context == "public" {
   4856                                None
   4857                            } else {
   4858                                Some(channel_context)
   4859                            },
   4860                            is_member,
   4861                        ) {
   4862                            log::warn!("Failed to process message through bot system: {}", e);
   4863                        }
   4864                    }
   4865                }
   4866 
   4867                // Notify when tagged
   4868                if msg.contains(format!("@{}", &username).as_str()) {
   4869                    *should_notify = true;
   4870                }
   4871                if let Some(ref to) = to_opt {
   4872                    if to == username && msg != "!up" {
   4873                        *should_notify = true;
   4874                    }
   4875                }
   4876 
   4877                // Remote moderation handling
   4878                let is_member_or_staff = users.members.iter().any(|(_, n)| n == &from)
   4879                    || users.staff.iter().any(|(_, n)| n == &from)
   4880                    || users.admin.iter().any(|(_, n)| n == &from);
   4881                let allowed_guest = {
   4882                    let list = allowlist.lock().unwrap();
   4883                    list.contains(&from)
   4884                };
   4885                let directed_to_me = to_opt.as_ref().map(|t| t == username).unwrap_or(false);
   4886                let via_members = new_msg.text.text().starts_with(members_tag);
   4887                let has_permission = is_member_or_staff || allowed_guest;
   4888                if msg.starts_with("#kick ") || msg.starts_with("#ban ") {
   4889                    if has_permission && (directed_to_me || via_members) {
   4890                        if let Some(target) = msg.strip_prefix("#kick ") {
   4891                            let user = target.trim().trim_start_matches('@');
   4892                            if !user.is_empty() {
   4893                                let _ = tx.send(PostType::Kick(String::new(), user.to_owned()));
   4894                            }
   4895                        } else if let Some(target) = msg.strip_prefix("#ban ") {
   4896                            let user = target.trim().trim_start_matches('@');
   4897                            if !user.is_empty() {
   4898                                // Always add to ban list
   4899                                let mut f = bad_usernames.lock().unwrap();
   4900                                f.push(user.to_owned());
   4901                                
   4902                                // Check if target is a member, staff, or admin - only kick guests
   4903                                let target_is_member = users.members.iter().any(|(_, n)| n == user)
   4904                                    || users.staff.iter().any(|(_, n)| n == user)
   4905                                    || users.admin.iter().any(|(_, n)| n == user);
   4906                                
   4907                                if target_is_member {
   4908                                    // Member banned but not kicked
   4909                                    let response = format!("@{} has been added to ban list (member not kicked)", user);
   4910                                    let _ = tx.send(PostType::Post(response, Some(from.clone())));
   4911                                } else {
   4912                                    // Guest banned and kicked
   4913                                    let _ = tx.send(PostType::Kick(String::new(), user.to_owned()));
   4914                                }
   4915                            }
   4916                        }
   4917                    } else if directed_to_me && !has_permission {
   4918                        let msg = "You don't have permission to do that.".to_owned();
   4919                        let _ = tx.send(PostType::Post(msg, Some(from.clone())));
   4920                    }
   4921                }
   4922 
   4923                if let Some(alt) = alt_account {
   4924                    if *alt_forwarding_enabled.lock().unwrap() {
   4925                        let text = new_msg.text.text();
   4926                        if (text.starts_with(members_tag) || text.starts_with(staffs_tag))
   4927                            && from != alt
   4928                        {
   4929                            let _ = tx.send(PostType::Post(text.clone(), Some(alt.to_owned())));
   4930                        }
   4931                        if from == alt && to_opt.as_deref() == Some(username) {
   4932                            if let Some(stripped) = msg.strip_prefix("/m ") {
   4933                                let _ = tx.send(PostType::Post(
   4934                                    stripped.to_owned(),
   4935                                    Some(SEND_TO_MEMBERS.to_owned()),
   4936                                ));
   4937                                // Echo the message back to the alt so it can confirm
   4938                                let confirm = format!("{}{} - {}", members_tag, username, stripped);
   4939                                let _ = tx.send(PostType::Post(confirm, Some(alt.to_owned())));
   4940                            } else if let Some(stripped) = msg.strip_prefix("/s ") {
   4941                                let _ = tx.send(PostType::Post(
   4942                                    stripped.to_owned(),
   4943                                    Some(SEND_TO_STAFFS.to_owned()),
   4944                                ));
   4945                                let confirm = format!("{}{} - {}", staffs_tag, username, stripped);
   4946                                let _ = tx.send(PostType::Post(confirm, Some(alt.to_owned())));
   4947                            }
   4948                        }
   4949                    }
   4950                }
   4951 
   4952                let is_guest = users.guests.iter().any(|(_, n)| n == &from);
   4953                if from != username && is_guest {
   4954                    // Check if user is in allowlist first
   4955                    let is_allowed = {
   4956                        let allowed_users = allowlist.lock().unwrap();
   4957                        allowed_users.contains(&from)
   4958                    };
   4959 
   4960                    if is_allowed {
   4961                        send_mod_log(
   4962                            tx,
   4963                            *mod_logs_enabled.lock().unwrap(),
   4964                            format!(
   4965                                "MOD LOG: User '{}' is allowlisted, bypassing all filters",
   4966                                from
   4967                            ),
   4968                        );
   4969                    } else {
   4970                        let bad_name = {
   4971                            let filters = bad_usernames.lock().unwrap();
   4972                            filters
   4973                                .iter()
   4974                                .any(|f| from.to_lowercase().contains(&f.to_lowercase()))
   4975                        };
   4976                        let bad_name_exact = {
   4977                            let filters = bad_exact_usernames.lock().unwrap();
   4978                            filters.iter().any(|f| f == &from)
   4979                        };
   4980                        let bad_msg = {
   4981                            let filters = bad_messages.lock().unwrap();
   4982                            filters
   4983                                .iter()
   4984                                .any(|f| msg.to_lowercase().contains(&f.to_lowercase()))
   4985                        };
   4986 
   4987                        if bad_name_exact || bad_name || bad_msg {
   4988                            let reason = if bad_name_exact {
   4989                                "exact username match"
   4990                            } else if bad_name {
   4991                                "username filter match"
   4992                            } else {
   4993                                "message filter match"
   4994                            };
   4995                            send_mod_log(
   4996                                tx,
   4997                                *mod_logs_enabled.lock().unwrap(),
   4998                                format!(
   4999                                    "MOD LOG: FILTER KICK - Kicking '{}' for {}: '{}'",
   5000                                    from, reason, msg
   5001                                ),
   5002                            );
   5003                            let _ = tx.send(PostType::Kick(String::new(), from.clone()));
   5004                        } else {
   5005                            let res = score_message(&msg);
   5006                            if let Some(act) = action_from_score(res.score) {
   5007                                match act {
   5008                                    Action::Warn => {
   5009                                        if to_opt.is_none() {
   5010                                            let reason = res
   5011                                                .reason
   5012                                                .map(|r| r.description())
   5013                                                .unwrap_or("breaking the rules");
   5014                                            let warn = format!(
   5015                                            "@{username} - @{from}'s message was flagged for {reason}."
   5016                                        );
   5017                                            let _ =
   5018                                                tx.send(PostType::Post(warn, Some("0".to_owned())));
   5019                                        }
   5020                                    }
   5021                                    Action::Kick => {
   5022                                        send_mod_log(tx, *mod_logs_enabled.lock().unwrap(), format!("MOD LOG: HARM SCORE KICK - Kicking '{}' for message: '{}'", from, msg));
   5023                                        let _ =
   5024                                            tx.send(PostType::Kick(String::new(), from.clone()));
   5025                                    }
   5026                                    Action::Ban => {
   5027                                        send_mod_log(tx, *mod_logs_enabled.lock().unwrap(), format!("MOD LOG: HARM SCORE BAN - Banning '{}' for message: '{}'", from, msg));
   5028                                        let _ =
   5029                                            tx.send(PostType::Kick(String::new(), from.clone()));
   5030                                        let mut f = bad_usernames.lock().unwrap();
   5031                                        f.push(from.clone());
   5032                                    }
   5033                                }
   5034                            }
   5035                        }
   5036                    }
   5037                }
   5038 
   5039                // AI Processing - only for guests and ignore messages from logged-in user
   5040                if *ai_enabled.lock().unwrap() && openai_client.is_some() && from != username {
   5041                    // Check if user is a guest (not member, staff, or admin)
   5042                    let is_guest = users.guests.iter().any(|(_, n)| n == &from);
   5043 
   5044                    let ai_mode_val = ai_mode.lock().unwrap().clone();
   5045                    process_ai_message(
   5046                        &from,
   5047                        &msg,
   5048                        &to_opt,
   5049                        username,
   5050                        &ai_mode_val,
   5051                        openai_client.as_ref().unwrap(),
   5052                        system_intel,
   5053                        moderation_strictness,
   5054                        mod_logs_enabled,
   5055                        is_guest, // Pass guest status
   5056                        tx,
   5057                        bad_usernames,
   5058                        ai_conversation_memory,
   5059                        user_warnings,
   5060                        master_account,
   5061                    );
   5062                }
   5063            }
   5064        }
   5065    }
   5066 }
   5067 
   5068 // Helper function to send MOD LOG messages only when enabled
   5069 fn send_mod_log(tx: &crossbeam_channel::Sender<PostType>, mod_logs_enabled: bool, message: String) {
   5070    if mod_logs_enabled {
   5071        // Use try_send to avoid panicking if channel is closed
   5072        let _ = tx.try_send(PostType::Post(message, Some("0".to_owned())));
   5073    }
   5074 }
   5075 
   5076 // Function to check for specific violations that should trigger warnings in alt mode
   5077 fn check_warning_violations(message: &str) -> Option<String> {
   5078    let msg_lower = message.to_lowercase();
   5079 
   5080    // Check for CP-related content
   5081    let cp_patterns = [
   5082        "cheese pizza",
   5083        "cp links",
   5084        "young models",
   5085        "trading cp",
   5086        "pedo stuff",
   5087        "kiddie porn",
   5088        "jailbait",
   5089        "preteen",
   5090        "underage nudes",
   5091        "r@ygold",
   5092        "hussyfan",
   5093        "ptsc",
   5094        "pthc",
   5095        "young boy",
   5096        "young girl",
   5097        "loli",
   5098        "shota",
   5099    ];
   5100 
   5101    for pattern in &cp_patterns {
   5102        if msg_lower.contains(pattern) {
   5103            return Some("inappropriate content involving minors".to_string());
   5104        }
   5105    }
   5106 
   5107    // Check for pornography requests/sharing
   5108    let porn_patterns = [
   5109        "send nudes",
   5110        "porn links",
   5111        "naked pics",
   5112        "sex videos",
   5113        "adult content",
   5114        "xxx links",
   5115        "porn site",
   5116        "onlyfans",
   5117        "cam girl",
   5118        "webcam show",
   5119    ];
   5120 
   5121    for pattern in &porn_patterns {
   5122        if msg_lower.contains(pattern) {
   5123            return Some("inappropriate adult content".to_string());
   5124        }
   5125    }
   5126 
   5127    // Check for gun/weapon purchases
   5128    let gun_patterns = [
   5129        "buy gun",
   5130        "selling gun",
   5131        "purchase weapon",
   5132        "buy ammo",
   5133        "ammunition for sale",
   5134        "selling weapons",
   5135        "firearm for sale",
   5136        "gun dealer",
   5137        "weapon trade",
   5138        "buy rifle",
   5139        "selling pistol",
   5140        "handgun for sale",
   5141    ];
   5142 
   5143    for pattern in &gun_patterns {
   5144        if msg_lower.contains(pattern) {
   5145            return Some("attempting to buy/sell weapons".to_string());
   5146        }
   5147    }
   5148 
   5149    // Check for account hacking services
   5150    let hack_patterns = [
   5151        "hack facebook",
   5152        "hack instagram",
   5153        "hack account",
   5154        "social media hack",
   5155        "password crack",
   5156        "account recovery service",
   5157        "hack someone",
   5158        "breach account",
   5159        "steal password",
   5160        "facebook hacker",
   5161        "instagram hacker",
   5162        "account takeover",
   5163    ];
   5164 
   5165    for pattern in &hack_patterns {
   5166        if msg_lower.contains(pattern) {
   5167            return Some("offering/requesting account hacking services".to_string());
   5168        }
   5169    }
   5170 
   5171    // Check for spam (excessive repetition)
   5172    let words: Vec<&str> = message.split_whitespace().collect();
   5173    if words.len() > 10 {
   5174        let unique_words: std::collections::HashSet<&str> = words.iter().cloned().collect();
   5175        if (unique_words.len() as f32) / (words.len() as f32) < 0.4 {
   5176            return Some("spamming/excessive repetition".to_string());
   5177        }
   5178    }
   5179 
   5180    // Check for excessive caps (more than 70% of message in caps)
   5181    let caps_count = message.chars().filter(|c| c.is_uppercase()).count();
   5182    let letter_count = message.chars().filter(|c| c.is_alphabetic()).count();
   5183    if letter_count > 20 && caps_count as f32 / letter_count as f32 > 0.7 {
   5184        return Some("excessive use of capital letters".to_string());
   5185    }
   5186 
   5187    None
   5188 }
   5189 
   5190 fn process_ai_message(
   5191    from: &str,
   5192    msg: &str,
   5193    to_opt: &Option<String>,
   5194    username: &str,
   5195    ai_mode: &str,
   5196    openai_client: &OpenAIClient<OpenAIConfig>,
   5197    system_intel: &str,
   5198    moderation_strictness: &str,
   5199    mod_logs_enabled: &Arc<Mutex<bool>>,
   5200    is_guest: bool, // New parameter to indicate if user is a guest
   5201    tx: &crossbeam_channel::Sender<PostType>,
   5202    bad_usernames: &Arc<Mutex<Vec<String>>>,
   5203    ai_conversation_memory: &Arc<Mutex<std::collections::HashMap<String, Vec<(String, String)>>>>,
   5204    user_warnings: &Arc<Mutex<std::collections::HashMap<String, u32>>>,
   5205    master_account: Option<&str>,
   5206 ) {
   5207    if from == username {
   5208        return; // Don't process our own messages
   5209    }
   5210 
   5211    // Check if message is directed at another user (tagged at start or end)
   5212    let msg_trimmed = msg.trim();
   5213    let is_directed_at_other = {
   5214        // Check for @username at the start (first word)
   5215        let first_word = msg_trimmed.split_whitespace().next().unwrap_or("");
   5216        let starts_with_tag = first_word.starts_with('@') && first_word != format!("@{}", username);
   5217 
   5218        // Check for @username at the end (last word)
   5219        let last_word = msg_trimmed.split_whitespace().last().unwrap_or("");
   5220        let ends_with_tag = last_word.starts_with('@') && last_word != format!("@{}", username);
   5221 
   5222        starts_with_tag || ends_with_tag
   5223    };
   5224 
   5225    let client = openai_client.clone();
   5226    let msg_content = msg.to_string();
   5227    let from_user = from.to_string();
   5228    let username_owned = username.to_string();
   5229    let ai_mode_owned = ai_mode.to_string();
   5230    let system_intel_owned = system_intel.to_string();
   5231    let strictness_owned = moderation_strictness.to_string();
   5232    let tx_clone = tx.clone();
   5233    let bad_usernames_clone = Arc::clone(bad_usernames);
   5234    let to_opt_clone = to_opt.clone();
   5235    let memory_clone = Arc::clone(ai_conversation_memory);
   5236    let mod_logs_enabled_val = *mod_logs_enabled.lock().unwrap(); // Capture the current value
   5237 
   5238    // Check if we should do moderation based on mode and user status
   5239    let should_do_moderation = match ai_mode {
   5240        "off" => {
   5241            send_mod_log(
   5242                tx,
   5243                *mod_logs_enabled.lock().unwrap(),
   5244                format!(
   5245                    "MOD LOG: AI disabled, skipping moderation for '{}': '{}'",
   5246                    from_user, msg_content
   5247                ),
   5248            );
   5249            false // No moderation when completely off
   5250        }
   5251        _ => {
   5252            if !is_guest {
   5253                send_mod_log(
   5254                    tx,
   5255                    *mod_logs_enabled.lock().unwrap(),
   5256                    format!(
   5257                        "MOD LOG: Skipping moderation for member/staff '{}': '{}'",
   5258                        from_user, msg_content
   5259                    ),
   5260                );
   5261                false // Don't moderate members, staff, or admins
   5262            } else {
   5263                true // Only moderate guests
   5264            }
   5265        }
   5266    };
   5267 
   5268    // Alt mode warning system - check for specific violations when master account is set
   5269    if let Some(master) = master_account {
   5270        if is_guest {
   5271            if let Some(violation_reason) = check_warning_violations(&msg_content) {
   5272                // Increment warning count for user
   5273                let warning_count = {
   5274                    let mut warnings = user_warnings.lock().unwrap();
   5275                    let count = warnings.entry(from_user.clone()).or_insert(0);
   5276                    *count += 1;
   5277                    *count
   5278                };
   5279 
   5280                send_mod_log(
   5281                    tx,
   5282                    *mod_logs_enabled.lock().unwrap(),
   5283                    format!(
   5284                        "MOD LOG: WARNING {} for '{}' - {}: '{}'",
   5285                        warning_count, from_user, violation_reason, msg_content
   5286                    ),
   5287                );
   5288 
   5289                if warning_count >= 3 {
   5290                    // Send kick command to master account via PM
   5291                    let kick_msg = format!("#kick @{}", from_user);
   5292                    let _ = tx.send(PostType::Post(kick_msg, Some(master.to_string())));
   5293 
   5294                    // Reset warning count after kick command
   5295                    {
   5296                        let mut warnings = user_warnings.lock().unwrap();
   5297                        warnings.remove(&from_user);
   5298                    }
   5299 
   5300                    send_mod_log(
   5301                        tx,
   5302                        *mod_logs_enabled.lock().unwrap(),
   5303                        format!(
   5304                            "MOD LOG: Sent kick command to master for '{}' after 3 warnings",
   5305                            from_user
   5306                        ),
   5307                    );
   5308                    return; // Exit early
   5309                } else {
   5310                    // Send warning to user
   5311                    let warning_msg = format!("@{} Warning {}/3: Please avoid {}. Further violations may result in removal.",
   5312                        from_user, warning_count, violation_reason);
   5313                    let _ = tx.send(PostType::Post(warning_msg, None));
   5314                    return; // Exit early, don't proceed with normal moderation
   5315                }
   5316            }
   5317        }
   5318    }
   5319 
   5320    // Do immediate quick moderation check first (synchronous and fast)
   5321    if should_do_moderation {
   5322        send_mod_log(
   5323            tx,
   5324            *mod_logs_enabled.lock().unwrap(),
   5325            format!(
   5326                "MOD LOG: Checking guest message from '{}': '{}'",
   5327                from_user, msg_content
   5328            ),
   5329        );
   5330 
   5331        if let Some(should_moderate) = quick_moderation_check(&msg_content) {
   5332            if should_moderate {
   5333                send_mod_log(
   5334                    tx,
   5335                    *mod_logs_enabled.lock().unwrap(),
   5336                    format!(
   5337                        "MOD LOG: QUICK PATTERN MATCH - Kicking '{}' for message: '{}'",
   5338                        from_user, msg_content
   5339                    ),
   5340                );
   5341                log::warn!(
   5342                    "IMMEDIATE KICK - Quick moderation flagged message from {}: {}",
   5343                    from_user,
   5344                    msg_content
   5345                );
   5346                // Kick immediately without waiting for AI processing
   5347                let _ = tx.send(PostType::Kick(String::new(), from_user.clone()));
   5348                let mut filters = bad_usernames.lock().unwrap();
   5349                filters.push(from_user.clone());
   5350                return; // Exit early, no need for further processing
   5351            } else {
   5352                send_mod_log(tx, *mod_logs_enabled.lock().unwrap(), format!("MOD LOG: Quick patterns matched but flagged as false positive for '{}': '{}'", from_user, msg_content));
   5353            }
   5354        } else {
   5355            send_mod_log(
   5356                tx,
   5357                *mod_logs_enabled.lock().unwrap(),
   5358                format!(
   5359                    "MOD LOG: No quick patterns matched, sending to AI analysis for '{}': '{}'",
   5360                    from_user, msg_content
   5361                ),
   5362            );
   5363        }
   5364    }
   5365 
   5366    // Create a dedicated runtime for this AI processing task
   5367    // Using thread::spawn to avoid blocking the main thread and prevent interference between message processing
   5368    thread::spawn(move || {
   5369        let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
   5370        rt.block_on(async move {
   5371            // Continue with AI moderation if needed (and not already kicked by quick check)
   5372            if should_do_moderation {
   5373                // If quick check was inconclusive, use AI analysis
   5374                if let Some(should_moderate) = check_ai_moderation(&client, &msg_content, &strictness_owned).await {
   5375                    if should_moderate {
   5376                        send_mod_log(&tx_clone, mod_logs_enabled_val, format!("MOD LOG: AI ANALYSIS - KICKING '{}' for message: '{}' [AI: YES]", from_user, msg_content));
   5377                        log::info!("AI moderation flagged message from {}: {}", from_user, msg_content);
   5378                        let _ = tx_clone.send(PostType::Kick(String::new(), from_user.clone()));
   5379                        let mut filters = bad_usernames_clone.lock().unwrap();
   5380                        filters.push(from_user.clone());
   5381                        return; // Exit early if moderated
   5382                    } else {
   5383                        send_mod_log(&tx_clone, mod_logs_enabled_val, format!("MOD LOG: AI ANALYSIS - ALLOWING '{}' message: '{}' [AI: NO]", from_user, msg_content));
   5384                    }
   5385                } else {
   5386                    send_mod_log(&tx_clone, mod_logs_enabled_val, format!("MOD LOG: AI ANALYSIS - API FAILED for '{}': '{}' [AI: ERROR]", from_user, msg_content));
   5387                }
   5388            }
   5389 
   5390            // Now handle different AI modes for responses (only if not moderated)
   5391            // Skip responses if message is directed at another user
   5392            if is_directed_at_other {
   5393                send_mod_log(&tx_clone, mod_logs_enabled_val, format!("MOD LOG: Skipping AI response - message from '{}' is directed at another user: '{}'", from_user, msg_content));
   5394                return;
   5395            }
   5396 
   5397            match ai_mode_owned.as_str() {
   5398                "mod_only" => {
   5399                    // Only moderation, no responses - already handled above
   5400                }
   5401                "off" => {
   5402                    // Completely off - no moderation, no responses
   5403                }
   5404                "reply_all" => {
   5405                    // Store user message in memory
   5406                    {
   5407                        let mut memory = memory_clone.lock().unwrap();
   5408                        let history = memory.entry(from_user.clone()).or_insert_with(Vec::new);
   5409                        history.push(("user".to_string(), msg_content.clone()));
   5410                        // Keep only last 10 messages per user to prevent memory overflow
   5411                        if history.len() > 10 {
   5412                            history.remove(0);
   5413                        }
   5414                    }
   5415 
   5416                    if let Some(response) = generate_ai_response_with_memory(&client, &msg_content, &system_intel_owned, &username_owned, &memory_clone, &from_user).await {
   5417                        // Calculate realistic delay based on response length
   5418                        let delay_ms = calculate_realistic_delay(&response);
   5419                        tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
   5420 
   5421                        // Store AI response in memory
   5422                        {
   5423                            let mut memory = memory_clone.lock().unwrap();
   5424                            let history = memory.entry(from_user.clone()).or_insert_with(Vec::new);
   5425                            history.push(("assistant".to_string(), response.clone()));
   5426                        }
   5427 
   5428                        // Tag the user we're replying to
   5429                        let tagged_response = format!("@{} {}", from_user, response);
   5430                        let _ = tx_clone.send(PostType::Post(tagged_response, None));
   5431                    }
   5432                }
   5433                "reply_ping" => {
   5434                    let is_mentioned = msg_content.contains(&format!("@{}", username_owned));
   5435                    let is_directed = to_opt_clone.as_ref().map(|t| t == &username_owned).unwrap_or(false);
   5436 
   5437                    if is_mentioned || is_directed {
   5438                        // Store user message in memory
   5439                        {
   5440                            let mut memory = memory_clone.lock().unwrap();
   5441                            let history = memory.entry(from_user.clone()).or_insert_with(Vec::new);
   5442                            history.push(("user".to_string(), msg_content.clone()));
   5443                            // Keep only last 10 messages per user to prevent memory overflow
   5444                            if history.len() > 10 {
   5445                                history.remove(0);
   5446                            }
   5447                        }
   5448 
   5449                        if let Some(response) = generate_ai_response_with_memory(&client, &msg_content, &system_intel_owned, &username_owned, &memory_clone, &from_user).await {
   5450                            // Calculate realistic delay based on response length
   5451                            let delay_ms = calculate_realistic_delay(&response);
   5452                            tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
   5453 
   5454                            // Store AI response in memory
   5455                            {
   5456                                let mut memory = memory_clone.lock().unwrap();
   5457                                let history = memory.entry(from_user.clone()).or_insert_with(Vec::new);
   5458                                history.push(("assistant".to_string(), response.clone()));
   5459                            }
   5460 
   5461                            // Tag the user we're replying to
   5462                            let tagged_response = format!("@{} {}", from_user, response);
   5463                            let reply_target = if is_directed {
   5464                                Some(from_user)
   5465                            } else {
   5466                                None
   5467                            };
   5468                            let _ = tx_clone.send(PostType::Post(tagged_response, reply_target));
   5469                        }
   5470                    }
   5471                }
   5472                _ => {} // Unknown mode
   5473            }
   5474        });
   5475    });
   5476 }
   5477 
   5478 fn quick_moderation_check(message: &str) -> Option<bool> {
   5479    let msg_lower = message.to_lowercase();
   5480 
   5481    // Quick patterns for obvious violations (case insensitive)
   5482    let illegal_request_patterns = [
   5483        "cheese pizza",
   5484        "cp links",
   5485        "young models",
   5486        "trading cp",
   5487        "pedo stuff",
   5488        "kiddie porn",
   5489        "jailbait",
   5490        "preteen",
   5491        "underage nudes",
   5492        "r@ygold",
   5493        "hussyfan",
   5494        "ptsc",
   5495        "pthc",
   5496    ];
   5497 
   5498    // Check for obvious illegal content requests
   5499    for pattern in &illegal_request_patterns {
   5500        if msg_lower.contains(pattern) {
   5501            return Some(true); // Should moderate
   5502        }
   5503    }
   5504 
   5505    // Check for excessive repetition (basic spam detection)
   5506    let words: Vec<&str> = message.split_whitespace().collect();
   5507    if words.len() > 10 {
   5508        let unique_words: std::collections::HashSet<&str> = words.iter().cloned().collect();
   5509        if (unique_words.len() as f32) / (words.len() as f32) < 0.5 {
   5510            return Some(true); // Too repetitive, likely spam
   5511        }
   5512    }
   5513 
   5514    // No quick violations found, need AI analysis
   5515    None
   5516 }
   5517 
   5518 async fn check_ai_moderation(
   5519    client: &OpenAIClient<OpenAIConfig>,
   5520    message: &str,
   5521    strictness: &str,
   5522 ) -> Option<bool> {
   5523    let strictness_guidance = match strictness {
   5524        "strict" => "Be very strict. Moderate anything that could potentially violate rules. When in doubt, moderate.",
   5525        "lenient" => "Be very lenient. Only moderate clear, obvious violations. Heavily favor free speech. When in doubt, allow.",
   5526        _ => "Use balanced judgment. Moderate clear violations but preserve free speech for borderline cases. When in doubt about context, lean towards allowing."
   5527    };
   5528 
   5529    let system_prompt = format!(
   5530        "You are a chat moderation system enforcing these rules with {} strictness:\n\
   5531        {}\n\
   5532        \n\
   5533        STRICT ENFORCEMENT (ALWAYS MODERATE):\n\
   5534        - No sharing, requesting, or distributing illegal content (CP, revenge porn, etc.)\n\
   5535        - No spam, excessive advertising, or repetitive messages\n\
   5536        - No gore/snuff content sharing or detailed descriptions\n\
   5537        - No doxxing or sharing personal information without consent\n\
   5538        - No harassment, threats, or coordinated attacks on individuals\n\
   5539        - No sexual content involving minors or requests for such content\n\
   5540        - No violence against children or threats of harm to minors\n\
   5541        - No discussion of harming, killing, or abusing children\n\
   5542        \n\
   5543        CONTEXT-SENSITIVE ENFORCEMENT:\n\
   5544        - Discussions MENTIONING illegal content negatively/critically = ALLOWED\n\
   5545        - Educational/awareness discussions about sensitive topics = ALLOWED\n\
   5546        - News reporting or academic discussion = ALLOWED\n\
   5547        - Coded language clearly requesting illegal content = MODERATE\n\
   5548        - Off-topic discussions should be gently redirected, not moderated unless excessive\n\
   5549        \n\
   5550        FREEDOM OF SPEECH PRIORITY:\n\
   5551        - Allow controversial opinions and political discussions\n\
   5552        - Allow strong language and heated debates\n\
   5553        - Allow dark humor that doesn't promote illegal activity\n\
   5554        - When in doubt about context, lean towards ALLOWING the message\n\
   5555        \n\
   5556        Examples:\n\
   5557        ALLOW: 'I stumbled upon some CP online and reported it immediately - disgusting'\n\
   5558        MODERATE: 'Anyone got cheese pizza links?' or 'Looking for young models'\n\
   5559        MODERATE: 'I want to fuck a small kid' or 'I really wanna fuck a small kid'\n\
   5560        MODERATE: 'should I kill my kid' or 'I want to hurt my child'\n\
   5561        MODERATE: 'how to harm children' or 'ways to kill kids'\n\
   5562        ALLOW: 'That politician is a complete idiot and should be voted out'\n\
   5563        MODERATE: 'Someone should assassinate that politician'\n\
   5564        ALLOW: 'I disagree with your opinion strongly, you're wrong'\n\
   5565        MODERATE: 'You should kill yourself, here's your home address...'\n\
   5566        \n\
   5567        Analyze the message context carefully. Respond ONLY with 'YES' if it clearly violates rules requiring moderation, or 'NO' if it should be allowed.",
   5568        strictness, strictness_guidance
   5569    );
   5570 
   5571    let request = CreateChatCompletionRequestArgs::default()
   5572        .model("gpt-3.5-turbo")
   5573        .messages([
   5574            ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
   5575                content: ChatCompletionRequestSystemMessageContent::Text(system_prompt),
   5576                name: None,
   5577            }),
   5578            ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
   5579                content: ChatCompletionRequestUserMessageContent::Text(message.to_string()),
   5580                name: None,
   5581            }),
   5582        ])
   5583        .max_tokens(10u16)
   5584        .build();
   5585 
   5586    match request {
   5587        Ok(req) => {
   5588            match client.chat().create(req).await {
   5589                Ok(response) => {
   5590                    if let Some(choice) = response.choices.first() {
   5591                        if let Some(content) = &choice.message.content {
   5592                            let ai_response = content.trim().to_uppercase();
   5593                            let should_moderate = ai_response == "YES";
   5594 
   5595                            // Enhanced logging for debugging
   5596                            log::info!("AI MODERATION DEBUG - Message: '{}' | AI Response: '{}' | Decision: {} | Strictness: {}",
   5597                                message, content.trim(), if should_moderate { "MODERATE" } else { "ALLOW" }, strictness);
   5598 
   5599                            return Some(should_moderate);
   5600                        } else {
   5601                            log::error!(
   5602                                "AI moderation: No content in response for message: '{}'",
   5603                                message
   5604                            );
   5605                        }
   5606                    } else {
   5607                        log::error!(
   5608                            "AI moderation: No choices in response for message: '{}'",
   5609                            message
   5610                        );
   5611                    }
   5612                }
   5613                Err(e) => {
   5614                    log::error!("AI moderation API error for message '{}': {}", message, e);
   5615                }
   5616            }
   5617        }
   5618        Err(e) => {
   5619            log::error!(
   5620                "AI moderation request build error for message '{}': {}",
   5621                message,
   5622                e
   5623            );
   5624        }
   5625    }
   5626    None
   5627 }
   5628 
   5629 async fn generate_ai_response_with_memory(
   5630    client: &OpenAIClient<OpenAIConfig>,
   5631    message: &str,
   5632    system_intel: &str,
   5633    username: &str,
   5634    conversation_memory: &Arc<Mutex<std::collections::HashMap<String, Vec<(String, String)>>>>,
   5635    from_user: &str,
   5636 ) -> Option<String> {
   5637    let system_prompt = format!(
   5638        "{}\n\nYou are chatting as '{}'. Respond naturally and helpfully to messages. \
   5639        Keep responses concise (under 200 characters) and appropriate for a chat room. \
   5640        Don't be overly formal. Be engaging but not overwhelming. \
   5641        Use conversation history to provide contextual responses.",
   5642        system_intel, username
   5643    );
   5644 
   5645    // Build message history with context
   5646    let mut messages = vec![ChatCompletionRequestMessage::System(
   5647        ChatCompletionRequestSystemMessage {
   5648            content: ChatCompletionRequestSystemMessageContent::Text(system_prompt),
   5649            name: None,
   5650        },
   5651    )];
   5652 
   5653    // Add conversation history for context
   5654    {
   5655        let memory = conversation_memory.lock().unwrap();
   5656        if let Some(history) = memory.get(from_user) {
   5657            // Add the last few messages for context (limit to avoid token overflow)
   5658            let recent_history = if history.len() > 8 {
   5659                &history[history.len() - 8..]
   5660            } else {
   5661                history
   5662            };
   5663            for (role, content) in recent_history {
   5664                match role.as_str() {
   5665                    "user" => {
   5666                        messages.push(ChatCompletionRequestMessage::User(
   5667                            ChatCompletionRequestUserMessage {
   5668                                content: ChatCompletionRequestUserMessageContent::Text(
   5669                                    content.clone(),
   5670                                ),
   5671                                name: Some(from_user.to_string()),
   5672                            },
   5673                        ));
   5674                    }
   5675                    "assistant" => {
   5676                        messages.push(ChatCompletionRequestMessage::Assistant(
   5677                            ChatCompletionRequestAssistantMessage {
   5678                                content: Some(ChatCompletionRequestAssistantMessageContent::Text(
   5679                                    content.clone(),
   5680                                )),
   5681                                name: Some(username.to_string()),
   5682                                ..Default::default()
   5683                            },
   5684                        ));
   5685                    }
   5686                    _ => {}
   5687                }
   5688            }
   5689        }
   5690    }
   5691 
   5692    // Add the current message
   5693    messages.push(ChatCompletionRequestMessage::User(
   5694        ChatCompletionRequestUserMessage {
   5695            content: ChatCompletionRequestUserMessageContent::Text(message.to_string()),
   5696            name: Some(from_user.to_string()),
   5697        },
   5698    ));
   5699 
   5700    let request = CreateChatCompletionRequestArgs::default()
   5701        .model("gpt-3.5-turbo")
   5702        .messages(messages)
   5703        .max_tokens(150u16)
   5704        .temperature(0.8) // Add some randomness to responses
   5705        .build();
   5706 
   5707    match request {
   5708        Ok(req) => match client.chat().create(req).await {
   5709            Ok(response) => {
   5710                if let Some(choice) = response.choices.first() {
   5711                    if let Some(content) = &choice.message.content {
   5712                        return Some(content.trim().to_string());
   5713                    }
   5714                }
   5715            }
   5716            Err(e) => {
   5717                log::error!("AI response error: {}", e);
   5718            }
   5719        },
   5720        Err(e) => {
   5721            log::error!("AI request build error: {}", e);
   5722        }
   5723    }
   5724    None
   5725 }
   5726 
   5727 fn calculate_realistic_delay(response: &str) -> u64 {
   5728    use rand::Rng;
   5729    let mut rng = rand::thread_rng();
   5730 
   5731    // Base delay for thinking time (3-8 seconds) - increased for more realistic pauses
   5732    let base_delay = rng.gen_range(3000..8000);
   5733 
   5734    // Typing speed simulation: 25-65 WPM (words per minute) - slower, more human-like
   5735    // Average word length ~5 characters, so 125-325 characters per minute
   5736    let chars_per_minute = rng.gen_range(125.0..325.0);
   5737    let chars_per_ms = chars_per_minute / 60000.0; // Convert to chars per millisecond
   5738 
   5739    let typing_delay = (response.len() as f64 / chars_per_ms) as u64;
   5740 
   5741    // Add some random variance (±30%) - increased variance for more natural feel
   5742    let total_delay = base_delay + typing_delay;
   5743    let variance = (total_delay as f64 * 0.3) as u64;
   5744    let final_delay = total_delay + rng.gen_range(0..variance) - (variance / 2);
   5745 
   5746    // Cap the delay between 2-25 seconds to avoid being too slow but allow for longer responses
   5747    final_delay.clamp(2000, 25000)
   5748 }
   5749 
   5750 fn update_messages(
   5751    new_messages: Vec<Message>,
   5752    mut messages: MutexGuard<Vec<Message>>,
   5753    datetime_fmt: &str,
   5754    members_tag: &str,
   5755    staffs_tag: &str,
   5756    alt_account: Option<&str>,
   5757    master_account: Option<&str>,
   5758 ) {
   5759    let mut old_msg_ptr = 0;
   5760    for mut new_msg in new_messages.into_iter() {
   5761        if let Some((from, Some(to), _, _)) = get_message(&new_msg.text, members_tag, staffs_tag) {
   5762            if let Some(master) = master_account {
   5763                if to == master && from != master {
   5764                    new_msg.hide = true;
   5765                }
   5766            }
   5767            if let Some(alt) = alt_account {
   5768                if to == alt && from != alt {
   5769                    new_msg.hide = true;
   5770                }
   5771            }
   5772        }
   5773        loop {
   5774            if let Some(old_msg) = messages.get_mut(old_msg_ptr) {
   5775                let new_parsed_dt = parse_date(&new_msg.date, datetime_fmt);
   5776                let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   5777                if new_parsed_dt < parsed_dt {
   5778                    old_msg.deleted = true;
   5779                    old_msg_ptr += 1;
   5780                    continue;
   5781                }
   5782                if new_parsed_dt == parsed_dt {
   5783                    if old_msg.text != new_msg.text {
   5784                        let mut found = false;
   5785                        let mut x = 0;
   5786                        loop {
   5787                            x += 1;
   5788                            if let Some(old_msg) = messages.get(old_msg_ptr + x) {
   5789                                let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   5790                                if new_parsed_dt == parsed_dt {
   5791                                    if old_msg.text == new_msg.text {
   5792                                        found = true;
   5793                                        break;
   5794                                    }
   5795                                    continue;
   5796                                }
   5797                            }
   5798                            break;
   5799                        }
   5800                        if !found {
   5801                            messages.insert(old_msg_ptr, new_msg);
   5802                            old_msg_ptr += 1;
   5803                        }
   5804                    }
   5805                    old_msg_ptr += 1;
   5806                    break;
   5807                }
   5808            }
   5809            messages.insert(old_msg_ptr, new_msg);
   5810            old_msg_ptr += 1;
   5811            break;
   5812        }
   5813    }
   5814    messages.truncate(1000);
   5815 }
   5816 
   5817 fn log_chat_message(msg: &Message, username: &str) {
   5818    if let Ok(path) = confy::get_configuration_file_path("bhcli", None) {
   5819        if let Some(dir) = path.parent() {
   5820            let log_filename = format!("{}-log.txt", username);
   5821            let log_path = dir.join(log_filename);
   5822            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(log_path) {
   5823                let _ = writeln!(f, "{} - {}", msg.date, msg.text.text());
   5824            }
   5825        }
   5826    }
   5827 }
   5828 
   5829 fn delete_message(
   5830    client: &Client,
   5831    full_url: &str,
   5832    params: &mut Vec<(&str, String)>,
   5833    date: String,
   5834    text: String,
   5835 ) -> anyhow::Result<()> {
   5836    params.extend(vec![
   5837        ("action", "admin".to_owned()),
   5838        ("do", "clean".to_owned()),
   5839        ("what", "choose".to_owned()),
   5840    ]);
   5841    let clean_resp_txt = client.post(full_url).form(&params).send()?.text()?;
   5842    let doc = Document::from(clean_resp_txt.as_str());
   5843    let nc = doc
   5844        .find(Attr("name", "nc"))
   5845        .next()
   5846        .context("nc not found")?;
   5847    let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   5848    let msgs = extract_messages(&doc)?;
   5849    if let Some(msg) = msgs
   5850        .iter()
   5851        .find(|m| m.date == date && m.text.text() == text)
   5852    {
   5853        let msg_id = msg.id.context("msg id not found")?;
   5854        params.extend(vec![
   5855            ("nc", nc_value.to_owned()),
   5856            ("what", "selected".to_owned()),
   5857            ("mid[]", format!("{}", msg_id)),
   5858        ]);
   5859        client.post(full_url).form(&params).send()?;
   5860    }
   5861    Ok(())
   5862 }
   5863 
   5864 fn fetch_clean_messages(
   5865    client: &Client,
   5866    base_url: &str,
   5867    page_php: &str,
   5868    session: &str,
   5869 ) -> anyhow::Result<Vec<CleanMessage>> {
   5870    let full_url = format!("{}/{}", base_url, page_php);
   5871    let url = format!("{}?action=post&session={}", full_url, session);
   5872    let resp_text = client.get(&url).send()?.text()?;
   5873    let doc = Document::from(resp_text.as_str());
   5874    let nc = doc
   5875        .find(Attr("name", "nc"))
   5876        .next()
   5877        .context("nc not found")?;
   5878    let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   5879    let params = vec![
   5880        ("lang", LANG.to_owned()),
   5881        ("nc", nc_value),
   5882        ("session", session.to_owned()),
   5883        ("action", "admin".to_owned()),
   5884        ("do", "clean".to_owned()),
   5885        ("what", "choose".to_owned()),
   5886    ];
   5887    let clean_resp_txt = client.post(&full_url).form(&params).send()?.text()?;
   5888    let doc = Document::from(clean_resp_txt.as_str());
   5889 
   5890    let mut messages = Vec::new();
   5891 
   5892    // Parse the HTML for clean messages with checkboxes
   5893    for div in doc.find(Attr("class", "msg")) {
   5894        if let Some(checkbox) = div.find(Name("input")).next() {
   5895            if let Some(value) = checkbox.attr("value") {
   5896                let message_id = value.to_string();
   5897 
   5898                // Extract the message content
   5899                let full_text = div.text();
   5900 
   5901                // Parse the date, sender, and content from the message
   5902                // Format varies in clean mode, try to extract what we can
   5903                if let Some(date_end) = full_text.find(" - ") {
   5904                    let date = full_text[..date_end].trim().to_string();
   5905                    let rest = &full_text[date_end + 3..];
   5906 
   5907                    // Try to extract username and content
   5908                    let mut from = "Unknown".to_string();
   5909                    let mut content = rest.to_string();
   5910 
   5911                    // Look for patterns like [username] or <username>
   5912                    if let Some(bracket_start) = rest.find('[') {
   5913                        if let Some(bracket_end) = rest.find(']') {
   5914                            from = rest[bracket_start + 1..bracket_end].trim().to_string();
   5915                            content = rest[bracket_end + 1..]
   5916                                .trim_start_matches(" - ")
   5917                                .to_string();
   5918                        }
   5919                    } else if let Some(angle_start) = rest.find('<') {
   5920                        if let Some(angle_end) = rest.find('>') {
   5921                            from = rest[angle_start + 1..angle_end].trim().to_string();
   5922                            content = rest[angle_end + 1..].trim_start_matches(" - ").to_string();
   5923                        }
   5924                    } else {
   5925                        // If no clear username pattern, try to extract first word as username
   5926                        if let Some(space_pos) = rest.find(' ') {
   5927                            from = rest[..space_pos].trim().to_string();
   5928                            content = rest[space_pos + 1..].to_string();
   5929                        }
   5930                    }
   5931 
   5932                    messages.push(CleanMessage::new(message_id, date, from, content));
   5933                } else {
   5934                    // Fallback for messages without clear date format
   5935                    messages.push(CleanMessage::new(
   5936                        message_id,
   5937                        "Unknown".to_string(),
   5938                        "Unknown".to_string(),
   5939                        full_text,
   5940                    ));
   5941                }
   5942            }
   5943        }
   5944    }
   5945 
   5946    Ok(messages)
   5947 }
   5948 
   5949 fn fetch_inbox_messages(
   5950    client: &Client,
   5951    base_url: &str,
   5952    session: &str,
   5953 ) -> anyhow::Result<Vec<InboxMessage>> {
   5954    let url = format!("{}?action=inbox&session={}", base_url, session);
   5955 
   5956    let response = client.get(&url).send()?;
   5957    let text = response.text()?;
   5958 
   5959    let document = Document::from(text.as_str());
   5960    let mut messages = Vec::new();
   5961 
   5962    // Parse the HTML for inbox messages
   5963    for div in document.find(Attr("class", "msg")) {
   5964        if let Some(checkbox) = div.find(Name("input")).next() {
   5965            if let Some(value) = checkbox.attr("value") {
   5966                let message_id = value.to_string();
   5967 
   5968                // Extract the message content
   5969                let full_text = div.text();
   5970 
   5971                // Parse the date, sender, recipient, and content from the message
   5972                // Format: "08-17 00:56:26 - [sender to recipient] - content"
   5973                if let Some(date_end) = full_text.find(" - ") {
   5974                    let date = full_text[..date_end].trim().to_string();
   5975                    let rest = &full_text[date_end + 3..];
   5976 
   5977                    if let Some(bracket_start) = rest.find('[') {
   5978                        if let Some(bracket_end) = rest.find(']') {
   5979                            let sender_info = &rest[bracket_start + 1..bracket_end];
   5980                            let content = rest[bracket_end + 1..]
   5981                                .trim_start_matches(" - ")
   5982                                .to_string();
   5983 
   5984                            // Parse "sender to recipient"
   5985                            if let Some(to_pos) = sender_info.find(" to ") {
   5986                                let from = sender_info[..to_pos].trim().to_string();
   5987                                let to = sender_info[to_pos + 4..].trim().to_string();
   5988 
   5989                                messages
   5990                                    .push(InboxMessage::new(message_id, date, from, to, content));
   5991                            }
   5992                        }
   5993                    }
   5994                }
   5995            }
   5996        }
   5997    }
   5998 
   5999    Ok(messages)
   6000 }
   6001 
   6002 impl ChatClient {
   6003    fn new(params: Params) -> Self {
   6004        // println!("session[2026] : {:?}",params.session);
   6005        let mut c = new_default_le_chat_php_client(params.clone());
   6006        c.config.url = params.url.unwrap_or(
   6007            "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion/index.php"
   6008                .to_owned(),
   6009        );
   6010        c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned());
   6011        c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned());
   6012        c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned());
   6013        c.config.keepalive_send_to = params.keepalive_send_to.unwrap_or("0".to_owned());
   6014 
   6015        Self {
   6016            le_chat_php_client: c,
   6017            bot_manager: None,
   6018        }
   6019    }
   6020 
   6021    fn set_bot_manager(&mut self, bot_manager: Arc<Mutex<BotManager>>) {
   6022        self.le_chat_php_client.bot_manager = Some(bot_manager);
   6023    }
   6024 
   6025    fn setup_bot_message_bridge(&mut self) {
   6026        if let Some(bot_mgr) = &self.le_chat_php_client.bot_manager {
   6027            let main_tx = self.le_chat_php_client.tx.clone();
   6028            let bot_mgr_clone = Arc::clone(bot_mgr);
   6029 
   6030            // Get all bot receivers for message forwarding
   6031            let bot_receivers = if let Ok(manager) = bot_mgr_clone.lock() {
   6032                manager.get_all_bot_receivers()
   6033            } else {
   6034                Vec::new()
   6035            };
   6036 
   6037            if !bot_receivers.is_empty() {
   6038                log::info!(
   6039                    "Setting up bot message bridge for {} bots",
   6040                    bot_receivers.len()
   6041                );
   6042 
   6043                // Start a bridge thread to forward bot messages to main client
   6044                thread::spawn(move || {
   6045                    log::info!("Bot message bridge thread started");
   6046 
   6047                    loop {
   6048                        let mut any_message = false;
   6049 
   6050                        // Check messages from all bot receivers
   6051                        for (bot_name, rx) in &bot_receivers {
   6052                            if let Ok(receiver) = rx.try_lock() {
   6053                                // Try to receive messages from this bot
   6054                                while let Ok(bot_message) = receiver.try_recv() {
   6055                                    log::debug!(
   6056                                        "Bot '{}' message forwarded to main client",
   6057                                        bot_name
   6058                                    );
   6059 
   6060                                    // Forward to main client
   6061                                    if let Err(e) = main_tx.try_send(bot_message) {
   6062                                        log::warn!(
   6063                                            "Failed to forward bot message to main client: {}",
   6064                                            e
   6065                                        );
   6066                                    } else {
   6067                                        any_message = true;
   6068                                    }
   6069                                }
   6070                            }
   6071                        }
   6072 
   6073                        // If no messages were processed, sleep a bit
   6074                        if !any_message {
   6075                            thread::sleep(std::time::Duration::from_millis(10));
   6076                        }
   6077                    }
   6078                });
   6079 
   6080                log::info!("Bot message bridge setup completed");
   6081            } else {
   6082                log::warn!("No bot receivers found for message bridge");
   6083            }
   6084        }
   6085    }
   6086 
   6087    fn run_forever(&mut self) {
   6088        self.le_chat_php_client.run_forever();
   6089    }
   6090 }
   6091 
   6092 fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient {
   6093    let (color_tx, color_rx) = crossbeam_channel::unbounded();
   6094    let (tx, rx) = crossbeam_channel::unbounded();
   6095    let session = params.session.clone();
   6096 
   6097    // Store original identity values before moving params
   6098    let original_username = params.username.clone();
   6099    let original_color = params.guest_color.clone();
   6100    let username_for_manager = params.username.clone();
   6101 
   6102    // Load alt forwarding setting from config
   6103    let alt_forwarding_enabled = if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) {
   6104        cfg.alt_forwarding_enabled
   6105    } else {
   6106        true // Default to enabled
   6107    };
   6108 
   6109    // Initialize OpenAI client if API key is available
   6110    let openai_client = std::env::var("OPENAI_API_KEY").ok().map(|api_key| {
   6111        let config = OpenAIConfig::new().with_api_key(api_key);
   6112        OpenAIClient::with_config(config)
   6113    });
   6114 
   6115    // Initialize AI service and runtime
   6116    let ai_service = Arc::new(AIService::new());
   6117    let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
   6118 
   6119    // Load AI settings from profile or use defaults
   6120    let (ai_enabled, ai_mode, system_intel, moderation_strictness, mod_logs_enabled) = if let Ok(
   6121        cfg,
   6122    ) =
   6123        confy::load::<MyConfig>("bhcli", None)
   6124    {
   6125        if let Some(profile_cfg) = cfg.profiles.get(&params.profile) {
   6126            let mode = if profile_cfg.ai_mode == "mod" {
   6127                "mod_only".to_string() // Convert old "mod" mode to "mod_only"
   6128            } else {
   6129                profile_cfg.ai_mode.clone()
   6130            };
   6131            (
   6132                profile_cfg.ai_enabled, // Use the stored setting
   6133                mode,
   6134                if profile_cfg.system_intel.is_empty() {
   6135                    "You are a helpful AI assistant in a chat room. Be friendly and follow community guidelines.".to_string()
   6136                } else {
   6137                    profile_cfg.system_intel.clone()
   6138                },
   6139                profile_cfg.moderation_strictness.clone(),
   6140                profile_cfg.mod_logs_enabled,
   6141            )
   6142        } else {
   6143            (
   6144                params.ai_enabled,
   6145                params.ai_mode,
   6146                params.system_intel,
   6147                "balanced".to_string(),
   6148                true,
   6149            )
   6150        }
   6151    } else {
   6152        (
   6153            params.ai_enabled,
   6154            params.ai_mode,
   6155            params.system_intel,
   6156            "balanced".to_string(),
   6157            true,
   6158        )
   6159    };
   6160 
   6161    // println!("session[2050] : {:?}",params.session);
   6162    let mut client = LeChatPHPClient {
   6163        base_client: BaseClient {
   6164            username: params.username,
   6165            password: params.password,
   6166        },
   6167        max_login_retry: params.max_login_retry,
   6168        guest_color: params.guest_color,
   6169        // session: params.session,
   6170        session,
   6171        last_key_event: None,
   6172        client: params.client,
   6173        manual_captcha: params.manual_captcha,
   6174        sxiv: params.sxiv,
   6175        refresh_rate: params.refresh_rate,
   6176        config: if params.profile == "404_chatroom" {
   6177            LeChatPHPConfig::new_404_chatroom_not_found_config()
   6178        } else {
   6179            LeChatPHPConfig::new_black_hat_chat_config()
   6180        },
   6181        is_muted: Arc::new(Mutex::new(false)),
   6182        show_sys: false,
   6183        display_guest_view: false,
   6184        display_member_view: false,
   6185        display_hidden_msgs: false,
   6186        tx,
   6187        rx: Arc::new(Mutex::new(rx)),
   6188        color_tx,
   6189        color_rx: Arc::new(Mutex::new(color_rx)),
   6190        bad_username_filters: Arc::new(Mutex::new(params.bad_usernames)),
   6191        bad_exact_username_filters: Arc::new(Mutex::new(params.bad_exact_usernames)),
   6192        bad_message_filters: Arc::new(Mutex::new(params.bad_messages)),
   6193        allowlist: Arc::new(Mutex::new(params.allowlist)),
   6194        account_manager: {
   6195            let mut manager = AccountManager::new(username_for_manager);
   6196            if let Some(alt) = params.alt_account {
   6197                manager.set_alt_account(alt);
   6198            }
   6199            if let Some(master) = params.master_account {
   6200                manager.set_master_account(master);
   6201            }
   6202            manager
   6203        },
   6204        profile: params.profile,
   6205        display_pm_only: false,
   6206        display_staff_view: false,
   6207        display_master_pm_view: false,
   6208        clean_mode: false,
   6209        inbox_mode: false,
   6210        alt_forwarding_enabled: Arc::new(Mutex::new(alt_forwarding_enabled)),
   6211        current_username: original_username,
   6212        current_color: original_color,
   6213        ai_enabled: Arc::new(Mutex::new(ai_enabled)),
   6214        ai_mode: Arc::new(Mutex::new(ai_mode)),
   6215        system_intel,
   6216        moderation_strictness,
   6217        mod_logs_enabled: Arc::new(Mutex::new(mod_logs_enabled)),
   6218        openai_client,
   6219        ai_conversation_memory: Arc::new(Mutex::new(std::collections::HashMap::new())),
   6220        user_warnings: Arc::new(Mutex::new(std::collections::HashMap::new())),
   6221        identities: params.identities,
   6222        chatops_router: if ai_service.is_available() {
   6223            ChatOpsRouter::new_with_ai(Arc::clone(&ai_service), Arc::clone(&runtime))
   6224        } else {
   6225            ChatOpsRouter::new()
   6226        },
   6227        ai_service: Arc::clone(&ai_service),
   6228        runtime: Arc::clone(&runtime),
   6229        bot_manager: None,
   6230    };
   6231 
   6232    // Initialize default identities
   6233    client.ensure_default_identities();
   6234 
   6235    client
   6236 }
   6237 
   6238 struct ChatClient {
   6239    le_chat_php_client: LeChatPHPClient,
   6240    #[allow(dead_code)]
   6241    bot_manager: Option<Arc<Mutex<BotManager>>>,
   6242 }
   6243 
   6244 #[derive(Debug, Clone)]
   6245 struct Params {
   6246    url: Option<String>,
   6247    page_php: Option<String>,
   6248    datetime_fmt: Option<String>,
   6249    members_tag: Option<String>,
   6250    username: String,
   6251    password: String,
   6252    guest_color: String,
   6253    client: Client,
   6254    manual_captcha: bool,
   6255    sxiv: bool,
   6256    refresh_rate: u64,
   6257    max_login_retry: isize,
   6258    keepalive_send_to: Option<String>,
   6259    session: Option<String>,
   6260    bad_usernames: Vec<String>,
   6261    bad_exact_usernames: Vec<String>,
   6262    bad_messages: Vec<String>,
   6263    allowlist: Vec<String>,
   6264    alt_account: Option<String>,
   6265    master_account: Option<String>,
   6266    profile: String,
   6267    ai_enabled: bool,
   6268    ai_mode: String,
   6269    system_intel: String,
   6270    identities: HashMap<String, Vec<String>>,
   6271 }
   6272 
   6273 #[derive(Clone)]
   6274 enum ExitSignal {
   6275    Terminate,
   6276    NeedLogin,
   6277 }
   6278 struct Sig {
   6279    tx: crossbeam_channel::Sender<ExitSignal>,
   6280    rx: crossbeam_channel::Receiver<ExitSignal>,
   6281    nb_rx: usize,
   6282 }
   6283 
   6284 impl Sig {
   6285    fn new() -> Self {
   6286        let (tx, rx) = crossbeam_channel::unbounded();
   6287        let nb_rx = 0;
   6288        Self { tx, rx, nb_rx }
   6289    }
   6290 
   6291    fn clone(&mut self) -> crossbeam_channel::Receiver<ExitSignal> {
   6292        self.nb_rx += 1;
   6293        self.rx.clone()
   6294    }
   6295 
   6296    fn signal(&self, signal: &ExitSignal) {
   6297        for _ in 0..self.nb_rx {
   6298            self.tx.send(signal.clone()).unwrap();
   6299        }
   6300    }
   6301 }
   6302 
   6303 fn trim_newline(s: &mut String) {
   6304    if s.ends_with('\n') {
   6305        s.pop();
   6306        if s.ends_with('\r') {
   6307            s.pop();
   6308        }
   6309    }
   6310 }
   6311 
   6312 fn replace_newline_escape(s: &str) -> String {
   6313    s.replace("\\n", "\n")
   6314 }
   6315 
   6316 fn get_guest_color(wanted: Option<String>) -> String {
   6317    match wanted.as_deref() {
   6318        Some("beige") => "F5F5DC",
   6319        Some("blue-violet") => "8A2BE2",
   6320        Some("brown") => "A52A2A",
   6321        Some("cyan") => "00FFFF",
   6322        Some("sky-blue") => "00BFFF",
   6323        Some("gold") => "FFD700",
   6324        Some("gray") => "808080",
   6325        Some("green") => "008000",
   6326        Some("hot-pink") => "FF69B4",
   6327        Some("light-blue") => "ADD8E6",
   6328        Some("light-green") => "90EE90",
   6329        Some("lime-green") => "32CD32",
   6330        Some("magenta") => "FF00FF",
   6331        Some("olive") => "808000",
   6332        Some("orange") => "FFA500",
   6333        Some("orange-red") => "FF4500",
   6334        Some("red") => "FF0000",
   6335        Some("royal-blue") => "4169E1",
   6336        Some("see-green") => "2E8B57",
   6337        Some("sienna") => "A0522D",
   6338        Some("silver") => "C0C0C0",
   6339        Some("tan") => "D2B48C",
   6340        Some("teal") => "008080",
   6341        Some("violet") => "EE82EE",
   6342        Some("white") => "FFFFFF",
   6343        Some("yellow") => "FFFF00",
   6344        Some("yellow-green") => "9ACD32",
   6345        Some(other) => COLOR1_RGX
   6346            .captures(other)
   6347            .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())),
   6348        None => "",
   6349    }
   6350    .to_owned()
   6351 }
   6352 
   6353 fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client {
   6354    let ua = "Dasho's Black Hat Chat Client v1.0-Epic";
   6355    let mut builder = reqwest::blocking::ClientBuilder::new()
   6356        .redirect(Policy::none())
   6357        .cookie_store(true)
   6358        .user_agent(ua);
   6359    if !no_proxy {
   6360        let proxy = reqwest::Proxy::all(socks_proxy_url).unwrap();
   6361        builder = builder.proxy(proxy);
   6362    }
   6363    builder.build().unwrap()
   6364 }
   6365 
   6366 fn ask_username(username: Option<String>) -> String {
   6367    username.unwrap_or_else(|| {
   6368        print!("username: ");
   6369        let mut username_input = String::new();
   6370        io::stdout().flush().unwrap();
   6371        io::stdin().read_line(&mut username_input).unwrap();
   6372        trim_newline(&mut username_input);
   6373        username_input
   6374    })
   6375 }
   6376 
   6377 fn ask_password(password: Option<String>) -> String {
   6378    password.unwrap_or_else(|| rpassword::prompt_password("Password: ").unwrap())
   6379 }
   6380 
   6381 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
   6382 #[serde(rename_all = "camelCase")]
   6383 pub struct DkfNotifierResp {
   6384    #[serde(rename = "NewMessageSound")]
   6385    pub new_message_sound: bool,
   6386    #[serde(rename = "TaggedSound")]
   6387    pub tagged_sound: bool,
   6388    #[serde(rename = "PmSound")]
   6389    pub pm_sound: bool,
   6390    #[serde(rename = "InboxCount")]
   6391    pub inbox_count: i64,
   6392    #[serde(rename = "LastMessageCreatedAt")]
   6393    pub last_message_created_at: String,
   6394 }
   6395 
   6396 fn start_dkf_notifier(client: &Client, dkf_api_key: &str) {
   6397    let client = client.clone();
   6398    let dkf_api_key = dkf_api_key.to_owned();
   6399    let mut last_known_date = Utc::now();
   6400    thread::spawn(move || {
   6401        #[cfg(feature = "audio")]
   6402        let audio_output = OutputStream::try_default().ok();
   6403        #[cfg(feature = "audio")]
   6404        let stream_handle = audio_output.as_ref().map(|(_, handle)| handle);
   6405 
   6406        loop {
   6407            let params: Vec<(&str, String)> = vec![(
   6408                "last_known_date",
   6409                last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
   6410            )];
   6411            let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL);
   6412            if let Ok(resp) = client
   6413                .post(right_url)
   6414                .form(&params)
   6415                .header("DKF_API_KEY", &dkf_api_key)
   6416                .send()
   6417            {
   6418                if let Ok(txt) = resp.text() {
   6419                    if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) {
   6420                        if v.pm_sound || v.tagged_sound {
   6421                            #[cfg(feature = "audio")]
   6422                            if let Some(handle) = &stream_handle {
   6423                                if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) {
   6424                                    let _ = handle.play_raw(source.convert_samples());
   6425                                }
   6426                            }
   6427                        }
   6428                        last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at)
   6429                            .unwrap()
   6430                            .with_timezone(&Utc);
   6431                    }
   6432                }
   6433            }
   6434            thread::sleep(Duration::from_secs(5));
   6435        }
   6436    });
   6437 }
   6438 
   6439 // Start thread that looks for new emails on DNMX every minutes.
   6440 fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) {
   6441    let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)];
   6442    let login_url = format!("{}/src/redirect.php", DNMX_URL);
   6443    client.post(login_url).form(&params).send().unwrap();
   6444 
   6445    let client_clone = client.clone();
   6446    thread::spawn(move || {
   6447        #[cfg(feature = "audio")]
   6448        let audio_output = OutputStream::try_default().ok();
   6449        #[cfg(feature = "audio")]
   6450        let stream_handle = audio_output.as_ref().map(|(_, handle)| handle);
   6451 
   6452        loop {
   6453            let right_url = format!("{}/src/right_main.php", DNMX_URL);
   6454            if let Ok(resp) = client_clone.get(right_url).send() {
   6455                let mut nb_mails = 0;
   6456                let doc = Document::from(resp.text().unwrap().as_str());
   6457                if let Some(table) = doc.find(Name("table")).nth(7) {
   6458                    table.find(Name("tr")).skip(1).for_each(|n| {
   6459                        if let Some(td) = n.find(Name("td")).nth(2) {
   6460                            if td.find(Name("b")).nth(0).is_some() {
   6461                                nb_mails += 1;
   6462                            }
   6463                        }
   6464                    });
   6465                }
   6466                if nb_mails > 0 {
   6467                    log::error!("{} new mails", nb_mails);
   6468                    #[cfg(feature = "audio")]
   6469                    if let Some(handle) = &stream_handle {
   6470                        if let Ok(source) = Decoder::new_mp3(Cursor::new(SOUND1)) {
   6471                            let _ = handle.play_raw(source.convert_samples());
   6472                        }
   6473                    }
   6474                }
   6475            }
   6476            thread::sleep(Duration::from_secs(60));
   6477        }
   6478    });
   6479 }
   6480 
   6481 //Strange
   6482 #[derive(Debug, Deserialize)]
   6483 struct Commands {
   6484    commands: HashMap<String, String>,
   6485 }
   6486 
   6487 impl Default for Commands {
   6488    fn default() -> Self {
   6489        Commands {
   6490            commands: HashMap::new(), // Initialize commands with empty HashMap
   6491        }
   6492    }
   6493 }
   6494 
   6495 // Strange
   6496 // Function to read the configuration file and parse it
   6497 fn read_commands_file(file_path: &str) -> Result<Commands, Box<dyn std::error::Error>> {
   6498    // Read the contents of the file
   6499    let commands_content = std::fs::read_to_string(file_path)?;
   6500    // log::error!("Read file contents: {}", commands_content);
   6501    // Deserialize the contents into a Commands struct
   6502    let commands: Commands = toml::from_str(&commands_content)?;
   6503    // log::error!(
   6504    //     "Deserialized file contents into Commands struct: {:?}",
   6505    //     commands
   6506    // );
   6507 
   6508    Ok(commands)
   6509 }
   6510 
   6511 // Install man page on first run
   6512 fn install_manpage() -> anyhow::Result<()> {
   6513    const MANPAGE_CONTENT: &str = include_str!("../manpage/bhcli.1");
   6514 
   6515    let home = std::env::var("HOME")?;
   6516    let man_dir = format!("{}/.local/share/man/man1", home);
   6517    let man_path = format!("{}/bhcli.1", man_dir);
   6518 
   6519    // Check if man page already exists
   6520    if std::path::Path::new(&man_path).exists() {
   6521        return Ok(());
   6522    }
   6523 
   6524    // Create directory if it doesn't exist
   6525    std::fs::create_dir_all(&man_dir)?;
   6526 
   6527    // Write man page
   6528    std::fs::write(&man_path, MANPAGE_CONTENT)?;
   6529 
   6530    // Update man database (try both user and system mandb commands)
   6531    // Ignore errors if mandb fails (it's not critical)
   6532    let _ = Command::new("mandb")
   6533        .arg("-u")
   6534        .arg(&format!("{}/.local/share/man", home))
   6535        .output();
   6536 
   6537    println!("Man page installed to {}", man_path);
   6538    println!("Access it anytime with: man bhcli");
   6539    println!();
   6540 
   6541    Ok(())
   6542 }
   6543 
   6544 fn main() -> anyhow::Result<()> {
   6545    // Install man page on first run
   6546    let _ = install_manpage();
   6547 
   6548    let mut opts: Opts = Opts::parse();
   6549    
   6550    // If --404 flag is set, use the 404_chatroom profile
   6551    if opts.use_404 {
   6552        opts.profile = "404_chatroom".to_string();
   6553    }
   6554    
   6555    // println!("Parsed Session: {:?}", opts.session);
   6556 
   6557    // Configs file
   6558    if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) {
   6559        println!("Config path: {:?}", config_path);
   6560    }
   6561    let mut alt_account = None;
   6562    let mut master_account = None;
   6563    let mut identities = HashMap::new();
   6564    if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) {
   6565        if opts.dkf_api_key.is_none() {
   6566            opts.dkf_api_key = cfg.dkf_api_key;
   6567        }
   6568        if let Some(default_profile) = cfg.profiles.get(&opts.profile) {
   6569            if opts.username.is_none() {
   6570                opts.username = Some(default_profile.username.clone());
   6571                opts.password = Some(default_profile.password.clone());
   6572            }
   6573            identities = default_profile.identities.clone();
   6574        }
   6575        let bad_usernames = cfg.bad_usernames.clone();
   6576        let bad_exact_usernames = cfg.bad_exact_usernames.clone();
   6577        let bad_messages = cfg.bad_messages.clone();
   6578        let allowlist_cfg = cfg.allowlist.clone();
   6579        opts.bad_usernames = Some(bad_usernames);
   6580        opts.bad_exact_usernames = Some(bad_exact_usernames);
   6581        opts.bad_messages = Some(bad_messages);
   6582        opts.allowlist = Some(allowlist_cfg);
   6583        if let Some(profile_cfg) = cfg.profiles.get(&opts.profile) {
   6584            alt_account = profile_cfg.alt_account.clone().or(cfg.alt_account);
   6585            master_account = profile_cfg.master_account.clone().or(cfg.master_account);
   6586        } else {
   6587            alt_account = cfg.alt_account;
   6588            master_account = cfg.master_account;
   6589        }
   6590    }
   6591 
   6592    let logfile = FileAppender::builder()
   6593        .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}")))
   6594        .build("bhcli.log")?;
   6595 
   6596    let config = log4rs::config::Config::builder()
   6597        .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile)))
   6598        .build(
   6599            log4rs::config::Root::builder()
   6600                .appender("logfile")
   6601                .build(LevelFilter::Error),
   6602        )?;
   6603 
   6604    log4rs::init_config(config)?;
   6605 
   6606    let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy);
   6607 
   6608    // If dnmx username is set, start mail notifier thread
   6609    if let Some(dnmx_username) = opts.dnmx_username {
   6610        start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap())
   6611    }
   6612 
   6613    if let Some(dkf_api_key) = &opts.dkf_api_key {
   6614        start_dkf_notifier(&client, dkf_api_key);
   6615    }
   6616 
   6617    let guest_color = get_guest_color(opts.guest_color);
   6618    let username = ask_username(opts.username);
   6619    let password = ask_password(opts.password);
   6620 
   6621    let params = Params {
   6622        url: opts.url,
   6623        page_php: opts.page_php,
   6624        datetime_fmt: opts.datetime_fmt,
   6625        members_tag: opts.members_tag,
   6626        username,
   6627        password,
   6628        guest_color,
   6629        client: client.clone(),
   6630        manual_captcha: opts.manual_captcha,
   6631        sxiv: opts.sxiv,
   6632        refresh_rate: opts.refresh_rate,
   6633        max_login_retry: opts.max_login_retry,
   6634        keepalive_send_to: opts.keepalive_send_to,
   6635        session: opts.session.clone(),
   6636        bad_usernames: opts.bad_usernames.unwrap_or_default(),
   6637        bad_exact_usernames: opts.bad_exact_usernames.unwrap_or_default(),
   6638        bad_messages: opts.bad_messages.unwrap_or_default(),
   6639        allowlist: opts.allowlist.unwrap_or_default(),
   6640        alt_account,
   6641        master_account,
   6642        profile: opts.profile.clone(),
   6643        ai_enabled: false,  // Disable AI by default
   6644        ai_mode: "off".to_string(),
   6645        system_intel: "You are a helpful AI assistant in a chat room. Be friendly and follow community guidelines.".to_string(),
   6646        identities,
   6647    };
   6648    // println!("Session[2378]: {:?}", opts.session);
   6649 
   6650    // Initialize bot system if bot parameter is provided
   6651    let bot_manager = if let Some(bot_name) = &opts.bot {
   6652        let ai_service = Arc::new(AIService::new());
   6653        let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
   6654 
   6655        let mut bot_manager = BotManager::new(Some(ai_service), Some(runtime));
   6656 
   6657        // Configure bot data directory
   6658        let _bot_data_dir = opts
   6659            .bot_data_dir
   6660            .clone()
   6661            .unwrap_or_else(|| format!("bot_data/{}", bot_name));
   6662 
   6663        // Use same credentials as main client
   6664        let bot_url = params.url.clone().unwrap_or_else(|| {
   6665            "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion/index.php"
   6666                .to_string()
   6667        });
   6668 
   6669        match bot_manager.add_bot(
   6670            bot_name.clone(),
   6671            params.username.clone(),
   6672            params.password.clone(),
   6673            bot_url,
   6674            opts.bot_admins.clone(),
   6675        ) {
   6676            Ok(_) => {
   6677                println!("🤖 Bot '{}' configured successfully", bot_name);
   6678 
   6679                // Start the bot
   6680                if let Err(e) = bot_manager.start_bot(bot_name) {
   6681                    eprintln!("❌ Failed to start bot '{}': {}", bot_name, e);
   6682                } else {
   6683                    println!("🚀 Bot '{}' started and running in background", bot_name);
   6684                }
   6685            }
   6686            Err(e) => {
   6687                eprintln!("❌ Failed to configure bot '{}': {}", bot_name, e);
   6688            }
   6689        }
   6690 
   6691        Some(Arc::new(Mutex::new(bot_manager)))
   6692    } else {
   6693        None
   6694    };
   6695 
   6696    // Pass bot_manager to ChatClient
   6697    let mut chat_client = ChatClient::new(params);
   6698    if let Some(bot_mgr) = &bot_manager {
   6699        chat_client.set_bot_manager(Arc::clone(bot_mgr));
   6700        // Create bridge between bot messages and main client
   6701        chat_client.setup_bot_message_bridge();
   6702    }
   6703    chat_client.run_forever();
   6704 
   6705    // Clean up bot system when main client exits
   6706    if let Some(bot_mgr) = bot_manager {
   6707        println!("🔄 Shutting down bot system...");
   6708        if let Err(e) = bot_mgr.lock().unwrap().stop_all() {
   6709            eprintln!("⚠️ Error stopping bot system: {}", e);
   6710        } else {
   6711            println!("✅ Bot system stopped successfully");
   6712        }
   6713    }
   6714 
   6715    Ok(())
   6716 }
   6717 
   6718 #[derive(Debug, Clone)]
   6719 enum PostType {
   6720    Post(String, Option<String>),              // Message, SendTo
   6721    PM(String, String),                        // To, Message
   6722    Kick(String, String),                      // Message, Username
   6723    Upload(String, String, String),            // FilePath, SendTo, Message
   6724    DeleteLast,                                // DeleteLast
   6725    Delete(String),                            // Delete message
   6726    DeleteAll,                                 // DeleteAll
   6727    KeepAlive(String),                         // SendTo for keepalive
   6728    NewNickname(String),                       // NewUsername
   6729    NewColor(String),                          // NewColor
   6730    Profile(String, String, bool, bool, bool), // NewColor, NewUsername, Incognito, Bold, Italic
   6731    SetIncognito(bool),                        // Set incognito mode on/off
   6732    Ignore(String),                            // Username
   6733    Unignore(String),                          // Username
   6734    Clean(String, String),                     // Clean message
   6735 }
   6736 
   6737 // Get username of other user (or ours if it's the only one)
   6738 fn get_username(
   6739    own_username: &str,
   6740    root: &StyledText,
   6741    members_tag: &str,
   6742    staffs_tag: &str,
   6743 ) -> Option<String> {
   6744    match get_message(root, members_tag, staffs_tag) {
   6745        Some((from, Some(to), _, _)) => {
   6746            if from == own_username {
   6747                return Some(to);
   6748            }
   6749            return Some(from);
   6750        }
   6751        Some((from, None, _, _)) => {
   6752            return Some(from);
   6753        }
   6754        _ => return None,
   6755    }
   6756 }
   6757 
   6758 // Extract "from"/"to"/"message content" from a "StyledText"
   6759 fn get_message(
   6760    root: &StyledText,
   6761    members_tag: &str,
   6762    staffs_tag: &str,
   6763 ) -> Option<(String, Option<String>, String, Option<String>)> { // Added channel info
   6764    if let StyledText::Styled(_, children) = root {
   6765        let msg = children.get(0)?.text();
   6766        match children.get(children.len() - 1)? {
   6767            StyledText::Styled(_, children) => {
   6768                let from = match children.get(children.len() - 1)? {
   6769                    StyledText::Text(t) => t.to_owned(),
   6770                    _ => return None,
   6771                };
   6772                return Some((from, None, msg, None)); // Public channel
   6773            }
   6774            StyledText::Text(t) => {
   6775                if t == &members_tag {
   6776                    let from = match children.get(children.len() - 2)? {
   6777                        StyledText::Styled(_, children) => {
   6778                            match children.get(children.len() - 1)? {
   6779                                StyledText::Text(t) => t.to_owned(),
   6780                                _ => return None,
   6781                            }
   6782                        }
   6783                        _ => return None,
   6784                    };
   6785                    return Some((from, None, msg, Some("members".to_string())));
   6786                } else if t == &staffs_tag {
   6787                    let from = match children.get(children.len() - 2)? {
   6788                        StyledText::Styled(_, children) => {
   6789                            match children.get(children.len() - 1)? {
   6790                                StyledText::Text(t) => t.to_owned(),
   6791                                _ => return None,
   6792                            }
   6793                        }
   6794                        _ => return None,
   6795                    };
   6796                    return Some((from, None, msg, Some("staff".to_string())));
   6797                } else if t == "[" {
   6798                    let from = match children.get(children.len() - 2)? {
   6799                        StyledText::Styled(_, children) => {
   6800                            match children.get(children.len() - 1)? {
   6801                                StyledText::Text(t) => t.to_owned(),
   6802                                _ => return None,
   6803                            }
   6804                        }
   6805                        _ => return None,
   6806                    };
   6807                    let to = match children.get(2)? {
   6808                        StyledText::Styled(_, children) => {
   6809                            match children.get(children.len() - 1)? {
   6810                                StyledText::Text(t) => Some(t.to_owned()),
   6811                                _ => return None,
   6812                            }
   6813                        }
   6814                        _ => return None,
   6815                    };
   6816                    return Some((from, to, msg, None)); // Private message
   6817                }
   6818            }
   6819            _ => return None,
   6820        }
   6821    }
   6822    return None;
   6823 }
   6824 
   6825 #[derive(Debug, PartialEq, Clone)]
   6826 enum MessageType {
   6827    UserMsg,
   6828    SysMsg,
   6829 }
   6830 
   6831 #[derive(Debug, PartialEq, Clone)]
   6832 struct Message {
   6833    id: Option<usize>,
   6834    typ: MessageType,
   6835    date: String,
   6836    upload_link: Option<String>,
   6837    text: StyledText,
   6838    deleted: bool, // Either or not a message was deleted on the chat
   6839    hide: bool,    // Either ot not to hide a specific message
   6840 }
   6841 
   6842 impl Message {
   6843    fn new(
   6844        id: Option<usize>,
   6845        typ: MessageType,
   6846        date: String,
   6847        upload_link: Option<String>,
   6848        text: StyledText,
   6849    ) -> Self {
   6850        Self {
   6851            id,
   6852            typ,
   6853            date,
   6854            upload_link,
   6855            text,
   6856            deleted: false,
   6857            hide: false,
   6858        }
   6859    }
   6860 }
   6861 
   6862 #[derive(Debug, Clone)]
   6863 struct InboxMessage {
   6864    id: String,      // message ID for deletion
   6865    date: String,    // formatted date string
   6866    from: String,    // sender username
   6867    to: String,      // recipient (usually "0" or username)
   6868    content: String, // message content
   6869    selected: bool,  // for deletion selection
   6870 }
   6871 
   6872 impl InboxMessage {
   6873    fn new(id: String, date: String, from: String, to: String, content: String) -> Self {
   6874        Self {
   6875            id,
   6876            date,
   6877            from,
   6878            to,
   6879            content,
   6880            selected: false,
   6881        }
   6882    }
   6883 }
   6884 
   6885 #[derive(Debug, Clone)]
   6886 struct CleanMessage {
   6887    id: String,   // message ID for deletion
   6888    date: String, // formatted date string
   6889    #[allow(dead_code)]
   6890    from: String, // sender username
   6891    content: String, // message content
   6892    selected: bool, // for deletion selection
   6893 }
   6894 
   6895 impl CleanMessage {
   6896    fn new(id: String, date: String, from: String, content: String) -> Self {
   6897        Self {
   6898            id,
   6899            date,
   6900            from,
   6901            content,
   6902            selected: false,
   6903        }
   6904    }
   6905 }
   6906 
   6907 #[derive(Debug, PartialEq, Clone)]
   6908 enum StyledText {
   6909    Styled(tuiColor, Vec<StyledText>),
   6910    Text(String),
   6911    None,
   6912 }
   6913 
   6914 impl StyledText {
   6915    fn walk<F>(&self, mut clb: F)
   6916    where
   6917        F: FnMut(&StyledText),
   6918    {
   6919        let mut v: Vec<&StyledText> = vec![self];
   6920        loop {
   6921            if let Some(e) = v.pop() {
   6922                clb(e);
   6923                if let StyledText::Styled(_, children) = e {
   6924                    v.extend(children);
   6925                }
   6926                continue;
   6927            }
   6928            break;
   6929        }
   6930    }
   6931 
   6932    fn text(&self) -> String {
   6933        let mut s = String::new();
   6934        self.walk(|n| {
   6935            if let StyledText::Text(t) = n {
   6936                s += t;
   6937            }
   6938        });
   6939        s
   6940    }
   6941 
   6942    // Return a vector of each text parts & what color it should be
   6943    fn colored_text(&self) -> Vec<(tuiColor, String)> {
   6944        let mut out: Vec<(tuiColor, String)> = vec![];
   6945        let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)];
   6946        loop {
   6947            if let Some((el_color, e)) = v.pop() {
   6948                match e {
   6949                    StyledText::Styled(tui_color, children) => {
   6950                        for child in children {
   6951                            v.push((*tui_color, child));
   6952                        }
   6953                    }
   6954                    StyledText::Text(t) => {
   6955                        out.push((el_color, t.to_owned()));
   6956                    }
   6957                    StyledText::None => {}
   6958                }
   6959                continue;
   6960            }
   6961            break;
   6962        }
   6963        out
   6964    }
   6965 }
   6966 
   6967 fn parse_color(color_str: &str) -> tuiColor {
   6968    let mut color = tuiColor::White;
   6969    if color_str == "red" {
   6970        return tuiColor::Red;
   6971    }
   6972    if let Ok(rgb) = Rgb::from_hex_str(color_str) {
   6973        color = tuiColor::Rgb(
   6974            rgb.get_red() as u8,
   6975            rgb.get_green() as u8,
   6976            rgb.get_blue() as u8,
   6977        );
   6978    }
   6979    color
   6980 }
   6981 
   6982 fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option<String>) {
   6983    match e.data() {
   6984        select::node::Data::Element(_, _) => {
   6985            let mut upload_link: Option<String> = None;
   6986            match e.name() {
   6987                Some("span") => {
   6988                    if let Some(style) = e.attr("style") {
   6989                        if let Some(captures) = COLOR_RGX.captures(style) {
   6990                            let color_match = captures.get(1).unwrap().as_str();
   6991                            color = parse_color(color_match);
   6992                        }
   6993                    }
   6994                }
   6995                Some("font") => {
   6996                    if let Some(color_str) = e.attr("color") {
   6997                        color = parse_color(color_str);
   6998                    }
   6999                }
   7000                Some("a") => {
   7001                    color = tuiColor::White;
   7002                    if let (Some("attachement"), Some(href)) = (e.attr("class"), e.attr("href")) {
   7003                        upload_link = Some(href.to_owned());
   7004                    }
   7005                }
   7006                Some("style") => {
   7007                    return (StyledText::None, None);
   7008                }
   7009                Some("form") | Some("button") | Some("input") | Some("textarea")
   7010                | Some("select") | Some("option") | Some("script") | Some("noscript")
   7011                | Some("iframe") | Some("details") | Some("summary") | Some("label") => {
   7012                    // Strip out form elements and script elements that can break terminal rendering
   7013                    return (StyledText::None, None);
   7014                }
   7015                _ => {}
   7016            }
   7017            let mut children_texts: Vec<StyledText> = vec![];
   7018            let children = e.children();
   7019            for child in children {
   7020                let (st, ul) = process_node(child, color);
   7021                if ul.is_some() {
   7022                    upload_link = ul;
   7023                }
   7024                children_texts.push(st);
   7025            }
   7026            children_texts.reverse();
   7027            (StyledText::Styled(color, children_texts), upload_link)
   7028        }
   7029        select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None),
   7030        select::node::Data::Comment(_) => (StyledText::None, None),
   7031    }
   7032 }
   7033 
   7034 #[derive(Clone)]
   7035 struct Users {
   7036    admin: Vec<(tuiColor, String)>,
   7037    staff: Vec<(tuiColor, String)>,
   7038    members: Vec<(tuiColor, String)>,
   7039    guests: Vec<(tuiColor, String)>,
   7040 }
   7041 
   7042 impl Default for Users {
   7043    fn default() -> Self {
   7044        Self {
   7045            admin: Default::default(),
   7046            staff: Default::default(),
   7047            members: Default::default(),
   7048            guests: Default::default(),
   7049        }
   7050    }
   7051 }
   7052 
   7053 impl Users {
   7054    fn all(&self) -> Vec<&(tuiColor, String)> {
   7055        let mut out = Vec::new();
   7056        out.extend(&self.admin);
   7057        out.extend(&self.staff);
   7058        out.extend(&self.members);
   7059        out.extend(&self.guests);
   7060        out
   7061    }
   7062 
   7063    // fn is_guest(&self, name: &str) -> bool {
   7064    //     self.guests.iter().find(|(_, username)| username == name).is_some()
   7065    // }
   7066 }
   7067 
   7068 fn extract_users(doc: &Document) -> Users {
   7069    let mut users = Users::default();
   7070 
   7071    if let Some(chatters) = doc.find(Attr("id", "chatters")).next() {
   7072        if let Some(tr) = chatters.find(Name("tr")).next() {
   7073            let mut th_count = 0;
   7074            for e in tr.children() {
   7075                if let select::node::Data::Element(_, _) = e.data() {
   7076                    if e.name() == Some("th") {
   7077                        th_count += 1;
   7078                        continue;
   7079                    }
   7080                    for user_span in e.find(Name("span")) {
   7081                        if let Some(user_style) = user_span.attr("style") {
   7082                            if let Some(captures) = COLOR_RGX.captures(user_style) {
   7083                                if let Some(color_match) = captures.get(1) {
   7084                                    let color = color_match.as_str().to_owned();
   7085                                    let tui_color = parse_color(&color);
   7086                                    let username = user_span.text();
   7087                                    match th_count {
   7088                                        1 => users.admin.push((tui_color, username)),
   7089                                        2 => users.staff.push((tui_color, username)),
   7090                                        3 => users.members.push((tui_color, username)),
   7091                                        4 => users.guests.push((tui_color, username)),
   7092                                        _ => {}
   7093                                    }
   7094                                }
   7095                            }
   7096                        }
   7097                    }
   7098                }
   7099            }
   7100        }
   7101    }
   7102    users
   7103 }
   7104 
   7105 fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str {
   7106    s.strip_suffix(suffix).unwrap_or(s)
   7107 }
   7108 
   7109 fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
   7110    s.strip_prefix(prefix).unwrap_or(s)
   7111 }
   7112 
   7113 fn parse_forwarded_username(
   7114    text: &str,
   7115    members_tag: &str,
   7116    staffs_tag: &str,
   7117 ) -> Option<(&'static str, String)> {
   7118    lazy_static! {
   7119        static ref FORWARD_RGX: Regex = Regex::new(r"^\[[^\]]+ to [^\]]+\]\s*").unwrap();
   7120    }
   7121 
   7122    if let Some(mat) = FORWARD_RGX.find(text) {
   7123        let mut rest = text[mat.end()..].trim_start();
   7124        // Some forwarded messages contain a leading dash or colon after the
   7125        // forwarding header. Trim those so we can properly match the tags.
   7126        rest = rest
   7127            .trim_start_matches(|c: char| c == '-' || c == ':')
   7128            .trim_start();
   7129 
   7130        if let Some(rem) = rest.strip_prefix(members_tag) {
   7131            let name = rem
   7132                .trim_start()
   7133                .split(|c: char| c == ' ' || c == ':' || c == '-')
   7134                .next()
   7135                .unwrap_or("")
   7136                .trim_matches('@')
   7137                .to_owned();
   7138            return Some(("/m", name));
   7139        } else if let Some(rem) = rest.strip_prefix(staffs_tag) {
   7140            let name = rem
   7141                .trim_start()
   7142                .split(|c: char| c == ' ' || c == ':' || c == '-')
   7143                .next()
   7144                .unwrap_or("")
   7145                .trim_matches('@')
   7146                .to_owned();
   7147            return Some(("/s", name));
   7148        }
   7149    }
   7150    None
   7151 }
   7152 
   7153 fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> {
   7154    let msgs = doc
   7155        .find(Attr("id", "messages"))
   7156        .next()
   7157        .ok_or(anyhow!("failed to get messages div"))?
   7158        .find(Attr("class", "msg"))
   7159        .filter_map(|tag| {
   7160            let mut id: Option<usize> = None;
   7161            if let Some(checkbox) = tag.find(Name("input")).next() {
   7162                if let Some(value_attr) = checkbox.attr("value") {
   7163                    if !value_attr.is_empty() {
   7164                        match value_attr.parse::<usize>() {
   7165                            Ok(val) => id = Some(val),
   7166                            Err(_) => {
   7167                                // Silently skip invalid message IDs instead of printing error
   7168                                // This is common when parsing HTML that might have malformed or missing attributes
   7169                            }
   7170                        }
   7171                    }
   7172                    // Silently skip checkboxes without value attributes - this is normal
   7173                }
   7174            }
   7175            if let Some(date_node) = tag.find(Name("small")).next() {
   7176                if let Some(msg_span) = tag.find(Name("span")).next() {
   7177                    let date = remove_suffix(&date_node.text(), " - ").to_owned();
   7178                    let typ = match msg_span.attr("class") {
   7179                        Some("usermsg") => MessageType::UserMsg,
   7180                        Some("sysmsg") => MessageType::SysMsg,
   7181                        _ => return None,
   7182                    };
   7183                    let (text, upload_link) = process_node(msg_span, tuiColor::White);
   7184                    return Some(Message::new(id, typ, date, upload_link, text));
   7185                }
   7186            }
   7187            None
   7188        })
   7189        .collect::<Vec<_>>();
   7190    Ok(msgs)
   7191 }
   7192 
   7193 fn draw_notes_pane(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) {
   7194    use tui::layout::{Constraint, Direction, Layout};
   7195    use tui::style::{Color, Modifier, Style};
   7196    use tui::text::{Span, Spans};
   7197    use tui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
   7198 
   7199    let size = f.size();
   7200    
   7201    // Clear the entire screen
   7202    f.render_widget(Clear, size);
   7203    
   7204    // Create main layout
   7205    let chunks = Layout::default()
   7206        .direction(Direction::Vertical)
   7207        .constraints([
   7208            Constraint::Length(3), // Header
   7209            Constraint::Min(1),    // Content
   7210            Constraint::Length(3), // Status/command line
   7211        ])
   7212        .split(size);
   7213 
   7214    // Header with note type and tabs
   7215    let current_type = app.get_current_notes_type();
   7216    let mut header_spans = vec![
   7217        Span::styled("Notes: ", Style::default().fg(Color::Yellow)),
   7218    ];
   7219    
   7220    for (i, note_type) in app.notes_available_types.iter().enumerate() {
   7221        if i == app.notes_type_index {
   7222            header_spans.push(Span::styled(
   7223                format!("[{}]", note_type),
   7224                Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD),
   7225            ));
   7226        } else {
   7227            header_spans.push(Span::styled(
   7228                format!(" {} ", note_type),
   7229                Style::default().fg(Color::Gray),
   7230            ));
   7231        }
   7232        if i < app.notes_available_types.len() - 1 {
   7233            header_spans.push(Span::raw(" "));
   7234        }
   7235    }
   7236    header_spans.push(Span::raw(" | Tab to cycle | :w to save | :q to quit | :wq to save & quit"));
   7237 
   7238    let header = Paragraph::new(Spans::from(header_spans))
   7239        .block(Block::default().borders(Borders::ALL).title("BHCLI Notes"))
   7240        .wrap(Wrap { trim: true });
   7241    f.render_widget(header, chunks[0]);
   7242 
   7243    // Content area with text and scrolling support
   7244    let content_height = chunks[1].height.saturating_sub(2) as usize; // Account for borders
   7245    let visible_start = app.notes_scroll_offset;
   7246    let visible_end = std::cmp::min(visible_start + content_height, app.notes_content.len());
   7247    
   7248    let content_lines: Vec<Spans> = app.notes_content[visible_start..visible_end].iter().enumerate().map(|(visible_idx, line)| {
   7249        let line_idx = visible_start + visible_idx;
   7250        let mut spans = vec![];
   7251        
   7252        // Handle empty lines by showing at least a space with cursor if on this line
   7253        let display_line = if line.is_empty() && line_idx == app.notes_cursor_pos.0 {
   7254            " "
   7255        } else {
   7256            line
   7257        };
   7258        
   7259        // Determine if this line has visual selection
   7260        let has_visual_selection = app.notes_vim_mode == VimMode::Visual && 
   7261            app.notes_visual_start.is_some() &&
   7262            line_idx == app.notes_cursor_pos.0;
   7263        
   7264        for (col_idx, ch) in display_line.char_indices() {
   7265            let mut style = Style::default();
   7266            
   7267            // Cursor highlighting
   7268            if line_idx == app.notes_cursor_pos.0 {
   7269                if col_idx == app.notes_cursor_pos.1 {
   7270                    match app.notes_vim_mode {
   7271                        VimMode::Normal => {
   7272                            style = Style::default().bg(Color::Gray).fg(Color::Black);
   7273                        }
   7274                        VimMode::Insert => {
   7275                            style = Style::default().bg(Color::Yellow).fg(Color::Black);
   7276                        }
   7277                        _ => {}
   7278                    }
   7279                }
   7280            }
   7281            
   7282            // Visual selection highlighting
   7283            if has_visual_selection {
   7284                if let Some(start_pos) = app.notes_visual_start {
   7285                    let current_pos = (line_idx, col_idx);
   7286                    let selection_start = if start_pos <= app.notes_cursor_pos { start_pos } else { app.notes_cursor_pos };
   7287                    let selection_end = if start_pos <= app.notes_cursor_pos { app.notes_cursor_pos } else { start_pos };
   7288                    
   7289                    if current_pos >= selection_start && current_pos < selection_end {
   7290                        style = Style::default().bg(Color::Blue).fg(Color::White);
   7291                    }
   7292                }
   7293            }
   7294            
   7295            spans.push(Span::styled(ch.to_string(), style));
   7296        }
   7297        
   7298        // Add cursor at end of line if needed (for empty lines or when cursor is at end)
   7299        if line_idx == app.notes_cursor_pos.0 && app.notes_cursor_pos.1 >= line.len() {
   7300            match app.notes_vim_mode {
   7301                VimMode::Normal => {
   7302                    // Show cursor as highlighted space
   7303                    spans.push(Span::styled(" ", Style::default().bg(Color::Gray)));
   7304                }
   7305                VimMode::Insert => {
   7306                    // Show cursor as yellow pipe
   7307                    spans.push(Span::styled("|", Style::default().fg(Color::Yellow)));
   7308                }
   7309                _ => {}
   7310            }
   7311        }
   7312        
   7313        // For completely empty lines not at cursor position, add a fake space to show the line exists
   7314        if spans.is_empty() {
   7315            spans.push(Span::raw(" "));
   7316        }
   7317        
   7318        Spans::from(spans)
   7319    }).collect();
   7320 
   7321    // Determine border color based on vim mode
   7322    let border_color = match app.notes_vim_mode {
   7323        VimMode::Insert => Color::LightBlue,
   7324        VimMode::Visual => Color::Green,
   7325        _ => Color::White,
   7326    };
   7327 
   7328    let content_block = Block::default()
   7329        .borders(Borders::ALL)
   7330        .border_style(Style::default().fg(border_color))
   7331        .title(format!("{} Notes", current_type));
   7332    let content = Paragraph::new(content_lines)
   7333        .block(content_block)
   7334        .wrap(Wrap { trim: false });
   7335    f.render_widget(content, chunks[1]);
   7336 
   7337    // Status line
   7338    let status_text = if app.notes_search_mode {
   7339        format!("/{}", app.notes_search_query)
   7340    } else {
   7341        match app.notes_vim_mode {
   7342            VimMode::Normal => {
   7343                let modified = if app.notes_modified { " [modified]" } else { "" };
   7344                let last_edited = app.notes_last_edited.as_deref().unwrap_or("never");
   7345                let number_prefix = if let Some(ref prefix) = app.notes_number_prefix {
   7346                    format!("{}", prefix)
   7347                } else {
   7348                    String::new()
   7349                };
   7350                
   7351                let search_info = if let Some(current_idx) = app.notes_current_match_index {
   7352                    format!(" | Match ({}/{})", current_idx + 1, app.notes_search_matches.len())
   7353                } else {
   7354                    String::new()
   7355                };
   7356                
   7357                format!("-- NORMAL --{} | {}Line {}, Col {} | Last edited: {}{} | w/b:word $:end 0:start /{{pattern}}:search n/N:next/prev", 
   7358                        modified, 
   7359                        number_prefix,
   7360                        app.notes_cursor_pos.0 + 1, 
   7361                        app.notes_cursor_pos.1 + 1,
   7362                        last_edited,
   7363                        search_info)
   7364            }
   7365            VimMode::Insert => {
   7366                format!("-- INSERT -- | Line {}, Col {} | Use arrow keys or hjkl to navigate", 
   7367                        app.notes_cursor_pos.0 + 1, 
   7368                        app.notes_cursor_pos.1 + 1)
   7369            }
   7370            VimMode::Visual => {
   7371                let selection_info = if let Some(start) = app.notes_visual_start {
   7372                    format!(" | Selection: {}:{} to {}:{}", 
   7373                           start.0 + 1, start.1 + 1,
   7374                           app.notes_cursor_pos.0 + 1, app.notes_cursor_pos.1 + 1)
   7375                } else {
   7376                    String::new()
   7377                };
   7378                format!("-- VISUAL --{} | Press x to delete selection", selection_info)
   7379            }
   7380            VimMode::Command => {
   7381                format!(":{}", app.notes_vim_command)
   7382            }
   7383        }
   7384    };
   7385 
   7386    let status = Paragraph::new(status_text)
   7387        .block(Block::default().borders(Borders::ALL));
   7388    f.render_widget(status, chunks[2]);
   7389 }
   7390 
   7391 fn draw_message_editor_ui(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App) {
   7392    use tui::layout::{Constraint, Direction, Layout};
   7393    use tui::style::{Color, Style};
   7394    use tui::text::{Span, Spans};
   7395    use tui::widgets::{Block, Borders, Clear, Paragraph, Wrap};
   7396 
   7397    let size = f.size();
   7398    
   7399    // Clear the entire screen
   7400    f.render_widget(Clear, size);
   7401    
   7402    // Create main layout
   7403    let chunks = Layout::default()
   7404        .direction(Direction::Vertical)
   7405        .constraints([
   7406            Constraint::Length(3), // Header
   7407            Constraint::Min(1),    // Content
   7408            Constraint::Length(3), // Status/command line
   7409        ])
   7410        .split(size);
   7411 
   7412    // Header
   7413    let header_text = Spans::from(vec![
   7414        Span::styled("Message Editor", Style::default().fg(Color::Yellow)),
   7415        Span::raw(" - Press :w to send, :q to cancel"),
   7416    ]);
   7417    let header = Paragraph::new(header_text)
   7418        .block(Block::default().borders(Borders::ALL).title("Editor"));
   7419    f.render_widget(header, chunks[0]);
   7420 
   7421    // Determine border color based on mode
   7422    let border_color = match app.msg_editor_vim_mode {
   7423        VimMode::Insert => Color::LightBlue,
   7424        VimMode::Visual => Color::Green, 
   7425        _ => Color::White,
   7426    };
   7427 
   7428    // Content area with scrolling
   7429    let content_height = chunks[1].height.saturating_sub(2) as usize; // Account for borders
   7430    
   7431    // Calculate visible content range based on cursor and scroll
   7432    let total_lines = app.msg_editor_content.len().max(1);
   7433    let cursor_line = app.msg_editor_cursor_pos.0;
   7434    
   7435    // Ensure cursor is visible
   7436    if cursor_line < app.msg_editor_scroll_offset {
   7437        app.msg_editor_scroll_offset = cursor_line;
   7438    } else if cursor_line >= app.msg_editor_scroll_offset + content_height {
   7439        app.msg_editor_scroll_offset = cursor_line.saturating_sub(content_height - 1);
   7440    }
   7441    
   7442    // Get visible lines - use same cursor logic as notes editor
   7443    let end_line = (app.msg_editor_scroll_offset + content_height).min(total_lines);
   7444    let visible_content: Vec<_> = app.msg_editor_content
   7445        .get(app.msg_editor_scroll_offset..end_line)
   7446        .unwrap_or(&[])
   7447        .iter()
   7448        .enumerate()
   7449        .map(|(i, line)| {
   7450            let line_num = app.msg_editor_scroll_offset + i;
   7451            let mut spans = vec![];
   7452            
   7453            // Handle empty lines by showing at least a space with cursor if on this line
   7454            let display_line = if line.is_empty() && line_num == cursor_line {
   7455                " "
   7456            } else {
   7457                line
   7458            };
   7459            
   7460            // Determine if this line has visual selection
   7461            let has_visual_selection = app.msg_editor_vim_mode == VimMode::Visual && 
   7462                app.msg_editor_visual_start.is_some() &&
   7463                line_num == cursor_line;
   7464            
   7465            for (col_idx, ch) in display_line.char_indices() {
   7466                let mut style = Style::default();
   7467                
   7468                // Cursor highlighting
   7469                if line_num == cursor_line {
   7470                    if col_idx == app.msg_editor_cursor_pos.1 {
   7471                        match app.msg_editor_vim_mode {
   7472                            VimMode::Normal => {
   7473                                style = Style::default().bg(Color::Gray).fg(Color::Black);
   7474                            }
   7475                            VimMode::Insert => {
   7476                                style = Style::default().bg(Color::Yellow).fg(Color::Black);
   7477                            }
   7478                            _ => {}
   7479                        }
   7480                    }
   7481                }
   7482                
   7483                // Visual selection highlighting
   7484                if has_visual_selection {
   7485                    if let Some((start_line, start_col)) = app.msg_editor_visual_start {
   7486                        let start_pos = (start_line, start_col);
   7487                        let current_pos = (line_num, col_idx);
   7488                        let selection_start = if start_pos <= app.msg_editor_cursor_pos { start_pos } else { app.msg_editor_cursor_pos };
   7489                        let selection_end = if start_pos <= app.msg_editor_cursor_pos { app.msg_editor_cursor_pos } else { start_pos };
   7490                        
   7491                        if current_pos >= selection_start && current_pos < selection_end {
   7492                            style = Style::default().bg(Color::Blue).fg(Color::White);
   7493                        }
   7494                    }
   7495                }
   7496                
   7497                spans.push(Span::styled(ch.to_string(), style));
   7498            }
   7499            
   7500            // Add cursor at end of line if needed (for empty lines or when cursor is at end)
   7501            if line_num == cursor_line && app.msg_editor_cursor_pos.1 >= line.len() {
   7502                match app.msg_editor_vim_mode {
   7503                    VimMode::Normal => {
   7504                        // Show cursor as highlighted space
   7505                        spans.push(Span::styled(" ", Style::default().bg(Color::Gray)));
   7506                    }
   7507                    VimMode::Insert => {
   7508                        // Show cursor as yellow pipe
   7509                        spans.push(Span::styled("|", Style::default().fg(Color::Yellow)));
   7510                    }
   7511                    _ => {}
   7512                }
   7513            }
   7514            
   7515            // For completely empty lines not at cursor position, add a fake space to show the line exists
   7516            if spans.is_empty() {
   7517                spans.push(Span::raw(" "));
   7518            }
   7519            
   7520            Spans::from(spans)
   7521        })
   7522        .collect();
   7523 
   7524    let content = Paragraph::new(visible_content)
   7525        .block(
   7526            Block::default()
   7527                .borders(Borders::ALL)
   7528                .title("Message")
   7529                .border_style(Style::default().fg(border_color))
   7530        )
   7531        .wrap(Wrap { trim: false });
   7532    f.render_widget(content, chunks[1]);
   7533 
   7534    // Status line
   7535    let status_text = if app.msg_editor_search_mode {
   7536        Spans::from(vec![
   7537            Span::styled(format!("/{}", app.msg_editor_search_query), Style::default().fg(Color::Cyan)),
   7538        ])
   7539    } else {
   7540        let mode_text = match app.msg_editor_vim_mode {
   7541            VimMode::Normal => "NORMAL",
   7542            VimMode::Insert => "INSERT", 
   7543            VimMode::Command => "COMMAND",
   7544            VimMode::Visual => "VISUAL",
   7545        };
   7546        
   7547        let number_prefix = if let Some(ref prefix) = app.msg_editor_number_prefix {
   7548            format!("{}", prefix)
   7549        } else {
   7550            String::new()
   7551        };
   7552        
   7553        match app.msg_editor_vim_mode {
   7554            VimMode::Normal => {
   7555                let search_info = if let Some(current_idx) = app.msg_editor_current_match_index {
   7556                    format!(" | Match ({}/{})", current_idx + 1, app.msg_editor_search_matches.len())
   7557                } else {
   7558                    String::new()
   7559                };
   7560                
   7561                Spans::from(vec![
   7562                    Span::styled(format!("-- {} --", mode_text), Style::default().fg(Color::Yellow)),
   7563                    Span::raw(format!(" | {}Cursor: {}:{} | Lines: {}{} | w/b:word $:end 0:start /{{pattern}}:search n/N:next/prev | :w to send, :q to cancel", 
   7564                             number_prefix,
   7565                             app.msg_editor_cursor_pos.0 + 1, 
   7566                             app.msg_editor_cursor_pos.1 + 1,
   7567                             app.msg_editor_content.len(),
   7568                             search_info)),
   7569                ])
   7570            }
   7571            VimMode::Command => {
   7572                Spans::from(vec![
   7573                    Span::styled(format!(":{}", app.msg_editor_vim_command), Style::default().fg(Color::Cyan)),
   7574                ])
   7575            }
   7576            _ => {
   7577                Spans::from(vec![
   7578                    Span::styled(format!("-- {} --", mode_text), Style::default().fg(Color::Yellow)),
   7579                    Span::raw(format!(" | Cursor: {}:{} | Lines: {} | :w to send, :q to cancel", 
   7580                             app.msg_editor_cursor_pos.0 + 1, 
   7581                             app.msg_editor_cursor_pos.1 + 1,
   7582                             app.msg_editor_content.len())),
   7583                ])
   7584            }
   7585        }
   7586    };
   7587    
   7588    let status = Paragraph::new(status_text)
   7589        .block(Block::default().borders(Borders::ALL));
   7590    f.render_widget(status, chunks[2]);
   7591 }
   7592 
   7593 fn draw_terminal_frame(
   7594    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   7595    app: &mut App,
   7596    messages: &Arc<Mutex<Vec<Message>>>,
   7597    users: &Arc<Mutex<Users>>,
   7598    username: &str,
   7599 ) {
   7600    if app.notes_mode {
   7601        draw_notes_pane(f, app);
   7602        return;
   7603    }
   7604    
   7605    if app.msg_editor_mode {
   7606        draw_message_editor_ui(f, app);
   7607        return;
   7608    }
   7609    
   7610    if app.long_message.is_none() {
   7611        let hchunks = Layout::default()
   7612            .direction(Direction::Horizontal)
   7613            .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref())
   7614            .split(f.size());
   7615 
   7616        {
   7617            // Determine textbox height based on input mode
   7618            let textbox_height = match app.input_mode {
   7619                InputMode::MultilineEditing => 8, // Larger height for multiline mode
   7620                _ => 3,                           // Default height for single-line modes
   7621            };
   7622 
   7623            let chunks = Layout::default()
   7624                .direction(Direction::Vertical)
   7625                .constraints(
   7626                    [
   7627                        Constraint::Length(1),
   7628                        Constraint::Length(textbox_height),
   7629                        Constraint::Min(1),
   7630                    ]
   7631                    .as_ref(),
   7632                )
   7633                .split(hchunks[0]);
   7634 
   7635            render_help_txt(f, app, chunks[0], username);
   7636            render_textbox(f, app, chunks[1]);
   7637            if app.clean_mode {
   7638                render_clean_messages(f, app, chunks[2]);
   7639            } else {
   7640                render_messages(f, app, chunks[2], messages);
   7641            }
   7642            render_users(f, hchunks[1], users);
   7643        }
   7644    } else {
   7645        let hchunks = Layout::default()
   7646            .direction(Direction::Horizontal)
   7647            .constraints([Constraint::Min(1)])
   7648            .split(f.size());
   7649        {
   7650            render_long_message(f, app, hchunks[0]);
   7651        }
   7652    }
   7653 }
   7654 
   7655 fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiColor, String)>> {
   7656    let txt = msg_txt.text();
   7657 
   7658    // For simple text (like help messages), use a much simpler approach
   7659    // Check if this looks like plain text content (no HTML, just text with newlines)
   7660    let is_plain_text = !txt.contains('<')
   7661        && !txt.contains('>')
   7662        && msg_txt
   7663            .colored_text()
   7664            .iter()
   7665            .all(|(color, _)| *color == tuiColor::White);
   7666 
   7667    if is_plain_text {
   7668        // This is plain text, handle it simply
   7669        let mut result = Vec::new();
   7670 
   7671        // Split by existing newlines first to preserve intended line breaks
   7672        for original_line in txt.split('\n') {
   7673            if original_line.len() <= w {
   7674                // Line fits, add it as-is
   7675                result.push(vec![(tuiColor::White, original_line.to_string())]);
   7676            } else {
   7677                // Line is too long, wrap it
   7678                let wrapped = textwrap::fill(original_line, w);
   7679                for wrapped_line in wrapped.split('\n') {
   7680                    result.push(vec![(tuiColor::White, wrapped_line.to_string())]);
   7681                }
   7682            }
   7683        }
   7684        return result;
   7685    }
   7686 
   7687    // Fallback to original complex logic for colored text
   7688    let original_lines: Vec<&str> = txt.split('\n').collect();
   7689    let mut wrapped_lines = Vec::new();
   7690 
   7691    // Only wrap individual lines that are too long
   7692    for line in original_lines {
   7693        if line.len() <= w {
   7694            wrapped_lines.push(line.to_string());
   7695        } else {
   7696            // Use textwrap only on lines that are actually too long
   7697            let wrapped = textwrap::fill(line, w);
   7698            for wrapped_line in wrapped.split('\n') {
   7699                wrapped_lines.push(wrapped_line.to_string());
   7700            }
   7701        }
   7702    }
   7703 
   7704    let splits = wrapped_lines
   7705        .iter()
   7706        .map(|s| s.as_str())
   7707        .collect::<Vec<&str>>();
   7708    let mut new_lines: Vec<Vec<(tuiColor, String)>> = Vec::new();
   7709    let mut ctxt = msg_txt.colored_text();
   7710    ctxt.reverse();
   7711    let mut ptr = 0;
   7712    let mut split_idx = 0;
   7713    let mut line: Vec<(tuiColor, String)> = Vec::new();
   7714    let mut first_in_line = true;
   7715    loop {
   7716        if let Some((color, mut txt)) = ctxt.pop() {
   7717            txt = txt.replace("\n", "");
   7718            if let Some(split) = splits.get(split_idx) {
   7719                if let Some(chr) = txt.chars().next() {
   7720                    if chr == ' ' && first_in_line {
   7721                        let skipped: String = txt.chars().skip(1).collect();
   7722                        txt = skipped;
   7723                    }
   7724                }
   7725 
   7726                let remain = split.len() - ptr;
   7727                if txt.len() <= remain {
   7728                    ptr += txt.len();
   7729                    line.push((color, txt));
   7730                    first_in_line = false;
   7731                } else {
   7732                    //line.push((color, txt[0..remain].to_owned()));
   7733                    if let Some(valid_slice) = txt.get(0..remain) {
   7734                        line.push((color, valid_slice.to_owned()));
   7735                    } else {
   7736                        let valid_remain = txt
   7737                            .char_indices()
   7738                            .take_while(|&(i, _)| i < remain)
   7739                            .last()
   7740                            .map(|(i, _)| i)
   7741                            .unwrap_or(txt.len());
   7742 
   7743                        line.push((color, txt[..valid_remain].to_owned()));
   7744                    }
   7745 
   7746                    new_lines.push(line.clone());
   7747                    line.clear();
   7748                    line.push((tuiColor::White, line_prefix.to_owned()));
   7749                    //ctxt.push((color, txt[(remain)..].to_owned()));
   7750                    if let Some(valid_slice) = txt.get(remain..) {
   7751                        ctxt.push((color, valid_slice.to_owned()));
   7752                    } else {
   7753                        let valid_remain = txt
   7754                            .char_indices()
   7755                            .skip_while(|&(i, _)| i < remain) // Find first valid boundary after remain
   7756                            .map(|(i, _)| i)
   7757                            .next()
   7758                            .unwrap_or(txt.len());
   7759 
   7760                        ctxt.push((color, txt[valid_remain..].to_owned()));
   7761                    }
   7762 
   7763                    ptr = 0;
   7764                    split_idx += 1;
   7765                    first_in_line = true;
   7766                }
   7767            }
   7768        } else {
   7769            new_lines.push(line);
   7770            break;
   7771        }
   7772    }
   7773    new_lines
   7774 }
   7775 
   7776 fn render_long_message(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   7777    if let Some(m) = &app.long_message {
   7778        let new_lines = gen_lines(&m.text, (r.width - 2) as usize, "");
   7779 
   7780        let mut rows = vec![];
   7781        for line in new_lines.into_iter() {
   7782            let spans_vec: Vec<Span> = line
   7783                .into_iter()
   7784                .map(|(color, txt)| Span::styled(txt, Style::default().fg(color)))
   7785                .collect();
   7786            rows.push(Spans::from(spans_vec));
   7787        }
   7788 
   7789        // Calculate how many lines can be displayed in the available height
   7790        let available_height = (r.height - 2) as usize; // -2 for borders
   7791        let total_lines = rows.len();
   7792 
   7793        // Adjust scroll offset to prevent scrolling beyond content
   7794        let max_scroll = if total_lines > available_height {
   7795            total_lines - available_height
   7796        } else {
   7797            0
   7798        };
   7799        app.long_message_scroll_offset = app.long_message_scroll_offset.min(max_scroll);
   7800 
   7801        // Apply scrolling by taking a slice of the rows
   7802        let visible_rows = if total_lines > available_height {
   7803            rows.into_iter()
   7804                .skip(app.long_message_scroll_offset)
   7805                .take(available_height)
   7806                .collect()
   7807        } else {
   7808            rows
   7809        };
   7810 
   7811        let messages_list_items: Vec<ListItem> = visible_rows
   7812            .into_iter()
   7813            .map(|spans| ListItem::new(spans))
   7814            .collect();
   7815 
   7816        let title = if total_lines > available_height {
   7817            format!("Message (line {}/{}) - j/k or ↑/↓ to scroll, PgUp/PgDn for fast scroll, Enter/Esc to exit",
   7818                    app.long_message_scroll_offset + 1,
   7819                    total_lines)
   7820        } else {
   7821            "Message - Enter/Esc to exit".to_string()
   7822        };
   7823 
   7824        let messages_list = List::new(messages_list_items)
   7825            .block(Block::default().borders(Borders::ALL).title(title))
   7826            .highlight_style(
   7827                Style::default()
   7828                    .bg(tuiColor::Rgb(50, 50, 50))
   7829                    .add_modifier(Modifier::BOLD),
   7830            );
   7831 
   7832        f.render_widget(messages_list, r);
   7833    }
   7834 }
   7835 
   7836 fn render_help_txt(
   7837    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   7838    app: &mut App,
   7839    r: Rect,
   7840    curr_user: &str,
   7841 ) {
   7842    let (mut msg, style) = match app.input_mode {
   7843        InputMode::Normal => (
   7844            vec![
   7845                Span::raw("Press "),
   7846                Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
   7847                Span::raw(" to exit, "),
   7848                Span::styled("Q", Style::default().add_modifier(Modifier::BOLD)),
   7849                Span::raw(" to logout, "),
   7850                Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
   7851                Span::raw(" to start editing."),
   7852            ],
   7853            Style::default(),
   7854        ),
   7855        InputMode::Editing | InputMode::EditingErr => (
   7856            vec![
   7857                Span::raw("Press "),
   7858                Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
   7859                Span::raw(" to stop editing, "),
   7860                Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
   7861                Span::raw(" to record the message"),
   7862            ],
   7863            Style::default(),
   7864        ),
   7865        InputMode::LongMessage => (vec![], Style::default()),
   7866        InputMode::MultilineEditing => (
   7867            vec![
   7868                Span::raw("Press "),
   7869                Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
   7870                Span::raw(" to exit multiline mode, "),
   7871                Span::styled("Ctrl+L", Style::default().add_modifier(Modifier::BOLD)),
   7872                Span::raw(" to send"),
   7873            ],
   7874            Style::default(),
   7875        ),
   7876        InputMode::Notes => (vec![], Style::default()),
   7877        InputMode::MessageEditor => (vec![], Style::default()),
   7878    };
   7879    msg.extend(vec![Span::raw(format!(" | {}", curr_user))]);
   7880    if app.is_muted {
   7881        let fg = tuiColor::Red;
   7882        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7883        msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]);
   7884    } else {
   7885        let fg = tuiColor::LightGreen;
   7886        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7887        msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]);
   7888    }
   7889 
   7890    //Strange
   7891    if app.display_guest_view {
   7892        let fg = tuiColor::LightGreen;
   7893        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7894        msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   7895    } else {
   7896        let fg = tuiColor::Gray;
   7897        let style = Style::default().fg(fg);
   7898        msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   7899    }
   7900 
   7901    //Strange
   7902    if app.display_member_view {
   7903        let fg = tuiColor::LightGreen;
   7904        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7905        msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   7906    } else {
   7907        let fg = tuiColor::Gray;
   7908        let style = Style::default().fg(fg);
   7909        msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   7910    }
   7911 
   7912    if app.display_hidden_msgs {
   7913        let fg = tuiColor::LightGreen;
   7914        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7915        msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   7916    } else {
   7917        let fg = tuiColor::Gray;
   7918        let style = Style::default().fg(fg);
   7919        msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   7920    }
   7921 
   7922    if app.clean_mode {
   7923        let fg = tuiColor::LightGreen;
   7924        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7925        msg.extend(vec![Span::raw(" | "), Span::styled("C", style)]);
   7926    } else {
   7927        let fg = tuiColor::Gray;
   7928        let style = Style::default().fg(fg);
   7929        msg.extend(vec![Span::raw(" | "), Span::styled("C", style)]);
   7930    }
   7931 
   7932    if app.inbox_mode {
   7933        let fg = tuiColor::LightBlue;
   7934        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   7935        msg.extend(vec![Span::raw(" | "), Span::styled("O", style)]);
   7936    } else {
   7937        let fg = tuiColor::Gray;
   7938        let style = Style::default().fg(fg);
   7939        msg.extend(vec![Span::raw(" | "), Span::styled("O", style)]);
   7940    }
   7941    let mut text = Text::from(Spans::from(msg));
   7942    text.patch_style(style);
   7943    let help_message = Paragraph::new(text);
   7944    f.render_widget(help_message, r);
   7945 }
   7946 
   7947 fn render_textbox(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   7948    let w = (r.width - 3) as usize;
   7949    let str = app.input.clone();
   7950 
   7951    // Handle multiline vs single line display differently
   7952    let (input_widget, cursor_x, cursor_y) = match app.input_mode {
   7953        InputMode::MultilineEditing => {
   7954            // For multiline, we need to properly handle line wrapping and newlines
   7955            let lines: Vec<&str> = str.split('\n').collect();
   7956            let text_width = (r.width - 3) as usize; // Account for borders
   7957            let available_height = (r.height - 2) as usize; // Account for borders
   7958 
   7959            // Calculate total visual lines (including wrapped lines)
   7960            let mut total_visual_lines = 0;
   7961            let mut line_visual_counts = Vec::new();
   7962            for line in &lines {
   7963                let line_len = line.chars().count();
   7964                let visual_count = if line_len == 0 {
   7965                    1
   7966                } else {
   7967                    (line_len + text_width - 1) / text_width
   7968                };
   7969                line_visual_counts.push(visual_count);
   7970                total_visual_lines += visual_count;
   7971            }
   7972 
   7973            // Calculate which line the cursor is on and position within that line
   7974            let mut cursor_line = 0;
   7975            let mut chars_before_cursor = 0;
   7976            let mut current_pos = 0;
   7977            let mut cursor_visual_line = 0; // Track visual lines including wrapping
   7978 
   7979            for (line_idx, line) in lines.iter().enumerate() {
   7980                let line_len = line.chars().count();
   7981                if current_pos + line_len >= app.input_idx {
   7982                    cursor_line = line_idx;
   7983                    chars_before_cursor = app.input_idx - current_pos;
   7984 
   7985                    // Calculate how many visual lines this cursor position creates due to wrapping
   7986                    let chars_in_current_line = chars_before_cursor;
   7987                    let wrapped_lines_before = chars_in_current_line / text_width;
   7988                    cursor_visual_line += wrapped_lines_before;
   7989                    chars_before_cursor = chars_in_current_line % text_width;
   7990                    break;
   7991                }
   7992                current_pos += line_len + 1; // +1 for the newline character
   7993                cursor_visual_line += line_visual_counts[line_idx];
   7994            }
   7995 
   7996            // Ensure cursor is within bounds
   7997            if cursor_line < lines.len() {
   7998                let current_line_len = lines[cursor_line].chars().count();
   7999                chars_before_cursor = chars_before_cursor.min(current_line_len % text_width);
   8000            }
   8001 
   8002            // Auto-scroll to keep cursor visible
   8003            if cursor_visual_line < app.multiline_scroll_offset {
   8004                app.multiline_scroll_offset = cursor_visual_line;
   8005            } else if cursor_visual_line >= app.multiline_scroll_offset + available_height {
   8006                app.multiline_scroll_offset = cursor_visual_line - available_height + 1;
   8007            }
   8008 
   8009            // Ensure scroll offset doesn't exceed content
   8010            if total_visual_lines <= available_height {
   8011                app.multiline_scroll_offset = 0;
   8012            } else {
   8013                app.multiline_scroll_offset = app
   8014                    .multiline_scroll_offset
   8015                    .min(total_visual_lines - available_height);
   8016            }
   8017 
   8018            // Create the paragraph with proper line breaks and scrolling
   8019            let input = Paragraph::new(str.as_str())
   8020                .style(Style::default().fg(tuiColor::Cyan))
   8021                .block(
   8022                    Block::default()
   8023                        .borders(Borders::ALL)
   8024                        .title("Input (Multiline)"),
   8025                )
   8026                .wrap(Wrap { trim: false })
   8027                .scroll((app.multiline_scroll_offset as u16, 0));
   8028 
   8029            // Calculate cursor position accounting for wrapping and scrolling
   8030            let cursor_x = r.x + 1 + chars_before_cursor as u16;
   8031            let cursor_y = r.y + 1 + (cursor_visual_line - app.multiline_scroll_offset) as u16;
   8032 
   8033            (input, cursor_x, cursor_y)
   8034        }
   8035        _ => {
   8036            // Single line handling (existing logic)
   8037            let mut input_str = str.as_str();
   8038            let mut overflow = 0;
   8039            if app.input_idx >= w {
   8040                overflow = std::cmp::max(app.input.width() - w, 0);
   8041                input_str = &str[overflow..];
   8042            }
   8043 
   8044            let input = Paragraph::new(input_str)
   8045                .style(match app.input_mode {
   8046                    InputMode::LongMessage => Style::default(),
   8047                    InputMode::Normal => Style::default(),
   8048                    InputMode::Editing => Style::default().fg(tuiColor::Yellow),
   8049                    InputMode::EditingErr => Style::default().fg(tuiColor::Red),
   8050                    InputMode::MultilineEditing => Style::default().fg(tuiColor::Cyan),
   8051                    InputMode::Notes => Style::default(),
   8052                    InputMode::MessageEditor => Style::default(),
   8053                })
   8054                .block(Block::default().borders(Borders::ALL).title("Input"));
   8055 
   8056            let cursor_x = r.x + app.input_idx as u16 - overflow as u16 + 1;
   8057            let cursor_y = r.y + 1;
   8058 
   8059            (input, cursor_x, cursor_y)
   8060        }
   8061    };
   8062 
   8063    f.render_widget(input_widget, r);
   8064 
   8065    // Set cursor position based on input mode
   8066    match app.input_mode {
   8067        InputMode::LongMessage => {}
   8068        InputMode::Normal => {}
   8069        InputMode::Editing | InputMode::EditingErr | InputMode::MultilineEditing => {
   8070            // Make the cursor visible and position it correctly
   8071            f.set_cursor(cursor_x, cursor_y);
   8072        }
   8073        InputMode::Notes => {}
   8074        InputMode::MessageEditor => {}
   8075    }
   8076 }
   8077 
   8078 fn render_messages(
   8079    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   8080    app: &mut App,
   8081    r: Rect,
   8082    messages: &Arc<Mutex<Vec<Message>>>,
   8083 ) {
   8084    if app.inbox_mode {
   8085        render_inbox_messages(f, app, r);
   8086        return;
   8087    }
   8088 
   8089    // Messages
   8090    app.items.items.clear();
   8091    let messages = messages.lock().unwrap();
   8092    let messages_list_items: Vec<ListItem> = messages
   8093        .iter()
   8094        .filter_map(|m| {
   8095            if app.clean_mode {
   8096                // In clean mode show all messages
   8097            } else {
   8098                if !app.display_hidden_msgs && m.hide {
   8099                    return None;
   8100                }
   8101                // Simulate a guest view (remove "PMs" and "Members chat" messages)
   8102                if app.display_guest_view {
   8103                    // TODO: this is not efficient at all
   8104                    let text = m.text.text();
   8105                    if text.starts_with(&app.members_tag) || text.starts_with(&app.staffs_tag) {
   8106                        return None;
   8107                    }
   8108                    if let Some((_, Some(_), _, _)) =
   8109                        get_message(&m.text, &app.members_tag, &app.staffs_tag)
   8110                    {
   8111                        return None;
   8112                    }
   8113                }
   8114 
   8115                // Strange
   8116                // Display only messages from members and staff
   8117                if app.display_member_view {
   8118                    // In members mode, include only messages from members and staff
   8119                    let text = m.text.text();
   8120                    if !text.starts_with(&app.members_tag) && !text.starts_with(&app.staffs_tag) {
   8121                        return None;
   8122                    }
   8123                    if let Some((_, Some(_), _, _)) =
   8124                        get_message(&m.text, &app.members_tag, &app.staffs_tag)
   8125                    {
   8126                        return None;
   8127                    }
   8128                }
   8129 
   8130                if app.display_pm_only {
   8131                    match get_message(&m.text, &app.members_tag, &app.staffs_tag) {
   8132                        Some((_, Some(_), _, _)) => {}
   8133                        _ => return None,
   8134                    }
   8135                }
   8136 
   8137                if app.display_staff_view {
   8138                    let text = m.text.text();
   8139                    if !text.starts_with(&app.staffs_tag) {
   8140                        return None;
   8141                    }
   8142                }
   8143 
   8144                if app.display_master_pm_view {
   8145                    // Master PM view filtering is now handled by client-level account manager
   8146                    // This view mode is only enabled when master account is configured
   8147                    match get_message(&m.text, &app.members_tag, &app.staffs_tag) {
   8148                        Some((_, Some(_), _, _)) => {
   8149                            // Show PMs when in master PM view mode
   8150                        }
   8151                        _ => return None,
   8152                    }
   8153                }
   8154 
   8155                if app.filter != "" {
   8156                    if !m
   8157                        .text
   8158                        .text()
   8159                        .to_lowercase()
   8160                        .contains(&app.filter.to_lowercase())
   8161                    {
   8162                        return None;
   8163                    }
   8164                }
   8165            }
   8166 
   8167            app.items.items.push(m.clone());
   8168 
   8169            let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17).as_str());
   8170 
   8171            let mut rows = vec![];
   8172            let date_style = match (m.deleted, m.hide) {
   8173                (false, true) => Style::default().fg(tuiColor::Gray),
   8174                (false, _) => Style::default().fg(tuiColor::DarkGray),
   8175                (true, _) => Style::default().fg(tuiColor::Red),
   8176            };
   8177            let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)];
   8178            let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg;
   8179            let sep = if show_sys_sep { " * " } else { " - " };
   8180            spans_vec.push(Span::raw(sep));
   8181            for (idx, line) in new_lines.into_iter().enumerate() {
   8182                // Spams can take your whole screen, so we limit to 5 lines.
   8183                if idx >= 5 {
   8184                    spans_vec.push(Span::styled(
   8185                        "                 […]",
   8186                        Style::default().fg(tuiColor::White),
   8187                    ));
   8188                    rows.push(Spans::from(spans_vec));
   8189                    break;
   8190                }
   8191                for (color, txt) in line {
   8192                    spans_vec.push(Span::styled(txt, Style::default().fg(color)));
   8193                }
   8194                rows.push(Spans::from(spans_vec.clone()));
   8195                spans_vec.clear();
   8196            }
   8197 
   8198            let style = match (m.deleted, m.hide) {
   8199                (true, _) => Style::default().bg(tuiColor::Rgb(30, 0, 0)),
   8200                (_, true) => Style::default().bg(tuiColor::Rgb(20, 20, 20)),
   8201                _ => Style::default(),
   8202            };
   8203            Some(ListItem::new(rows).style(style))
   8204        })
   8205        .collect();
   8206 
   8207    let messages_list = List::new(messages_list_items)
   8208        .block(Block::default().borders(Borders::ALL).title("Messages"))
   8209        .highlight_style(
   8210            Style::default()
   8211                .bg(tuiColor::Rgb(50, 50, 50))
   8212                .add_modifier(Modifier::BOLD),
   8213        );
   8214    f.render_stateful_widget(messages_list, r, &mut app.items.state)
   8215 }
   8216 
   8217 fn render_inbox_messages(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   8218    let messages_list_items: Vec<ListItem> = app
   8219        .inbox_items
   8220        .items
   8221        .iter()
   8222        .map(|m| {
   8223            let date_style = Style::default().fg(tuiColor::DarkGray);
   8224            let from_style = Style::default().fg(tuiColor::LightBlue);
   8225            let to_style = Style::default().fg(tuiColor::White);
   8226            let content_style = Style::default().fg(tuiColor::White);
   8227            let selected_style = Style::default()
   8228                .fg(tuiColor::Red)
   8229                .add_modifier(Modifier::BOLD);
   8230 
   8231            let checkbox = if m.selected { "[X]" } else { "[ ]" };
   8232            let checkbox_span = Span::styled(
   8233                checkbox,
   8234                if m.selected {
   8235                    selected_style
   8236                } else {
   8237                    Style::default()
   8238                },
   8239            );
   8240 
   8241            let spans = vec![
   8242                checkbox_span,
   8243                Span::raw(" "),
   8244                Span::styled(&m.date, date_style),
   8245                Span::raw(" - ["),
   8246                Span::styled(&m.from, from_style),
   8247                Span::raw(" to "),
   8248                Span::styled(&m.to, to_style),
   8249                Span::raw("] - "),
   8250                Span::styled(&m.content, content_style),
   8251            ];
   8252 
   8253            ListItem::new(Spans::from(spans))
   8254        })
   8255        .collect();
   8256 
   8257    let messages_list = List::new(messages_list_items)
   8258        .block(Block::default().borders(Borders::ALL).title("Inbox (Shift+O to toggle, Space to check/uncheck, 'x' to delete checked, /clearinbox to clear all)"))
   8259        .highlight_style(
   8260            Style::default()
   8261                .bg(tuiColor::Rgb(50, 50, 50))
   8262                .add_modifier(Modifier::BOLD),
   8263        );
   8264    f.render_stateful_widget(messages_list, r, &mut app.inbox_items.state)
   8265 }
   8266 
   8267 fn render_clean_messages(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   8268    let messages_list_items: Vec<ListItem> = app
   8269        .clean_items
   8270        .items
   8271        .iter()
   8272        .map(|m| {
   8273            let date_style = Style::default().fg(tuiColor::DarkGray);
   8274            let content_style = Style::default().fg(tuiColor::White);
   8275            let selected_style = Style::default()
   8276                .fg(tuiColor::Red)
   8277                .add_modifier(Modifier::BOLD);
   8278 
   8279            let checkbox = if m.selected { "[X]" } else { "[ ]" };
   8280            let checkbox_span = Span::styled(
   8281                checkbox,
   8282                if m.selected {
   8283                    selected_style
   8284                } else {
   8285                    Style::default()
   8286                },
   8287            );
   8288 
   8289            let spans = vec![
   8290                checkbox_span,
   8291                Span::raw(" "),
   8292                Span::styled(&m.date, date_style),
   8293                Span::raw(" - "),
   8294                Span::styled(&m.content, content_style),
   8295            ];
   8296 
   8297            ListItem::new(Spans::from(spans))
   8298        })
   8299        .collect();
   8300 
   8301    let messages_list =
   8302        List::new(messages_list_items)
   8303            .block(Block::default().borders(Borders::ALL).title(
   8304                "Clean Mode (Shift+C to toggle, Space to check/uncheck, 'x' to delete checked)",
   8305            ))
   8306            .highlight_style(
   8307                Style::default()
   8308                    .bg(tuiColor::Rgb(50, 50, 50))
   8309                    .add_modifier(Modifier::BOLD),
   8310            );
   8311    f.render_stateful_widget(messages_list, r, &mut app.clean_items.state)
   8312 }
   8313 
   8314 fn render_users(f: &mut Frame<CrosstermBackend<io::Stdout>>, r: Rect, users: &Arc<Mutex<Users>>) {
   8315    // Users lists
   8316    let users = users.lock().unwrap();
   8317    let mut users_list: Vec<ListItem> = vec![];
   8318    let mut users_types: Vec<(&Vec<(tuiColor, String)>, &str)> = Vec::new();
   8319    users_types.push((&users.admin, "-- Admin --"));
   8320    users_types.push((&users.staff, "-- Staff --"));
   8321    users_types.push((&users.members, "-- Members --"));
   8322    users_types.push((&users.guests, "-- Guests --"));
   8323    for (users, label) in users_types.into_iter() {
   8324        users_list.push(ListItem::new(Span::raw(label)));
   8325        for (tui_color, username) in users.iter() {
   8326            let span = Span::styled(username, Style::default().fg(*tui_color));
   8327            users_list.push(ListItem::new(span));
   8328        }
   8329    }
   8330    let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users"));
   8331    f.render_widget(users, r);
   8332 }
   8333 
   8334 fn random_string(n: usize) -> String {
   8335    let s: Vec<u8> = thread_rng().sample_iter(&Alphanumeric).take(n).collect();
   8336    std::str::from_utf8(&s).unwrap().to_owned()
   8337 }
   8338 
   8339 #[derive(PartialEq)]
   8340 enum InputMode {
   8341    LongMessage,
   8342    Normal,
   8343    Editing,
   8344    EditingErr,
   8345    MultilineEditing,
   8346    Notes,
   8347    MessageEditor,
   8348 }
   8349 
   8350 #[derive(PartialEq, Clone)]
   8351 enum VimMode {
   8352    Normal,
   8353    Insert,
   8354    Command,
   8355    Visual,
   8356 }
   8357 
   8358 #[derive(Debug)]
   8359 enum EditorCommand {
   8360    Send(String),
   8361    Quit,
   8362    None,
   8363 }
   8364 
   8365 /// App holds the state of the application
   8366 struct App {
   8367    /// Current value of the input box
   8368    input: String,
   8369    input_idx: usize,
   8370    /// Current input mode
   8371    input_mode: InputMode,
   8372    /// Command history for up/down arrow navigation
   8373    command_history: Vec<String>,
   8374    command_history_index: Option<usize>,
   8375    temp_input: String, // Stores current input when browsing history
   8376    is_muted: bool,
   8377    show_sys: bool,
   8378    display_guest_view: bool,
   8379    display_member_view: bool,
   8380    display_hidden_msgs: bool,
   8381    items: StatefulList<Message>,
   8382    inbox_items: StatefulList<InboxMessage>,
   8383    clean_items: StatefulList<CleanMessage>,
   8384    filter: String,
   8385    members_tag: String,
   8386    staffs_tag: String,
   8387    long_message: Option<Message>,
   8388    long_message_scroll_offset: usize,
   8389    commands: Commands,
   8390 
   8391    display_pm_only: bool,
   8392    display_staff_view: bool,
   8393    display_master_pm_view: bool,
   8394    clean_mode: bool,
   8395    inbox_mode: bool,
   8396 
   8397    // Multiline input scrolling
   8398    multiline_scroll_offset: usize,
   8399 
   8400    // External editor state
   8401    external_editor_active: bool,
   8402 
   8403    // Formatting state for current identity
   8404    #[allow(dead_code)]
   8405    bold: bool,
   8406    #[allow(dead_code)]
   8407    italic: bool,
   8408 
   8409    // Notes pane state
   8410    notes_mode: bool,
   8411    notes_vim_mode: VimMode,
   8412    notes_cursor_pos: (usize, usize), // (line, col)
   8413    notes_content: Vec<String>,
   8414    notes_type_index: usize, // 0=Personal, 1=Public, 2=Staff, 3=Admin
   8415    notes_available_types: Vec<&'static str>,
   8416    notes_vim_command: String,
   8417    notes_modified: bool,
   8418    notes_scroll_offset: usize,
   8419    notes_visual_start: Option<(usize, usize)>, // Visual mode selection start
   8420    notes_last_edited: Option<String>, // Last edited timestamp
   8421    notes_pending_g: bool, // For gg/G commands
   8422    notes_number_prefix: Option<String>, // For number prefixes like 12j
   8423    notes_search_query: String, // For /{filter} searches
   8424    notes_search_mode: bool, // Whether we're in search mode
   8425    notes_search_matches: Vec<(usize, usize)>, // All search match positions (line, col)
   8426    notes_current_match_index: Option<usize>, // Current match index
   8427    notes_pending_d: bool, // For dd line deletion (waiting for second d)
   8428    notes_undo_history: Vec<Vec<String>>, // History of content states for undo
   8429    notes_undo_cursor_history: Vec<(usize, usize)>, // History of cursor positions
   8430    notes_undo_index: usize, // Current position in undo history
   8431 
   8432    // Message editor state
   8433    msg_editor_mode: bool,
   8434    msg_editor_vim_mode: VimMode,
   8435    msg_editor_cursor_pos: (usize, usize), // (line, col)
   8436    msg_editor_content: Vec<String>,
   8437    msg_editor_vim_command: String,
   8438    msg_editor_scroll_offset: usize,
   8439    msg_editor_visual_start: Option<(usize, usize)>,
   8440    msg_editor_pending_g: bool,
   8441    msg_editor_number_prefix: Option<String>, // For number prefixes like 12j
   8442    msg_editor_search_query: String, // For /{filter} searches
   8443    msg_editor_search_mode: bool, // Whether we're in search mode
   8444    msg_editor_search_matches: Vec<(usize, usize)>, // All search match positions (line, col)
   8445    msg_editor_current_match_index: Option<usize>, // Current match index
   8446    msg_editor_pending_d: bool, // For dd line deletion (waiting for second d)
   8447    msg_editor_undo_history: Vec<Vec<String>>, // History of content states for undo
   8448    msg_editor_undo_cursor_history: Vec<(usize, usize)>, // History of cursor positions
   8449    msg_editor_undo_index: usize, // Current position in undo history
   8450 }
   8451 impl Default for App {
   8452    fn default() -> App {
   8453        // Read commands from the file and set them as default values
   8454        let commands = if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) {
   8455            if let Some(config_path_str) = config_path.to_str() {
   8456                match read_commands_file(config_path_str) {
   8457                    Ok(commands) => commands,
   8458                    Err(err) => {
   8459                        log::error!(
   8460                            "Failed to read commands from config file - {} :
   8461 {}",
   8462                            config_path_str,
   8463                            err
   8464                        );
   8465                        Commands {
   8466                            commands: HashMap::new(),
   8467                        }
   8468                    }
   8469                }
   8470            } else {
   8471                log::error!("Failed to convert configuration file path to string.");
   8472                Commands {
   8473                    commands: HashMap::new(),
   8474                }
   8475            }
   8476        } else {
   8477            log::error!("Failed to get configuration file path.");
   8478            Commands {
   8479                commands: HashMap::new(),
   8480            }
   8481        };
   8482 
   8483        App {
   8484            input: String::new(),
   8485            input_idx: 0,
   8486            input_mode: InputMode::Normal,
   8487            command_history: Vec::new(),
   8488            command_history_index: None,
   8489            temp_input: String::new(),
   8490            is_muted: false,
   8491            show_sys: false,
   8492            display_guest_view: false,
   8493            display_member_view: false,
   8494            display_hidden_msgs: false,
   8495            items: StatefulList::new(),
   8496            inbox_items: StatefulList::new(),
   8497            clean_items: StatefulList::new(),
   8498            filter: "".to_owned(),
   8499            members_tag: "".to_owned(),
   8500            staffs_tag: "".to_owned(),
   8501            long_message: None,
   8502            long_message_scroll_offset: 0,
   8503            commands,
   8504            display_pm_only: false,
   8505            display_staff_view: false,
   8506            display_master_pm_view: false,
   8507            clean_mode: false,
   8508            inbox_mode: false,
   8509            multiline_scroll_offset: 0,
   8510            external_editor_active: false,
   8511            bold: false,
   8512            italic: false,
   8513            notes_mode: false,
   8514            notes_vim_mode: VimMode::Normal,
   8515            notes_cursor_pos: (0, 0),
   8516            notes_content: vec!["".to_string()],
   8517            notes_type_index: 0,
   8518            notes_available_types: vec!["Personal", "Public", "Staff", "Admin"],
   8519            notes_vim_command: String::new(),
   8520            notes_modified: false,
   8521            notes_scroll_offset: 0,
   8522            notes_visual_start: None,
   8523            notes_last_edited: None,
   8524            notes_pending_g: false,
   8525            notes_number_prefix: None,
   8526            notes_search_query: String::new(),
   8527            notes_search_mode: false,
   8528            notes_search_matches: Vec::new(),
   8529            notes_current_match_index: None,
   8530            notes_pending_d: false,
   8531            notes_undo_history: vec![vec!["".to_string()]], // Start with initial state
   8532            notes_undo_cursor_history: vec![(0, 0)],
   8533            notes_undo_index: 0,
   8534            msg_editor_mode: false,
   8535            msg_editor_vim_mode: VimMode::Normal,
   8536            msg_editor_cursor_pos: (0, 0),
   8537            msg_editor_content: vec!["".to_string()],
   8538            msg_editor_vim_command: String::new(),
   8539            msg_editor_scroll_offset: 0,
   8540            msg_editor_visual_start: None,
   8541            msg_editor_pending_g: false,
   8542            msg_editor_number_prefix: None,
   8543            msg_editor_search_query: String::new(),
   8544            msg_editor_search_mode: false,
   8545            msg_editor_search_matches: Vec::new(),
   8546            msg_editor_current_match_index: None,
   8547            msg_editor_pending_d: false,
   8548            msg_editor_undo_history: vec![vec!["".to_string()]], // Start with initial state
   8549            msg_editor_undo_cursor_history: vec![(0, 0)],
   8550            msg_editor_undo_index: 0,
   8551        }
   8552    }
   8553 }
   8554 
   8555 impl App {
   8556    fn update_filter(&mut self) {
   8557        if let Some(captures) = FIND_RGX.captures(&self.input) {
   8558            // Find
   8559            self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned();
   8560        }
   8561    }
   8562 
   8563    fn clear_filter(&mut self) {
   8564        if FIND_RGX.is_match(&self.input) {
   8565            self.filter = "".to_owned();
   8566            self.input = "".to_owned();
   8567            self.input_idx = 0;
   8568        }
   8569    }
   8570 
   8571    fn add_to_history(&mut self, command: String) {
   8572        if !command.is_empty() && !command.trim().is_empty() {
   8573            // Remove duplicate if it exists
   8574            if let Some(pos) = self.command_history.iter().position(|x| *x == command) {
   8575                self.command_history.remove(pos);
   8576            }
   8577            // Add to the end (most recent)
   8578            self.command_history.push(command);
   8579            // Keep only last 100 commands
   8580            if self.command_history.len() > 100 {
   8581                self.command_history.remove(0);
   8582            }
   8583        }
   8584        // Reset history navigation
   8585        self.command_history_index = None;
   8586        self.temp_input.clear();
   8587    }
   8588 
   8589    fn navigate_history_up(&mut self) {
   8590        if self.command_history.is_empty() {
   8591            return;
   8592        }
   8593 
   8594        let current_input = self.input.clone();
   8595 
   8596        match self.command_history_index {
   8597            None => {
   8598                // First time navigating history, save current input
   8599                self.temp_input = current_input.clone();
   8600                // Find the most recent command that starts with current input
   8601                let matching_commands: Vec<(usize, &String)> = self
   8602                    .command_history
   8603                    .iter()
   8604                    .enumerate()
   8605                    .rev()
   8606                    .filter(|(_, cmd)| {
   8607                        if current_input.is_empty() {
   8608                            true
   8609                        } else {
   8610                            cmd.starts_with(&current_input)
   8611                        }
   8612                    })
   8613                    .collect();
   8614 
   8615                if let Some((idx, cmd)) = matching_commands.first() {
   8616                    self.command_history_index = Some(*idx);
   8617                    self.input = cmd.to_string();
   8618                    self.input_idx = self.input.chars().count();
   8619                }
   8620            }
   8621            Some(current_idx) => {
   8622                // Find next older matching command
   8623                let matching_commands: Vec<(usize, &String)> = self
   8624                    .command_history
   8625                    .iter()
   8626                    .enumerate()
   8627                    .rev()
   8628                    .filter(|(idx, cmd)| {
   8629                        *idx < current_idx
   8630                            && (self.temp_input.is_empty() || cmd.starts_with(&self.temp_input))
   8631                    })
   8632                    .collect();
   8633 
   8634                if let Some((idx, cmd)) = matching_commands.first() {
   8635                    self.command_history_index = Some(*idx);
   8636                    self.input = cmd.to_string();
   8637                    self.input_idx = self.input.chars().count();
   8638                }
   8639            }
   8640        }
   8641    }
   8642 
   8643    fn navigate_history_down(&mut self) {
   8644        if self.command_history.is_empty() {
   8645            return;
   8646        }
   8647 
   8648        match self.command_history_index {
   8649            None => {
   8650                // Not currently navigating history, do nothing
   8651            }
   8652            Some(current_idx) => {
   8653                // Find next newer matching command
   8654                let matching_commands: Vec<(usize, &String)> = self
   8655                    .command_history
   8656                    .iter()
   8657                    .enumerate()
   8658                    .filter(|(idx, cmd)| {
   8659                        *idx > current_idx
   8660                            && (self.temp_input.is_empty() || cmd.starts_with(&self.temp_input))
   8661                    })
   8662                    .collect();
   8663 
   8664                if let Some((idx, cmd)) = matching_commands.first() {
   8665                    self.command_history_index = Some(*idx);
   8666                    self.input = cmd.to_string();
   8667                    self.input_idx = self.input.chars().count();
   8668                } else {
   8669                    // No newer commands, go back to original input
   8670                    self.command_history_index = None;
   8671                    self.input = self.temp_input.clone();
   8672                    self.input_idx = self.input.chars().count();
   8673                }
   8674            }
   8675        }
   8676    }
   8677 
   8678    fn reset_history_navigation(&mut self) {
   8679        self.command_history_index = None;
   8680        self.temp_input.clear();
   8681    }
   8682 
   8683    // Notes functionality
   8684    fn enter_notes_mode(&mut self, client: &LeChatPHPClient) {
   8685        self.notes_mode = true;
   8686        self.input_mode = InputMode::Notes;
   8687        self.notes_vim_mode = VimMode::Normal;
   8688        self.notes_cursor_pos = (0, 0);
   8689        self.notes_modified = false;
   8690        self.notes_vim_command.clear();
   8691        self.notes_scroll_offset = 0;
   8692        self.notes_visual_start = None;
   8693        self.notes_pending_g = false;
   8694        self.notes_type_index = 0;
   8695        
   8696        // Set up available types based on user permissions
   8697        self.update_available_notes_types(client);
   8698        
   8699        // Only load content if we have available types
   8700        if !self.notes_available_types.is_empty() {
   8701            self.load_notes_content(client);
   8702        } else {
   8703            // No permission to view any notes
   8704            self.notes_content = vec!["You don't have permission to view any notes.".to_string()];
   8705        }
   8706    }
   8707 
   8708    fn exit_notes_mode(&mut self) {
   8709        self.notes_mode = false;
   8710        self.input_mode = InputMode::Normal;
   8711    }
   8712 
   8713    fn cycle_notes_type(&mut self, client: &LeChatPHPClient) {
   8714        // Update available types based on current permissions
   8715        self.update_available_notes_types(client);
   8716        
   8717        if !self.notes_available_types.is_empty() {
   8718            self.notes_type_index = (self.notes_type_index + 1) % self.notes_available_types.len();
   8719            self.load_notes_content(client);
   8720        } else {
   8721            // No types available - do nothing to prevent crash
   8722            return;
   8723        }
   8724    }
   8725 
   8726    fn update_available_notes_types(&mut self, client: &LeChatPHPClient) {
   8727        let user_role = client.determine_user_role();
   8728        let mut available_types = vec![];
   8729        
   8730        match user_role {
   8731            UserRole::Guest => {
   8732                // Guests can only view public notes (if any)
   8733                available_types.push("Public");
   8734            }
   8735            UserRole::Member => {
   8736                // Members can view personal and public notes
   8737                available_types.push("Personal");
   8738                available_types.push("Public");
   8739            }
   8740            UserRole::Staff => {
   8741                // Staff can view personal, public, and staff notes
   8742                available_types.push("Personal");
   8743                available_types.push("Public");
   8744                available_types.push("Staff");
   8745            }
   8746            UserRole::Admin => {
   8747                // Admins can view all types
   8748                available_types.push("Personal");
   8749                available_types.push("Public");
   8750                available_types.push("Staff");
   8751                available_types.push("Admin");
   8752            }
   8753        }
   8754        
   8755        self.notes_available_types = available_types;
   8756        
   8757        // Ensure current index is valid
   8758        if self.notes_type_index >= self.notes_available_types.len() && !self.notes_available_types.is_empty() {
   8759            self.notes_type_index = 0;
   8760        }
   8761    }
   8762 
   8763    fn load_notes_content(&mut self, client: &LeChatPHPClient) {
   8764        let note_type = match self.get_current_notes_type() {
   8765            "Personal" => "",
   8766            "Public" => "public",
   8767            "Staff" => "staff", 
   8768            "Admin" => "admin",
   8769            _ => "",
   8770        };
   8771        
   8772        match client.fetch_notes(note_type) {
   8773            Ok((content, last_edited)) => {
   8774                self.notes_content = content;
   8775                // Ensure cursor position is within bounds after loading new content
   8776                self.ensure_notes_cursor_bounds();
   8777                self.notes_modified = false;
   8778                self.notes_last_edited = last_edited;
   8779            }
   8780            Err(_) => {
   8781                self.notes_content = vec!["Failed to load notes".to_string()];
   8782                self.notes_cursor_pos = (0, 0);
   8783                self.notes_modified = false;
   8784                self.notes_last_edited = None;
   8785            }
   8786        }
   8787    }
   8788 
   8789    fn ensure_notes_cursor_bounds(&mut self) {
   8790        if self.notes_content.is_empty() {
   8791            self.notes_content = vec!["".to_string()];
   8792            self.notes_cursor_pos = (0, 0);
   8793            return;
   8794        }
   8795        
   8796        // Ensure row is within bounds
   8797        if self.notes_cursor_pos.0 >= self.notes_content.len() {
   8798            self.notes_cursor_pos.0 = self.notes_content.len() - 1;
   8799        }
   8800        
   8801        // Ensure column is within bounds
   8802        let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   8803        if self.notes_cursor_pos.1 > line_len {
   8804            self.notes_cursor_pos.1 = line_len;
   8805        }
   8806        
   8807        // Update scroll to make cursor visible
   8808        self.ensure_cursor_visible();
   8809    }
   8810 
   8811    fn get_current_notes_type(&self) -> &str {
   8812        if self.notes_available_types.is_empty() {
   8813            "None"
   8814        } else {
   8815            self.notes_available_types[self.notes_type_index]
   8816        }
   8817    }
   8818 
   8819    // Helper function to find next word boundary
   8820    fn find_next_word_boundary(line: &str, start_pos: usize) -> usize {
   8821        let chars: Vec<char> = line.chars().collect();
   8822        let mut pos = start_pos;
   8823        
   8824        if pos >= chars.len() {
   8825            return chars.len();
   8826        }
   8827        
   8828        // Skip current word if we're in the middle of it
   8829        if chars[pos].is_alphanumeric() || chars[pos] == '_' {
   8830            while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
   8831                pos += 1;
   8832            }
   8833        } else if !chars[pos].is_whitespace() {
   8834            // Skip punctuation
   8835            while pos < chars.len() && !chars[pos].is_whitespace() && !chars[pos].is_alphanumeric() && chars[pos] != '_' {
   8836                pos += 1;
   8837            }
   8838        }
   8839        
   8840        // Skip whitespace
   8841        while pos < chars.len() && chars[pos].is_whitespace() {
   8842            pos += 1;
   8843        }
   8844        
   8845        pos
   8846    }
   8847    
   8848    // Helper function to find previous word boundary
   8849    fn find_prev_word_boundary(line: &str, start_pos: usize) -> usize {
   8850        let chars: Vec<char> = line.chars().collect();
   8851        if start_pos == 0 || chars.is_empty() {
   8852            return 0;
   8853        }
   8854        
   8855        let mut pos = start_pos.saturating_sub(1);
   8856        
   8857        // Skip whitespace
   8858        while pos > 0 && chars[pos].is_whitespace() {
   8859            pos -= 1;
   8860        }
   8861        
   8862        if pos == 0 {
   8863            return 0;
   8864        }
   8865        
   8866        // Move to beginning of current word
   8867        if chars[pos].is_alphanumeric() || chars[pos] == '_' {
   8868            while pos > 0 && (chars[pos - 1].is_alphanumeric() || chars[pos - 1] == '_') {
   8869                pos -= 1;
   8870            }
   8871        } else {
   8872            while pos > 0 && !chars[pos - 1].is_whitespace() && !chars[pos - 1].is_alphanumeric() && chars[pos - 1] != '_' {
   8873                pos -= 1;
   8874            }
   8875        }
   8876        
   8877        pos
   8878    }
   8879 
   8880    // Helper function to find all matches in content
   8881    fn find_all_matches(content: &[String], query: &str) -> Vec<(usize, usize)> {
   8882        let mut matches = Vec::new();
   8883        if query.is_empty() {
   8884            return matches;
   8885        }
   8886        
   8887        for (line_idx, line) in content.iter().enumerate() {
   8888            let mut start = 0;
   8889            while let Some(col_idx) = line[start..].find(query) {
   8890                matches.push((line_idx, start + col_idx));
   8891                start = start + col_idx + 1; // Move past this match to find next
   8892            }
   8893        }
   8894        
   8895        matches
   8896    }
   8897 
   8898    // Navigate to next search match
   8899    fn notes_next_match(&mut self) {
   8900        if let Some(current_index) = self.notes_current_match_index {
   8901            if !self.notes_search_matches.is_empty() {
   8902                let new_index = (current_index + 1) % self.notes_search_matches.len();
   8903                self.notes_current_match_index = Some(new_index);
   8904                let (line, col) = self.notes_search_matches[new_index];
   8905                self.notes_cursor_pos = (line, col);
   8906                self.ensure_cursor_visible();
   8907            }
   8908        }
   8909    }
   8910 
   8911    // Navigate to previous search match
   8912    fn notes_prev_match(&mut self) {
   8913        if let Some(current_index) = self.notes_current_match_index {
   8914            if !self.notes_search_matches.is_empty() {
   8915                let new_index = if current_index == 0 {
   8916                    self.notes_search_matches.len() - 1
   8917                } else {
   8918                    current_index - 1
   8919                };
   8920                self.notes_current_match_index = Some(new_index);
   8921                let (line, col) = self.notes_search_matches[new_index];
   8922                self.notes_cursor_pos = (line, col);
   8923                self.ensure_cursor_visible();
   8924            }
   8925        }
   8926    }
   8927 
   8928    // Clear search results when changing modes
   8929    fn clear_notes_search_results(&mut self) {
   8930        self.notes_search_matches.clear();
   8931        self.notes_current_match_index = None;
   8932    }
   8933 
   8934    // Navigate to next search match - message editor
   8935    fn msg_editor_next_match(&mut self) {
   8936        if let Some(current_index) = self.msg_editor_current_match_index {
   8937            if !self.msg_editor_search_matches.is_empty() {
   8938                let new_index = (current_index + 1) % self.msg_editor_search_matches.len();
   8939                self.msg_editor_current_match_index = Some(new_index);
   8940                let (line, col) = self.msg_editor_search_matches[new_index];
   8941                self.msg_editor_cursor_pos = (line, col);
   8942                self.ensure_msg_editor_cursor_visible();
   8943            }
   8944        }
   8945    }
   8946 
   8947    // Navigate to previous search match - message editor
   8948    fn msg_editor_prev_match(&mut self) {
   8949        if let Some(current_index) = self.msg_editor_current_match_index {
   8950            if !self.msg_editor_search_matches.is_empty() {
   8951                let new_index = if current_index == 0 {
   8952                    self.msg_editor_search_matches.len() - 1
   8953                } else {
   8954                    current_index - 1
   8955                };
   8956                self.msg_editor_current_match_index = Some(new_index);
   8957                let (line, col) = self.msg_editor_search_matches[new_index];
   8958                self.msg_editor_cursor_pos = (line, col);
   8959                self.ensure_msg_editor_cursor_visible();
   8960            }
   8961        }
   8962    }
   8963 
   8964    // Clear search results when changing modes - message editor
   8965    fn clear_msg_editor_search_results(&mut self) {
   8966        self.msg_editor_search_matches.clear();
   8967        self.msg_editor_current_match_index = None;
   8968    }
   8969 
   8970    fn handle_notes_vim_key(&mut self, key: char, client: &LeChatPHPClient) -> bool {
   8971        match self.notes_vim_mode {
   8972            VimMode::Normal => self.handle_notes_normal_mode(key),
   8973            VimMode::Insert => self.handle_notes_insert_mode(key),
   8974            VimMode::Command => self.handle_notes_command_mode(key, client),
   8975            VimMode::Visual => self.handle_notes_visual_mode(key),
   8976        }
   8977    }
   8978 
   8979    fn handle_notes_normal_mode(&mut self, key: char) -> bool {
   8980        // Handle search mode
   8981        if self.notes_search_mode {
   8982            match key {
   8983                '\r' => {
   8984                    // Execute search
   8985                    self.notes_search_mode = false;
   8986                    
   8987                    // Find all matches
   8988                    self.notes_search_matches = Self::find_all_matches(&self.notes_content, &self.notes_search_query);
   8989                    
   8990                    if !self.notes_search_matches.is_empty() {
   8991                        // Find the first match after current cursor position
   8992                        let current_pos = (self.notes_cursor_pos.0, self.notes_cursor_pos.1);
   8993                        let mut match_index = 0;
   8994                        
   8995                        for (i, &match_pos) in self.notes_search_matches.iter().enumerate() {
   8996                            if match_pos > current_pos {
   8997                                match_index = i;
   8998                                break;
   8999                            }
   9000                            // If no match after cursor, wrap to first match
   9001                            match_index = i;
   9002                        }
   9003                        
   9004                        self.notes_current_match_index = Some(match_index);
   9005                        let (line, col) = self.notes_search_matches[match_index];
   9006                        self.notes_cursor_pos = (line, col);
   9007                        self.ensure_cursor_visible();
   9008                    } else {
   9009                        self.notes_current_match_index = None;
   9010                    }
   9011                    
   9012                    self.notes_search_query.clear();
   9013                    return true;
   9014                }
   9015                '\x1b' => {
   9016                    // Escape - cancel search
   9017                    self.notes_search_mode = false;
   9018                    self.notes_search_query.clear();
   9019                    return true;
   9020                }
   9021                '\x08' => {
   9022                    // Backspace
   9023                    self.notes_search_query.pop();
   9024                    return true;
   9025                }
   9026                c if c.is_ascii() && !c.is_control() => {
   9027                    self.notes_search_query.push(c);
   9028                    return true;
   9029                }
   9030                _ => return true,
   9031            }
   9032        }
   9033 
   9034        // Handle pending 'g' commands
   9035        if self.notes_pending_g {
   9036            self.notes_pending_g = false;
   9037            match key {
   9038                'g' => {
   9039                    // gg - go to top
   9040                    self.notes_cursor_pos = (0, 0);
   9041                    self.notes_scroll_offset = 0;
   9042                    return true;
   9043                }
   9044                _ => {
   9045                    // Invalid g command, fall through
   9046                }
   9047            }
   9048        }
   9049 
   9050        // Handle pending 'd' commands (dd for line deletion)
   9051        if self.notes_pending_d {
   9052            self.notes_pending_d = false;
   9053            match key {
   9054                'd' => {
   9055                    // dd - delete line
   9056                    self.handle_notes_dd();
   9057                    return true;
   9058                }
   9059                '\x1b' => {
   9060                    // Escape - cancel dd
   9061                    return true;
   9062                }
   9063                _ => {
   9064                    // Invalid d command, fall through to normal processing
   9065                }
   9066            }
   9067        }
   9068 
   9069        // Handle number prefixes - special handling for '0'
   9070        if key.is_ascii_digit() {
   9071            if self.notes_number_prefix.is_none() {
   9072                // First digit
   9073                if key == '0' {
   9074                    // '0' as first digit should be treated as motion (start of line), not number prefix
   9075                    // Fall through to normal key handling
   9076                } else {
   9077                    // '1'-'9' as first digit starts number prefix
   9078                    self.notes_number_prefix = Some(String::new());
   9079                    self.notes_number_prefix.as_mut().unwrap().push(key);
   9080                    return true;
   9081                }
   9082            } else {
   9083                // Subsequent digit (including '0') can be added to existing prefix
   9084                self.notes_number_prefix.as_mut().unwrap().push(key);
   9085                return true;
   9086            }
   9087        }
   9088 
   9089        // Get repetition count
   9090        let count = if let Some(ref prefix) = self.notes_number_prefix {
   9091            prefix.parse::<usize>().unwrap_or(1)
   9092        } else {
   9093            1
   9094        };
   9095        
   9096        // Clear number prefix after using it
   9097        self.notes_number_prefix = None;
   9098 
   9099        // Clear pending states if any other key is pressed (except the expected ones)
   9100        let should_clear_pending_states = match key {
   9101            'd' if !self.notes_pending_d => false, // Allow first 'd'
   9102            'd' | '\x1b' => false, // Allow second 'd' or escape when pending
   9103            _ if self.notes_pending_d => true, // Clear pending 'd' for any other key
   9104            _ => false,
   9105        };
   9106        
   9107        if should_clear_pending_states {
   9108            self.notes_pending_d = false;
   9109        }
   9110 
   9111        match key {
   9112            'h' => {
   9113                for _ in 0..count {
   9114                    if self.notes_cursor_pos.1 > 0 {
   9115                        self.notes_cursor_pos.1 -= 1;
   9116                    } else {
   9117                        break;
   9118                    }
   9119                }
   9120                self.ensure_cursor_visible();
   9121                true
   9122            }
   9123            'j' => {
   9124                for _ in 0..count {
   9125                    if self.notes_cursor_pos.0 < self.notes_content.len() - 1 {
   9126                        self.notes_cursor_pos.0 += 1;
   9127                        let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9128                        if self.notes_cursor_pos.1 > line_len {
   9129                            self.notes_cursor_pos.1 = line_len;
   9130                        }
   9131                    } else {
   9132                        break;
   9133                    }
   9134                }
   9135                self.ensure_cursor_visible();
   9136                true
   9137            }
   9138            'k' => {
   9139                for _ in 0..count {
   9140                    if self.notes_cursor_pos.0 > 0 {
   9141                        self.notes_cursor_pos.0 -= 1;
   9142                        let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9143                        if self.notes_cursor_pos.1 > line_len {
   9144                            self.notes_cursor_pos.1 = line_len;
   9145                        }
   9146                    } else {
   9147                        break;
   9148                    }
   9149                }
   9150                self.ensure_cursor_visible();
   9151                true
   9152            }
   9153            'l' => {
   9154                for _ in 0..count {
   9155                    let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9156                    if self.notes_cursor_pos.1 < line_len {
   9157                        self.notes_cursor_pos.1 += 1;
   9158                    } else {
   9159                        break;
   9160                    }
   9161                }
   9162                self.ensure_cursor_visible();
   9163                true
   9164            }
   9165            'w' => {
   9166                // Word forward
   9167                for _ in 0..count {
   9168                    let current_line = &self.notes_content[self.notes_cursor_pos.0];
   9169                    let new_col = Self::find_next_word_boundary(current_line, self.notes_cursor_pos.1);
   9170                    
   9171                    if new_col < current_line.len() {
   9172                        self.notes_cursor_pos.1 = new_col;
   9173                    } else if self.notes_cursor_pos.0 < self.notes_content.len() - 1 {
   9174                        // Move to beginning of next line
   9175                        self.notes_cursor_pos.0 += 1;
   9176                        self.notes_cursor_pos.1 = 0;
   9177                        // Skip to first non-whitespace character
   9178                        let next_line = &self.notes_content[self.notes_cursor_pos.0];
   9179                        for (i, ch) in next_line.chars().enumerate() {
   9180                            if !ch.is_whitespace() {
   9181                                self.notes_cursor_pos.1 = i;
   9182                                break;
   9183                            }
   9184                        }
   9185                    } else {
   9186                        break;
   9187                    }
   9188                }
   9189                self.ensure_cursor_visible();
   9190                true
   9191            }
   9192            'b' => {
   9193                // Word backward
   9194                for _ in 0..count {
   9195                    let current_line = &self.notes_content[self.notes_cursor_pos.0];
   9196                    let new_col = Self::find_prev_word_boundary(current_line, self.notes_cursor_pos.1);
   9197                    
   9198                    if new_col < self.notes_cursor_pos.1 {
   9199                        self.notes_cursor_pos.1 = new_col;
   9200                    } else if self.notes_cursor_pos.0 > 0 {
   9201                        // Move to end of previous line
   9202                        self.notes_cursor_pos.0 -= 1;
   9203                        self.notes_cursor_pos.1 = self.notes_content[self.notes_cursor_pos.0].len();
   9204                    } else {
   9205                        break;
   9206                    }
   9207                }
   9208                self.ensure_cursor_visible();
   9209                true
   9210            }
   9211            '$' => {
   9212                // End of line
   9213                self.notes_cursor_pos.1 = self.notes_content[self.notes_cursor_pos.0].len();
   9214                self.ensure_cursor_visible();
   9215                true
   9216            }
   9217            '0' => {
   9218                // Beginning of line
   9219                self.notes_cursor_pos.1 = 0;
   9220                self.ensure_cursor_visible();
   9221                true
   9222            }
   9223            '/' => {
   9224                // Start search
   9225                self.notes_search_mode = true;
   9226                self.notes_search_query.clear();
   9227                true
   9228            }
   9229            'G' => {
   9230                // Go to end of file
   9231                self.notes_cursor_pos.0 = self.notes_content.len() - 1;
   9232                self.notes_cursor_pos.1 = self.notes_content[self.notes_cursor_pos.0].len();
   9233                self.ensure_cursor_visible();
   9234                true
   9235            }
   9236            'g' => {
   9237                // Start of gg command
   9238                self.notes_pending_g = true;
   9239                true
   9240            }
   9241            'i' => {
   9242                // Save state before entering insert mode
   9243                self.save_notes_state();
   9244                self.clear_notes_search_results(); // Clear search on mode change
   9245                self.notes_vim_mode = VimMode::Insert;
   9246                true
   9247            }
   9248            'a' => {
   9249                // Save state before entering insert mode
   9250                self.save_notes_state();
   9251                self.clear_notes_search_results(); // Clear search on mode change
   9252                self.notes_vim_mode = VimMode::Insert;
   9253                let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9254                if self.notes_cursor_pos.1 < line_len {
   9255                    self.notes_cursor_pos.1 += 1;
   9256                }
   9257                true
   9258            }
   9259            'A' => {
   9260                // Append at end of line
   9261                // Save state before entering insert mode
   9262                self.save_notes_state();
   9263                self.clear_notes_search_results(); // Clear search on mode change
   9264                self.notes_vim_mode = VimMode::Insert;
   9265                self.notes_cursor_pos.1 = self.notes_content[self.notes_cursor_pos.0].len();
   9266                true
   9267            }
   9268            'x' => {
   9269                // Delete character under cursor
   9270                // Save state before making changes
   9271                self.save_notes_state();
   9272                let (line, col) = self.notes_cursor_pos;
   9273                if col < self.notes_content[line].len() {
   9274                    self.notes_content[line].remove(col);
   9275                    self.notes_modified = true;
   9276                    self.update_last_edited();
   9277                }
   9278                true
   9279            }
   9280            'v' => {
   9281                // Enter visual mode
   9282                self.notes_vim_mode = VimMode::Visual;
   9283                self.notes_visual_start = Some(self.notes_cursor_pos);
   9284                true
   9285            }
   9286            'u' => {
   9287                // Undo
   9288                self.notes_undo();
   9289                true
   9290            }
   9291            'd' => {
   9292                // First 'd' - wait for second one
   9293                self.notes_pending_d = true;
   9294                true
   9295            }
   9296            ':' => {
   9297                self.notes_vim_mode = VimMode::Command;
   9298                self.notes_vim_command.clear();
   9299                true
   9300            }
   9301            'n' => {
   9302                // Next search match
   9303                self.notes_next_match();
   9304                true
   9305            }
   9306            'N' => {
   9307                // Previous search match
   9308                self.notes_prev_match();
   9309                true
   9310            }
   9311            _ => false,
   9312        }
   9313    }
   9314 
   9315    fn handle_notes_insert_mode(&mut self, key: char) -> bool {
   9316        if key == '\x1b' {
   9317            // Escape key
   9318            self.notes_vim_mode = VimMode::Normal;
   9319            if self.notes_cursor_pos.1 > 0 {
   9320                self.notes_cursor_pos.1 -= 1;
   9321            }
   9322            self.update_last_edited();
   9323            return true;
   9324        }
   9325 
   9326        // Clear search results when in insert mode (mode switch)
   9327        if !self.notes_search_matches.is_empty() {
   9328            self.clear_notes_search_results();
   9329        }
   9330 
   9331        match key {
   9332            '\n' | '\r' => {
   9333                let (line, col) = self.notes_cursor_pos;
   9334                let current_line = self.notes_content[line].clone();
   9335                let (left, right) = current_line.split_at(col);
   9336                self.notes_content[line] = left.to_string();
   9337                self.notes_content.insert(line + 1, right.to_string());
   9338                self.notes_cursor_pos = (line + 1, 0);
   9339                self.notes_modified = true;
   9340                self.ensure_cursor_visible();
   9341                true
   9342            }
   9343            '\x08' | '\x7f' => {
   9344                // Backspace
   9345                if self.notes_cursor_pos.1 > 0 {
   9346                    let (line, col) = self.notes_cursor_pos;
   9347                    self.notes_content[line].remove(col - 1);
   9348                    self.notes_cursor_pos.1 -= 1;
   9349                    self.notes_modified = true;
   9350                } else if self.notes_cursor_pos.0 > 0 {
   9351                    // Join with previous line
   9352                    let current_line = self.notes_content.remove(self.notes_cursor_pos.0);
   9353                    self.notes_cursor_pos.0 -= 1;
   9354                    self.notes_cursor_pos.1 = self.notes_content[self.notes_cursor_pos.0].len();
   9355                    self.notes_content[self.notes_cursor_pos.0].push_str(&current_line);
   9356                    self.notes_modified = true;
   9357                    self.ensure_cursor_visible();
   9358                }
   9359                true
   9360            }
   9361            c if c.is_ascii() && !c.is_control() => {
   9362                let (line, col) = self.notes_cursor_pos;
   9363                self.notes_content[line].insert(col, c);
   9364                self.notes_cursor_pos.1 += 1;
   9365                self.notes_modified = true;
   9366                true
   9367            }
   9368            _ => false,
   9369        }
   9370    }
   9371 
   9372    fn handle_notes_visual_mode(&mut self, key: char) -> bool {
   9373        match key {
   9374            'h' => {
   9375                if self.notes_cursor_pos.1 > 0 {
   9376                    self.notes_cursor_pos.1 -= 1;
   9377                }
   9378                self.ensure_cursor_visible();
   9379                true
   9380            }
   9381            'j' => {
   9382                if self.notes_cursor_pos.0 < self.notes_content.len() - 1 {
   9383                    self.notes_cursor_pos.0 += 1;
   9384                    let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9385                    if self.notes_cursor_pos.1 > line_len {
   9386                        self.notes_cursor_pos.1 = line_len;
   9387                    }
   9388                }
   9389                self.ensure_cursor_visible();
   9390                true
   9391            }
   9392            'k' => {
   9393                if self.notes_cursor_pos.0 > 0 {
   9394                    self.notes_cursor_pos.0 -= 1;
   9395                    let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9396                    if self.notes_cursor_pos.1 > line_len {
   9397                        self.notes_cursor_pos.1 = line_len;
   9398                    }
   9399                }
   9400                self.ensure_cursor_visible();
   9401                true
   9402            }
   9403            'l' => {
   9404                let line_len = self.notes_content[self.notes_cursor_pos.0].len();
   9405                if self.notes_cursor_pos.1 < line_len {
   9406                    self.notes_cursor_pos.1 += 1;
   9407                }
   9408                self.ensure_cursor_visible();
   9409                true
   9410            }
   9411            'x' => {
   9412                // Delete selected text
   9413                self.delete_visual_selection();
   9414                self.notes_vim_mode = VimMode::Normal;
   9415                self.notes_visual_start = None;
   9416                true
   9417            }
   9418            '\x1b' => {
   9419                // Escape - exit visual mode
   9420                self.notes_vim_mode = VimMode::Normal;
   9421                self.notes_visual_start = None;
   9422                true
   9423            }
   9424            _ => false,
   9425        }
   9426    }
   9427 
   9428    fn handle_notes_command_mode(&mut self, key: char, client: &LeChatPHPClient) -> bool {
   9429        match key {
   9430            '\n' | '\r' => {
   9431                self.execute_notes_vim_command(client);
   9432                self.notes_vim_mode = VimMode::Normal;
   9433                true
   9434            }
   9435            '\x1b' => {
   9436                // Escape
   9437                self.notes_vim_mode = VimMode::Normal;
   9438                self.notes_vim_command.clear();
   9439                true
   9440            }
   9441            '\x08' | '\x7f' => {
   9442                // Backspace
   9443                self.notes_vim_command.pop();
   9444                true
   9445            }
   9446            c if c.is_ascii() => {
   9447                self.notes_vim_command.push(c);
   9448                true
   9449            }
   9450            _ => false,
   9451        }
   9452    }
   9453 
   9454    fn execute_notes_vim_command(&mut self, client: &LeChatPHPClient) {
   9455        match self.notes_vim_command.as_str() {
   9456            "w" => {
   9457                // Save notes
   9458                if let Err(_) = self.save_notes_to_server(client) {
   9459                    // TODO: Show error message
   9460                } else {
   9461                    self.notes_modified = false;
   9462                    self.update_last_edited();
   9463                }
   9464            }
   9465            "q" => {
   9466                if !self.notes_modified {
   9467                    self.exit_notes_mode();
   9468                }
   9469                // TODO: Show warning if modified
   9470            }
   9471            "wq" => {
   9472                // Save and quit
   9473                if let Err(_) = self.save_notes_to_server(client) {
   9474                    // TODO: Show error message, don't quit
   9475                } else {
   9476                    self.notes_modified = false;
   9477                    self.update_last_edited();
   9478                    self.exit_notes_mode();
   9479                }
   9480            }
   9481            _ => {}
   9482        }
   9483        self.notes_vim_command.clear();
   9484    }
   9485 
   9486    fn save_notes_to_server(&self, client: &LeChatPHPClient) -> Result<(), Box<dyn std::error::Error>> {
   9487        let note_type = match self.get_current_notes_type() {
   9488            "Personal" => "",
   9489            "Public" => "public",
   9490            "Staff" => "staff",
   9491            "Admin" => "admin",
   9492            _ => "",
   9493        };
   9494        
   9495        client.save_notes(note_type, &self.notes_content)
   9496    }
   9497 
   9498    fn handle_notes_dd(&mut self) {
   9499        // Save state before making changes
   9500        self.save_notes_state();
   9501        
   9502        let (line, _) = self.notes_cursor_pos;
   9503        if self.notes_content.len() > 1 {
   9504            self.notes_content.remove(line);
   9505            if line >= self.notes_content.len() {
   9506                self.notes_cursor_pos.0 = self.notes_content.len() - 1;
   9507            }
   9508            self.notes_cursor_pos.1 = 0;
   9509            self.notes_modified = true;
   9510        } else {
   9511            // Clear the only line
   9512            self.notes_content[0].clear();
   9513            self.notes_cursor_pos = (0, 0);
   9514            self.notes_modified = true;
   9515        }
   9516    }
   9517 
   9518    fn ensure_cursor_visible(&mut self) {
   9519        let visible_lines = 50; // Conservative estimate - UI will handle actual height
   9520        let (line, _) = self.notes_cursor_pos;
   9521        
   9522        if line < self.notes_scroll_offset {
   9523            self.notes_scroll_offset = line;
   9524        } else if line >= self.notes_scroll_offset + visible_lines {
   9525            self.notes_scroll_offset = line - visible_lines + 1;
   9526        }
   9527    }
   9528 
   9529    fn update_last_edited(&mut self) {
   9530        use chrono::Local;
   9531        let now = Local::now();
   9532        let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
   9533        self.notes_last_edited = Some(format!("Modified locally at {}", timestamp));
   9534    }
   9535 
   9536    fn delete_visual_selection(&mut self) {
   9537        if let Some(start) = self.notes_visual_start {
   9538            let end = self.notes_cursor_pos;
   9539            let (start_pos, end_pos) = if start <= end {
   9540                (start, end)
   9541            } else {
   9542                (end, start)
   9543            };
   9544 
   9545            // Simple single-line selection for now
   9546            if start_pos.0 == end_pos.0 {
   9547                let line = start_pos.0;
   9548                let start_col = start_pos.1;
   9549                let end_col = end_pos.1;
   9550                
   9551                if start_col < end_col && end_col <= self.notes_content[line].len() {
   9552                    self.notes_content[line].drain(start_col..end_col);
   9553                    self.notes_cursor_pos = start_pos;
   9554                    self.notes_modified = true;
   9555                    self.update_last_edited();
   9556                }
   9557            }
   9558        }
   9559    }
   9560 
   9561    // Message editor functionality
   9562    fn enter_message_editor_mode(&mut self) {
   9563        self.msg_editor_mode = true;
   9564        self.input_mode = InputMode::MessageEditor;
   9565        self.msg_editor_vim_mode = VimMode::Normal;
   9566        self.msg_editor_cursor_pos = (0, 0);
   9567        self.msg_editor_vim_command.clear();
   9568        self.msg_editor_scroll_offset = 0;
   9569        self.msg_editor_visual_start = None;
   9570        self.msg_editor_pending_g = false;
   9571 
   9572        // Copy input content to editor, split by lines
   9573        if !self.input.is_empty() {
   9574            self.msg_editor_content = self.input.split('\n').map(|s| s.to_string()).collect();
   9575        } else {
   9576            self.msg_editor_content = vec!["".to_string()];
   9577        }
   9578        
   9579        // Position cursor at end
   9580        if !self.msg_editor_content.is_empty() {
   9581            let last_line = self.msg_editor_content.len() - 1;
   9582            let last_col = self.msg_editor_content[last_line].len();
   9583            self.msg_editor_cursor_pos = (last_line, last_col);
   9584        }
   9585    }
   9586 
   9587    fn exit_message_editor_mode(&mut self) {
   9588        self.msg_editor_mode = false;
   9589        self.input_mode = InputMode::Editing;
   9590    }
   9591 
   9592 
   9593    fn handle_msg_editor_vim_key(&mut self, key: char) -> EditorCommand {
   9594        match self.msg_editor_vim_mode {
   9595            VimMode::Normal => {
   9596                self.handle_msg_editor_normal_mode(key);
   9597                EditorCommand::None
   9598            }
   9599            VimMode::Insert => {
   9600                self.handle_msg_editor_insert_mode(key);
   9601                EditorCommand::None
   9602            }
   9603            VimMode::Command => self.handle_msg_editor_command_mode(key),
   9604            VimMode::Visual => {
   9605                self.handle_msg_editor_visual_mode(key);
   9606                EditorCommand::None
   9607            }
   9608        }
   9609    }
   9610 
   9611    fn handle_msg_editor_normal_mode(&mut self, key: char) -> bool {
   9612        // Handle search mode
   9613        if self.msg_editor_search_mode {
   9614            match key {
   9615                '\r' => {
   9616                    // Execute search
   9617                    self.msg_editor_search_mode = false;
   9618                    
   9619                    // Find all matches
   9620                    self.msg_editor_search_matches = Self::find_all_matches(&self.msg_editor_content, &self.msg_editor_search_query);
   9621                    
   9622                    if !self.msg_editor_search_matches.is_empty() {
   9623                        // Find the first match after current cursor position
   9624                        let current_pos = (self.msg_editor_cursor_pos.0, self.msg_editor_cursor_pos.1);
   9625                        let mut match_index = 0;
   9626                        
   9627                        for (i, &match_pos) in self.msg_editor_search_matches.iter().enumerate() {
   9628                            if match_pos > current_pos {
   9629                                match_index = i;
   9630                                break;
   9631                            }
   9632                            // If no match after cursor, wrap to first match
   9633                            match_index = i;
   9634                        }
   9635                        
   9636                        self.msg_editor_current_match_index = Some(match_index);
   9637                        let (line, col) = self.msg_editor_search_matches[match_index];
   9638                        self.msg_editor_cursor_pos = (line, col);
   9639                        self.ensure_msg_editor_cursor_visible();
   9640                    } else {
   9641                        self.msg_editor_current_match_index = None;
   9642                    }
   9643                    
   9644                    self.msg_editor_search_query.clear();
   9645                    return true;
   9646                }
   9647                '\x1b' => {
   9648                    // Escape - cancel search
   9649                    self.msg_editor_search_mode = false;
   9650                    self.msg_editor_search_query.clear();
   9651                    return true;
   9652                }
   9653                '\x08' => {
   9654                    // Backspace
   9655                    self.msg_editor_search_query.pop();
   9656                    return true;
   9657                }
   9658                c if c.is_ascii() && !c.is_control() => {
   9659                    self.msg_editor_search_query.push(c);
   9660                    return true;
   9661                }
   9662                _ => return true,
   9663            }
   9664        }
   9665 
   9666        // Handle pending 'g' commands
   9667        if self.msg_editor_pending_g {
   9668            self.msg_editor_pending_g = false;
   9669            match key {
   9670                'g' => {
   9671                    // gg - go to top
   9672                    self.msg_editor_cursor_pos = (0, 0);
   9673                    self.msg_editor_scroll_offset = 0;
   9674                    return true;
   9675                }
   9676                _ => {}
   9677            }
   9678        }
   9679 
   9680        // Handle pending 'd' commands (dd for line deletion)
   9681        if self.msg_editor_pending_d {
   9682            self.msg_editor_pending_d = false;
   9683            match key {
   9684                'd' => {
   9685                    // dd - delete line
   9686                    self.handle_msg_editor_dd();
   9687                    return true;
   9688                }
   9689                '\x1b' => {
   9690                    // Escape - cancel dd
   9691                    return true;
   9692                }
   9693                _ => {
   9694                    // Invalid d command, fall through to normal processing
   9695                }
   9696            }
   9697        }
   9698 
   9699        // Handle number prefixes - special handling for '0'
   9700        if key.is_ascii_digit() {
   9701            if self.msg_editor_number_prefix.is_none() {
   9702                // First digit
   9703                if key == '0' {
   9704                    // '0' as first digit should be treated as motion (start of line), not number prefix
   9705                    // Fall through to normal key handling
   9706                } else {
   9707                    // '1'-'9' as first digit starts number prefix
   9708                    self.msg_editor_number_prefix = Some(String::new());
   9709                    self.msg_editor_number_prefix.as_mut().unwrap().push(key);
   9710                    return true;
   9711                }
   9712            } else {
   9713                // Subsequent digit (including '0') can be added to existing prefix
   9714                self.msg_editor_number_prefix.as_mut().unwrap().push(key);
   9715                return true;
   9716            }
   9717        }
   9718 
   9719        // Get repetition count
   9720        let count = if let Some(ref prefix) = self.msg_editor_number_prefix {
   9721            prefix.parse::<usize>().unwrap_or(1)
   9722        } else {
   9723            1
   9724        };
   9725        
   9726        // Clear number prefix after using it
   9727        self.msg_editor_number_prefix = None;
   9728 
   9729        // Clear pending states if any other key is pressed (except the expected ones)
   9730        let should_clear_pending_states = match key {
   9731            'd' if !self.msg_editor_pending_d => false, // Allow first 'd'
   9732            'd' | '\x1b' => false, // Allow second 'd' or escape when pending
   9733            _ if self.msg_editor_pending_d => true, // Clear pending 'd' for any other key
   9734            _ => false,
   9735        };
   9736        
   9737        if should_clear_pending_states {
   9738            self.msg_editor_pending_d = false;
   9739        }
   9740 
   9741        match key {
   9742            'h' => {
   9743                for _ in 0..count {
   9744                    if self.msg_editor_cursor_pos.1 > 0 {
   9745                        self.msg_editor_cursor_pos.1 -= 1;
   9746                    } else {
   9747                        break;
   9748                    }
   9749                }
   9750                self.ensure_msg_editor_cursor_visible();
   9751                true
   9752            }
   9753            'j' => {
   9754                for _ in 0..count {
   9755                    if self.msg_editor_cursor_pos.0 < self.msg_editor_content.len() - 1 {
   9756                        self.msg_editor_cursor_pos.0 += 1;
   9757                        let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9758                        if self.msg_editor_cursor_pos.1 > line_len {
   9759                            self.msg_editor_cursor_pos.1 = line_len;
   9760                        }
   9761                    } else {
   9762                        break;
   9763                    }
   9764                }
   9765                self.ensure_msg_editor_cursor_visible();
   9766                true
   9767            }
   9768            'k' => {
   9769                for _ in 0..count {
   9770                    if self.msg_editor_cursor_pos.0 > 0 {
   9771                        self.msg_editor_cursor_pos.0 -= 1;
   9772                        let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9773                        if self.msg_editor_cursor_pos.1 > line_len {
   9774                            self.msg_editor_cursor_pos.1 = line_len;
   9775                        }
   9776                    } else {
   9777                        break;
   9778                    }
   9779                }
   9780                self.ensure_msg_editor_cursor_visible();
   9781                true
   9782            }
   9783            'l' => {
   9784                for _ in 0..count {
   9785                    let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9786                    if self.msg_editor_cursor_pos.1 < line_len {
   9787                        self.msg_editor_cursor_pos.1 += 1;
   9788                    } else {
   9789                        break;
   9790                    }
   9791                }
   9792                self.ensure_msg_editor_cursor_visible();
   9793                true
   9794            }
   9795            'w' => {
   9796                // Word forward
   9797                for _ in 0..count {
   9798                    let current_line = &self.msg_editor_content[self.msg_editor_cursor_pos.0];
   9799                    let new_col = Self::find_next_word_boundary(current_line, self.msg_editor_cursor_pos.1);
   9800                    
   9801                    if new_col < current_line.len() {
   9802                        self.msg_editor_cursor_pos.1 = new_col;
   9803                    } else if self.msg_editor_cursor_pos.0 < self.msg_editor_content.len() - 1 {
   9804                        // Move to beginning of next line
   9805                        self.msg_editor_cursor_pos.0 += 1;
   9806                        self.msg_editor_cursor_pos.1 = 0;
   9807                        // Skip to first non-whitespace character
   9808                        let next_line = &self.msg_editor_content[self.msg_editor_cursor_pos.0];
   9809                        for (i, ch) in next_line.chars().enumerate() {
   9810                            if !ch.is_whitespace() {
   9811                                self.msg_editor_cursor_pos.1 = i;
   9812                                break;
   9813                            }
   9814                        }
   9815                    } else {
   9816                        break;
   9817                    }
   9818                }
   9819                self.ensure_msg_editor_cursor_visible();
   9820                true
   9821            }
   9822            'b' => {
   9823                // Word backward
   9824                for _ in 0..count {
   9825                    let current_line = &self.msg_editor_content[self.msg_editor_cursor_pos.0];
   9826                    let new_col = Self::find_prev_word_boundary(current_line, self.msg_editor_cursor_pos.1);
   9827                    
   9828                    if new_col < self.msg_editor_cursor_pos.1 {
   9829                        self.msg_editor_cursor_pos.1 = new_col;
   9830                    } else if self.msg_editor_cursor_pos.0 > 0 {
   9831                        // Move to end of previous line
   9832                        self.msg_editor_cursor_pos.0 -= 1;
   9833                        self.msg_editor_cursor_pos.1 = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9834                    } else {
   9835                        break;
   9836                    }
   9837                }
   9838                self.ensure_msg_editor_cursor_visible();
   9839                true
   9840            }
   9841            '$' => {
   9842                // End of line
   9843                self.msg_editor_cursor_pos.1 = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9844                self.ensure_msg_editor_cursor_visible();
   9845                true
   9846            }
   9847            '0' => {
   9848                // Beginning of line
   9849                self.msg_editor_cursor_pos.1 = 0;
   9850                self.ensure_msg_editor_cursor_visible();
   9851                true
   9852            }
   9853            '/' => {
   9854                // Start search
   9855                self.msg_editor_search_mode = true;
   9856                self.msg_editor_search_query.clear();
   9857                true
   9858            }
   9859            'G' => {
   9860                self.msg_editor_cursor_pos.0 = self.msg_editor_content.len() - 1;
   9861                self.msg_editor_cursor_pos.1 = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9862                self.ensure_msg_editor_cursor_visible();
   9863                true
   9864            }
   9865            'g' => {
   9866                self.msg_editor_pending_g = true;
   9867                true
   9868            }
   9869            'i' => {
   9870                // Save state before entering insert mode
   9871                self.save_msg_editor_state();
   9872                self.clear_msg_editor_search_results(); // Clear search on mode change
   9873                self.msg_editor_vim_mode = VimMode::Insert;
   9874                true
   9875            }
   9876            'a' => {
   9877                // Save state before entering insert mode
   9878                self.save_msg_editor_state();
   9879                self.clear_msg_editor_search_results(); // Clear search on mode change
   9880                self.msg_editor_vim_mode = VimMode::Insert;
   9881                let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9882                if self.msg_editor_cursor_pos.1 < line_len {
   9883                    self.msg_editor_cursor_pos.1 += 1;
   9884                }
   9885                true
   9886            }
   9887            'A' => {
   9888                // Save state before entering insert mode
   9889                self.save_msg_editor_state();
   9890                self.clear_msg_editor_search_results(); // Clear search on mode change
   9891                self.msg_editor_vim_mode = VimMode::Insert;
   9892                self.msg_editor_cursor_pos.1 = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9893                true
   9894            }
   9895            'x' => {
   9896                // Save state before making changes
   9897                self.save_msg_editor_state();
   9898                let (line, col) = self.msg_editor_cursor_pos;
   9899                if col < self.msg_editor_content[line].len() {
   9900                    self.msg_editor_content[line].remove(col);
   9901                }
   9902                true
   9903            }
   9904            'v' => {
   9905                self.msg_editor_vim_mode = VimMode::Visual;
   9906                self.msg_editor_visual_start = Some(self.msg_editor_cursor_pos);
   9907                true
   9908            }
   9909            'u' => {
   9910                // Undo
   9911                self.msg_editor_undo();
   9912                true
   9913            }
   9914            'd' => {
   9915                // First 'd' - wait for second one
   9916                self.msg_editor_pending_d = true;
   9917                true
   9918            }
   9919            ':' => {
   9920                self.msg_editor_vim_mode = VimMode::Command;
   9921                self.msg_editor_vim_command.clear();
   9922                true
   9923            }
   9924            'n' => {
   9925                // Next search match
   9926                self.msg_editor_next_match();
   9927                true
   9928            }
   9929            'N' => {
   9930                // Previous search match
   9931                self.msg_editor_prev_match();
   9932                true
   9933            }
   9934            _ => false,
   9935        }
   9936    }
   9937 
   9938    fn handle_msg_editor_insert_mode(&mut self, key: char) -> bool {
   9939        if key == '\x1b' {
   9940            self.msg_editor_vim_mode = VimMode::Normal;
   9941            if self.msg_editor_cursor_pos.1 > 0 {
   9942                self.msg_editor_cursor_pos.1 -= 1;
   9943            }
   9944            return true;
   9945        }
   9946 
   9947        // Clear search results when in insert mode (mode switch)
   9948        if !self.msg_editor_search_matches.is_empty() {
   9949            self.clear_msg_editor_search_results();
   9950        }
   9951 
   9952        match key {
   9953            '\n' | '\r' => {
   9954                let (line, col) = self.msg_editor_cursor_pos;
   9955                let current_line = self.msg_editor_content[line].clone();
   9956                let (left, right) = current_line.split_at(col);
   9957                self.msg_editor_content[line] = left.to_string();
   9958                self.msg_editor_content.insert(line + 1, right.to_string());
   9959                self.msg_editor_cursor_pos = (line + 1, 0);
   9960                self.ensure_msg_editor_cursor_visible();
   9961                true
   9962            }
   9963            '\x08' | '\x7f' => {
   9964                if self.msg_editor_cursor_pos.1 > 0 {
   9965                    let (line, col) = self.msg_editor_cursor_pos;
   9966                    self.msg_editor_content[line].remove(col - 1);
   9967                    self.msg_editor_cursor_pos.1 -= 1;
   9968                } else if self.msg_editor_cursor_pos.0 > 0 {
   9969                    let current_line = self.msg_editor_content.remove(self.msg_editor_cursor_pos.0);
   9970                    self.msg_editor_cursor_pos.0 -= 1;
   9971                    self.msg_editor_cursor_pos.1 = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
   9972                    self.msg_editor_content[self.msg_editor_cursor_pos.0].push_str(&current_line);
   9973                    self.ensure_msg_editor_cursor_visible();
   9974                }
   9975                true
   9976            }
   9977            c if c.is_ascii() && !c.is_control() => {
   9978                let (line, col) = self.msg_editor_cursor_pos;
   9979                self.msg_editor_content[line].insert(col, c);
   9980                self.msg_editor_cursor_pos.1 += 1;
   9981                true
   9982            }
   9983            _ => false,
   9984        }
   9985    }
   9986 
   9987    fn handle_msg_editor_visual_mode(&mut self, key: char) -> bool {
   9988        match key {
   9989            'h' => {
   9990                if self.msg_editor_cursor_pos.1 > 0 {
   9991                    self.msg_editor_cursor_pos.1 -= 1;
   9992                }
   9993                self.ensure_msg_editor_cursor_visible();
   9994                true
   9995            }
   9996            'j' => {
   9997                if self.msg_editor_cursor_pos.0 < self.msg_editor_content.len() - 1 {
   9998                    self.msg_editor_cursor_pos.0 += 1;
   9999                    let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
  10000                    if self.msg_editor_cursor_pos.1 > line_len {
  10001                        self.msg_editor_cursor_pos.1 = line_len;
  10002                    }
  10003                }
  10004                self.ensure_msg_editor_cursor_visible();
  10005                true
  10006            }
  10007            'k' => {
  10008                if self.msg_editor_cursor_pos.0 > 0 {
  10009                    self.msg_editor_cursor_pos.0 -= 1;
  10010                    let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
  10011                    if self.msg_editor_cursor_pos.1 > line_len {
  10012                        self.msg_editor_cursor_pos.1 = line_len;
  10013                    }
  10014                }
  10015                self.ensure_msg_editor_cursor_visible();
  10016                true
  10017            }
  10018            'l' => {
  10019                let line_len = self.msg_editor_content[self.msg_editor_cursor_pos.0].len();
  10020                if self.msg_editor_cursor_pos.1 < line_len {
  10021                    self.msg_editor_cursor_pos.1 += 1;
  10022                }
  10023                self.ensure_msg_editor_cursor_visible();
  10024                true
  10025            }
  10026            'x' => {
  10027                self.delete_msg_editor_visual_selection();
  10028                self.msg_editor_vim_mode = VimMode::Normal;
  10029                self.msg_editor_visual_start = None;
  10030                true
  10031            }
  10032            '\x1b' => {
  10033                self.msg_editor_vim_mode = VimMode::Normal;
  10034                self.msg_editor_visual_start = None;
  10035                true
  10036            }
  10037            _ => false,
  10038        }
  10039    }
  10040 
  10041    fn handle_msg_editor_command_mode(&mut self, key: char) -> EditorCommand {
  10042        match key {
  10043            '\n' | '\r' => {
  10044                let command = self.execute_msg_editor_vim_command();
  10045                self.msg_editor_vim_mode = VimMode::Normal;
  10046                return command;
  10047            }
  10048            '\x1b' => {
  10049                self.msg_editor_vim_mode = VimMode::Normal;
  10050                self.msg_editor_vim_command.clear();
  10051                EditorCommand::None
  10052            }
  10053            '\x08' | '\x7f' => {
  10054                self.msg_editor_vim_command.pop();
  10055                EditorCommand::None
  10056            }
  10057            c if c.is_ascii() => {
  10058                self.msg_editor_vim_command.push(c);
  10059                EditorCommand::None
  10060            }
  10061            _ => EditorCommand::None,
  10062        }
  10063    }
  10064 
  10065    fn execute_msg_editor_vim_command(&mut self) -> EditorCommand {
  10066        let command = match self.msg_editor_vim_command.as_str() {
  10067            "w" => {
  10068                // Send message and exit
  10069                let content = self.msg_editor_content.join("\n");
  10070                self.exit_message_editor_mode();
  10071                self.msg_editor_vim_command.clear();
  10072                EditorCommand::Send(content)
  10073            }
  10074            "q" => {
  10075                // Quit without sending
  10076                self.exit_message_editor_mode();
  10077                self.msg_editor_vim_command.clear();
  10078                EditorCommand::Quit
  10079            }
  10080            "wq" => {
  10081                // Send and quit (same as :w)
  10082                let content = self.msg_editor_content.join("\n");
  10083                self.exit_message_editor_mode();
  10084                self.msg_editor_vim_command.clear();
  10085                EditorCommand::Send(content)
  10086            }
  10087            _ => {
  10088                self.msg_editor_vim_command.clear();
  10089                EditorCommand::None
  10090            }
  10091        };
  10092        command
  10093    }
  10094 
  10095    fn handle_msg_editor_dd(&mut self) {
  10096        // Save state before making changes
  10097        self.save_msg_editor_state();
  10098        
  10099        let (line, _) = self.msg_editor_cursor_pos;
  10100        if self.msg_editor_content.len() > 1 {
  10101            self.msg_editor_content.remove(line);
  10102            if line >= self.msg_editor_content.len() {
  10103                self.msg_editor_cursor_pos.0 = self.msg_editor_content.len() - 1;
  10104            }
  10105            self.msg_editor_cursor_pos.1 = 0;
  10106        } else {
  10107            self.msg_editor_content[0].clear();
  10108            self.msg_editor_cursor_pos = (0, 0);
  10109        }
  10110    }
  10111 
  10112    fn ensure_msg_editor_cursor_visible(&mut self) {
  10113        let visible_lines = 50; // Conservative estimate - UI will handle actual height
  10114        let (line, _) = self.msg_editor_cursor_pos;
  10115        
  10116        if line < self.msg_editor_scroll_offset {
  10117            self.msg_editor_scroll_offset = line;
  10118        } else if line >= self.msg_editor_scroll_offset + visible_lines {
  10119            self.msg_editor_scroll_offset = line - visible_lines + 1;
  10120        }
  10121    }
  10122 
  10123    fn delete_msg_editor_visual_selection(&mut self) {
  10124        if let Some(start) = self.msg_editor_visual_start {
  10125            let end = self.msg_editor_cursor_pos;
  10126            let (start_pos, end_pos) = if start <= end {
  10127                (start, end)
  10128            } else {
  10129                (end, start)
  10130            };
  10131 
  10132            if start_pos.0 == end_pos.0 {
  10133                let line = start_pos.0;
  10134                let start_col = start_pos.1;
  10135                let end_col = end_pos.1;
  10136                
  10137                if start_col < end_col && end_col <= self.msg_editor_content[line].len() {
  10138                    self.msg_editor_content[line].drain(start_col..end_col);
  10139                    self.msg_editor_cursor_pos = start_pos;
  10140                }
  10141            }
  10142        }
  10143    }
  10144 
  10145    // Undo/Redo functionality for notes editor
  10146    fn save_notes_state(&mut self) {
  10147        // Limit history size to prevent memory bloat
  10148        const MAX_HISTORY: usize = 100;
  10149        
  10150        // Truncate history if we're not at the end (when doing new action after undo)
  10151        if self.notes_undo_index < self.notes_undo_history.len() - 1 {
  10152            self.notes_undo_history.truncate(self.notes_undo_index + 1);
  10153            self.notes_undo_cursor_history.truncate(self.notes_undo_index + 1);
  10154        }
  10155        
  10156        // Add new state
  10157        self.notes_undo_history.push(self.notes_content.clone());
  10158        self.notes_undo_cursor_history.push(self.notes_cursor_pos);
  10159        
  10160        // Limit history size
  10161        if self.notes_undo_history.len() > MAX_HISTORY {
  10162            self.notes_undo_history.remove(0);
  10163            self.notes_undo_cursor_history.remove(0);
  10164        } else {
  10165            self.notes_undo_index += 1;
  10166        }
  10167        
  10168        if self.notes_undo_history.len() > MAX_HISTORY {
  10169            self.notes_undo_index = MAX_HISTORY - 1;
  10170        }
  10171    }
  10172    
  10173    fn notes_undo(&mut self) {
  10174        if self.notes_undo_index > 0 {
  10175            self.notes_undo_index -= 1;
  10176            self.notes_content = self.notes_undo_history[self.notes_undo_index].clone();
  10177            self.notes_cursor_pos = self.notes_undo_cursor_history[self.notes_undo_index];
  10178            self.notes_modified = true;
  10179            self.ensure_cursor_visible();
  10180        }
  10181    }
  10182    
  10183    fn notes_redo(&mut self) {
  10184        if self.notes_undo_index < self.notes_undo_history.len() - 1 {
  10185            self.notes_undo_index += 1;
  10186            self.notes_content = self.notes_undo_history[self.notes_undo_index].clone();
  10187            self.notes_cursor_pos = self.notes_undo_cursor_history[self.notes_undo_index];
  10188            self.notes_modified = true;
  10189            self.ensure_cursor_visible();
  10190        }
  10191    }
  10192 
  10193    // Undo/Redo functionality for message editor
  10194    fn save_msg_editor_state(&mut self) {
  10195        // Limit history size to prevent memory bloat
  10196        const MAX_HISTORY: usize = 100;
  10197        
  10198        // Truncate history if we're not at the end (when doing new action after undo)
  10199        if self.msg_editor_undo_index < self.msg_editor_undo_history.len() - 1 {
  10200            self.msg_editor_undo_history.truncate(self.msg_editor_undo_index + 1);
  10201            self.msg_editor_undo_cursor_history.truncate(self.msg_editor_undo_index + 1);
  10202        }
  10203        
  10204        // Add new state
  10205        self.msg_editor_undo_history.push(self.msg_editor_content.clone());
  10206        self.msg_editor_undo_cursor_history.push(self.msg_editor_cursor_pos);
  10207        
  10208        // Limit history size
  10209        if self.msg_editor_undo_history.len() > MAX_HISTORY {
  10210            self.msg_editor_undo_history.remove(0);
  10211            self.msg_editor_undo_cursor_history.remove(0);
  10212        } else {
  10213            self.msg_editor_undo_index += 1;
  10214        }
  10215        
  10216        if self.msg_editor_undo_history.len() > MAX_HISTORY {
  10217            self.msg_editor_undo_index = MAX_HISTORY - 1;
  10218        }
  10219    }
  10220    
  10221    fn msg_editor_undo(&mut self) {
  10222        if self.msg_editor_undo_index > 0 {
  10223            self.msg_editor_undo_index -= 1;
  10224            self.msg_editor_content = self.msg_editor_undo_history[self.msg_editor_undo_index].clone();
  10225            self.msg_editor_cursor_pos = self.msg_editor_undo_cursor_history[self.msg_editor_undo_index];
  10226            self.ensure_msg_editor_cursor_visible();
  10227        }
  10228    }
  10229    
  10230    fn msg_editor_redo(&mut self) {
  10231        if self.msg_editor_undo_index < self.msg_editor_undo_history.len() - 1 {
  10232            self.msg_editor_undo_index += 1;
  10233            self.msg_editor_content = self.msg_editor_undo_history[self.msg_editor_undo_index].clone();
  10234            self.msg_editor_cursor_pos = self.msg_editor_undo_cursor_history[self.msg_editor_undo_index];
  10235            self.ensure_msg_editor_cursor_visible();
  10236        }
  10237    }
  10238 }
  10239 
  10240 pub enum Event<I> {
  10241    Input(I),
  10242    Tick,
  10243    Terminate,
  10244    NeedLogin,
  10245 }
  10246 
  10247 /// A small event handler that wrap termion input and tick events. Each event
  10248 /// type is handled in its own thread and returned to a common `Receiver`
  10249 struct Events {
  10250    messages_updated_rx: crossbeam_channel::Receiver<()>,
  10251    exit_rx: crossbeam_channel::Receiver<ExitSignal>,
  10252    rx: crossbeam_channel::Receiver<Event<CEvent>>,
  10253 }
  10254 
  10255 #[derive(Debug, Clone)]
  10256 struct Config {
  10257    pub exit_rx: crossbeam_channel::Receiver<ExitSignal>,
  10258    pub messages_updated_rx: crossbeam_channel::Receiver<()>,
  10259    pub tick_rate: Duration,
  10260 }
  10261 
  10262 impl Events {
  10263    fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) {
  10264        let (tx, rx) = crossbeam_channel::unbounded();
  10265        let tick_rate = config.tick_rate;
  10266        let exit_rx = config.exit_rx;
  10267        let messages_updated_rx = config.messages_updated_rx;
  10268        let exit_rx1 = exit_rx.clone();
  10269        let thread_handle = thread::spawn(move || {
  10270            let mut last_tick = Instant::now();
  10271            loop {
  10272                // poll for tick rate duration, if no events, sent tick event.
  10273                let timeout = tick_rate
  10274                    .checked_sub(last_tick.elapsed())
  10275                    .unwrap_or_else(|| Duration::from_secs(0));
  10276                if event::poll(timeout).unwrap() {
  10277                    let evt = event::read().unwrap();
  10278                    match evt {
  10279                        CEvent::FocusGained => {}
  10280                        CEvent::FocusLost => {}
  10281                        CEvent::Paste(_) => {}
  10282                        CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(),
  10283                        CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(),
  10284                        CEvent::Mouse(mouse_event) => {
  10285                            match mouse_event.kind {
  10286                                MouseEventKind::ScrollDown
  10287                                | MouseEventKind::ScrollUp
  10288                                | MouseEventKind::Down(_) => {
  10289                                    tx.send(Event::Input(evt)).unwrap();
  10290                                }
  10291                                _ => {}
  10292                            };
  10293                        }
  10294                    };
  10295                }
  10296                if last_tick.elapsed() >= tick_rate {
  10297                    select! {
  10298                        recv(&exit_rx1) -> _ => break,
  10299                        default => {},
  10300                    }
  10301                    last_tick = Instant::now();
  10302                }
  10303            }
  10304        });
  10305        (
  10306            Events {
  10307                rx,
  10308                exit_rx,
  10309                messages_updated_rx,
  10310            },
  10311            thread_handle,
  10312        )
  10313    }
  10314 
  10315    fn next(&self) -> Result<Event<CEvent>, crossbeam_channel::RecvError> {
  10316        select! {
  10317            recv(&self.rx) -> evt => evt,
  10318            recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick),
  10319            recv(&self.exit_rx) -> v => match v {
  10320                Ok(ExitSignal::Terminate) => Ok(Event::Terminate),
  10321                Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin),
  10322                Err(_) => Ok(Event::Terminate),
  10323            },
  10324        }
  10325    }
  10326 }
  10327 
  10328 #[cfg(test)]
  10329 mod tests {
  10330    use super::*;
  10331 
  10332    #[test]
  10333    fn gen_lines_test() {
  10334        let txt = StyledText::Styled(
  10335            tuiColor::White,
  10336            vec![
  10337                StyledText::Styled(
  10338                    tuiColor::Rgb(255, 255, 255),
  10339                    vec![
  10340                        StyledText::Text(" prmdbba pwuv💓".to_owned()),
  10341                        StyledText::Styled(
  10342                            tuiColor::Rgb(255, 255, 255),
  10343                            vec![StyledText::Styled(
  10344                                tuiColor::Rgb(0, 255, 0),
  10345                                vec![StyledText::Text("PMW".to_owned())],
  10346                            )],
  10347                        ),
  10348                        StyledText::Styled(
  10349                            tuiColor::Rgb(255, 255, 255),
  10350                            vec![StyledText::Styled(
  10351                                tuiColor::Rgb(255, 255, 255),
  10352                                vec![StyledText::Text("A".to_owned())],
  10353                            )],
  10354                        ),
  10355                        StyledText::Styled(
  10356                            tuiColor::Rgb(255, 255, 255),
  10357                            vec![StyledText::Styled(
  10358                                tuiColor::Rgb(0, 255, 0),
  10359                                vec![StyledText::Text("XOS".to_owned())],
  10360                            )],
  10361                        ),
  10362                        StyledText::Text(
  10363                            "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(),
  10364                        ),
  10365                    ],
  10366                ),
  10367                StyledText::Text(" - ".to_owned()),
  10368                StyledText::Styled(
  10369                    tuiColor::Rgb(255, 255, 255),
  10370                    vec![StyledText::Text("rytxvgs".to_owned())],
  10371                ),
  10372            ],
  10373        );
  10374        let lines = gen_lines(&txt, 71, "");
  10375        assert_eq!(lines.len(), 2);
  10376    }
  10377 
  10378    #[test]
  10379    fn parse_forwarded_username_member() {
  10380        let text = "[Alice to Bob] [M] @foo: hi";
  10381        assert_eq!(
  10382            parse_forwarded_username(text, "[M] ", "[Staff] "),
  10383            Some(("/m", "foo".to_owned()))
  10384        );
  10385    }
  10386 
  10387    #[test]
  10388    fn parse_forwarded_username_staff() {
  10389        let text = "[Jack to Squareeyes] [Staff] @bar: hey";
  10390        assert_eq!(
  10391            parse_forwarded_username(text, "[M] ", "[Staff] "),
  10392            Some(("/s", "bar".to_owned()))
  10393        );
  10394    }
  10395 
  10396    #[test]
  10397    fn parse_forwarded_username_with_dash() {
  10398        let text = "[Dasho to Dexter] - [M] rex - @sh4d0w most welcome";
  10399        assert_eq!(
  10400            parse_forwarded_username(text, "[M] ", "[Staff] "),
  10401            Some(("/m", "rex".to_owned()))
  10402        );
  10403    }
  10404 
  10405    #[test]
  10406    fn test_quick_moderation_violence_patterns() {
  10407        // Test that the quick moderation function works for spam and CP patterns
  10408        // Violence patterns are not included in quick_moderation_check
  10409        assert_eq!(quick_moderation_check("should I kill my kid"), None);
  10410        assert_eq!(quick_moderation_check("Should I Kill My Kid"), None);
  10411        assert_eq!(quick_moderation_check("I want to kill my child"), None);
  10412        assert_eq!(quick_moderation_check("how to hurt my kid"), None);
  10413        assert_eq!(quick_moderation_check("gonna kill a child"), None);
  10414 
  10415        // Test that safe messages are not caught
  10416        assert_eq!(quick_moderation_check("my kid is driving me crazy"), None);
  10417        assert_eq!(quick_moderation_check("I love my child"), None);
  10418        assert_eq!(quick_moderation_check("hello everyone"), None);
  10419 
  10420        // Test existing patterns still work
  10421        assert_eq!(
  10422            quick_moderation_check("looking for cheese pizza"),
  10423            Some(true)
  10424        );
  10425        assert_eq!(quick_moderation_check("young models"), Some(true));
  10426    }
  10427 
  10428    #[test]
  10429    fn test_warning_violations() {
  10430        // Test CP-related content
  10431        assert_eq!(
  10432            check_warning_violations("looking for cheese pizza"),
  10433            Some("inappropriate content involving minors".to_string())
  10434        );
  10435        assert_eq!(
  10436            check_warning_violations("young boy pics"),
  10437            Some("inappropriate content involving minors".to_string())
  10438        );
  10439        assert_eq!(
  10440            check_warning_violations("trading CP"),
  10441            Some("inappropriate content involving minors".to_string())
  10442        );
  10443 
  10444        // Test pornography patterns
  10445        assert_eq!(
  10446            check_warning_violations("send nudes"),
  10447            Some("inappropriate adult content".to_string())
  10448        );
  10449        assert_eq!(
  10450            check_warning_violations("porn links anyone?"),
  10451            Some("inappropriate adult content".to_string())
  10452        );
  10453        assert_eq!(
  10454            check_warning_violations("check out my onlyfans"),
  10455            Some("inappropriate adult content".to_string())
  10456        );
  10457 
  10458        // Test gun/weapon purchases
  10459        assert_eq!(
  10460            check_warning_violations("want to buy gun"),
  10461            Some("attempting to buy/sell weapons".to_string())
  10462        );
  10463        assert_eq!(
  10464            check_warning_violations("selling pistol"),
  10465            Some("attempting to buy/sell weapons".to_string())
  10466        );
  10467        assert_eq!(
  10468            check_warning_violations("firearm for sale"),
  10469            Some("attempting to buy/sell weapons".to_string())
  10470        );
  10471 
  10472        // Test account hacking
  10473        assert_eq!(
  10474            check_warning_violations("can hack facebook account"),
  10475            Some("offering/requesting account hacking services".to_string())
  10476        );
  10477        assert_eq!(
  10478            check_warning_violations("instagram hacker available"),
  10479            Some("offering/requesting account hacking services".to_string())
  10480        );
  10481        assert_eq!(
  10482            check_warning_violations("password crack service"),
  10483            Some("offering/requesting account hacking services".to_string())
  10484        );
  10485 
  10486        // Test spam detection
  10487        assert_eq!(
  10488            check_warning_violations("buy buy buy buy buy buy buy buy buy buy buy"),
  10489            Some("spamming/excessive repetition".to_string())
  10490        );
  10491 
  10492        // Test excessive caps
  10493        assert_eq!(
  10494            check_warning_violations("THIS IS A VERY LONG MESSAGE WITH TOO MANY CAPS"),
  10495            Some("excessive use of capital letters".to_string())
  10496        );
  10497 
  10498        // Test normal messages (should return None)
  10499        assert_eq!(check_warning_violations("hello everyone"), None);
  10500        assert_eq!(check_warning_violations("how are you today?"), None);
  10501        assert_eq!(check_warning_violations("I ordered pizza for dinner"), None);
  10502        assert_eq!(check_warning_violations("My gun collection is nice"), None);
  10503        // Should be fine, not buying/selling
  10504    }
  10505 
  10506    #[test]
  10507    fn test_warning_tracking() {
  10508        use std::collections::HashMap;
  10509        use std::sync::{Arc, Mutex};
  10510 
  10511        // Create a simple warning tracking HashMap like the one in LeChatPHPClient
  10512        let mut user_warnings: HashMap<String, u32> = HashMap::new();
  10513 
  10514        // Test warning increment
  10515        assert_eq!(user_warnings.get("testuser"), None);
  10516 
  10517        // Simulate warnings
  10518        user_warnings.insert("testuser".to_string(), 1);
  10519        assert_eq!(user_warnings.get("testuser"), Some(&1));
  10520 
  10521        user_warnings.insert("testuser".to_string(), 2);
  10522        assert_eq!(user_warnings.get("testuser"), Some(&2));
  10523 
  10524        user_warnings.insert("testuser".to_string(), 3);
  10525        assert_eq!(user_warnings.get("testuser"), Some(&3));
  10526 
  10527        // Test clearing warnings
  10528        user_warnings.remove("testuser");
  10529        assert_eq!(user_warnings.get("testuser"), None);
  10530    }
  10531 
  10532    #[test]
  10533    fn test_directed_message_detection() {
  10534        // Test messages directed at other users (should not trigger AI responses)
  10535 
  10536        // Messages starting with @username
  10537        assert!(is_message_directed_at_other(
  10538            "@alice hello there",
  10539            "botname"
  10540        ));
  10541        assert!(is_message_directed_at_other(
  10542            "@bob how are you doing?",
  10543            "botname"
  10544        ));
  10545 
  10546        // Messages ending with @username
  10547        assert!(is_message_directed_at_other(
  10548            "hello there @alice",
  10549            "botname"
  10550        ));
  10551        assert!(is_message_directed_at_other(
  10552            "this is for you @bob",
  10553            "botname"
  10554        ));
  10555 
  10556        // Single @username messages
  10557        assert!(is_message_directed_at_other("@alice", "botname"));
  10558 
  10559        // Messages directed at the bot (should return false - these should trigger responses)
  10560        assert!(!is_message_directed_at_other("@botname hello", "botname"));
  10561        assert!(!is_message_directed_at_other("hello @botname", "botname"));
  10562        assert!(!is_message_directed_at_other("@botname", "botname"));
  10563 
  10564        // Messages with @username in the middle (should return false - not directed)
  10565        assert!(!is_message_directed_at_other(
  10566            "I think @alice said something",
  10567            "botname"
  10568        ));
  10569        assert!(!is_message_directed_at_other(
  10570            "hey everyone, @alice is awesome and cool",
  10571            "botname"
  10572        ));
  10573 
  10574        // Messages ending with @username (should return true - directed)
  10575        assert!(is_message_directed_at_other(
  10576            "I think something about @bob",
  10577            "botname"
  10578        ));
  10579        assert!(is_message_directed_at_other(
  10580            "this message is for @alice",
  10581            "botname"
  10582        ));
  10583 
  10584        // Messages without any @mentions (should return false)
  10585        assert!(!is_message_directed_at_other("hello everyone", "botname"));
  10586        assert!(!is_message_directed_at_other(
  10587            "how is everyone doing?",
  10588            "botname"
  10589        ));
  10590    }
  10591 
  10592    // Helper function to test the directed message logic
  10593    fn is_message_directed_at_other(msg: &str, username: &str) -> bool {
  10594        let msg_trimmed = msg.trim();
  10595 
  10596        // Check for @username at the start (first word)
  10597        let first_word = msg_trimmed.split_whitespace().next().unwrap_or("");
  10598        let starts_with_tag = first_word.starts_with('@') && first_word != format!("@{}", username);
  10599 
  10600        // Check for @username at the end (last word)
  10601        let last_word = msg_trimmed.split_whitespace().last().unwrap_or("");
  10602        let ends_with_tag = last_word.starts_with('@') && last_word != format!("@{}", username);
  10603 
  10604        starts_with_tag || ends_with_tag
  10605    }
  10606 
  10607    // Mock OpenAI client for testing
  10608    struct MockOpenAIClient {
  10609        should_moderate: bool,
  10610        should_error: bool,
  10611    }
  10612 
  10613    impl MockOpenAIClient {
  10614        fn new(should_moderate: bool) -> Self {
  10615            Self {
  10616                should_moderate,
  10617                should_error: false,
  10618            }
  10619        }
  10620 
  10621        fn new_with_error() -> Self {
  10622            Self {
  10623                should_moderate: false,
  10624                should_error: true,
  10625            }
  10626        }
  10627 
  10628        async fn mock_moderation_response(
  10629            &self,
  10630            _message: &str,
  10631            _strictness: &str,
  10632        ) -> Option<bool> {
  10633            if self.should_error {
  10634                return None;
  10635            }
  10636            Some(self.should_moderate)
  10637        }
  10638    }
  10639 
  10640    #[tokio::test]
  10641    async fn test_ai_moderation_system_prompt_generation() {
  10642        // Test that different strictness levels generate appropriate prompts
  10643        let strictness_levels = vec!["strict", "lenient", "balanced"];
  10644 
  10645        for strictness in strictness_levels {
  10646            let guidance = match strictness {
  10647                "strict" => "Be very strict. Moderate anything that could potentially violate rules. When in doubt, moderate.",
  10648                "lenient" => "Be very lenient. Only moderate clear, obvious violations. Heavily favor free speech. When in doubt, allow.",
  10649                _ => "Use balanced judgment. Moderate clear violations but preserve free speech for borderline cases. When in doubt about context, lean towards allowing."
  10650            };
  10651 
  10652            // Verify the guidance is correct for each strictness level
  10653            assert!(guidance.len() > 0);
  10654            if strictness == "strict" {
  10655                assert!(guidance.contains("When in doubt, moderate"));
  10656            } else if strictness == "lenient" {
  10657                assert!(guidance.contains("When in doubt, allow"));
  10658            } else {
  10659                assert!(guidance.contains("When in doubt about context, lean towards allowing"));
  10660            }
  10661        }
  10662    }
  10663 
  10664    #[tokio::test]
  10665    async fn test_ai_moderation_mock_responses() {
  10666        // Test mock client that should moderate
  10667        let mock_client = MockOpenAIClient::new(true);
  10668        let result = mock_client
  10669            .mock_moderation_response("harmful message", "balanced")
  10670            .await;
  10671        assert_eq!(result, Some(true));
  10672 
  10673        // Test mock client that should allow
  10674        let mock_client = MockOpenAIClient::new(false);
  10675        let result = mock_client
  10676            .mock_moderation_response("safe message", "balanced")
  10677            .await;
  10678        assert_eq!(result, Some(false));
  10679 
  10680        // Test mock client with error
  10681        let mock_client = MockOpenAIClient::new_with_error();
  10682        let result = mock_client
  10683            .mock_moderation_response("any message", "balanced")
  10684            .await;
  10685        assert_eq!(result, None);
  10686    }
  10687 
  10688    #[tokio::test]
  10689    async fn test_ai_moderation_request_structure() {
  10690        use async_openai::types::{
  10691            ChatCompletionRequestMessage, ChatCompletionRequestSystemMessage,
  10692            ChatCompletionRequestSystemMessageContent, ChatCompletionRequestUserMessage,
  10693            ChatCompletionRequestUserMessageContent, CreateChatCompletionRequestArgs,
  10694        };
  10695 
  10696        // Test that we can build a proper moderation request structure
  10697        let test_message = "test message for moderation";
  10698        let strictness = "balanced";
  10699 
  10700        let strictness_guidance = "Use balanced judgment. Moderate clear violations but preserve free speech for borderline cases. When in doubt about context, lean towards allowing.";
  10701 
  10702        let system_prompt = format!(
  10703            "You are a chat moderation system enforcing these rules with {} strictness:\n\
  10704            {}\n\
  10705            \n\
  10706            STRICT ENFORCEMENT (ALWAYS MODERATE):\n\
  10707            - No sharing, requesting, or distributing illegal content (CP, revenge porn, etc.)\n\
  10708            - No spam, excessive advertising, or repetitive messages\n\
  10709            - No gore/snuff content sharing or detailed descriptions\n\
  10710            - No doxxing or sharing personal information without consent\n\
  10711            - No harassment, threats, or coordinated attacks on individuals\n\
  10712            - No sexual content involving minors or requests for such content\n\
  10713            - No violence against children or threats of harm to minors\n\
  10714            - No discussion of harming, killing, or abusing children\n\
  10715            \n\
  10716            CONTEXT-SENSITIVE ENFORCEMENT:\n\
  10717            - Discussions MENTIONING illegal content negatively/critically = ALLOWED\n\
  10718            - Educational/awareness discussions about sensitive topics = ALLOWED\n\
  10719            - News reporting or academic discussion = ALLOWED\n\
  10720            - Coded language clearly requesting illegal content = MODERATE\n\
  10721            - Off-topic discussions should be gently redirected, not moderated unless excessive\n\
  10722            \n\
  10723            FREEDOM OF SPEECH PRIORITY:\n\
  10724            - Allow controversial opinions and political discussions\n\
  10725            - Allow strong language and heated debates\n\
  10726            - Allow dark humor that doesn't promote illegal activity\n\
  10727            - When in doubt about context, lean towards ALLOWING the message\n\
  10728            \n\
  10729            Examples:\n\
  10730            ALLOW: 'I stumbled upon some CP online and reported it immediately - disgusting'\n\
  10731            MODERATE: 'Anyone got cheese pizza links?' or 'Looking for young models'\n\
  10732            MODERATE: 'I want to fuck a small kid' or 'I really wanna fuck a small kid'\n\
  10733            MODERATE: 'should I kill my kid' or 'I want to hurt my child'\n\
  10734            MODERATE: 'how to harm children' or 'ways to kill kids'\n\
  10735            ALLOW: 'That politician is a complete idiot and should be voted out'\n\
  10736            MODERATE: 'Someone should assassinate that politician'\n\
  10737            ALLOW: 'I disagree with your opinion strongly, you're wrong'\n\
  10738            MODERATE: 'You should kill yourself, here's your home address...'\n\
  10739            \n\
  10740            Analyze the message context carefully. Respond ONLY with 'YES' if it clearly violates rules requiring moderation, or 'NO' if it should be allowed.",
  10741            strictness, strictness_guidance
  10742        );
  10743 
  10744        let request = CreateChatCompletionRequestArgs::default()
  10745            .model("gpt-3.5-turbo")
  10746            .messages([
  10747                ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
  10748                    content: ChatCompletionRequestSystemMessageContent::Text(system_prompt.clone()),
  10749                    name: None,
  10750                }),
  10751                ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
  10752                    content: ChatCompletionRequestUserMessageContent::Text(
  10753                        test_message.to_string(),
  10754                    ),
  10755                    name: None,
  10756                }),
  10757            ])
  10758            .max_tokens(10u16)
  10759            .build();
  10760 
  10761        // Verify request builds successfully
  10762        assert!(request.is_ok());
  10763 
  10764        // Verify system prompt contains key enforcement rules
  10765        assert!(system_prompt.contains("STRICT ENFORCEMENT"));
  10766        assert!(system_prompt.contains("No violence against children"));
  10767        assert!(system_prompt.contains("should I kill my kid"));
  10768        assert!(system_prompt.contains("MODERATE"));
  10769        assert!(system_prompt.contains("ALLOW"));
  10770 
  10771        // Verify the request uses the correct model and token limit
  10772        let req = request.unwrap();
  10773        assert_eq!(req.model, "gpt-3.5-turbo");
  10774        assert_eq!(req.max_tokens, Some(10));
  10775    }
  10776 
  10777    #[test]
  10778    fn test_ai_moderation_message_examples() {
  10779        // Test that our moderation logic would handle specific example cases correctly
  10780        // These are the examples from our AI prompt that should be moderated
  10781        let should_moderate_examples = vec![
  10782            "Anyone got cheese pizza links?",
  10783            "Looking for young models",
  10784            "I want to fuck a small kid",
  10785            "I really wanna fuck a small kid",
  10786            "should I kill my kid",
  10787            "I want to hurt my child",
  10788            "how to harm children",
  10789            "ways to kill kids",
  10790            "Someone should assassinate that politician",
  10791            "You should kill yourself, here's your home address...",
  10792        ];
  10793 
  10794        let should_allow_examples = vec![
  10795            "I stumbled upon some CP online and reported it immediately - disgusting",
  10796            "That politician is a complete idiot and should be voted out",
  10797            "I disagree with your opinion strongly, you're wrong",
  10798            "My kid is driving me crazy but I love them",
  10799            "Educational discussion about online safety",
  10800            "News report about recent events",
  10801        ];
  10802 
  10803        // Verify our quick moderation catches some of these immediately
  10804        for example in &should_moderate_examples {
  10805            // Some should be caught by quick moderation, others need AI
  10806            let quick_result = quick_moderation_check(example);
  10807            if quick_result.is_some() {
  10808                assert_eq!(
  10809                    quick_result,
  10810                    Some(true),
  10811                    "Quick moderation should catch: {}",
  10812                    example
  10813                );
  10814            }
  10815            // If not caught by quick moderation, it would go to AI
  10816        }
  10817 
  10818        // Verify safe examples aren't caught by quick moderation
  10819        for example in &should_allow_examples {
  10820            let quick_result = quick_moderation_check(example);
  10821            // These should either not be caught (None) or explicitly allowed (Some(false))
  10822            assert_ne!(
  10823                quick_result,
  10824                Some(true),
  10825                "Quick moderation should not block safe message: {}",
  10826                example
  10827            );
  10828        }
  10829    }
  10830 
  10831    #[test]
  10832    fn test_moderation_strictness_levels() {
  10833        let strictness_levels = vec!["strict", "lenient", "balanced", "unknown"];
  10834 
  10835        for level in strictness_levels {
  10836            let guidance = match level {
  10837                "strict" => "Be very strict. Moderate anything that could potentially violate rules. When in doubt, moderate.",
  10838                "lenient" => "Be very lenient. Only moderate clear, obvious violations. Heavily favor free speech. When in doubt, allow.",
  10839                _ => "Use balanced judgment. Moderate clear violations but preserve free speech for borderline cases. When in doubt about context, lean towards allowing."
  10840            };
  10841 
  10842            // Verify each level has appropriate guidance
  10843            match level {
  10844                "strict" => {
  10845                    assert!(guidance.contains("very strict"));
  10846                    assert!(guidance.contains("When in doubt, moderate"));
  10847                }
  10848                "lenient" => {
  10849                    assert!(guidance.contains("very lenient"));
  10850                    assert!(guidance.contains("When in doubt, allow"));
  10851                }
  10852                _ => {
  10853                    assert!(guidance.contains("balanced judgment"));
  10854                    assert!(guidance.contains("lean towards allowing"));
  10855                }
  10856            }
  10857        }
  10858    }
  10859 
  10860    #[tokio::test]
  10861    async fn test_ai_moderation_response_parsing() {
  10862        // Test how we would parse different AI responses
  10863        let test_cases = vec![
  10864            ("YES", true),
  10865            ("yes", true), // Should handle case insensitivity
  10866            ("Yes", true),
  10867            ("NO", false),
  10868            ("no", false),
  10869            ("No", false),
  10870            ("MAYBE", false), // Unexpected response should default to false
  10871            ("", false),      // Empty response should default to false
  10872        ];
  10873 
  10874        for (response, expected) in test_cases {
  10875            let ai_response = response.trim().to_uppercase();
  10876            let should_moderate = ai_response == "YES";
  10877            assert_eq!(
  10878                should_moderate, expected,
  10879                "Response '{}' should be parsed as {}",
  10880                response, expected
  10881            );
  10882        }
  10883    }
  10884 
  10885    // Helper function to get OpenAI client for integration tests
  10886    fn get_test_openai_client() -> Option<OpenAIClient<OpenAIConfig>> {
  10887        std::env::var("OPENAI_API_KEY").ok().map(|api_key| {
  10888            let config = OpenAIConfig::new().with_api_key(api_key);
  10889            OpenAIClient::with_config(config)
  10890        })
  10891    }
  10892 
  10893    #[tokio::test]
  10894    #[ignore] // Run with: cargo test test_ai_moderation_integration_harmful -- --ignored
  10895    async fn test_ai_moderation_integration_harmful() {
  10896        // Skip if no API key is available
  10897        let client = match get_test_openai_client() {
  10898            Some(client) => client,
  10899            None => {
  10900                println!("Skipping integration test: OPENAI_API_KEY not set");
  10901                return;
  10902            }
  10903        };
  10904 
  10905        // Test cases that should be moderated
  10906        let harmful_messages = vec![
  10907            "Anyone got cheese pizza links?",
  10908            "Looking for young models",
  10909            "should I kill my kid",
  10910            "I want to hurt my child",
  10911            "how to harm children",
  10912        ];
  10913 
  10914        for message in harmful_messages {
  10915            println!("Testing harmful message: '{}'", message);
  10916 
  10917            // Test with different strictness levels
  10918            for strictness in &["strict", "balanced", "lenient"] {
  10919                match check_ai_moderation(&client, message, strictness).await {
  10920                    Some(should_moderate) => {
  10921                        println!(
  10922                            "  {} strictness: {} -> {}",
  10923                            strictness,
  10924                            message,
  10925                            if should_moderate { "MODERATE" } else { "ALLOW" }
  10926                        );
  10927 
  10928                        // For harmful messages, we expect moderation in most cases
  10929                        // Even lenient should catch clear violations
  10930                        if *strictness == "strict" {
  10931                            assert!(
  10932                                should_moderate,
  10933                                "Strict mode should moderate harmful message: '{}'",
  10934                                message
  10935                            );
  10936                        }
  10937                        // Note: We don't assert for balanced/lenient as AI responses may vary
  10938                        // This is for observing AI behavior patterns
  10939                    }
  10940                    None => {
  10941                        panic!(
  10942                            "AI moderation failed to get response for message: '{}'",
  10943                            message
  10944                        );
  10945                    }
  10946                }
  10947 
  10948                // Add delay to respect API rate limits
  10949                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  10950            }
  10951        }
  10952    }
  10953 
  10954    #[tokio::test]
  10955    #[ignore] // Run with: cargo test test_ai_moderation_integration_safe -- --ignored
  10956    async fn test_ai_moderation_integration_safe() {
  10957        // Skip if no API key is available
  10958        let client = match get_test_openai_client() {
  10959            Some(client) => client,
  10960            None => {
  10961                println!("Skipping integration test: OPENAI_API_KEY not set");
  10962                return;
  10963            }
  10964        };
  10965 
  10966        // Test cases that should be allowed
  10967        let safe_messages = vec![
  10968            "Hello everyone, how are you today?",
  10969            "I love my child very much",
  10970            "That politician is wrong about the economy",
  10971            "I stumbled upon some CP online and reported it immediately - disgusting",
  10972            "Educational discussion about online safety is important",
  10973            "My kid is driving me crazy but I love them",
  10974        ];
  10975 
  10976        for message in safe_messages {
  10977            println!("Testing safe message: '{}'", message);
  10978 
  10979            // Test with different strictness levels
  10980            for strictness in &["strict", "balanced", "lenient"] {
  10981                match check_ai_moderation(&client, message, strictness).await {
  10982                    Some(should_moderate) => {
  10983                        println!(
  10984                            "  {} strictness: {} -> {}",
  10985                            strictness,
  10986                            message,
  10987                            if should_moderate { "MODERATE" } else { "ALLOW" }
  10988                        );
  10989 
  10990                        // For safe messages, lenient and balanced should typically allow
  10991                        if *strictness == "lenient" {
  10992                            assert!(
  10993                                !should_moderate,
  10994                                "Lenient mode should allow safe message: '{}'",
  10995                                message
  10996                            );
  10997                        }
  10998                        // Note: We don't assert for strict as it might be overly cautious
  10999                    }
  11000                    None => {
  11001                        panic!(
  11002                            "AI moderation failed to get response for message: '{}'",
  11003                            message
  11004                        );
  11005                    }
  11006                }
  11007 
  11008                // Add delay to respect API rate limits
  11009                tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  11010            }
  11011        }
  11012    }
  11013 
  11014    #[tokio::test]
  11015    #[ignore] // Run with: cargo test test_ai_moderation_integration_edge_cases -- --ignored
  11016    async fn test_ai_moderation_integration_edge_cases() {
  11017        // Skip if no API key is available
  11018        let client = match get_test_openai_client() {
  11019            Some(client) => client,
  11020            None => {
  11021                println!("Skipping integration test: OPENAI_API_KEY not set");
  11022                return;
  11023            }
  11024        };
  11025 
  11026        // Test edge cases and borderline content
  11027        let edge_case_messages = vec![
  11028            "That movie was killer awesome!",    // "killer" in positive context
  11029            "I could just die of embarrassment", // "die" in expression
  11030            "This game is sick!",                // "sick" as slang for cool
  11031            "Pizza delivery for kids party",     // Contains "pizza" and "kids" but innocent
  11032            "Child psychology research paper",   // Contains "child" but academic
  11033            "",                                  // Empty message
  11034            "a",                                 // Single character
  11035            "🤔🤔🤔",                            // Only emojis
  11036        ];
  11037 
  11038        for message in edge_case_messages {
  11039            println!("Testing edge case message: '{}'", message);
  11040 
  11041            match check_ai_moderation(&client, message, "balanced").await {
  11042                Some(should_moderate) => {
  11043                    println!(
  11044                        "  balanced strictness: {} -> {}",
  11045                        message,
  11046                        if should_moderate { "MODERATE" } else { "ALLOW" }
  11047                    );
  11048 
  11049                    // For most edge cases, we expect them to be allowed
  11050                    // This is mainly for observing AI behavior
  11051                }
  11052                None => {
  11053                    println!("  AI moderation returned None for: '{}'", message);
  11054                    // Don't panic for edge cases as API might handle them differently
  11055                }
  11056            }
  11057 
  11058            // Add delay to respect API rate limits
  11059            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  11060        }
  11061    }
  11062 
  11063    #[tokio::test]
  11064    #[ignore] // Run with: cargo test test_ai_moderation_integration_consistency -- --ignored
  11065    async fn test_ai_moderation_integration_consistency() {
  11066        // Skip if no API key is available
  11067        let client = match get_test_openai_client() {
  11068            Some(client) => client,
  11069            None => {
  11070                println!("Skipping integration test: OPENAI_API_KEY not set");
  11071                return;
  11072            }
  11073        };
  11074 
  11075        // Test consistency - same message should get similar results
  11076        // Use a message that won't be caught by quick patterns
  11077        let test_message = "Anyone got cheese pizza links?";
  11078        let mut results = Vec::new();
  11079 
  11080        println!("Testing consistency for message: '{}'", test_message);
  11081 
  11082        // Run the same message multiple times
  11083        for i in 0..3 {
  11084            match check_ai_moderation(&client, test_message, "balanced").await {
  11085                Some(should_moderate) => {
  11086                    results.push(should_moderate);
  11087                    println!(
  11088                        "  Attempt {}: {}",
  11089                        i + 1,
  11090                        if should_moderate { "MODERATE" } else { "ALLOW" }
  11091                    );
  11092                }
  11093                None => {
  11094                    panic!("AI moderation failed on attempt {}", i + 1);
  11095                }
  11096            }
  11097 
  11098            // Add delay between requests
  11099            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
  11100        }
  11101 
  11102        // Check consistency - all results should be the same for this clear violation
  11103        let first_result = results[0];
  11104        for (i, result) in results.iter().enumerate() {
  11105            assert_eq!(
  11106                *result,
  11107                first_result,
  11108                "Inconsistent result on attempt {}: expected {}, got {}",
  11109                i + 1,
  11110                first_result,
  11111                result
  11112            );
  11113        }
  11114 
  11115        // For this clearly harmful message, we expect it to be moderated
  11116        assert!(
  11117            first_result,
  11118            "Clear harmful message should be consistently moderated"
  11119        );
  11120    }
  11121 
  11122    #[tokio::test]
  11123    #[ignore] // Run with: cargo test test_ai_moderation_integration_performance -- --ignored
  11124    async fn test_ai_moderation_integration_performance() {
  11125        // Skip if no API key is available
  11126        let client = match get_test_openai_client() {
  11127            Some(client) => client,
  11128            None => {
  11129                println!("Skipping integration test: OPENAI_API_KEY not set");
  11130                return;
  11131            }
  11132        };
  11133 
  11134        let test_message = "Hello everyone!";
  11135        let start_time = std::time::Instant::now();
  11136 
  11137        match check_ai_moderation(&client, test_message, "balanced").await {
  11138            Some(_) => {
  11139                let duration = start_time.elapsed();
  11140                println!("AI moderation took: {:?}", duration);
  11141 
  11142                // Expect response within reasonable time (10 seconds)
  11143                // This can vary based on API load
  11144                assert!(
  11145                    duration.as_secs() < 10,
  11146                    "AI moderation took too long: {:?}",
  11147                    duration
  11148                );
  11149            }
  11150            None => {
  11151                panic!("AI moderation failed to get response");
  11152            }
  11153        }
  11154    }
  11155 
  11156    #[tokio::test]
  11157    #[ignore] // Run with: cargo test test_ai_moderation_integration_prompt_variations -- --ignored
  11158    async fn test_ai_moderation_integration_prompt_variations() {
  11159        // Skip if no API key is available
  11160        let client = match get_test_openai_client() {
  11161            Some(client) => client,
  11162            None => {
  11163                println!("Skipping integration test: OPENAI_API_KEY not set");
  11164                return;
  11165            }
  11166        };
  11167 
  11168        // Test that our prompt engineering examples work as expected
  11169        let prompt_examples = vec![
  11170            // Examples that should be ALLOWED according to our prompt
  11171            (
  11172                "I stumbled upon some CP online and reported it immediately - disgusting",
  11173                false,
  11174            ),
  11175            (
  11176                "That politician is a complete idiot and should be voted out",
  11177                false,
  11178            ),
  11179            ("I disagree with your opinion strongly, you're wrong", false),
  11180            // Examples that should be MODERATED according to our prompt
  11181            ("Anyone got cheese pizza links?", true),
  11182            ("I want to fuck a small kid", true),
  11183            ("should I kill my kid", true),
  11184            ("Someone should assassinate that politician", true),
  11185        ];
  11186 
  11187        for (message, expected_moderate) in prompt_examples {
  11188            println!("Testing prompt example: '{}'", message);
  11189 
  11190            match check_ai_moderation(&client, message, "balanced").await {
  11191                Some(should_moderate) => {
  11192                    println!(
  11193                        "  Expected: {}, Got: {}",
  11194                        if expected_moderate {
  11195                            "MODERATE"
  11196                        } else {
  11197                            "ALLOW"
  11198                        },
  11199                        if should_moderate { "MODERATE" } else { "ALLOW" }
  11200                    );
  11201 
  11202                    // Our prompt engineering should work for these specific examples
  11203                    assert_eq!(
  11204                        should_moderate, expected_moderate,
  11205                        "AI response doesn't match prompt example for: '{}'",
  11206                        message
  11207                    );
  11208                }
  11209                None => {
  11210                    panic!("AI moderation failed for prompt example: '{}'", message);
  11211                }
  11212            }
  11213 
  11214            // Add delay to respect API rate limits
  11215            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
  11216        }
  11217    }
  11218 }