bhcli-simple

A simplified version of bhcli
git clone https://git.dasho.dev/bhcli-simple.git
Log | Files | Refs | README

main.rs (139527B)


      1 mod bhc;
      2 mod lechatphp;
      3 mod util;
      4 mod harm;
      5 
      6 use crate::lechatphp::LoginErr;
      7 use anyhow::{anyhow, Context};
      8 use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
      9 use clap::Parser;
     10 use clipboard::ClipboardContext;
     11 use clipboard::ClipboardProvider;
     12 use colors_transform::{Color, Rgb};
     13 use crossbeam_channel::{self, after, select};
     14 use crossterm::event;
     15 use crossterm::event::Event as CEvent;
     16 use crossterm::event::{MouseEvent, MouseEventKind};
     17 use crossterm::{
     18    event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers},
     19    execute,
     20    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
     21 };
     22 use lazy_static::lazy_static;
     23 use linkify::LinkFinder;
     24 use log::LevelFilter;
     25 use log4rs::append::file::FileAppender;
     26 use log4rs::encode::pattern::PatternEncoder;
     27 use rand::distributions::Alphanumeric;
     28 use rand::{thread_rng, Rng};
     29 use regex::Regex;
     30 use reqwest::blocking::multipart;
     31 use reqwest::blocking::Client;
     32 use reqwest::redirect::Policy;
     33 use rodio::{source::Source, Decoder, OutputStream};
     34 use select::document::Document;
     35 use select::predicate::{Attr, Name};
     36 use serde_derive::{Deserialize, Serialize};
     37 use std::collections::HashMap;
     38 use std::io::Cursor;
     39 use std::io::{self, Write};
     40 use std::fs::OpenOptions;
     41 use std::process::Command;
     42 use std::sync::Mutex;
     43 use std::sync::{Arc, MutexGuard};
     44 use std::thread;
     45 use std::time::Duration;
     46 use std::time::Instant;
     47 use tui::layout::Rect;
     48 use tui::style::Color as tuiColor;
     49 use tui::{
     50    backend::CrosstermBackend,
     51    layout::{Constraint, Direction, Layout},
     52    style::{Modifier, Style},
     53    text::{Span, Spans, Text},
     54    widgets::{Block, Borders, List, ListItem, Paragraph},
     55    Frame, Terminal,
     56 };
     57 use unicode_width::UnicodeWidthStr;
     58 use util::StatefulList;
     59 use harm::{action_from_score, score_message, Action};
     60 
     61 const LANG: &str = "en";
     62 const SEND_TO_ALL: &str = "s *";
     63 const SEND_TO_MEMBERS: &str = "s ?";
     64 const SEND_TO_STAFFS: &str = "s %";
     65 const SEND_TO_ADMINS: &str = "s _";
     66 const SOUND1: &[u8] = include_bytes!("sound1.mp3");
     67 const DKF_URL: &str = "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion";
     68 const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down";
     69 const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down";
     70 const KICKED_ERR: &str = "You have been kicked";
     71 const REG_ERR: &str = "This nickname is a registered member";
     72 const NICKNAME_ERR: &str = "Invalid nickname";
     73 const CAPTCHA_WG_ERR: &str = "Wrong Captcha";
     74 const CAPTCHA_FAILED_SOLVE_ERR: &str = "Failed solve captcha";
     75 const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out";
     76 const UNKNOWN_ERR: &str = "Unknown error";
     77 const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion";
     78 
     79 lazy_static! {
     80    static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap();
     81    static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap();
     82    static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap();
     83    static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap();
     84    static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap();
     85    static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap();
     86    static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap();
     87    static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap();
     88    static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap();
     89    static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap();
     90    static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap();
     91    static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap();
     92    static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap();
     93 }
     94 
     95 fn default_empty_str() -> String {
     96    "".to_string()
     97 }
     98 
     99 #[derive(Debug, Serialize, Deserialize)]
    100 struct Profile {
    101    username: String,
    102    password: String,
    103    #[serde(default = "default_empty_str")]
    104    url: String,
    105    #[serde(default = "default_empty_str")]
    106    date_format: String,
    107    #[serde(default = "default_empty_str")]
    108    page_php: String,
    109    #[serde(default = "default_empty_str")]
    110    members_tag: String,
    111    #[serde(default = "default_empty_str")]
    112    keepalive_send_to: String,
    113 }
    114 
    115 #[derive(Default, Debug, Serialize, Deserialize)]
    116 struct MyConfig {
    117    dkf_api_key: Option<String>,
    118    #[serde(default)]
    119    bad_usernames: Vec<String>,
    120    #[serde(default)]
    121    bad_exact_usernames: Vec<String>,
    122    #[serde(default)]
    123    bad_messages: Vec<String>,
    124    #[serde(default)]
    125    allowlist: Vec<String>,
    126    #[serde(default)]
    127    commands: HashMap<String, String>,
    128    profiles: HashMap<String, Profile>,
    129 }
    130 
    131 #[derive(Parser)]
    132 #[command(name = "bhcli")]
    133 #[command(author = "Dasho <o_o@dasho.dev>")]
    134 #[command(version = "0.1.0")]
    135 
    136 struct Opts {
    137    #[arg(long, env = "DKF_API_KEY")]
    138    dkf_api_key: Option<String>,
    139    #[arg(short, long, env = "BHC_USERNAME")]
    140    username: Option<String>,
    141    #[arg(short, long, env = "BHC_PASSWORD")]
    142    password: Option<String>,
    143    #[arg(short, long, env = "BHC_MANUAL_CAPTCHA")]
    144    manual_captcha: bool,
    145    #[arg(short, long, env = "BHC_GUEST_COLOR")]
    146    guest_color: Option<String>,
    147    #[arg(short, long, env = "BHC_REFRESH_RATE", default_value = "5")]
    148    refresh_rate: u64,
    149    #[arg(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")]
    150    max_login_retry: isize,
    151    #[arg(long)]
    152    url: Option<String>,
    153    #[arg(long)]
    154    page_php: Option<String>,
    155    #[arg(long)]
    156    datetime_fmt: Option<String>,
    157    #[arg(long)]
    158    members_tag: Option<String>,
    159    #[arg(short, long)]
    160    dan: bool,
    161    #[arg(
    162        short,
    163        long,
    164        env = "BHC_PROXY_URL",
    165        default_value = "socks5h://127.0.0.1:9050"
    166    )]
    167    socks_proxy_url: String,
    168    #[arg(long)]
    169    no_proxy: bool,
    170    #[arg(long, env = "DNMX_USERNAME")]
    171    dnmx_username: Option<String>,
    172    #[arg(long, env = "DNMX_PASSWORD")]
    173    dnmx_password: Option<String>,
    174    #[arg(short = 'c', long, default_value = "default")]
    175    profile: String,
    176 
    177    //Strange
    178    #[arg(long,default_value = "0")]
    179    keepalive_send_to: Option<String>,
    180 
    181    #[arg(long)]
    182    session: Option<String>,
    183 
    184    #[arg(long)]
    185    sxiv: bool,
    186 
    187    #[arg(skip)]
    188    bad_usernames: Option<Vec<String>>,
    189    #[arg(skip)]
    190    bad_exact_usernames: Option<Vec<String>>,
    191    #[arg(skip)]
    192    bad_messages: Option<Vec<String>>,
    193    #[arg(skip)]
    194    allowlist: Option<Vec<String>>,
    195 }
    196 
    197 struct LeChatPHPConfig {
    198    url: String,
    199    datetime_fmt: String,
    200    page_php: String,
    201    keepalive_send_to: String,
    202    members_tag: String,
    203    staffs_tag: String,
    204 }
    205 
    206 impl LeChatPHPConfig {
    207    fn new_black_hat_chat_config() -> Self {
    208        Self {
    209            url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(),
    210            datetime_fmt: "%m-%d %H:%M:%S".to_owned(),
    211            page_php: "chat.php".to_owned(),
    212            keepalive_send_to: "0".to_owned(),
    213            members_tag: "[M] ".to_owned(),
    214            staffs_tag: "[Staff] ".to_owned(),
    215        }
    216    }
    217 }
    218 
    219 struct BaseClient {
    220    username: String,
    221    password: String,
    222 }
    223 
    224 struct LeChatPHPClient {
    225    base_client: BaseClient,
    226    guest_color: String,
    227    client: Client,
    228    session: Option<String>,
    229    config: LeChatPHPConfig,
    230    last_key_event: Option<KeyCode>,
    231    manual_captcha: bool,
    232    sxiv: bool,
    233    refresh_rate: u64,
    234    max_login_retry: isize,
    235 
    236    is_muted: Arc<Mutex<bool>>,
    237    show_sys: bool,
    238    display_guest_view: bool,
    239    display_member_view: bool,
    240    display_hidden_msgs: bool,
    241    tx: crossbeam_channel::Sender<PostType>,
    242    rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>,
    243 
    244    color_tx: crossbeam_channel::Sender<()>,
    245    color_rx: Arc<Mutex<crossbeam_channel::Receiver<()>>>,
    246 
    247    bad_username_filters: Arc<Mutex<Vec<String>>>,
    248    bad_exact_username_filters: Arc<Mutex<Vec<String>>>,
    249    bad_message_filters: Arc<Mutex<Vec<String>>>,
    250    allowlist: Arc<Mutex<Vec<String>>>,
    251 }
    252 
    253 impl LeChatPHPClient {
    254    fn run_forever(&mut self) {
    255        let max_retry = self.max_login_retry;
    256        let mut attempt = 0;
    257        loop {
    258            match self.login() {
    259                Err(e) => match e {
    260                    LoginErr::KickedErr
    261                    | LoginErr::RegErr
    262                    | LoginErr::NicknameErr
    263                    | LoginErr::UnknownErr => {
    264                        log::error!("{}", e);
    265                        println!("Login error: {}", e); // Print error message
    266                        break;
    267                    }
    268                    LoginErr::CaptchaFailedSolveErr => {
    269                        log::error!("{}", e);
    270                        println!("Captcha failed to solve: {}", e); // Print error message
    271                        continue;
    272                    }
    273                    LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {}
    274                    LoginErr::ServerDownErr | LoginErr::ServerDown500Err => {
    275                        log::error!("{}", e);
    276                        println!("Server is down: {}", e); // Print error message
    277                    }
    278                    LoginErr::Reqwest(err) => {
    279                        if err.is_connect() {
    280                            log::error!("{}\nIs tor proxy enabled ?", err);
    281                            println!("Connection error: {}\nIs tor proxy enabled ?", err); // Print error message
    282                            break;
    283                        } else if err.is_timeout() {
    284                            log::error!("timeout: {}", err);
    285                            println!("Timeout error: {}", err); // Print error message
    286                        } else {
    287                            log::error!("{}", err);
    288                            println!("Reqwest error: {}", err); // Print error message
    289                        }
    290                    }
    291                },
    292 
    293                Ok(()) => {
    294                    attempt = 0;
    295                    match self.get_msgs() {
    296                        Ok(ExitSignal::NeedLogin) => {}
    297                        Ok(ExitSignal::Terminate) => return,
    298                        Err(e) => log::error!("{:?}", e),
    299                    }
    300                }
    301            }
    302            attempt += 1;
    303            if max_retry > 0 && attempt > max_retry {
    304                break;
    305            }
    306            self.session = None;
    307            let retry_in = Duration::from_secs(2);
    308            let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt);
    309            if max_retry > 0 {
    310                msg += &format!("/{}", max_retry);
    311            }
    312            println!("{}", msg);
    313            thread::sleep(retry_in);
    314        }
    315    }
    316 
    317    fn start_keepalive_thread(
    318        &self,
    319        exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    320        last_post_rx: crossbeam_channel::Receiver<()>,
    321    ) -> thread::JoinHandle<()> {
    322        let tx = self.tx.clone();
    323        let send_to = self.config.keepalive_send_to.clone();
    324        thread::spawn(move || loop {
    325            let clb = || {
    326                tx.send(PostType::Post("keep alive".to_owned(), Some(send_to.clone())))
    327                    .unwrap();
    328                tx.send(PostType::DeleteLast).unwrap();
    329            };
    330            let timeout = after(Duration::from_secs(60 * 55));
    331            select! {
    332                // Whenever we send a message to chat server,
    333                // we will receive a message on this channel
    334                // and reset the timer for next keepalive.
    335                recv(&last_post_rx) -> _ => {},
    336                recv(&exit_rx) -> _ => return,
    337                recv(&timeout) -> _ => clb(),
    338            }
    339        })
    340    }
    341 
    342    // Thread that POST to chat server
    343    fn start_post_msg_thread(
    344        &self,
    345        exit_rx: crossbeam_channel::Receiver<ExitSignal>,
    346        last_post_tx: crossbeam_channel::Sender<()>,
    347    ) -> thread::JoinHandle<()> {
    348        let client = self.client.clone();
    349        let rx = Arc::clone(&self.rx);
    350        let full_url = format!("{}/{}", &self.config.url, &self.config.page_php);
    351        let session = self.session.clone().unwrap();
    352        let url = format!("{}?action=post&session={}", &full_url, &session);
    353        thread::spawn(move || loop {
    354            // select! macro fucks all the LSP, therefore the code gymnastic here
    355            let clb = |v: Result<PostType, crossbeam_channel::RecvError>| match v {
    356                Ok(post_type_recv) => post_msg(
    357                    &client,
    358                    post_type_recv,
    359                    &full_url,
    360                    session.clone(),
    361                    &url,
    362                    &last_post_tx,
    363                ),
    364                Err(_) => return,
    365            };
    366            let rx = rx.lock().unwrap();
    367            select! {
    368                recv(&exit_rx) -> _ => return,
    369                recv(&rx) -> v => clb(v),
    370            }
    371        })
    372    }
    373 
    374    // Thread that update messages every "refresh_rate"
    375    fn start_get_msgs_thread(
    376        &self,
    377        sig: &Arc<Mutex<Sig>>,
    378        messages: &Arc<Mutex<Vec<Message>>>,
    379        users: &Arc<Mutex<Users>>,
    380        messages_updated_tx: crossbeam_channel::Sender<()>,
    381    ) -> thread::JoinHandle<()> {
    382        let client = self.client.clone();
    383        let messages = Arc::clone(messages);
    384        let users = Arc::clone(users);
    385        let session = self.session.clone().unwrap();
    386        let username = self.base_client.username.clone();
    387        let refresh_rate = self.refresh_rate;
    388        let base_url = self.config.url.clone();
    389        let page_php = self.config.page_php.clone();
    390        let datetime_fmt = self.config.datetime_fmt.clone();
    391        let is_muted = Arc::clone(&self.is_muted);
    392        let exit_rx = sig.lock().unwrap().clone();
    393        let sig = Arc::clone(sig);
    394        let members_tag = self.config.members_tag.clone();
    395        let tx = self.tx.clone();
    396        let bad_usernames = Arc::clone(&self.bad_username_filters);
    397        let bad_exact_usernames = Arc::clone(&self.bad_exact_username_filters);
    398        let bad_messages = Arc::clone(&self.bad_message_filters);
    399        let allowlist = Arc::clone(&self.allowlist);
    400        thread::spawn(move || loop {
    401            let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    402            let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
    403            let mut should_notify = false;
    404 
    405            if let Err(err) = get_msgs(
    406                &client,
    407                &base_url,
    408                &page_php,
    409                &session,
    410                &username,
    411                &users,
    412                &sig,
    413                &messages_updated_tx,
    414                &members_tag,
    415                &datetime_fmt,
    416                &messages,
    417                &mut should_notify,
    418                &tx,
    419                &bad_usernames,
    420                &bad_exact_usernames,
    421                &bad_messages,
    422                &allowlist,
    423            ) {
    424                log::error!("{}", err);
    425            };
    426 
    427            let muted = { *is_muted.lock().unwrap() };
    428            if should_notify && !muted {
    429                if let Err(err) = stream_handle.play_raw(source.convert_samples()) {
    430                    log::error!("{}", err);
    431                }
    432            }
    433 
    434            let timeout = after(Duration::from_secs(refresh_rate));
    435            select! {
    436                recv(&exit_rx) -> _ => return,
    437                recv(&timeout) -> _ => {},
    438            }
    439        })
    440    }
    441 
    442    fn get_msgs(&mut self) -> anyhow::Result<ExitSignal> {
    443        let terminate_signal: ExitSignal;
    444 
    445        let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new()));
    446        let users: Arc<Mutex<Users>> = Arc::new(Mutex::new(Users::default()));
    447 
    448        // Create default app state
    449        let mut app = App::default();
    450 
    451        // Each threads gets a clone of the receiver.
    452        // When someone calls ".signal", all threads receive it,
    453        // and knows that they have to terminate.
    454        let sig = Arc::new(Mutex::new(Sig::new()));
    455 
    456        let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded();
    457        let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded();
    458 
    459        let h1 = self.start_keepalive_thread(sig.lock().unwrap().clone(), last_post_rx);
    460        let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx);
    461        let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx);
    462 
    463        // Terminal initialization
    464        let mut stdout = io::stdout();
    465        enable_raw_mode().unwrap();
    466        execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    467        let backend = CrosstermBackend::new(stdout);
    468        let mut terminal = Terminal::new(backend)?;
    469 
    470        // Setup event handlers
    471        let (events, h4) = Events::with_config(Config {
    472            messages_updated_rx,
    473            exit_rx: sig.lock().unwrap().clone(),
    474            tick_rate: Duration::from_millis(250),
    475        });
    476 
    477        loop {
    478            app.is_muted = *self.is_muted.lock().unwrap();
    479            app.show_sys = self.show_sys;
    480            app.display_guest_view = self.display_guest_view;
    481            app.display_member_view = self.display_member_view;
    482            app.display_hidden_msgs = self.display_hidden_msgs;
    483            app.members_tag = self.config.members_tag.clone();
    484            app.staffs_tag = self.config.staffs_tag.clone();
    485 
    486            // process()
    487            // Draw UI
    488            terminal.draw(|f| {
    489                draw_terminal_frame(f, &mut app, &messages, &users, &self.base_client.username);
    490            })?;
    491 
    492            // Handle input
    493            match self.handle_input(&events, &mut app, &messages, &users) {
    494                Err(ExitSignal::Terminate) => {
    495                    terminate_signal = ExitSignal::Terminate;
    496                    sig.lock().unwrap().signal(&terminate_signal);
    497                    break;
    498                }
    499                Err(ExitSignal::NeedLogin) => {
    500                    terminate_signal = ExitSignal::NeedLogin;
    501                    sig.lock().unwrap().signal(&terminate_signal);
    502                    break;
    503                }
    504                Ok(_) => continue,
    505            };
    506        }
    507 
    508        // Cleanup before leaving
    509        disable_raw_mode()?;
    510        execute!(
    511            terminal.backend_mut(),
    512            LeaveAlternateScreen,
    513            DisableMouseCapture
    514        )?;
    515        terminal.show_cursor()?;
    516        terminal.clear()?;
    517        terminal.set_cursor(0, 0)?;
    518 
    519        h1.join().unwrap();
    520        h2.join().unwrap();
    521        h3.join().unwrap();
    522        h4.join().unwrap();
    523 
    524        Ok(terminate_signal)
    525    }
    526 
    527    fn post_msg(&self, post_type: PostType) -> anyhow::Result<()> {
    528        self.tx.send(post_type)?;
    529        Ok(())
    530    }
    531 
    532    fn login(&mut self) -> Result<(), LoginErr> {
    533        // If we provided a session, skip login process
    534        if self.session.is_some() {
    535            // println!("Session in params: {:?}", self.session); 
    536            return Ok(());
    537        }
    538        // println!("self.session is not Some");
    539        // println!("self.sxiv = {:?}", self.sxiv);
    540        self.session = Some(lechatphp::login(
    541            &self.client,
    542            &self.config.url,
    543            &self.config.page_php,
    544            &self.base_client.username,
    545            &self.base_client.password,
    546            &self.guest_color,
    547            self.manual_captcha,
    548            self.sxiv,
    549        )?);
    550        Ok(())
    551    }
    552 
    553    fn logout(&mut self) -> anyhow::Result<()> {
    554        if let Some(session) = &self.session {
    555            lechatphp::logout(
    556                &self.client,
    557                &self.config.url,
    558                &self.config.page_php,
    559                session,
    560            )?;
    561            self.session = None;
    562        }
    563        Ok(())
    564    }
    565 
    566    fn start_cycle(&self, color_only: bool) {
    567        let username = self.base_client.username.clone();
    568        let tx = self.tx.clone();
    569        let color_rx = Arc::clone(&self.color_rx);
    570        thread::spawn(move || {
    571            let mut idx = 0;
    572            let colors = [
    573                "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF",
    574                "#6633FF", "#CC33FF", "#efefef",
    575            ];
    576            loop {
    577                let color_rx = color_rx.lock().unwrap();
    578                let timeout = after(Duration::from_millis(5200));
    579                select! {
    580                    recv(&color_rx) -> _ => break,
    581                    recv(&timeout) -> _ => {}
    582                }
    583                idx = (idx + 1) % colors.len();
    584                let color = colors[idx].to_owned();
    585                if !color_only {
    586                    let name = format!("{}{}", username, random_string(14));
    587                    log::error!("New name : {}", name);
    588                    tx.send(PostType::Profile(color, name)).unwrap();
    589                } else {
    590                    tx.send(PostType::NewColor(color)).unwrap();
    591                }
    592                // tx.send(PostType::Post("!up".to_owned(), Some(username.clone())))
    593                //     .unwrap();
    594                // tx.send(PostType::DeleteLast).unwrap();
    595            }
    596            let msg = PostType::Profile("#90ee90".to_owned(), username);
    597            tx.send(msg).unwrap();
    598        });
    599    }
    600 
    601    fn save_filters(&self) {
    602        if let Ok(mut cfg) = confy::load::<MyConfig>("bhcli", None) {
    603            cfg.bad_usernames = self.bad_username_filters.lock().unwrap().clone();
    604            cfg.bad_exact_usernames = self.bad_exact_username_filters.lock().unwrap().clone();
    605            cfg.bad_messages = self.bad_message_filters.lock().unwrap().clone();
    606            cfg.allowlist = self.allowlist.lock().unwrap().clone();
    607            if let Err(e) = confy::store("bhcli", None, cfg) {
    608                log::error!("failed to store config: {}", e);
    609            }
    610        }
    611    }
    612 
    613    fn list_filters(&self, usernames: bool) -> String {
    614        let list = if usernames {
    615            self.bad_username_filters.lock().unwrap().clone()
    616        } else {
    617            self.bad_message_filters.lock().unwrap().clone()
    618        };
    619        if list.is_empty() {
    620            String::from("(empty)")
    621        } else {
    622            list.join(", ")
    623        }
    624    }
    625 
    626    fn list_exact_filters(&self) -> String {
    627        let list = self.bad_exact_username_filters.lock().unwrap().clone();
    628        if list.is_empty() {
    629            String::from("(empty)")
    630        } else {
    631            list.join(", ")
    632        }
    633    }
    634 
    635    fn remove_filter(&self, term: &str, usernames: bool) -> bool {
    636        if usernames {
    637            {
    638                let mut filters = self.bad_username_filters.lock().unwrap();
    639                if let Some(pos) = filters.iter().position(|x| x == term) {
    640                    filters.remove(pos);
    641                    return true;
    642                }
    643            }
    644            {
    645                let mut filters = self.bad_exact_username_filters.lock().unwrap();
    646                if let Some(pos) = filters.iter().position(|x| x == term) {
    647                    filters.remove(pos);
    648                    return true;
    649                }
    650            }
    651            false
    652        } else {
    653            let mut filters = self.bad_message_filters.lock().unwrap();
    654            if let Some(pos) = filters.iter().position(|x| x == term) {
    655                filters.remove(pos);
    656                true
    657            } else {
    658                false
    659            }
    660        }
    661    }
    662 
    663    fn apply_ban_filters(&self, users: &Arc<Mutex<Users>>) {
    664        let users = users.lock().unwrap();
    665        let name_filters = self.bad_username_filters.lock().unwrap().clone();
    666        let exact_filters = self.bad_exact_username_filters.lock().unwrap().clone();
    667        for (_, name) in &users.guests {
    668            if exact_filters.iter().any(|f| f == name)
    669                || name_filters
    670                    .iter()
    671                    .any(|f| name.to_lowercase().contains(&f.to_lowercase()))
    672            {
    673                let _ = self.tx.send(PostType::Kick(String::new(), name.clone()));
    674            }
    675        }
    676    }
    677 
    678    fn process_command(&mut self, input: &str, app: &mut App, users: &Arc<Mutex<Users>>) -> bool {
    679        if input == "/dl" {
    680            self.post_msg(PostType::DeleteLast).unwrap();
    681        } else if let Some(captures) = DLX_RGX.captures(input) {
    682            let x: usize = captures.get(1).unwrap().as_str().parse().unwrap();
    683            for _ in 0..x {
    684                self.post_msg(PostType::DeleteLast).unwrap();
    685            }
    686        } else if input == "/dall" {
    687            self.post_msg(PostType::DeleteAll).unwrap();
    688        } else if input == "/cycles" {
    689            self.color_tx.send(()).unwrap();
    690        } else if input == "/cycle1" {
    691            self.start_cycle(true);
    692        } else if input == "/cycle2" {
    693            self.start_cycle(false);
    694        } else if input == "/kall" {
    695            let username = "s _".to_owned();
    696            let msg = "".to_owned();
    697            self.post_msg(PostType::Kick(msg, username)).unwrap();
    698        } else if let Some(captures) = PM_RGX.captures(input) {
    699            let username = &captures[1];
    700            let msg = captures[2].to_owned();
    701            let to = Some(username.to_owned());
    702            self.post_msg(PostType::Post(msg, to)).unwrap();
    703            app.input = format!("/pm {} ", username);
    704            app.input_idx = app.input.width();
    705        } else if let Some(captures) = NEW_NICKNAME_RGX.captures(input) {
    706            let new_nickname = captures[1].to_owned();
    707            self.post_msg(PostType::NewNickname(new_nickname)).unwrap();
    708        } else if let Some(captures) = NEW_COLOR_RGX.captures(input) {
    709            let new_color = captures[1].to_owned();
    710            self.post_msg(PostType::NewColor(new_color)).unwrap();
    711        } else if let Some(captures) = KICK_RGX.captures(input) {
    712            let username = captures[1].to_owned();
    713            let msg = captures[2].to_owned();
    714            self.post_msg(PostType::Kick(msg, username)).unwrap();
    715        } else if input.starts_with("/banname ") || input.starts_with("/ban ") {
    716            let mut name = if input.starts_with("/banname ") {
    717                remove_prefix(input, "/banname ")
    718            } else {
    719                remove_prefix(input, "/ban ")
    720            };
    721            let exact = name.starts_with('"') && name.ends_with('"') && name.len() >= 2;
    722            if exact {
    723                name = &name[1..name.len()-1];
    724            }
    725            let name = name.to_owned();
    726            if exact {
    727                let mut f = self.bad_exact_username_filters.lock().unwrap();
    728                f.push(name.clone());
    729            } else {
    730                let mut f = self.bad_username_filters.lock().unwrap();
    731                f.push(name.clone());
    732            }
    733            self.save_filters();
    734            self.post_msg(PostType::Kick(String::new(), name.clone())).unwrap();
    735            self.apply_ban_filters(users);
    736            let msg = if exact {
    737                format!("Banned exact user \"{}\"", name)
    738            } else {
    739                format!("Banned userfilter \"{}\"", name)
    740            };
    741            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    742        } else if input.starts_with("/banmsg ") || input.starts_with("/filter ") {
    743            let term = if input.starts_with("/banmsg ") {
    744                remove_prefix(input, "/banmsg ")
    745            } else {
    746                remove_prefix(input, "/filter ")
    747            };
    748            let term = term.to_owned();
    749            {
    750                let mut f = self.bad_message_filters.lock().unwrap();
    751                f.push(term.clone());
    752            }
    753            self.save_filters();
    754            let msg = format!("Filtering messages including \"{}\"", term);
    755            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    756        } else if input == "/banlist" {
    757            let list = self.list_filters(true);
    758            let list_exact = self.list_exact_filters();
    759            let msg = format!("Banned names: {}", list) +
    760                &if list_exact.is_empty() {
    761                    String::new()
    762                } else {
    763                    format!("\nBanned exact names: {}", list_exact)
    764                };
    765            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    766        } else if input == "/filterlist" {
    767            let list = self.list_filters(false);
    768            let msg = format!("Filtered messages: {}", list);
    769            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    770        } else if input.starts_with("/unban ") {
    771            let mut name = remove_prefix(input, "/unban ");
    772            if name.starts_with('"') && name.ends_with('"') && name.len() >= 2 {
    773                name = &name[1..name.len() - 1];
    774            }
    775            if self.remove_filter(name, true) {
    776                self.save_filters();
    777                let msg = format!("Unbanned {}", name);
    778                self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    779            }
    780        } else if input.starts_with("/unfilter ") {
    781            let term = remove_prefix(input, "/unfilter ");
    782            if self.remove_filter(term, false) {
    783                self.save_filters();
    784                let msg = format!("Unfiltered \"{}\"", term);
    785                self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    786            }
    787        } else if input.starts_with("/allow ") {
    788            let user = remove_prefix(input, "/allow ").to_owned();
    789            {
    790                let mut list = self.allowlist.lock().unwrap();
    791                if !list.contains(&user) {
    792                    list.push(user.clone());
    793                }
    794            }
    795            self.save_filters();
    796            let msg = format!("Allowed {}", user);
    797            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    798        } else if input.starts_with("/revoke ") {
    799            let user = remove_prefix(input, "/revoke ").to_owned();
    800            {
    801                let mut list = self.allowlist.lock().unwrap();
    802                if let Some(pos) = list.iter().position(|u| u == &user) {
    803                    list.remove(pos);
    804                }
    805            }
    806            self.save_filters();
    807            let msg = format!("Revoked {}", user);
    808            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    809        } else if input == "/allowlist" {
    810            let list = self.allowlist.lock().unwrap().clone();
    811            let out = if list.is_empty() { String::from("(empty)") } else { list.join(", ") };
    812            let msg = format!("Allowlist: {}", out);
    813            self.post_msg(PostType::Post(msg, Some("0".to_owned()))).unwrap();
    814        } else if let Some(captures) = IGNORE_RGX.captures(input) {
    815            let username = captures[1].to_owned();
    816            self.post_msg(PostType::Ignore(username)).unwrap();
    817        } else if let Some(captures) = UNIGNORE_RGX.captures(input) {
    818            let username = captures[1].to_owned();
    819            self.post_msg(PostType::Unignore(username)).unwrap();
    820        } else if let Some(captures) = UPLOAD_RGX.captures(input) {
    821            let file_path = captures[1].to_owned();
    822            let send_to = match captures.get(2) {
    823                Some(to_match) => match to_match.as_str() {
    824                    "members" => SEND_TO_MEMBERS,
    825                    "staffs" => SEND_TO_STAFFS,
    826                    "admins" => SEND_TO_ADMINS,
    827                    _ => SEND_TO_ALL,
    828                },
    829                None => SEND_TO_ALL,
    830            }
    831            .to_owned();
    832            let msg = match captures.get(3) {
    833                Some(msg_match) => msg_match.as_str().to_owned(),
    834                None => "".to_owned(),
    835            };
    836            self.post_msg(PostType::Upload(file_path, send_to, msg)).unwrap();
    837        } else if input.starts_with("!warn") {
    838            let msg = input.trim_start_matches("!warn").trim();
    839            let msg = if msg.starts_with('@') {
    840                msg.to_owned()
    841            } else if msg.is_empty() {
    842                String::new()
    843            } else {
    844                format!("@{}", msg)
    845            };
    846            let end_msg = format!(
    847                "This is your warning - {}, will be kicked next. Please read the !-rules / https://4-0-4.io/bhc-rules",
    848                msg
    849            );
    850            self
    851                .post_msg(PostType::Post(end_msg, None))
    852                .unwrap();
    853        } else {
    854            return false;
    855        }
    856        true
    857    }
    858 
    859    fn handle_input(
    860        &mut self,
    861        events: &Events,
    862        app: &mut App,
    863        messages: &Arc<Mutex<Vec<Message>>>,
    864        users: &Arc<Mutex<Users>>,
    865    ) -> Result<(), ExitSignal> {
    866        match events.next() {
    867            Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin),
    868            Ok(Event::Terminate) => return Err(ExitSignal::Terminate),
    869            Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt),
    870            _ => Ok(()),
    871        }
    872    }
    873 
    874    fn handle_event(
    875        &mut self,
    876        app: &mut App,
    877        messages: &Arc<Mutex<Vec<Message>>>,
    878        users: &Arc<Mutex<Users>>,
    879        event: event::Event,
    880    ) -> Result<(), ExitSignal> {
    881        match event {
    882            event::Event::Resize(_cols, _rows) => Ok(()),
    883            event::Event::FocusGained => Ok(()),
    884            event::Event::FocusLost => Ok(()),
    885            event::Event::Paste(_) => Ok(()),
    886            event::Event::Key(key_event) => self.handle_key_event(app, messages, users, key_event),
    887            event::Event::Mouse(mouse_event) => self.handle_mouse_event(app, mouse_event),
    888        }
    889    }
    890 
    891    fn handle_key_event(
    892        &mut self,
    893        app: &mut App,
    894        messages: &Arc<Mutex<Vec<Message>>>,
    895        users: &Arc<Mutex<Users>>,
    896        key_event: KeyEvent,
    897    ) -> Result<(), ExitSignal> {
    898        if app.input_mode != InputMode::Normal {
    899            self.last_key_event = None;
    900        }
    901        match app.input_mode {
    902            InputMode::LongMessage => {
    903                self.handle_long_message_mode_key_event(app, key_event, messages)
    904            }
    905            InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages),
    906            InputMode::Editing | InputMode::EditingErr => {
    907                self.handle_editing_mode_key_event(app, key_event, users)
    908            }
    909        }
    910    }
    911 
    912    fn handle_long_message_mode_key_event(
    913        &mut self,
    914        app: &mut App,
    915        key_event: KeyEvent,
    916        messages: &Arc<Mutex<Vec<Message>>>,
    917    ) -> Result<(), ExitSignal> {
    918        match key_event {
    919            KeyEvent {
    920                code: KeyCode::Enter,
    921                modifiers: KeyModifiers::NONE,
    922                ..
    923            }
    924            | KeyEvent {
    925                code: KeyCode::Esc,
    926                modifiers: KeyModifiers::NONE,
    927                ..
    928            } => self.handle_long_message_mode_key_event_esc(app),
    929            KeyEvent {
    930                code: KeyCode::Char('d'),
    931                modifiers: KeyModifiers::CONTROL,
    932                ..
    933            } => self.handle_long_message_mode_key_event_ctrl_d(app, messages),
    934            _ => {}
    935        }
    936        Ok(())
    937    }
    938 
    939    fn handle_normal_mode_key_event(
    940        &mut self,
    941        app: &mut App,
    942        key_event: KeyEvent,
    943        messages: &Arc<Mutex<Vec<Message>>>,
    944    ) -> Result<(), ExitSignal> {
    945        match key_event {
    946            KeyEvent {
    947                code: KeyCode::Char('/'),
    948                modifiers: KeyModifiers::NONE,
    949                ..
    950            } => self.handle_normal_mode_key_event_slash(app),
    951            KeyEvent {
    952                code: KeyCode::Char('j'),
    953                modifiers: KeyModifiers::NONE,
    954                ..
    955            }
    956            | KeyEvent {
    957                code: KeyCode::Down,
    958                modifiers: KeyModifiers::NONE,
    959                ..
    960            } => self.handle_normal_mode_key_event_down(app),
    961            KeyEvent {
    962                code: KeyCode::Char('J'),
    963                modifiers: KeyModifiers::SHIFT,
    964                ..
    965            } => self.handle_normal_mode_key_event_j(app,5),
    966            KeyEvent {
    967                code: KeyCode::Char('k'),
    968                modifiers: KeyModifiers::NONE,
    969                ..
    970            }
    971            | KeyEvent {
    972                code: KeyCode::Up,
    973                modifiers: KeyModifiers::NONE,
    974                ..
    975            } => self.handle_normal_mode_key_event_up(app),
    976            KeyEvent {
    977                code: KeyCode::Char('K'),
    978                modifiers: KeyModifiers::SHIFT,
    979                ..
    980            } => self.handle_normal_mode_key_event_k(app,5),
    981            KeyEvent {
    982                code: KeyCode::Enter,
    983                modifiers: KeyModifiers::NONE,
    984                ..
    985            } => self.handle_normal_mode_key_event_enter(app, messages),
    986            KeyEvent {
    987                code: KeyCode::Backspace,
    988                modifiers: KeyModifiers::NONE,
    989                ..
    990            } => self.handle_normal_mode_key_event_backspace(app, messages),
    991            KeyEvent {
    992                code: KeyCode::Char('y'),
    993                modifiers: KeyModifiers::NONE,
    994                ..
    995            }
    996            | KeyEvent {
    997                code: KeyCode::Char('c'),
    998                modifiers: KeyModifiers::CONTROL,
    999                ..
   1000            } => self.handle_normal_mode_key_event_yank(app),
   1001            KeyEvent {
   1002                code: KeyCode::Char('Y'),
   1003                modifiers: KeyModifiers::SHIFT,
   1004                ..
   1005            } => self.handle_normal_mode_key_event_yank_link(app),
   1006 
   1007            //Strange
   1008            KeyEvent {
   1009                code: KeyCode::Char('D'),
   1010                modifiers: KeyModifiers::SHIFT,
   1011                ..
   1012            } => self.handle_normal_mode_key_event_download_link(app),
   1013 
   1014            //Strange
   1015            KeyEvent {
   1016                code: KeyCode::Char('d'),
   1017                modifiers: KeyModifiers::NONE,
   1018                ..
   1019            } => self.handle_normal_mode_key_event_download_and_view(app),
   1020 
   1021            // KeyEvent {
   1022            //     code: KeyCode::Char('d'),
   1023            //     modifiers: KeyModifiers::NONE,
   1024            //     ..
   1025            // } => self.handle_normal_mode_key_event_debug(app),
   1026            // KeyEvent {
   1027            //     code: KeyCode::Char('D'),
   1028            //     modifiers: KeyModifiers::SHIFT,
   1029            //     ..
   1030            // } => self.handle_normal_mode_key_event_debug2(app),
   1031            KeyEvent {
   1032                code: KeyCode::Char('m'),
   1033                modifiers: KeyModifiers::NONE,
   1034                ..
   1035            } => self.handle_normal_mode_key_event_toggle_mute(),
   1036            KeyEvent {
   1037                code: KeyCode::Char('S'),
   1038                modifiers: KeyModifiers::SHIFT,
   1039                ..
   1040            } => self.handle_normal_mode_key_event_toggle_sys(),
   1041            KeyEvent {
   1042                code: KeyCode::Char('M'),
   1043                modifiers: KeyModifiers::SHIFT,
   1044                ..
   1045            } => self.handle_normal_mode_key_event_toggle_member_view(),
   1046            KeyEvent {
   1047                code: KeyCode::Char('G'),
   1048                modifiers: KeyModifiers::SHIFT,
   1049                ..
   1050            } => self.handle_normal_mode_key_event_toggle_guest_view(),
   1051            KeyEvent {
   1052                code: KeyCode::Char('H'),
   1053                modifiers: KeyModifiers::SHIFT,
   1054                ..
   1055            } => self.handle_normal_mode_key_event_toggle_hidden(),
   1056            KeyEvent {
   1057                code: KeyCode::Char('i'),
   1058                modifiers: KeyModifiers::NONE,
   1059                ..
   1060            } => self.handle_normal_mode_key_event_input_mode(app),
   1061            KeyEvent {
   1062                code: KeyCode::Char('Q'),
   1063                modifiers: KeyModifiers::SHIFT,
   1064                ..
   1065            } => self.handle_normal_mode_key_event_logout()?,
   1066            KeyEvent {
   1067                code: KeyCode::Char('q'),
   1068                modifiers: KeyModifiers::NONE,
   1069                ..
   1070            } => self.handle_normal_mode_key_event_exit()?,
   1071            KeyEvent {
   1072                code: KeyCode::Char('t'),
   1073                modifiers: KeyModifiers::NONE,
   1074                ..
   1075            } => self.handle_normal_mode_key_event_tag(app),
   1076            KeyEvent {
   1077                code: KeyCode::Char('p'),
   1078                modifiers: KeyModifiers::NONE,
   1079                ..
   1080            } => self.handle_normal_mode_key_event_pm(app),
   1081            KeyEvent {
   1082                code: KeyCode::Char('k'),
   1083                modifiers: KeyModifiers::CONTROL,
   1084                ..
   1085            } => self.handle_normal_mode_key_event_kick(app),
   1086            KeyEvent {
   1087                code: KeyCode::Char('b'),
   1088                modifiers: KeyModifiers::CONTROL,
   1089                ..
   1090            } => self.handle_normal_mode_key_event_ban(app),
   1091            KeyEvent {
   1092                code: KeyCode::Char('B'),
   1093                modifiers,
   1094                ..
   1095            } if modifiers.contains(KeyModifiers::CONTROL) => {
   1096                self.handle_normal_mode_key_event_ban_exact(app)
   1097            }
   1098            KeyEvent {
   1099                code: KeyCode::Char('w'),
   1100                modifiers: KeyModifiers::CONTROL,
   1101                ..
   1102            } => self.handle_normal_mode_key_event_warn(app),
   1103            KeyEvent {
   1104                code: KeyCode::Char('T'),
   1105                modifiers: KeyModifiers::SHIFT,
   1106                ..
   1107            } => self.handle_normal_mode_key_event_translate(app, messages),
   1108            KeyEvent {
   1109                code: KeyCode::Char('u'),
   1110                modifiers: KeyModifiers::CONTROL,
   1111                ..
   1112            }
   1113            | KeyEvent {
   1114                code: KeyCode::PageUp,
   1115                modifiers: KeyModifiers::NONE,
   1116                ..
   1117            } => self.handle_normal_mode_key_event_page_up(app),
   1118            KeyEvent {
   1119                code: KeyCode::Char('d'),
   1120                modifiers: KeyModifiers::CONTROL,
   1121                ..
   1122            }
   1123            | KeyEvent {
   1124                code: KeyCode::PageDown,
   1125                modifiers: KeyModifiers::NONE,
   1126                ..
   1127            } => self.handle_normal_mode_key_event_page_down(app),
   1128            KeyEvent {
   1129                code: KeyCode::Esc,
   1130                modifiers: KeyModifiers::NONE,
   1131                ..
   1132            } => self.handle_normal_mode_key_event_esc(app),
   1133            KeyEvent {
   1134                code: KeyCode::Char('u'),
   1135                modifiers: KeyModifiers::SHIFT,
   1136                ..
   1137            } => self.handle_normal_mode_key_event_shift_u(app),
   1138            KeyEvent {
   1139                code: KeyCode::Char('g'),
   1140                modifiers: KeyModifiers::NONE,
   1141                ..
   1142            } => self.handle_normal_mode_key_event_g(app),
   1143            _ => {}
   1144        }
   1145        self.last_key_event = Some(key_event.code);
   1146        Ok(())
   1147    }
   1148 
   1149    fn handle_editing_mode_key_event(
   1150        &mut self,
   1151        app: &mut App,
   1152        key_event: KeyEvent,
   1153        users: &Arc<Mutex<Users>>,
   1154    ) -> Result<(), ExitSignal> {
   1155        app.input_mode = InputMode::Editing;
   1156        match key_event {
   1157            KeyEvent {
   1158                code: KeyCode::Enter,
   1159                modifiers,
   1160                ..
   1161            } if modifiers.contains(KeyModifiers::SHIFT)
   1162                || modifiers.contains(KeyModifiers::CONTROL) =>
   1163                self.handle_editing_mode_key_event_newline(app),
   1164            KeyEvent {
   1165                code: KeyCode::Enter,
   1166                modifiers: KeyModifiers::NONE,
   1167                ..
   1168            } => self.handle_editing_mode_key_event_enter(app, users)?,
   1169            KeyEvent {
   1170                code: KeyCode::Tab,
   1171                modifiers: KeyModifiers::NONE,
   1172                ..
   1173            } => self.handle_editing_mode_key_event_tab(app, users),
   1174            KeyEvent {
   1175                code: KeyCode::Char('c'),
   1176                modifiers: KeyModifiers::CONTROL,
   1177                ..
   1178            } => self.handle_editing_mode_key_event_ctrl_c(app),
   1179            KeyEvent {
   1180                code: KeyCode::Char('a'),
   1181                modifiers: KeyModifiers::CONTROL,
   1182                ..
   1183            } => self.handle_editing_mode_key_event_ctrl_a(app),
   1184            KeyEvent {
   1185                code: KeyCode::Char('e'),
   1186                modifiers: KeyModifiers::CONTROL,
   1187                ..
   1188            } => self.handle_editing_mode_key_event_ctrl_e(app),
   1189            KeyEvent {
   1190                code: KeyCode::Char('f'),
   1191                modifiers: KeyModifiers::CONTROL,
   1192                ..
   1193            } => self.handle_editing_mode_key_event_ctrl_f(app),
   1194            KeyEvent {
   1195                code: KeyCode::Char('b'),
   1196                modifiers: KeyModifiers::CONTROL,
   1197                ..
   1198            } => self.handle_editing_mode_key_event_ctrl_b(app),
   1199            KeyEvent {
   1200                code: KeyCode::Char('v'),
   1201                modifiers: KeyModifiers::CONTROL,
   1202                ..
   1203            } => self.handle_editing_mode_key_event_ctrl_v(app),
   1204            KeyEvent {
   1205                code: KeyCode::Left,
   1206                modifiers: KeyModifiers::NONE,
   1207                ..
   1208            } => self.handle_editing_mode_key_event_left(app),
   1209            KeyEvent {
   1210                code: KeyCode::Right,
   1211                modifiers: KeyModifiers::NONE,
   1212                ..
   1213            } => self.handle_editing_mode_key_event_right(app),
   1214            KeyEvent {
   1215                code: KeyCode::Down,
   1216                modifiers: KeyModifiers::NONE,
   1217                ..
   1218            } => self.handle_editing_mode_key_event_down(app),
   1219            KeyEvent {
   1220                code: KeyCode::Char(c),
   1221                modifiers: KeyModifiers::NONE,
   1222                ..
   1223            }
   1224            | KeyEvent {
   1225                code: KeyCode::Char(c),
   1226                modifiers: KeyModifiers::SHIFT,
   1227                ..
   1228            } => self.handle_editing_mode_key_event_shift_c(app, c),
   1229            KeyEvent {
   1230                code: KeyCode::Backspace,
   1231                modifiers: KeyModifiers::NONE,
   1232                ..
   1233            } => self.handle_editing_mode_key_event_backspace(app),
   1234            KeyEvent {
   1235                code: KeyCode::Delete,
   1236                modifiers: KeyModifiers::NONE,
   1237                ..
   1238            } => self.handle_editing_mode_key_event_delete(app),
   1239            KeyEvent {
   1240                code: KeyCode::Esc,
   1241                modifiers: KeyModifiers::NONE,
   1242                ..
   1243            } => self.handle_editing_mode_key_event_esc(app),
   1244            _ => {}
   1245        }
   1246        Ok(())
   1247    }
   1248 
   1249    fn handle_long_message_mode_key_event_esc(&mut self, app: &mut App) {
   1250        app.long_message = None;
   1251        app.input_mode = InputMode::Normal;
   1252    }
   1253 
   1254    fn handle_long_message_mode_key_event_ctrl_d(
   1255        &mut self,
   1256        app: &mut App,
   1257        messages: &Arc<Mutex<Vec<Message>>>,
   1258    ) {
   1259        if let Some(idx) = app.items.state.selected() {
   1260            if let Some(item) = app.items.items.get(idx) {
   1261                self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text()))
   1262                    .unwrap();
   1263                let mut messages = messages.lock().unwrap();
   1264                if let Some(pos) = messages
   1265                    .iter()
   1266                    .position(|m| m.date == item.date && m.text == item.text)
   1267                {
   1268                    messages[pos].hide = !messages[pos].hide;
   1269                }
   1270                app.long_message = None;
   1271                app.input_mode = InputMode::Normal;
   1272            }
   1273        }
   1274    }
   1275 
   1276    fn handle_normal_mode_key_event_up(&mut self, app: &mut App) {
   1277        app.items.previous()
   1278    }
   1279 
   1280    fn handle_normal_mode_key_event_down(&mut self, app: &mut App) {
   1281        app.items.next()
   1282    }
   1283 
   1284    fn handle_normal_mode_key_event_j(&mut self, app: &mut App, lines: usize) {
   1285        for _ in 0..lines {
   1286            app.items.next(); // Move to the next item
   1287        }
   1288    }
   1289 
   1290    fn handle_normal_mode_key_event_k(&mut self, app: &mut App, lines: usize) {
   1291        for _ in 0..lines {
   1292            app.items.previous(); // Move to the next item
   1293        }
   1294    }
   1295 
   1296    fn handle_normal_mode_key_event_slash(&mut self, app: &mut App) {
   1297        app.items.unselect();
   1298        app.input = "/".to_owned();
   1299        app.input_idx = app.input.width();
   1300        app.input_mode = InputMode::Editing;
   1301    }
   1302 
   1303    fn handle_normal_mode_key_event_enter(
   1304        &mut self,
   1305        app: &mut App,
   1306        messages: &Arc<Mutex<Vec<Message>>>,
   1307    ) {
   1308        if let Some(idx) = app.items.state.selected() {
   1309            if let Some(item) = app.items.items.get(idx) {
   1310                // If we have a filter, <enter> will "jump" to the message
   1311                if !app.filter.is_empty() {
   1312                    let idx = messages
   1313                        .lock()
   1314                        .unwrap()
   1315                        .iter()
   1316                        .enumerate()
   1317                        .find(|(_, e)| e.date == item.date)
   1318                        .map(|(i, _)| i);
   1319                    app.clear_filter();
   1320                    app.items.state.select(idx);
   1321                    return;
   1322                }
   1323                app.long_message = Some(item.clone());
   1324                app.input_mode = InputMode::LongMessage;
   1325            }
   1326        }
   1327    }
   1328 
   1329    fn handle_normal_mode_key_event_backspace(
   1330        &mut self,
   1331        app: &mut App,
   1332        messages: &Arc<Mutex<Vec<Message>>>,
   1333    ) {
   1334        if let Some(idx) = app.items.state.selected() {
   1335            if let Some(item) = app.items.items.get(idx) {
   1336                let mut messages = messages.lock().unwrap();
   1337                if let Some(pos) = messages
   1338                    .iter()
   1339                    .position(|m| m.date == item.date && m.text == item.text)
   1340                {
   1341                    if item.deleted {
   1342                        messages.remove(pos);
   1343                    } else {
   1344                        messages[pos].hide = !messages[pos].hide;
   1345                    }
   1346                }
   1347            }
   1348        }
   1349    }
   1350 
   1351    fn handle_normal_mode_key_event_yank(&mut self, app: &mut App) {
   1352        if let Some(idx) = app.items.state.selected() {
   1353            if let Some(item) = app.items.items.get(idx) {
   1354                if let Some(upload_link) = &item.upload_link {
   1355                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1356                    let mut out = format!("{}{}", self.config.url, upload_link);
   1357                    if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag) {
   1358                        out = format!("{} {}", msg, out);
   1359                    }
   1360                    ctx.set_contents(out).unwrap();
   1361                } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1362                {
   1363                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1364                    ctx.set_contents(msg).unwrap();
   1365                }
   1366            }
   1367        }
   1368    }
   1369 
   1370    fn handle_normal_mode_key_event_yank_link(&mut self, app: &mut App) {
   1371        if let Some(idx) = app.items.state.selected() {
   1372            if let Some(item) = app.items.items.get(idx) {
   1373                if let Some(upload_link) = &item.upload_link {
   1374                    let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1375                    let out = format!("{}{}", self.config.url, upload_link);
   1376                    ctx.set_contents(out).unwrap();
   1377                } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1378                {
   1379                    let finder = LinkFinder::new();
   1380                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   1381                    if let Some(link) = links.get(0) {
   1382                        let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1383                        ctx.set_contents(link.as_str().to_owned()).unwrap();
   1384                    }
   1385                }
   1386            }
   1387        }
   1388    }
   1389 
   1390    //Strange
   1391    fn handle_normal_mode_key_event_download_link(&mut self, app: &mut App) {
   1392        if let Some(idx) = app.items.state.selected() {
   1393            if let Some(item) = app.items.items.get(idx) {
   1394                if let Some(upload_link) = &item.upload_link {
   1395                    let url = format!("{}{}", self.config.url, upload_link);
   1396                    let _ = Command::new("curl")
   1397                        .args([
   1398                            "--socks5",
   1399                            "localhost:9050",
   1400                            "--socks5-hostname",
   1401                            "localhost:9050",
   1402                            &url,
   1403                        ])
   1404                        .arg("-o")
   1405                        .arg("download.img")
   1406                        .output()
   1407                        .expect("Failed to execute curl command");
   1408                } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1409                {
   1410                    let finder = LinkFinder::new();
   1411                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   1412                    if let Some(link) = links.first() {
   1413                        let url = link.as_str();
   1414                        let _ = Command::new("curl")
   1415                            .args([
   1416                                "--socks5",
   1417                                "localhost:9050",
   1418                                "--socks5-hostname",
   1419                                "localhost:9050",
   1420                                url,
   1421                            ])
   1422                            .arg("-o")
   1423                            .arg("download.img")
   1424                            .output()
   1425                            .expect("Failed to execute curl command");
   1426                    }
   1427                }
   1428            }
   1429        }
   1430    }
   1431 
   1432    //strageEdit
   1433    fn handle_normal_mode_key_event_download_and_view(&mut self, app: &mut App) {
   1434        if let Some(idx) = app.items.state.selected() {
   1435            if let Some(item) = app.items.items.get(idx) {
   1436                if let Some(upload_link) = &item.upload_link {
   1437                    let url = format!("{}{}", self.config.url, upload_link);
   1438                    let _ = Command::new("curl")
   1439                        .args([
   1440                            "--socks5",
   1441                            "localhost:9050",
   1442                            "--socks5-hostname",
   1443                            "localhost:9050",
   1444                            &url,
   1445                        ])
   1446                        .arg("-o")
   1447                        .arg("download.img")
   1448                        .output()
   1449                        .expect("Failed to execute curl command");
   1450 
   1451                    let _ = Command::new("xdg-open")
   1452                        .arg("./download.img")
   1453                        .output()
   1454                        .expect("Failed to execute sxiv command");
   1455                } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag)
   1456                {
   1457                    let finder = LinkFinder::new();
   1458                    let links: Vec<_> = finder.links(msg.as_str()).collect();
   1459                    if let Some(link) = links.first() {
   1460                        let url = link.as_str();
   1461                        let _ = Command::new("curl")
   1462                            .args([
   1463                                "--socks5",
   1464                                "localhost:9050",
   1465                                "--socks5-hostname",
   1466                                "localhost:9050",
   1467                                url,
   1468                            ])
   1469                            .arg("-o")
   1470                            .arg("download.img")
   1471                            .output()
   1472                            .expect("Failed to execute curl command");
   1473 
   1474                        let _ = Command::new("sxiv")
   1475                            .arg("./download.img")
   1476                            .output()
   1477                            .expect("Failed to execute sxiv command");
   1478                    }
   1479                }
   1480            }
   1481        }
   1482    }
   1483 
   1484    fn handle_normal_mode_key_event_toggle_mute(&mut self) {
   1485        let mut is_muted = self.is_muted.lock().unwrap();
   1486        *is_muted = !*is_muted;
   1487    }
   1488 
   1489    fn handle_normal_mode_key_event_toggle_sys(&mut self) {
   1490        self.show_sys = !self.show_sys;
   1491    }
   1492 
   1493    fn handle_normal_mode_key_event_toggle_guest_view(&mut self) {
   1494        self.display_guest_view = !self.display_guest_view;
   1495    }
   1496 
   1497    fn handle_normal_mode_key_event_toggle_member_view(&mut self) {
   1498        self.display_member_view = !self.display_member_view;
   1499    }
   1500 
   1501    fn handle_normal_mode_key_event_g(&mut self, app: &mut App) {
   1502        // Handle "gg" key combination
   1503        if self.last_key_event == Some(KeyCode::Char('g')) {
   1504            app.items.select_top();
   1505            self.last_key_event = None;
   1506        }
   1507    }
   1508 
   1509    fn handle_normal_mode_key_event_toggle_hidden(&mut self) {
   1510        self.display_hidden_msgs = !self.display_hidden_msgs;
   1511    }
   1512 
   1513    fn handle_normal_mode_key_event_input_mode(&mut self, app: &mut App) {
   1514        app.input_mode = InputMode::Editing;
   1515        app.items.unselect();
   1516    }
   1517 
   1518    fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> {
   1519        self.logout().unwrap();
   1520        return Err(ExitSignal::Terminate);
   1521    }
   1522 
   1523    fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> {
   1524        return Err(ExitSignal::Terminate);
   1525    }
   1526 
   1527    fn handle_normal_mode_key_event_tag(&mut self, app: &mut App) {
   1528        if let Some(idx) = app.items.state.selected() {
   1529            let text = &app.items.items.get(idx).unwrap().text;
   1530            if let Some(username) =
   1531                get_username(&self.base_client.username, &text, &self.config.members_tag)
   1532            {
   1533                if text.text().starts_with(&app.members_tag) {
   1534                    app.input = format!("/m @{} ", username);
   1535                } else {
   1536                    app.input = format!("@{} ", username);
   1537                }
   1538                app.input_idx = app.input.width();
   1539                app.input_mode = InputMode::Editing;
   1540                app.items.unselect();
   1541            }
   1542        }
   1543    }
   1544 
   1545    fn handle_normal_mode_key_event_pm(&mut self, app: &mut App) {
   1546        if let Some(idx) = app.items.state.selected() {
   1547            if let Some(username) = get_username(
   1548                &self.base_client.username,
   1549                &app.items.items.get(idx).unwrap().text,
   1550                &self.config.members_tag,
   1551            ) {
   1552                app.input = format!("/pm {} ", username);
   1553                app.input_idx = app.input.width();
   1554                app.input_mode = InputMode::Editing;
   1555                app.items.unselect();
   1556            }
   1557        }
   1558    }
   1559 
   1560    fn handle_normal_mode_key_event_kick(&mut self, app: &mut App) {
   1561        if let Some(idx) = app.items.state.selected() {
   1562            if let Some(username) = get_username(
   1563                &self.base_client.username,
   1564                &app.items.items.get(idx).unwrap().text,
   1565                &self.config.members_tag,
   1566            ) {
   1567                app.input = format!("/kick {} ", username);
   1568                app.input_idx = app.input.width();
   1569                app.input_mode = InputMode::Editing;
   1570                app.items.unselect();
   1571            }
   1572        }
   1573    }
   1574 
   1575    fn handle_normal_mode_key_event_ban(&mut self, app: &mut App) {
   1576        if let Some(idx) = app.items.state.selected() {
   1577            if let Some(username) = get_username(
   1578                &self.base_client.username,
   1579                &app.items.items.get(idx).unwrap().text,
   1580                &self.config.members_tag,
   1581            ) {
   1582                app.input = format!("/ban {} ", username);
   1583                app.input_idx = app.input.width();
   1584                app.input_mode = InputMode::Editing;
   1585                app.items.unselect();
   1586            }
   1587        }
   1588    }
   1589 
   1590    fn handle_normal_mode_key_event_ban_exact(&mut self, app: &mut App) {
   1591        if let Some(idx) = app.items.state.selected() {
   1592            if let Some(username) = get_username(
   1593                &self.base_client.username,
   1594                &app.items.items.get(idx).unwrap().text,
   1595                &self.config.members_tag,
   1596            ) {
   1597                app.input = format!(r#"/ban "{}" "#, username);
   1598                app.input_idx = app.input.width();
   1599                app.input_mode = InputMode::Editing;
   1600                app.items.unselect();
   1601            }
   1602        }
   1603    }
   1604 
   1605    //Strange
   1606    fn handle_normal_mode_key_event_translate(
   1607        &mut self,
   1608        app: &mut App,
   1609        messages: &Arc<Mutex<Vec<Message>>>,
   1610    ) {
   1611        log::error!("translate running");
   1612        if let Some(idx) = app.items.state.selected() {
   1613            log::error!("1353");
   1614            let mut message_lock = messages.lock().unwrap();
   1615            if let Some(message) = message_lock.get_mut(idx) {
   1616                log::error!("1356");
   1617                let original_text = &mut message.text;
   1618                let output = Command::new("trans")
   1619                    .arg("-b")
   1620                    .arg(&original_text.text())
   1621                    .output()
   1622                    .expect("Failed to execute translation command");
   1623 
   1624                if output.status.success() {
   1625                    if let Ok(new_text) = String::from_utf8(output.stdout) {
   1626                        *original_text = StyledText::Text(new_text.trim().to_owned());
   1627                        log::error!("Translation successful: {}", new_text);
   1628                    } else {
   1629                        log::error!("Failed to decode translation output as UTF-8");
   1630                    }
   1631                } else {
   1632                    log::error!("Translation command failed with error: {:?}", output.status);
   1633                }
   1634            }
   1635        }
   1636    }
   1637 
   1638    //Strange
   1639    fn handle_normal_mode_key_event_warn(&mut self, app: &mut App) {
   1640        if let Some(idx) = app.items.state.selected() {
   1641            if let Some(username) = get_username(
   1642                &self.base_client.username,
   1643                &app.items.items.get(idx).unwrap().text,
   1644                &self.config.members_tag,
   1645            ) {
   1646                app.input = format!("!warn @{} ", username);
   1647                app.input_idx = app.input.width();
   1648                app.input_mode = InputMode::Editing;
   1649                app.items.unselect();
   1650            }
   1651        }
   1652    }
   1653    fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) {
   1654        if let Some(idx) = app.items.state.selected() {
   1655            app.items.state.select(idx.checked_sub(10).or(Some(0)));
   1656        } else {
   1657            app.items.next();
   1658        }
   1659    }
   1660 
   1661    fn handle_normal_mode_key_event_page_down(&mut self, app: &mut App) {
   1662        if let Some(idx) = app.items.state.selected() {
   1663            let wanted_idx = idx + 10;
   1664            let max_idx = app.items.items.len() - 1;
   1665            let new_idx = std::cmp::min(wanted_idx, max_idx);
   1666            app.items.state.select(Some(new_idx));
   1667        } else {
   1668            app.items.next();
   1669        }
   1670    }
   1671 
   1672    fn handle_normal_mode_key_event_esc(&mut self, app: &mut App) {
   1673        app.items.unselect();
   1674    }
   1675 
   1676    fn handle_normal_mode_key_event_shift_u(&mut self, app: &mut App) {
   1677        app.items.state.select(Some(0));
   1678    }
   1679 
   1680    fn handle_editing_mode_key_event_enter(
   1681        &mut self,
   1682        app: &mut App,
   1683        users: &Arc<Mutex<Users>>,
   1684    ) -> Result<(), ExitSignal> {
   1685        if FIND_RGX.is_match(&app.input) {
   1686            return Ok(());
   1687        }
   1688 
   1689        let mut input: String = app.input.drain(..).collect();
   1690        input = replace_newline_escape(&input);
   1691        app.input_idx = 0;
   1692 
   1693        // Iterate over commands and execute associated actions
   1694        for (command, action) in &app.commands.commands {
   1695            // log::error!("command :{} action :{}", command, action);
   1696            let expected_input = format!("!{}", command);
   1697            if input == expected_input {
   1698                // Execute the action by posting a message
   1699                self.post_msg(PostType::Post(action.clone(), None)).unwrap();
   1700                // Return Ok(()) if the action is executed successfully
   1701                return Ok(());
   1702            }
   1703        }
   1704 
   1705        let mut cmd_input = input.clone();
   1706        let mut members_prefix = false;
   1707        if cmd_input.starts_with("/m ") {
   1708            members_prefix = true;
   1709            if remove_prefix(&cmd_input, "/m ").starts_with('/') {
   1710                cmd_input = remove_prefix(&cmd_input, "/m ").to_owned();
   1711            }
   1712        }
   1713 
   1714        if self.process_command(&cmd_input, app, users) {
   1715            if members_prefix {
   1716                app.input = "/m ".to_owned();
   1717                app.input_idx = app.input.width();
   1718            }
   1719            return Ok(());
   1720        }
   1721 
   1722        if members_prefix {
   1723            let msg = remove_prefix(&input, "/m ").to_owned();
   1724            let to = Some(SEND_TO_MEMBERS.to_owned());
   1725            self.post_msg(PostType::Post(msg, to)).unwrap();
   1726            app.input = "/m ".to_owned();
   1727            app.input_idx = app.input.width();
   1728        } else if input.starts_with("/a ") {
   1729            let msg = remove_prefix(&input, "/a ").to_owned();
   1730            let to = Some(SEND_TO_ADMINS.to_owned());
   1731            self.post_msg(PostType::Post(msg, to)).unwrap();
   1732            app.input = "/a ".to_owned();
   1733            app.input_idx = app.input.width();
   1734        } else if input.starts_with("/s ") {
   1735            let msg = remove_prefix(&input, "/s ").to_owned();
   1736            let to = Some(SEND_TO_STAFFS.to_owned());
   1737            self.post_msg(PostType::Post(msg, to)).unwrap();
   1738            app.input = "/s ".to_owned();
   1739            app.input_idx = app.input.width();
   1740        } else {
   1741            if input.starts_with("/") && !input.starts_with("/me ") {
   1742                app.input_idx = input.len();
   1743                app.input = input;
   1744                app.input_mode = InputMode::EditingErr;
   1745            } else {
   1746                self.post_msg(PostType::Post(input, None)).unwrap();
   1747            }
   1748        }
   1749        Ok(())
   1750    }
   1751 
   1752    fn handle_editing_mode_key_event_tab(&mut self, app: &mut App, users: &Arc<Mutex<Users>>) {
   1753        let (p1, p2) = app.input.split_at(app.input_idx);
   1754        if p2 == "" || p2.chars().nth(0) == Some(' ') {
   1755            let mut parts: Vec<&str> = p1.split(" ").collect();
   1756            if let Some(user_prefix) = parts.pop() {
   1757                let mut should_autocomplete = false;
   1758                let mut prefix = "";
   1759                if parts.len() == 1
   1760                    && ((parts[0] == "/kick" || parts[0] == "/k")
   1761                        || parts[0] == "/pm"
   1762                        || parts[0] == "/ignore"
   1763                        || parts[0] == "/unignore"
   1764                        || parts[0] == "/ban")
   1765                {
   1766                    should_autocomplete = true;
   1767                } else if user_prefix.starts_with("@") {
   1768                    should_autocomplete = true;
   1769                    prefix = "@";
   1770                }
   1771                if should_autocomplete {
   1772                    let user_prefix_norm = remove_prefix(user_prefix, prefix);
   1773                    let user_prefix_norm_len = user_prefix_norm.len();
   1774                    if let Some(name) = autocomplete_username(users, user_prefix_norm) {
   1775                        let complete_name = format!("{}{}", prefix, name);
   1776                        parts.push(complete_name.as_str());
   1777                        let p2 = p2.trim_start();
   1778                        if p2 != "" {
   1779                            parts.push(p2);
   1780                        }
   1781                        app.input = parts.join(" ");
   1782                        app.input_idx += name.len() - user_prefix_norm_len;
   1783                    }
   1784                }
   1785            }
   1786        }
   1787    }
   1788 
   1789    fn handle_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) {
   1790        app.clear_filter();
   1791        app.input = "".to_owned();
   1792        app.input_idx = 0;
   1793        app.input_mode = InputMode::Normal;
   1794    }
   1795 
   1796    fn handle_editing_mode_key_event_ctrl_a(&mut self, app: &mut App) {
   1797        app.input_idx = 0;
   1798    }
   1799 
   1800    fn handle_editing_mode_key_event_ctrl_e(&mut self, app: &mut App) {
   1801        app.input_idx = app.input.width();
   1802    }
   1803 
   1804    fn handle_editing_mode_key_event_ctrl_f(&mut self, app: &mut App) {
   1805        if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') {
   1806            app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width());
   1807        } else {
   1808            app.input_idx = app.input.width();
   1809        }
   1810    }
   1811 
   1812    fn handle_editing_mode_key_event_ctrl_b(&mut self, app: &mut App) {
   1813        if let Some(idx) = app.input_idx.checked_sub(2) {
   1814            let tmp = app
   1815                .input
   1816                .chars()
   1817                .take(idx)
   1818                .collect::<String>()
   1819                .chars()
   1820                .rev()
   1821                .collect::<String>();
   1822            if let Some(idx) = tmp.chars().position(|c| c == ' ') {
   1823                app.input_idx = std::cmp::max(tmp.width() - idx, 0);
   1824            } else {
   1825                app.input_idx = 0;
   1826            }
   1827        }
   1828    }
   1829 
   1830    fn handle_editing_mode_key_event_ctrl_v(&mut self, app: &mut App) {
   1831        let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap();
   1832        if let Ok(clipboard) = ctx.get_contents() {
   1833            let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   1834            app.input.insert_str(byte_position, &clipboard);
   1835            app.input_idx += clipboard.chars().count();
   1836        }
   1837    }
   1838 
   1839    fn handle_editing_mode_key_event_newline(&mut self, app: &mut App) {
   1840        let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   1841        app.input.insert(byte_position, '\n');
   1842        app.input_idx += 1;
   1843    }
   1844 
   1845    fn handle_editing_mode_key_event_left(&mut self, app: &mut App) {
   1846        if app.input_idx > 0 {
   1847            app.input_idx -= 1;
   1848        }
   1849    }
   1850 
   1851    fn handle_editing_mode_key_event_right(&mut self, app: &mut App) {
   1852        if app.input_idx < app.input.width() {
   1853            app.input_idx += 1;
   1854        }
   1855    }
   1856 
   1857    fn handle_editing_mode_key_event_down(&mut self, app: &mut App) {
   1858        app.input_mode = InputMode::Normal;
   1859        app.items.next();
   1860    }
   1861 
   1862    fn handle_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) {
   1863        let byte_position = byte_pos(&app.input, app.input_idx).unwrap();
   1864        app.input.insert(byte_position, c);
   1865 
   1866        app.input_idx += 1;
   1867        app.update_filter();
   1868    }
   1869 
   1870    fn handle_editing_mode_key_event_backspace(&mut self, app: &mut App) {
   1871        if app.input_idx > 0 {
   1872            app.input_idx -= 1;
   1873            app.input = remove_at(&app.input, app.input_idx);
   1874            app.update_filter();
   1875        }
   1876    }
   1877 
   1878    fn handle_editing_mode_key_event_delete(&mut self, app: &mut App) {
   1879        if app.input_idx > 0 && app.input_idx == app.input.width() {
   1880            app.input_idx -= 1;
   1881        }
   1882        app.input = remove_at(&app.input, app.input_idx);
   1883        app.update_filter();
   1884    }
   1885 
   1886    fn handle_editing_mode_key_event_esc(&mut self, app: &mut App) {
   1887        app.input_mode = InputMode::Normal;
   1888    }
   1889 
   1890    fn handle_mouse_event(
   1891        &mut self,
   1892        app: &mut App,
   1893        mouse_event: MouseEvent,
   1894    ) -> Result<(), ExitSignal> {
   1895        match mouse_event.kind {
   1896            MouseEventKind::ScrollDown => app.items.next(),
   1897            MouseEventKind::ScrollUp => app.items.previous(),
   1898            _ => {}
   1899        }
   1900        Ok(())
   1901    }
   1902 }
   1903 
   1904 // Give a char index, return the byte position
   1905 fn byte_pos(v: &str, idx: usize) -> Option<usize> {
   1906    let mut b = 0;
   1907    let mut chars = v.chars();
   1908    for _ in 0..idx {
   1909        if let Some(c) = chars.next() {
   1910            b += c.len_utf8();
   1911        } else {
   1912            return None;
   1913        }
   1914    }
   1915    Some(b)
   1916 }
   1917 
   1918 // Remove the character at idx (utf-8 aware)
   1919 fn remove_at(v: &str, idx: usize) -> String {
   1920    v.chars()
   1921        .enumerate()
   1922        .flat_map(|(i, c)| {
   1923            if i == idx {
   1924                return None;
   1925            }
   1926            Some(c)
   1927        })
   1928        .collect::<String>()
   1929 }
   1930 
   1931 // Autocomplete any username
   1932 fn autocomplete_username(users: &Arc<Mutex<Users>>, prefix: &str) -> Option<String> {
   1933    let users = users.lock().unwrap();
   1934    let all_users = users.all();
   1935    let prefix_lower = prefix.to_lowercase();
   1936    let filtered = all_users
   1937        .iter()
   1938        .find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower));
   1939    Some(filtered?.1.to_owned())
   1940 }
   1941 
   1942 fn set_profile_base_info(
   1943    client: &Client,
   1944    full_url: &str,
   1945    params: &mut Vec<(&str, String)>,
   1946 ) -> anyhow::Result<()> {
   1947    params.extend(vec![("action", "profile".to_owned())]);
   1948    let profile_resp = client.post(full_url).form(&params).send()?;
   1949    let profile_resp_txt = profile_resp.text().unwrap();
   1950    let doc = Document::from(profile_resp_txt.as_str());
   1951    let bold = doc.find(Attr("id", "bold")).next().unwrap();
   1952    let italic = doc.find(Attr("id", "italic")).next().unwrap();
   1953    let small = doc.find(Attr("id", "small")).next().unwrap();
   1954    if bold.attr("checked").is_some() {
   1955        params.push(("bold", "on".to_owned()));
   1956    }
   1957    if italic.attr("checked").is_some() {
   1958        params.push(("italic", "on".to_owned()));
   1959    }
   1960    if small.attr("checked").is_some() {
   1961        params.push(("small", "on".to_owned()));
   1962    }
   1963    let font_select = doc.find(Attr("name", "font")).next().unwrap();
   1964    let font = font_select.find(Name("option")).find_map(|el| {
   1965        if el.attr("selected").is_some() {
   1966            return Some(el.attr("value").unwrap());
   1967        }
   1968        None
   1969    });
   1970    params.push(("font", font.unwrap_or("").to_owned()));
   1971    Ok(())
   1972 }
   1973 
   1974 enum RetryErr {
   1975    Retry,
   1976    Exit,
   1977 }
   1978 
   1979 fn retry_fn<F>(mut clb: F)
   1980 where
   1981    F: FnMut() -> anyhow::Result<RetryErr>,
   1982 {
   1983    loop {
   1984        match clb() {
   1985            Ok(RetryErr::Retry) => continue,
   1986            Ok(RetryErr::Exit) => return,
   1987            Err(err) => {
   1988                log::error!("{}", err);
   1989                continue;
   1990            }
   1991        }
   1992    }
   1993 }
   1994 
   1995 fn post_msg(
   1996    client: &Client,
   1997    post_type_recv: PostType,
   1998    full_url: &str,
   1999    session: String,
   2000    url: &str,
   2001    last_post_tx: &crossbeam_channel::Sender<()>,
   2002 ) {
   2003    let mut should_reset_keepalive_timer = false;
   2004    retry_fn(|| -> anyhow::Result<RetryErr> {
   2005        let post_type = post_type_recv.clone();
   2006        let resp_text = client.get(url).send()?.text()?;
   2007        let doc = Document::from(resp_text.as_str());
   2008        let nc = doc
   2009            .find(Attr("name", "nc"))
   2010            .next()
   2011            .context("nc not found")?;
   2012        let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   2013        let postid = doc
   2014            .find(Attr("name", "postid"))
   2015            .next()
   2016            .context("failed to get postid")?;
   2017        let postid_value = postid
   2018            .attr("value")
   2019            .context("failed to get postid value")?
   2020            .to_owned();
   2021        let mut params: Vec<(&str, String)> = vec![
   2022            ("lang", LANG.to_owned()),
   2023            ("nc", nc_value.to_owned()),
   2024            ("session", session.clone()),
   2025        ];
   2026 
   2027        if let PostType::Clean(date, text) = post_type {
   2028            if let Err(e) = delete_message(&client, full_url, &mut params, date, text) {
   2029                log::error!("failed to delete message: {:?}", e);
   2030                return Ok(RetryErr::Retry);
   2031            }
   2032            return Ok(RetryErr::Exit);
   2033        }
   2034 
   2035        let mut req = client.post(full_url);
   2036        let mut form: Option<multipart::Form> = None;
   2037 
   2038        match post_type {
   2039            PostType::Post(msg, send_to) => {
   2040                should_reset_keepalive_timer = true;
   2041                params.extend(vec![
   2042                    ("action", "post".to_owned()),
   2043                    ("postid", postid_value.to_owned()),
   2044                    ("multi", "on".to_owned()),
   2045                    ("message", msg),
   2046                    ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())),
   2047                ]);
   2048            }
   2049            PostType::NewNickname(new_nickname) => {
   2050                set_profile_base_info(client, full_url, &mut params)?;
   2051                params.extend(vec![
   2052                    ("do", "save".to_owned()),
   2053                    ("timestamps", "on".to_owned()),
   2054                    ("newnickname", new_nickname),
   2055                ]);
   2056            }
   2057            PostType::NewColor(new_color) => {
   2058                set_profile_base_info(client, full_url, &mut params)?;
   2059                params.extend(vec![
   2060                    ("do", "save".to_owned()),
   2061                    ("timestamps", "on".to_owned()),
   2062                    ("colour", new_color),
   2063                ]);
   2064            }
   2065            PostType::Ignore(username) => {
   2066                set_profile_base_info(client, full_url, &mut params)?;
   2067                params.extend(vec![
   2068                    ("do", "save".to_owned()),
   2069                    ("timestamps", "on".to_owned()),
   2070                    ("ignore", username),
   2071                ]);
   2072            }
   2073            PostType::Unignore(username) => {
   2074                set_profile_base_info(client, full_url, &mut params)?;
   2075                params.extend(vec![
   2076                    ("do", "save".to_owned()),
   2077                    ("timestamps", "on".to_owned()),
   2078                    ("unignore", username),
   2079                ]);
   2080            }
   2081            PostType::Profile(new_color, new_nickname) => {
   2082                set_profile_base_info(client, full_url, &mut params)?;
   2083                params.extend(vec![
   2084                    ("do", "save".to_owned()),
   2085                    ("timestamps", "on".to_owned()),
   2086                    ("colour", new_color),
   2087                    ("newnickname", new_nickname),
   2088                ]);
   2089            }
   2090            PostType::Kick(msg, send_to) => {
   2091                params.extend(vec![
   2092                    ("action", "post".to_owned()),
   2093                    ("postid", postid_value.to_owned()),
   2094                    ("message", msg),
   2095                    ("sendto", send_to),
   2096                    ("kick", "kick".to_owned()),
   2097                    ("what", "purge".to_owned()),
   2098                ]);
   2099            }
   2100            PostType::DeleteLast | PostType::DeleteAll => {
   2101                params.extend(vec![("action", "delete".to_owned())]);
   2102                if let PostType::DeleteAll = post_type {
   2103                    params.extend(vec![
   2104                        ("sendto", SEND_TO_ALL.to_owned()),
   2105                        ("confirm", "yes".to_owned()),
   2106                        ("what", "all".to_owned()),
   2107                    ]);
   2108                } else {
   2109                    params.extend(vec![("sendto", "".to_owned()), ("what", "last".to_owned())]);
   2110                }
   2111            }
   2112            PostType::Upload(file_path, send_to, msg) => {
   2113                form = Some(
   2114                    match multipart::Form::new()
   2115                        .text("lang", LANG.to_owned())
   2116                        .text("nc", nc_value.to_owned())
   2117                        .text("session", session.clone())
   2118                        .text("action", "post".to_owned())
   2119                        .text("postid", postid_value.to_owned())
   2120                        .text("message", msg)
   2121                        .text("sendto", send_to.to_owned())
   2122                        .text("what", "purge".to_owned())
   2123                        .file("file", file_path)
   2124                    {
   2125                        Ok(f) => f,
   2126                        Err(e) => {
   2127                            log::error!("{:?}", e);
   2128                            return Ok(RetryErr::Exit);
   2129                        }
   2130                    },
   2131                );
   2132            }
   2133            PostType::Clean(_, _) => {}
   2134        }
   2135 
   2136        if let Some(form_content) = form {
   2137            req = req.multipart(form_content);
   2138        } else {
   2139            req = req.form(&params);
   2140        }
   2141        if let Err(err) = req.send() {
   2142            log::error!("{:?}", err.to_string());
   2143            if err.is_timeout() {
   2144                return Ok(RetryErr::Retry);
   2145            }
   2146        }
   2147        return Ok(RetryErr::Exit);
   2148    });
   2149    if should_reset_keepalive_timer {
   2150        last_post_tx.send(()).unwrap();
   2151    }
   2152 }
   2153 
   2154 fn parse_date(date: &str, datetime_fmt: &str) -> NaiveDateTime {
   2155    let now = Utc::now();
   2156    let date_fmt = format!("%Y-{}", datetime_fmt);
   2157    NaiveDateTime::parse_from_str(
   2158        format!("{}-{}", now.year(), date).as_str(),
   2159        date_fmt.as_str(),
   2160    )
   2161    .unwrap()
   2162 }
   2163 
   2164 fn get_msgs(
   2165    client: &Client,
   2166    base_url: &str,
   2167    page_php: &str,
   2168    session: &str,
   2169    username: &str,
   2170    users: &Arc<Mutex<Users>>,
   2171    sig: &Arc<Mutex<Sig>>,
   2172    messages_updated_tx: &crossbeam_channel::Sender<()>,
   2173    members_tag: &str,
   2174    datetime_fmt: &str,
   2175    messages: &Arc<Mutex<Vec<Message>>>,
   2176    should_notify: &mut bool,
   2177    tx: &crossbeam_channel::Sender<PostType>,
   2178    bad_usernames: &Arc<Mutex<Vec<String>>>,
   2179    bad_exact_usernames: &Arc<Mutex<Vec<String>>>,
   2180    bad_messages: &Arc<Mutex<Vec<String>>>,
   2181    allowlist: &Arc<Mutex<Vec<String>>>,
   2182 ) -> anyhow::Result<()> {
   2183    let url = format!(
   2184        "{}/{}?action=view&session={}&lang={}",
   2185        base_url, page_php, session, LANG
   2186    );
   2187    let resp_text = client.get(url).send()?.text()?;
   2188    let resp_text = resp_text.replace("<br>", "\n");
   2189    let doc = Document::from(resp_text.as_str());
   2190    let new_messages = match extract_messages(&doc) {
   2191        Ok(messages) => messages,
   2192        Err(_) => {
   2193            // Failed to get messages, probably need re-login
   2194            sig.lock().unwrap().signal(&ExitSignal::NeedLogin);
   2195            return Ok(());
   2196        }
   2197    };
   2198    let current_users = extract_users(&doc);
   2199    {
   2200        let previous = users.lock().unwrap();
   2201        let filters = bad_usernames.lock().unwrap();
   2202        let exact_filters = bad_exact_usernames.lock().unwrap();
   2203        for (_, name) in &current_users.guests {
   2204            if !previous.guests.iter().any(|(_, n)| n == name) {
   2205                if exact_filters.iter().any(|f| f == name)
   2206                    || filters.iter().any(|f| name.to_lowercase().contains(&f.to_lowercase()))
   2207                {
   2208                    let _ = tx.send(PostType::Kick(String::new(), name.clone()));
   2209                }
   2210            }
   2211        }
   2212    }
   2213    {
   2214        let messages = messages.lock().unwrap();
   2215        process_new_messages(
   2216            &new_messages,
   2217            &messages,
   2218            datetime_fmt,
   2219            members_tag,
   2220            username,
   2221            should_notify,
   2222            &current_users,
   2223            tx,
   2224            bad_usernames,
   2225            bad_exact_usernames,
   2226            bad_messages,
   2227            allowlist,
   2228        );
   2229        // Build messages vector. Tag deleted messages.
   2230        update_messages(new_messages, messages, datetime_fmt);
   2231        // Notify new messages has arrived.
   2232        // This ensure that we redraw the messages on the screen right away.
   2233        // Otherwise, the screen would not redraw until a keyboard event occurs.
   2234        messages_updated_tx.send(()).unwrap();
   2235    }
   2236    {
   2237        let mut u = users.lock().unwrap();
   2238        *u = current_users;
   2239    }
   2240    Ok(())
   2241 }
   2242 
   2243 fn process_new_messages(
   2244    new_messages: &Vec<Message>,
   2245    messages: &MutexGuard<Vec<Message>>,
   2246    datetime_fmt: &str,
   2247    members_tag: &str,
   2248    username: &str,
   2249    should_notify: &mut bool,
   2250    users: &Users,
   2251    tx: &crossbeam_channel::Sender<PostType>,
   2252    bad_usernames: &Arc<Mutex<Vec<String>>>,
   2253    bad_exact_usernames: &Arc<Mutex<Vec<String>>>,
   2254    bad_messages: &Arc<Mutex<Vec<String>>>,
   2255    allowlist: &Arc<Mutex<Vec<String>>>,
   2256 ) {
   2257    if let Some(last_known_msg) = messages.first() {
   2258        let last_known_msg_parsed_dt = parse_date(&last_known_msg.date, datetime_fmt);
   2259        let filtered = new_messages.iter().filter(|new_msg| {
   2260            last_known_msg_parsed_dt <= parse_date(&new_msg.date, datetime_fmt)
   2261                && !(new_msg.date == last_known_msg.date && last_known_msg.text == new_msg.text)
   2262        });
   2263        for new_msg in filtered {
   2264            log_chat_message(new_msg);
   2265            if let Some((from, to_opt, msg)) = get_message(&new_msg.text, members_tag) {
   2266                // Notify when tagged
   2267                if msg.contains(format!("@{}", &username).as_str()) {
   2268                    *should_notify = true;
   2269                }
   2270                if let Some(ref to) = to_opt {
   2271                    if to == username && msg != "!up" {
   2272                        *should_notify = true;
   2273                    }
   2274                }
   2275 
   2276                // Remote moderation handling
   2277                let is_member_or_staff = users.members.iter().any(|(_, n)| n == &from)
   2278                    || users.staff.iter().any(|(_, n)| n == &from)
   2279                    || users.admin.iter().any(|(_, n)| n == &from);
   2280                let allowed_guest = {
   2281                    let list = allowlist.lock().unwrap();
   2282                    list.contains(&from)
   2283                };
   2284                let directed_to_me = to_opt.as_ref().map(|t| t == username).unwrap_or(false);
   2285                let via_members = new_msg.text.text().starts_with(members_tag);
   2286                let has_permission = is_member_or_staff || allowed_guest;
   2287                if msg.starts_with("#kick ") || msg.starts_with("#ban ") {
   2288                    if has_permission && (directed_to_me || via_members) {
   2289                        if let Some(target) = msg.strip_prefix("#kick ") {
   2290                            let user = target.trim().trim_start_matches('@');
   2291                            if !user.is_empty() {
   2292                                let _ = tx.send(PostType::Kick(String::new(), user.to_owned()));
   2293                            }
   2294                        } else if let Some(target) = msg.strip_prefix("#ban ") {
   2295                            let user = target.trim().trim_start_matches('@');
   2296                            if !user.is_empty() {
   2297                                let _ = tx.send(PostType::Kick(String::new(), user.to_owned()));
   2298                                let mut f = bad_usernames.lock().unwrap();
   2299                                f.push(user.to_owned());
   2300                            }
   2301                        }
   2302                    } else if directed_to_me && !has_permission {
   2303                        let msg = "You don't have permission to do that.".to_owned();
   2304                        let _ = tx.send(PostType::Post(msg, Some(from.clone())));
   2305                    }
   2306                }
   2307 
   2308                let is_guest = users.guests.iter().any(|(_, n)| n == &from);
   2309                if from != username && is_guest {
   2310                    let bad_name = {
   2311                        let filters = bad_usernames.lock().unwrap();
   2312                        filters.iter().any(|f| from.to_lowercase().contains(&f.to_lowercase()))
   2313                    };
   2314                    let bad_name_exact = {
   2315                        let filters = bad_exact_usernames.lock().unwrap();
   2316                        filters.iter().any(|f| f == &from)
   2317                    };
   2318                    let bad_msg = {
   2319                        let filters = bad_messages.lock().unwrap();
   2320                        filters.iter().any(|f| msg.to_lowercase().contains(&f.to_lowercase()))
   2321                    };
   2322 
   2323                    if bad_name_exact || bad_name || bad_msg {
   2324                        let _ = tx.send(PostType::Kick(String::new(), from.clone()));
   2325                    } else {
   2326                        let res = score_message(&msg);
   2327                        if let Some(act) = action_from_score(res.score) {
   2328                            match act {
   2329                                Action::Warn => {
   2330                                    if to_opt.is_none() {
   2331                                        let reason = res
   2332                                            .reason
   2333                                            .map(|r| r.description())
   2334                                            .unwrap_or("breaking the rules");
   2335                                        let warn = format!(
   2336                                            "@{username} - @{from}'s message was flagged for {reason}."
   2337                                        );
   2338                                        let _ = tx.send(PostType::Post(warn, Some("0".to_owned())));
   2339                                    }
   2340                                }
   2341                                Action::Kick => {
   2342                                    let _ = tx.send(PostType::Kick(String::new(), from.clone()));
   2343                                }
   2344                                Action::Ban => {
   2345                                    let _ = tx.send(PostType::Kick(String::new(), from.clone()));
   2346                                    let mut f = bad_usernames.lock().unwrap();
   2347                                    f.push(from.clone());
   2348                                }
   2349                            }
   2350                        }
   2351                    }
   2352                }
   2353            }
   2354        }
   2355    }
   2356 }
   2357 
   2358 fn update_messages(
   2359    new_messages: Vec<Message>,
   2360    mut messages: MutexGuard<Vec<Message>>,
   2361    datetime_fmt: &str,
   2362 ) {
   2363    let mut old_msg_ptr = 0;
   2364    for new_msg in new_messages.into_iter() {
   2365        loop {
   2366            if let Some(old_msg) = messages.get_mut(old_msg_ptr) {
   2367                let new_parsed_dt = parse_date(&new_msg.date, datetime_fmt);
   2368                let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   2369                if new_parsed_dt < parsed_dt {
   2370                    old_msg.deleted = true;
   2371                    old_msg_ptr += 1;
   2372                    continue;
   2373                }
   2374                if new_parsed_dt == parsed_dt {
   2375                    if old_msg.text != new_msg.text {
   2376                        let mut found = false;
   2377                        let mut x = 0;
   2378                        loop {
   2379                            x += 1;
   2380                            if let Some(old_msg) = messages.get(old_msg_ptr + x) {
   2381                                let parsed_dt = parse_date(&old_msg.date, datetime_fmt);
   2382                                if new_parsed_dt == parsed_dt {
   2383                                    if old_msg.text == new_msg.text {
   2384                                        found = true;
   2385                                        break;
   2386                                    }
   2387                                    continue;
   2388                                }
   2389                            }
   2390                            break;
   2391                        }
   2392                        if !found {
   2393                            messages.insert(old_msg_ptr, new_msg);
   2394                            old_msg_ptr += 1;
   2395                        }
   2396                    }
   2397                    old_msg_ptr += 1;
   2398                    break;
   2399                }
   2400            }
   2401            messages.insert(old_msg_ptr, new_msg);
   2402            old_msg_ptr += 1;
   2403            break;
   2404        }
   2405    }
   2406    messages.truncate(1000);
   2407 }
   2408 
   2409 fn log_chat_message(msg: &Message) {
   2410    if let Ok(path) = confy::get_configuration_file_path("bhcli", None) {
   2411        if let Some(dir) = path.parent() {
   2412            let log_path = dir.join("chat-log.txt");
   2413            if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(log_path) {
   2414                let _ = writeln!(f, "{} - {}", msg.date, msg.text.text());
   2415            }
   2416        }
   2417    }
   2418 }
   2419 
   2420 fn delete_message(
   2421    client: &Client,
   2422    full_url: &str,
   2423    params: &mut Vec<(&str, String)>,
   2424    date: String,
   2425    text: String,
   2426 ) -> anyhow::Result<()> {
   2427    params.extend(vec![
   2428        ("action", "admin".to_owned()),
   2429        ("do", "clean".to_owned()),
   2430        ("what", "choose".to_owned()),
   2431    ]);
   2432    let clean_resp_txt = client.post(full_url).form(&params).send()?.text()?;
   2433    let doc = Document::from(clean_resp_txt.as_str());
   2434    let nc = doc
   2435        .find(Attr("name", "nc"))
   2436        .next()
   2437        .context("nc not found")?;
   2438    let nc_value = nc.attr("value").context("nc value not found")?.to_owned();
   2439    let msgs = extract_messages(&doc)?;
   2440    if let Some(msg) = msgs
   2441        .iter()
   2442        .find(|m| m.date == date && m.text.text() == text)
   2443    {
   2444        let msg_id = msg.id.context("msg id not found")?;
   2445        params.extend(vec![
   2446            ("nc", nc_value.to_owned()),
   2447            ("what", "selected".to_owned()),
   2448            ("mid[]", format!("{}", msg_id)),
   2449        ]);
   2450        client.post(full_url).form(&params).send()?;
   2451    }
   2452    Ok(())
   2453 }
   2454 
   2455 impl ChatClient {
   2456    fn new(params: Params) -> Self {
   2457        // println!("session[2026] : {:?}",params.session);
   2458        let mut c = new_default_le_chat_php_client(params.clone());
   2459        c.config.url = params.url.unwrap_or(
   2460            "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion/index.php"
   2461                .to_owned(),
   2462        );
   2463        c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned());
   2464        c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned());
   2465        c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned());
   2466        c.config.keepalive_send_to = params.keepalive_send_to.unwrap_or("0".to_owned());
   2467        // c.session = params.session;
   2468        Self {
   2469            le_chat_php_client: c,
   2470        }
   2471    }
   2472 
   2473    fn run_forever(&mut self) {
   2474        self.le_chat_php_client.run_forever();
   2475    }
   2476 }
   2477 
   2478 fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient {
   2479    let (color_tx, color_rx) = crossbeam_channel::unbounded();
   2480    let (tx, rx) = crossbeam_channel::unbounded();
   2481    let session = params.session.clone();
   2482    // println!("session[2050] : {:?}",params.session);
   2483    LeChatPHPClient {
   2484        base_client: BaseClient {
   2485            username: params.username,
   2486            password: params.password,
   2487        },
   2488        max_login_retry: params.max_login_retry,
   2489        guest_color: params.guest_color,
   2490        // session: params.session,
   2491        session,
   2492        last_key_event: None,
   2493        client: params.client,
   2494        manual_captcha: params.manual_captcha,
   2495        sxiv: params.sxiv,
   2496        refresh_rate: params.refresh_rate,
   2497        config: LeChatPHPConfig::new_black_hat_chat_config(),
   2498        is_muted: Arc::new(Mutex::new(false)),
   2499        show_sys: false,
   2500        display_guest_view: false,
   2501        display_member_view: false,
   2502        display_hidden_msgs: false,
   2503        tx,
   2504        rx: Arc::new(Mutex::new(rx)),
   2505        color_tx,
   2506        color_rx: Arc::new(Mutex::new(color_rx)),
   2507        bad_username_filters: Arc::new(Mutex::new(params.bad_usernames)),
   2508        bad_exact_username_filters: Arc::new(Mutex::new(params.bad_exact_usernames)),
   2509        bad_message_filters: Arc::new(Mutex::new(params.bad_messages)),
   2510        allowlist: Arc::new(Mutex::new(params.allowlist)),
   2511    }
   2512 }
   2513 
   2514 struct ChatClient {
   2515    le_chat_php_client: LeChatPHPClient,
   2516 }
   2517 
   2518 #[derive(Debug, Clone)]
   2519 struct Params {
   2520    url: Option<String>,
   2521    page_php: Option<String>,
   2522    datetime_fmt: Option<String>,
   2523    members_tag: Option<String>,
   2524    username: String,
   2525    password: String,
   2526    guest_color: String,
   2527    client: Client,
   2528    manual_captcha: bool,
   2529    sxiv: bool,
   2530    refresh_rate: u64,
   2531    max_login_retry: isize,
   2532    keepalive_send_to: Option<String>,
   2533    session: Option<String>,
   2534    bad_usernames: Vec<String>,
   2535    bad_exact_usernames: Vec<String>,
   2536    bad_messages: Vec<String>,
   2537    allowlist: Vec<String>,
   2538 }
   2539 
   2540 #[derive(Clone)]
   2541 enum ExitSignal {
   2542    Terminate,
   2543    NeedLogin,
   2544 }
   2545 struct Sig {
   2546    tx: crossbeam_channel::Sender<ExitSignal>,
   2547    rx: crossbeam_channel::Receiver<ExitSignal>,
   2548    nb_rx: usize,
   2549 }
   2550 
   2551 impl Sig {
   2552    fn new() -> Self {
   2553        let (tx, rx) = crossbeam_channel::unbounded();
   2554        let nb_rx = 0;
   2555        Self { tx, rx, nb_rx }
   2556    }
   2557 
   2558    fn clone(&mut self) -> crossbeam_channel::Receiver<ExitSignal> {
   2559        self.nb_rx += 1;
   2560        self.rx.clone()
   2561    }
   2562 
   2563    fn signal(&self, signal: &ExitSignal) {
   2564        for _ in 0..self.nb_rx {
   2565            self.tx.send(signal.clone()).unwrap();
   2566        }
   2567    }
   2568 }
   2569 
   2570 fn trim_newline(s: &mut String) {
   2571    if s.ends_with('\n') {
   2572        s.pop();
   2573        if s.ends_with('\r') {
   2574            s.pop();
   2575        }
   2576    }
   2577 }
   2578 
   2579 fn replace_newline_escape(s: &str) -> String {
   2580    s.replace("\\n", "\n")
   2581 }
   2582 
   2583 fn get_guest_color(wanted: Option<String>) -> String {
   2584    match wanted.as_deref() {
   2585        Some("beige") => "F5F5DC",
   2586        Some("blue-violet") => "8A2BE2",
   2587        Some("brown") => "A52A2A",
   2588        Some("cyan") => "00FFFF",
   2589        Some("sky-blue") => "00BFFF",
   2590        Some("gold") => "FFD700",
   2591        Some("gray") => "808080",
   2592        Some("green") => "008000",
   2593        Some("hot-pink") => "FF69B4",
   2594        Some("light-blue") => "ADD8E6",
   2595        Some("light-green") => "90EE90",
   2596        Some("lime-green") => "32CD32",
   2597        Some("magenta") => "FF00FF",
   2598        Some("olive") => "808000",
   2599        Some("orange") => "FFA500",
   2600        Some("orange-red") => "FF4500",
   2601        Some("red") => "FF0000",
   2602        Some("royal-blue") => "4169E1",
   2603        Some("see-green") => "2E8B57",
   2604        Some("sienna") => "A0522D",
   2605        Some("silver") => "C0C0C0",
   2606        Some("tan") => "D2B48C",
   2607        Some("teal") => "008080",
   2608        Some("violet") => "EE82EE",
   2609        Some("white") => "FFFFFF",
   2610        Some("yellow") => "FFFF00",
   2611        Some("yellow-green") => "9ACD32",
   2612        Some(other) => COLOR1_RGX
   2613            .captures(other)
   2614            .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())),
   2615        None => "",
   2616    }
   2617    .to_owned()
   2618 }
   2619 
   2620 fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client {
   2621    let ua = "Dasho's Black Hat Chat Client v0.1";
   2622    let mut builder = reqwest::blocking::ClientBuilder::new()
   2623        .redirect(Policy::none())
   2624        .cookie_store(true)
   2625        .user_agent(ua);
   2626    if !no_proxy {
   2627        let proxy = reqwest::Proxy::all(socks_proxy_url).unwrap();
   2628        builder = builder.proxy(proxy);
   2629    }
   2630    builder.build().unwrap()
   2631 }
   2632 
   2633 fn ask_username(username: Option<String>) -> String {
   2634    username.unwrap_or_else(|| {
   2635        print!("username: ");
   2636        let mut username_input = String::new();
   2637        io::stdout().flush().unwrap();
   2638        io::stdin().read_line(&mut username_input).unwrap();
   2639        trim_newline(&mut username_input);
   2640        username_input
   2641    })
   2642 }
   2643 
   2644 fn ask_password(password: Option<String>) -> String {
   2645    password.unwrap_or_else(|| rpassword::prompt_password("Password: ").unwrap())
   2646 }
   2647 
   2648 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
   2649 #[serde(rename_all = "camelCase")]
   2650 pub struct DkfNotifierResp {
   2651    #[serde(rename = "NewMessageSound")]
   2652    pub new_message_sound: bool,
   2653    #[serde(rename = "TaggedSound")]
   2654    pub tagged_sound: bool,
   2655    #[serde(rename = "PmSound")]
   2656    pub pm_sound: bool,
   2657    #[serde(rename = "InboxCount")]
   2658    pub inbox_count: i64,
   2659    #[serde(rename = "LastMessageCreatedAt")]
   2660    pub last_message_created_at: String,
   2661 }
   2662 
   2663 fn start_dkf_notifier(client: &Client, dkf_api_key: &str) {
   2664    let client = client.clone();
   2665    let dkf_api_key = dkf_api_key.to_owned();
   2666    let mut last_known_date = Utc::now();
   2667    thread::spawn(move || loop {
   2668        let (_stream, stream_handle) = OutputStream::try_default().unwrap();
   2669        let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
   2670 
   2671        let params: Vec<(&str, String)> = vec![(
   2672            "last_known_date",
   2673            last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
   2674        )];
   2675        let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL);
   2676        if let Ok(resp) = client
   2677            .post(right_url)
   2678            .form(&params)
   2679            .header("DKF_API_KEY", &dkf_api_key)
   2680            .send()
   2681        {
   2682            if let Ok(txt) = resp.text() {
   2683                if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) {
   2684                    if v.pm_sound || v.tagged_sound {
   2685                        stream_handle.play_raw(source.convert_samples()).unwrap();
   2686                    }
   2687                    last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at)
   2688                        .unwrap()
   2689                        .with_timezone(&Utc);
   2690                }
   2691            }
   2692        }
   2693        thread::sleep(Duration::from_secs(5));
   2694    });
   2695 }
   2696 
   2697 // Start thread that looks for new emails on DNMX every minutes.
   2698 fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) {
   2699    let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)];
   2700    let login_url = format!("{}/src/redirect.php", DNMX_URL);
   2701    client.post(login_url).form(&params).send().unwrap();
   2702 
   2703    let client_clone = client.clone();
   2704    thread::spawn(move || loop {
   2705        let (_stream, stream_handle) = OutputStream::try_default().unwrap();
   2706        let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap();
   2707 
   2708        let right_url = format!("{}/src/right_main.php", DNMX_URL);
   2709        if let Ok(resp) = client_clone.get(right_url).send() {
   2710            let mut nb_mails = 0;
   2711            let doc = Document::from(resp.text().unwrap().as_str());
   2712            if let Some(table) = doc.find(Name("table")).nth(7) {
   2713                table.find(Name("tr")).skip(1).for_each(|n| {
   2714                    if let Some(td) = n.find(Name("td")).nth(2) {
   2715                        if td.find(Name("b")).nth(0).is_some() {
   2716                            nb_mails += 1;
   2717                        }
   2718                    }
   2719                });
   2720            }
   2721            if nb_mails > 0 {
   2722                log::error!("{} new mails", nb_mails);
   2723                stream_handle.play_raw(source.convert_samples()).unwrap();
   2724            }
   2725        }
   2726        thread::sleep(Duration::from_secs(60));
   2727    });
   2728 }
   2729 
   2730 //Strange
   2731 #[derive(Debug, Deserialize)]
   2732 struct Commands {
   2733    commands: HashMap<String, String>,
   2734 }
   2735 
   2736 impl Default for Commands {
   2737    fn default() -> Self {
   2738        Commands {
   2739            commands: HashMap::new(), // Initialize commands with empty HashMap
   2740        }
   2741    }
   2742 }
   2743 
   2744 // Strange
   2745 // Function to read the configuration file and parse it
   2746 fn read_commands_file(file_path: &str) -> Result<Commands, Box<dyn std::error::Error>> {
   2747    // Read the contents of the file
   2748    let commands_content = std::fs::read_to_string(file_path)?;
   2749    // log::error!("Read file contents: {}", commands_content);
   2750    // Deserialize the contents into a Commands struct
   2751    let commands: Commands = toml::from_str(&commands_content)?;
   2752    // log::error!(
   2753    //     "Deserialized file contents into Commands struct: {:?}",
   2754    //     commands
   2755    // );
   2756 
   2757    Ok(commands)
   2758 }
   2759 
   2760 fn main() -> anyhow::Result<()> {
   2761    let mut opts: Opts = Opts::parse();
   2762    // println!("Parsed Session: {:?}", opts.session);
   2763 
   2764 
   2765    // Configs file
   2766    if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) {
   2767        println!("Config path: {:?}", config_path);
   2768    }
   2769    if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) {
   2770        if opts.dkf_api_key.is_none() {
   2771            opts.dkf_api_key = cfg.dkf_api_key;
   2772        }
   2773        if let Some(default_profile) = cfg.profiles.get(&opts.profile) {
   2774            if opts.username.is_none() {
   2775                opts.username = Some(default_profile.username.clone());
   2776                opts.password = Some(default_profile.password.clone());
   2777            }
   2778        }
   2779        let bad_usernames = cfg.bad_usernames.clone();
   2780        let bad_exact_usernames = cfg.bad_exact_usernames.clone();
   2781        let bad_messages = cfg.bad_messages.clone();
   2782        let allowlist_cfg = cfg.allowlist.clone();
   2783        opts.bad_usernames = Some(bad_usernames);
   2784        opts.bad_exact_usernames = Some(bad_exact_usernames);
   2785        opts.bad_messages = Some(bad_messages);
   2786        opts.allowlist = Some(allowlist_cfg);
   2787    }
   2788 
   2789    let logfile = FileAppender::builder()
   2790        .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}")))
   2791        .build("bhcli.log")?;
   2792 
   2793    let config = log4rs::config::Config::builder()
   2794        .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile)))
   2795        .build(
   2796            log4rs::config::Root::builder()
   2797                .appender("logfile")
   2798                .build(LevelFilter::Error),
   2799        )?;
   2800 
   2801    log4rs::init_config(config)?;
   2802 
   2803    let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy);
   2804 
   2805    // If dnmx username is set, start mail notifier thread
   2806    if let Some(dnmx_username) = opts.dnmx_username {
   2807        start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap())
   2808    }
   2809 
   2810    if let Some(dkf_api_key) = &opts.dkf_api_key {
   2811        start_dkf_notifier(&client, dkf_api_key);
   2812    }
   2813 
   2814    let guest_color = get_guest_color(opts.guest_color);
   2815    let username = ask_username(opts.username);
   2816    let password = ask_password(opts.password);
   2817 
   2818    let params = Params {
   2819        url: opts.url,
   2820        page_php: opts.page_php,
   2821        datetime_fmt: opts.datetime_fmt,
   2822        members_tag: opts.members_tag,
   2823        username,
   2824        password,
   2825        guest_color,
   2826        client: client.clone(),
   2827        manual_captcha: opts.manual_captcha,
   2828        sxiv: opts.sxiv,
   2829        refresh_rate: opts.refresh_rate,
   2830        max_login_retry: opts.max_login_retry,
   2831        keepalive_send_to: opts.keepalive_send_to,
   2832        session: opts.session.clone(),
   2833        bad_usernames: opts.bad_usernames.unwrap_or_default(),
   2834        bad_exact_usernames: opts.bad_exact_usernames.unwrap_or_default(),
   2835        bad_messages: opts.bad_messages.unwrap_or_default(),
   2836        allowlist: opts.allowlist.unwrap_or_default(),
   2837    };
   2838    // println!("Session[2378]: {:?}", opts.session);
   2839 
   2840 
   2841    ChatClient::new(params).run_forever();
   2842 
   2843    Ok(())
   2844 }
   2845 
   2846 #[derive(Debug, Clone)]
   2847 enum PostType {
   2848    Post(String, Option<String>),   // Message, SendTo
   2849    Kick(String, String),           // Message, Username
   2850    Upload(String, String, String), // FilePath, SendTo, Message
   2851    DeleteLast,                     // DeleteLast
   2852    DeleteAll,                      // DeleteAll
   2853    NewNickname(String),            // NewUsername
   2854    NewColor(String),               // NewColor
   2855    Profile(String, String),        // NewColor, NewUsername
   2856    Ignore(String),                 // Username
   2857    Unignore(String),               // Username
   2858    Clean(String, String),          // Clean message
   2859 }
   2860 
   2861 // Get username of other user (or ours if it's the only one)
   2862 fn get_username(own_username: &str, root: &StyledText, members_tag: &str) -> Option<String> {
   2863    match get_message(root, members_tag) {
   2864        Some((from, Some(to), _)) => {
   2865            if from == own_username {
   2866                return Some(to);
   2867            }
   2868            return Some(from);
   2869        }
   2870        Some((from, None, _)) => {
   2871            return Some(from);
   2872        }
   2873        _ => return None,
   2874    }
   2875 }
   2876 
   2877 // Extract "from"/"to"/"message content" from a "StyledText"
   2878 fn get_message(root: &StyledText, members_tag: &str) -> Option<(String, Option<String>, String)> {
   2879    if let StyledText::Styled(_, children) = root {
   2880        let msg = children.get(0)?.text();
   2881        match children.get(children.len() - 1)? {
   2882            StyledText::Styled(_, children) => {
   2883                let from = match children.get(children.len() - 1)? {
   2884                    StyledText::Text(t) => t.to_owned(),
   2885                    _ => return None,
   2886                };
   2887                return Some((from, None, msg));
   2888            }
   2889            StyledText::Text(t) => {
   2890                if t == &members_tag {
   2891                    let from = match children.get(children.len() - 2)? {
   2892                        StyledText::Styled(_, children) => {
   2893                            match children.get(children.len() - 1)? {
   2894                                StyledText::Text(t) => t.to_owned(),
   2895                                _ => return None,
   2896                            }
   2897                        }
   2898                        _ => return None,
   2899                    };
   2900                    return Some((from, None, msg));
   2901                } else if t == "[" {
   2902                    let from = match children.get(children.len() - 2)? {
   2903                        StyledText::Styled(_, children) => {
   2904                            match children.get(children.len() - 1)? {
   2905                                StyledText::Text(t) => t.to_owned(),
   2906                                _ => return None,
   2907                            }
   2908                        }
   2909                        _ => return None,
   2910                    };
   2911                    let to = match children.get(2)? {
   2912                        StyledText::Styled(_, children) => {
   2913                            match children.get(children.len() - 1)? {
   2914                                StyledText::Text(t) => Some(t.to_owned()),
   2915                                _ => return None,
   2916                            }
   2917                        }
   2918                        _ => return None,
   2919                    };
   2920                    return Some((from, to, msg));
   2921                }
   2922            }
   2923            _ => return None,
   2924        }
   2925    }
   2926    return None;
   2927 }
   2928 
   2929 #[derive(Debug, PartialEq, Clone)]
   2930 enum MessageType {
   2931    UserMsg,
   2932    SysMsg,
   2933 }
   2934 
   2935 #[derive(Debug, PartialEq, Clone)]
   2936 struct Message {
   2937    id: Option<usize>,
   2938    typ: MessageType,
   2939    date: String,
   2940    upload_link: Option<String>,
   2941    text: StyledText,
   2942    deleted: bool, // Either or not a message was deleted on the chat
   2943    hide: bool,    // Either ot not to hide a specific message
   2944 }
   2945 
   2946 impl Message {
   2947    fn new(
   2948        id: Option<usize>,
   2949        typ: MessageType,
   2950        date: String,
   2951        upload_link: Option<String>,
   2952        text: StyledText,
   2953    ) -> Self {
   2954        Self {
   2955            id,
   2956            typ,
   2957            date,
   2958            upload_link,
   2959            text,
   2960            deleted: false,
   2961            hide: false,
   2962        }
   2963    }
   2964 }
   2965 
   2966 #[derive(Debug, PartialEq, Clone)]
   2967 enum StyledText {
   2968    Styled(tuiColor, Vec<StyledText>),
   2969    Text(String),
   2970    None,
   2971 }
   2972 
   2973 impl StyledText {
   2974    fn walk<F>(&self, mut clb: F)
   2975    where
   2976        F: FnMut(&StyledText),
   2977    {
   2978        let mut v: Vec<&StyledText> = vec![self];
   2979        loop {
   2980            if let Some(e) = v.pop() {
   2981                clb(e);
   2982                if let StyledText::Styled(_, children) = e {
   2983                    v.extend(children);
   2984                }
   2985                continue;
   2986            }
   2987            break;
   2988        }
   2989    }
   2990 
   2991    fn text(&self) -> String {
   2992        let mut s = String::new();
   2993        self.walk(|n| {
   2994            if let StyledText::Text(t) = n {
   2995                s += t;
   2996            }
   2997        });
   2998        s
   2999    }
   3000 
   3001    // Return a vector of each text parts & what color it should be
   3002    fn colored_text(&self) -> Vec<(tuiColor, String)> {
   3003        let mut out: Vec<(tuiColor, String)> = vec![];
   3004        let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)];
   3005        loop {
   3006            if let Some((el_color, e)) = v.pop() {
   3007                match e {
   3008                    StyledText::Styled(tui_color, children) => {
   3009                        for child in children {
   3010                            v.push((*tui_color, child));
   3011                        }
   3012                    }
   3013                    StyledText::Text(t) => {
   3014                        out.push((el_color, t.to_owned()));
   3015                    }
   3016                    StyledText::None => {}
   3017                }
   3018                continue;
   3019            }
   3020            break;
   3021        }
   3022        out
   3023    }
   3024 }
   3025 
   3026 fn parse_color(color_str: &str) -> tuiColor {
   3027    let mut color = tuiColor::White;
   3028    if color_str == "red" {
   3029        return tuiColor::Red;
   3030    }
   3031    if let Ok(rgb) = Rgb::from_hex_str(color_str) {
   3032        color = tuiColor::Rgb(
   3033            rgb.get_red() as u8,
   3034            rgb.get_green() as u8,
   3035            rgb.get_blue() as u8,
   3036        );
   3037    }
   3038    color
   3039 }
   3040 
   3041 fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option<String>) {
   3042    match e.data() {
   3043        select::node::Data::Element(_, _) => {
   3044            let mut upload_link: Option<String> = None;
   3045            match e.name() {
   3046                Some("span") => {
   3047                    if let Some(style) = e.attr("style") {
   3048                        if let Some(captures) = COLOR_RGX.captures(style) {
   3049                            let color_match = captures.get(1).unwrap().as_str();
   3050                            color = parse_color(color_match);
   3051                        }
   3052                    }
   3053                }
   3054                Some("font") => {
   3055                    if let Some(color_str) = e.attr("color") {
   3056                        color = parse_color(color_str);
   3057                    }
   3058                }
   3059                Some("a") => {
   3060                    color = tuiColor::White;
   3061                    if let (Some("attachement"), Some(href)) = (e.attr("class"), e.attr("href")) {
   3062                        upload_link = Some(href.to_owned());
   3063                    }
   3064                }
   3065                Some("style") => {
   3066                    return (StyledText::None, None);
   3067                }
   3068                _ => {}
   3069            }
   3070            let mut children_texts: Vec<StyledText> = vec![];
   3071            let children = e.children();
   3072            for child in children {
   3073                let (st, ul) = process_node(child, color);
   3074                if ul.is_some() {
   3075                    upload_link = ul;
   3076                }
   3077                children_texts.push(st);
   3078            }
   3079            children_texts.reverse();
   3080            (StyledText::Styled(color, children_texts), upload_link)
   3081        }
   3082        select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None),
   3083        select::node::Data::Comment(_) => (StyledText::None, None),
   3084    }
   3085 }
   3086 
   3087 struct Users {
   3088    admin: Vec<(tuiColor, String)>,
   3089    staff: Vec<(tuiColor, String)>,
   3090    members: Vec<(tuiColor, String)>,
   3091    guests: Vec<(tuiColor, String)>,
   3092 }
   3093 
   3094 impl Default for Users {
   3095    fn default() -> Self {
   3096        Self {
   3097            admin: Default::default(),
   3098            staff: Default::default(),
   3099            members: Default::default(),
   3100            guests: Default::default(),
   3101        }
   3102    }
   3103 }
   3104 
   3105 impl Users {
   3106    fn all(&self) -> Vec<&(tuiColor, String)> {
   3107        let mut out = Vec::new();
   3108        out.extend(&self.admin);
   3109        out.extend(&self.staff);
   3110        out.extend(&self.members);
   3111        out.extend(&self.guests);
   3112        out
   3113    }
   3114 
   3115    // fn is_guest(&self, name: &str) -> bool {
   3116    //     self.guests.iter().find(|(_, username)| username == name).is_some()
   3117    // }
   3118 }
   3119 
   3120 fn extract_users(doc: &Document) -> Users {
   3121    let mut users = Users::default();
   3122 
   3123    if let Some(chatters) = doc.find(Attr("id", "chatters")).next() {
   3124        if let Some(tr) = chatters.find(Name("tr")).next() {
   3125            let mut th_count = 0;
   3126            for e in tr.children() {
   3127                if let select::node::Data::Element(_, _) = e.data() {
   3128                    if e.name() == Some("th") {
   3129                        th_count += 1;
   3130                        continue;
   3131                    }
   3132                    for user_span in e.find(Name("span")) {
   3133                        if let Some(user_style) = user_span.attr("style") {
   3134                            if let Some(captures) = COLOR_RGX.captures(user_style) {
   3135                                if let Some(color_match) = captures.get(1) {
   3136                                    let color = color_match.as_str().to_owned();
   3137                                    let tui_color = parse_color(&color);
   3138                                    let username = user_span.text();
   3139                                    match th_count {
   3140                                        1 => users.admin.push((tui_color, username)),
   3141                                        2 => users.staff.push((tui_color, username)),
   3142                                        3 => users.members.push((tui_color, username)),
   3143                                        4 => users.guests.push((tui_color, username)),
   3144                                        _ => {}
   3145                                    }
   3146                                }
   3147                            }
   3148                        }
   3149                    }
   3150                }
   3151            }
   3152        }
   3153    }
   3154    users
   3155 }
   3156 
   3157 fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str {
   3158    s.strip_suffix(suffix).unwrap_or(s)
   3159 }
   3160 
   3161 fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str {
   3162    s.strip_prefix(prefix).unwrap_or(s)
   3163 }
   3164 
   3165 fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> {
   3166    let msgs = doc
   3167        .find(Attr("id", "messages"))
   3168        .next()
   3169        .ok_or(anyhow!("failed to get messages div"))?
   3170        .find(Attr("class", "msg"))
   3171        .filter_map(|tag| {
   3172            let mut id: Option<usize> = None;
   3173            if let Some(checkbox) = tag.find(Name("input")).next() {
   3174                let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap();
   3175                id = Some(id_value);
   3176            }
   3177            if let Some(date_node) = tag.find(Name("small")).next() {
   3178                if let Some(msg_span) = tag.find(Name("span")).next() {
   3179                    let date = remove_suffix(&date_node.text(), " - ").to_owned();
   3180                    let typ = match msg_span.attr("class") {
   3181                        Some("usermsg") => MessageType::UserMsg,
   3182                        Some("sysmsg") => MessageType::SysMsg,
   3183                        _ => return None,
   3184                    };
   3185                    let (text, upload_link) = process_node(msg_span, tuiColor::White);
   3186                    return Some(Message::new(id, typ, date, upload_link, text));
   3187                }
   3188            }
   3189            None
   3190        })
   3191        .collect::<Vec<_>>();
   3192    Ok(msgs)
   3193 }
   3194 
   3195 fn draw_terminal_frame(
   3196    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   3197    app: &mut App,
   3198    messages: &Arc<Mutex<Vec<Message>>>,
   3199    users: &Arc<Mutex<Users>>,
   3200    username: &str,
   3201 ) {
   3202    if app.long_message.is_none() {
   3203        let hchunks = Layout::default()
   3204            .direction(Direction::Horizontal)
   3205            .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref())
   3206            .split(f.size());
   3207 
   3208        {
   3209            let chunks = Layout::default()
   3210                .direction(Direction::Vertical)
   3211                .constraints(
   3212                    [
   3213                        Constraint::Length(1),
   3214                        Constraint::Length(3),
   3215                        Constraint::Min(1),
   3216                    ]
   3217                    .as_ref(),
   3218                )
   3219                .split(hchunks[0]);
   3220 
   3221            render_help_txt(f, app, chunks[0], username);
   3222            render_textbox(f, app, chunks[1]);
   3223            render_messages(f, app, chunks[2], messages);
   3224            render_users(f, hchunks[1], users);
   3225        }
   3226    } else {
   3227        let hchunks = Layout::default()
   3228            .direction(Direction::Horizontal)
   3229            .constraints([Constraint::Min(1)])
   3230            .split(f.size());
   3231        {
   3232            render_long_message(f, app, hchunks[0]);
   3233        }
   3234    }
   3235 }
   3236 
   3237 fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiColor, String)>> {
   3238    let txt = msg_txt.text();
   3239    let wrapped = textwrap::fill(&txt, w);
   3240    let splits = wrapped.split("\n").collect::<Vec<&str>>();
   3241    let mut new_lines: Vec<Vec<(tuiColor, String)>> = Vec::new();
   3242    let mut ctxt = msg_txt.colored_text();
   3243    ctxt.reverse();
   3244    let mut ptr = 0;
   3245    let mut split_idx = 0;
   3246    let mut line: Vec<(tuiColor, String)> = Vec::new();
   3247    let mut first_in_line = true;
   3248    loop {
   3249        if let Some((color, mut txt)) = ctxt.pop() {
   3250            txt = txt.replace("\n", "");
   3251            if let Some(split) = splits.get(split_idx) {
   3252                if let Some(chr) = txt.chars().next() {
   3253                    if chr == ' ' && first_in_line {
   3254                        let skipped: String = txt.chars().skip(1).collect();
   3255                        txt = skipped;
   3256                    }
   3257                }
   3258 
   3259                let remain = split.len() - ptr;
   3260                if txt.len() <= remain {
   3261                    ptr += txt.len();
   3262                    line.push((color, txt));
   3263                    first_in_line = false;
   3264                } else {
   3265                    //line.push((color, txt[0..remain].to_owned()));
   3266                    if let Some(valid_slice) = txt.get(0..remain) {
   3267                        line.push((color, valid_slice.to_owned()));
   3268                    } else {
   3269                        let valid_remain = txt.char_indices()
   3270                            .take_while(|&(i, _)| i < remain)
   3271                            .last()
   3272                            .map(|(i, _)| i)
   3273                            .unwrap_or(txt.len());  
   3274 
   3275                        line.push((color, txt[..valid_remain].to_owned()));
   3276                    }
   3277 
   3278                    new_lines.push(line.clone());
   3279                    line.clear();
   3280                    line.push((tuiColor::White, line_prefix.to_owned()));
   3281                    //ctxt.push((color, txt[(remain)..].to_owned()));
   3282                    if let Some(valid_slice) = txt.get(remain..) {
   3283                        ctxt.push((color, valid_slice.to_owned()));
   3284                    } else {
   3285                        let valid_remain = txt.char_indices()
   3286                            .skip_while(|&(i, _)| i < remain) // Find first valid boundary after remain
   3287                            .map(|(i, _)| i)
   3288                            .next()
   3289                            .unwrap_or(txt.len());
   3290 
   3291                        ctxt.push((color, txt[valid_remain..].to_owned()));
   3292                    }
   3293 
   3294                    ptr = 0;
   3295                    split_idx += 1;
   3296                    first_in_line = true;
   3297                }
   3298            }
   3299        } else {
   3300            new_lines.push(line);
   3301            break;
   3302        }
   3303    }
   3304    new_lines
   3305 }
   3306 
   3307 fn render_long_message(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   3308    if let Some(m) = &app.long_message {
   3309        let new_lines = gen_lines(&m.text, (r.width - 2) as usize, "");
   3310 
   3311        let mut rows = vec![];
   3312        for line in new_lines.into_iter() {
   3313            let spans_vec: Vec<Span> = line
   3314                .into_iter()
   3315                .map(|(color, txt)| Span::styled(txt, Style::default().fg(color)))
   3316                .collect();
   3317            rows.push(Spans::from(spans_vec));
   3318        }
   3319 
   3320        let messages_list_items = vec![ListItem::new(rows)];
   3321 
   3322        let messages_list = List::new(messages_list_items)
   3323            .block(Block::default().borders(Borders::ALL).title(""))
   3324            .highlight_style(
   3325                Style::default()
   3326                    .bg(tuiColor::Rgb(50, 50, 50))
   3327                    .add_modifier(Modifier::BOLD),
   3328            );
   3329 
   3330        f.render_widget(messages_list, r);
   3331    }
   3332 }
   3333 
   3334 fn render_help_txt(
   3335    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   3336    app: &mut App,
   3337    r: Rect,
   3338    curr_user: &str,
   3339 ) {
   3340    let (mut msg, style) = match app.input_mode {
   3341        InputMode::Normal => (
   3342            vec![
   3343                Span::raw("Press "),
   3344                Span::styled("q", Style::default().add_modifier(Modifier::BOLD)),
   3345                Span::raw(" to exit, "),
   3346                Span::styled("Q", Style::default().add_modifier(Modifier::BOLD)),
   3347                Span::raw(" to logout, "),
   3348                Span::styled("i", Style::default().add_modifier(Modifier::BOLD)),
   3349                Span::raw(" to start editing."),
   3350            ],
   3351            Style::default(),
   3352        ),
   3353        InputMode::Editing | InputMode::EditingErr => (
   3354            vec![
   3355                Span::raw("Press "),
   3356                Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
   3357                Span::raw(" to stop editing, "),
   3358                Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
   3359                Span::raw(" to record the message"),
   3360            ],
   3361            Style::default(),
   3362        ),
   3363        InputMode::LongMessage => (vec![], Style::default()),
   3364    };
   3365    msg.extend(vec![Span::raw(format!(" | {}", curr_user))]);
   3366    if app.is_muted {
   3367        let fg = tuiColor::Red;
   3368        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   3369        msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]);
   3370    } else {
   3371        let fg = tuiColor::LightGreen;
   3372        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   3373        msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]);
   3374    }
   3375 
   3376    //Strange
   3377    if app.display_guest_view {
   3378        let fg = tuiColor::LightGreen;
   3379        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   3380        msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   3381    } else {
   3382        let fg = tuiColor::Gray;
   3383        let style = Style::default().fg(fg);
   3384        msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]);
   3385    }
   3386 
   3387    //Strange
   3388    if app.display_member_view {
   3389        let fg = tuiColor::LightGreen;
   3390        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   3391        msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   3392    } else {
   3393        let fg = tuiColor::Gray;
   3394        let style = Style::default().fg(fg);
   3395        msg.extend(vec![Span::raw(" | "), Span::styled("M", style)]);
   3396    }
   3397 
   3398    if app.display_hidden_msgs {
   3399        let fg = tuiColor::LightGreen;
   3400        let style = Style::default().fg(fg).add_modifier(Modifier::BOLD);
   3401        msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   3402    } else {
   3403        let fg = tuiColor::Gray;
   3404        let style = Style::default().fg(fg);
   3405        msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]);
   3406    }
   3407    let mut text = Text::from(Spans::from(msg));
   3408    text.patch_style(style);
   3409    let help_message = Paragraph::new(text);
   3410    f.render_widget(help_message, r);
   3411 }
   3412 
   3413 fn render_textbox(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) {
   3414    let w = (r.width - 3) as usize;
   3415    let str = app.input.clone();
   3416    let mut input_str = str.as_str();
   3417    let mut overflow = 0;
   3418    if app.input_idx >= w {
   3419        overflow = std::cmp::max(app.input.width() - w, 0);
   3420        input_str = &str[overflow..];
   3421    }
   3422    let input = Paragraph::new(input_str)
   3423        .style(match app.input_mode {
   3424            InputMode::LongMessage => Style::default(),
   3425            InputMode::Normal => Style::default(),
   3426            InputMode::Editing => Style::default().fg(tuiColor::Yellow),
   3427            InputMode::EditingErr => Style::default().fg(tuiColor::Red),
   3428        })
   3429        .block(Block::default().borders(Borders::ALL).title("Input"));
   3430    f.render_widget(input, r);
   3431    match app.input_mode {
   3432        InputMode::LongMessage => {}
   3433        InputMode::Normal =>
   3434            // Hide the cursor. `Frame` does this by default, so we don't need to do anything here
   3435            {}
   3436 
   3437        InputMode::Editing | InputMode::EditingErr => {
   3438            // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering
   3439            f.set_cursor(
   3440                // Put cursor past the end of the input text
   3441                r.x + app.input_idx as u16 - overflow as u16 + 1,
   3442                // Move one line down, from the border to the input line
   3443                r.y + 1,
   3444            )
   3445        }
   3446    }
   3447 }
   3448 
   3449 fn render_messages(
   3450    f: &mut Frame<CrosstermBackend<io::Stdout>>,
   3451    app: &mut App,
   3452    r: Rect,
   3453    messages: &Arc<Mutex<Vec<Message>>>,
   3454 ) {
   3455    // Messages
   3456    app.items.items.clear();
   3457    let messages = messages.lock().unwrap();
   3458    let messages_list_items: Vec<ListItem> = messages
   3459        .iter()
   3460        .filter_map(|m| {
   3461            if !app.display_hidden_msgs && m.hide {
   3462                return None;
   3463            }
   3464            // Simulate a guest view (remove "PMs" and "Members chat" messages)
   3465            if app.display_guest_view {
   3466                // TODO: this is not efficient at all
   3467                let text = m.text.text();
   3468                if text.starts_with(&app.members_tag) || text.starts_with(&app.staffs_tag) {
   3469                    return None;
   3470                }
   3471                if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) {
   3472                    return None;
   3473                }
   3474            }
   3475 
   3476            // Strange
   3477            // Display only messages from members and staff
   3478            if app.display_member_view {
   3479                // In members mode, include only messages from members and staff
   3480                let text = m.text.text();
   3481                if !text.starts_with(&app.members_tag) && !text.starts_with(&app.staffs_tag) {
   3482                    return None;
   3483                }
   3484                if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) {
   3485                    return None;
   3486                }
   3487            }
   3488 
   3489            if app.filter != "" {
   3490                if !m
   3491                    .text
   3492                    .text()
   3493                    .to_lowercase()
   3494                    .contains(&app.filter.to_lowercase())
   3495                {
   3496                    return None;
   3497                }
   3498            }
   3499 
   3500            app.items.items.push(m.clone());
   3501 
   3502            let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17).as_str());
   3503 
   3504            let mut rows = vec![];
   3505            let date_style = match (m.deleted, m.hide) {
   3506                (false, true) => Style::default().fg(tuiColor::Gray),
   3507                (false, _) => Style::default().fg(tuiColor::DarkGray),
   3508                (true, _) => Style::default().fg(tuiColor::Red),
   3509            };
   3510            let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)];
   3511            let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg;
   3512            let sep = if show_sys_sep { " * " } else { " - " };
   3513            spans_vec.push(Span::raw(sep));
   3514            for (idx, line) in new_lines.into_iter().enumerate() {
   3515                // Spams can take your whole screen, so we limit to 5 lines.
   3516                if idx >= 5 {
   3517                    spans_vec.push(Span::styled(
   3518                        "                 […]",
   3519                        Style::default().fg(tuiColor::White),
   3520                    ));
   3521                    rows.push(Spans::from(spans_vec));
   3522                    break;
   3523                }
   3524                for (color, txt) in line {
   3525                    spans_vec.push(Span::styled(txt, Style::default().fg(color)));
   3526                }
   3527                rows.push(Spans::from(spans_vec.clone()));
   3528                spans_vec.clear();
   3529            }
   3530 
   3531            let style = match (m.deleted, m.hide) {
   3532                (true, _) => Style::default().bg(tuiColor::Rgb(30, 0, 0)),
   3533                (_, true) => Style::default().bg(tuiColor::Rgb(20, 20, 20)),
   3534                _ => Style::default(),
   3535            };
   3536            Some(ListItem::new(rows).style(style))
   3537        })
   3538        .collect();
   3539 
   3540    let messages_list = List::new(messages_list_items)
   3541        .block(Block::default().borders(Borders::ALL).title("Messages"))
   3542        .highlight_style(
   3543            Style::default()
   3544                .bg(tuiColor::Rgb(50, 50, 50))
   3545                .add_modifier(Modifier::BOLD),
   3546        );
   3547    f.render_stateful_widget(messages_list, r, &mut app.items.state)
   3548 }
   3549 
   3550 fn render_users(f: &mut Frame<CrosstermBackend<io::Stdout>>, r: Rect, users: &Arc<Mutex<Users>>) {
   3551    // Users lists
   3552    let users = users.lock().unwrap();
   3553    let mut users_list: Vec<ListItem> = vec![];
   3554    let mut users_types: Vec<(&Vec<(tuiColor, String)>, &str)> = Vec::new();
   3555    users_types.push((&users.admin, "-- Admin --"));
   3556    users_types.push((&users.staff, "-- Staff --"));
   3557    users_types.push((&users.members, "-- Members --"));
   3558    users_types.push((&users.guests, "-- Guests --"));
   3559    for (users, label) in users_types.into_iter() {
   3560        users_list.push(ListItem::new(Span::raw(label)));
   3561        for (tui_color, username) in users.iter() {
   3562            let span = Span::styled(username, Style::default().fg(*tui_color));
   3563            users_list.push(ListItem::new(span));
   3564        }
   3565    }
   3566    let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users"));
   3567    f.render_widget(users, r);
   3568 }
   3569 
   3570 fn random_string(n: usize) -> String {
   3571    let s: Vec<u8> = thread_rng().sample_iter(&Alphanumeric).take(n).collect();
   3572    std::str::from_utf8(&s).unwrap().to_owned()
   3573 }
   3574 
   3575 #[derive(PartialEq)]
   3576 enum InputMode {
   3577    LongMessage,
   3578    Normal,
   3579    Editing,
   3580    EditingErr,
   3581 }
   3582 
   3583 /// App holds the state of the application
   3584 struct App {
   3585    /// Current value of the input box
   3586    input: String,
   3587    input_idx: usize,
   3588    /// Current input mode
   3589    input_mode: InputMode,
   3590    is_muted: bool,
   3591    show_sys: bool,
   3592    display_guest_view: bool,
   3593    display_member_view: bool,
   3594    display_hidden_msgs: bool,
   3595    items: StatefulList<Message>,
   3596    filter: String,
   3597    members_tag: String,
   3598    staffs_tag: String,
   3599    long_message: Option<Message>,
   3600    commands: Commands,
   3601 }
   3602 
   3603 impl Default for App {
   3604    fn default() -> App {
   3605        // Read commands from the file and set them as default values
   3606        let commands = if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) {
   3607            if let Some(config_path_str) = config_path.to_str() {
   3608                match read_commands_file(config_path_str) {
   3609                    Ok(commands) => commands,
   3610                    Err(err) => {
   3611                        log::error!(
   3612                            "Failed to read commands from config file - {} :
   3613 {}",
   3614                            config_path_str,
   3615                            err
   3616                        );
   3617                        Commands {
   3618                            commands: HashMap::new(),
   3619                        }
   3620                    }
   3621                }
   3622            } else {
   3623                log::error!("Failed to convert configuration file path to string.");
   3624                Commands {
   3625                    commands: HashMap::new(),
   3626                }
   3627            }
   3628        } else {
   3629            log::error!("Failed to get configuration file path.");
   3630            Commands {
   3631                commands: HashMap::new(),
   3632            }
   3633        };
   3634 
   3635        App {
   3636            input: String::new(),
   3637            input_idx: 0,
   3638            input_mode: InputMode::Normal,
   3639            is_muted: false,
   3640            show_sys: false,
   3641            display_guest_view: false,
   3642            display_member_view: false,
   3643            display_hidden_msgs: false,
   3644            items: StatefulList::new(),
   3645            filter: "".to_owned(),
   3646            members_tag: "".to_owned(),
   3647            staffs_tag: "".to_owned(),
   3648            long_message: None,
   3649            commands,
   3650        }
   3651    }
   3652 }
   3653 
   3654 impl App {
   3655    fn update_filter(&mut self) {
   3656        if let Some(captures) = FIND_RGX.captures(&self.input) {
   3657            // Find
   3658            self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned();
   3659        }
   3660    }
   3661 
   3662    fn clear_filter(&mut self) {
   3663        if FIND_RGX.is_match(&self.input) {
   3664            self.filter = "".to_owned();
   3665            self.input = "".to_owned();
   3666            self.input_idx = 0;
   3667        }
   3668    }
   3669 }
   3670 
   3671 pub enum Event<I> {
   3672    Input(I),
   3673    Tick,
   3674    Terminate,
   3675    NeedLogin,
   3676 }
   3677 
   3678 /// A small event handler that wrap termion input and tick events. Each event
   3679 /// type is handled in its own thread and returned to a common `Receiver`
   3680 struct Events {
   3681    messages_updated_rx: crossbeam_channel::Receiver<()>,
   3682    exit_rx: crossbeam_channel::Receiver<ExitSignal>,
   3683    rx: crossbeam_channel::Receiver<Event<CEvent>>,
   3684 }
   3685 
   3686 #[derive(Debug, Clone)]
   3687 struct Config {
   3688    pub exit_rx: crossbeam_channel::Receiver<ExitSignal>,
   3689    pub messages_updated_rx: crossbeam_channel::Receiver<()>,
   3690    pub tick_rate: Duration,
   3691 }
   3692 
   3693 impl Events {
   3694    fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) {
   3695        let (tx, rx) = crossbeam_channel::unbounded();
   3696        let tick_rate = config.tick_rate;
   3697        let exit_rx = config.exit_rx;
   3698        let messages_updated_rx = config.messages_updated_rx;
   3699        let exit_rx1 = exit_rx.clone();
   3700        let thread_handle = thread::spawn(move || {
   3701            let mut last_tick = Instant::now();
   3702            loop {
   3703                // poll for tick rate duration, if no events, sent tick event.
   3704                let timeout = tick_rate
   3705                    .checked_sub(last_tick.elapsed())
   3706                    .unwrap_or_else(|| Duration::from_secs(0));
   3707                if event::poll(timeout).unwrap() {
   3708                    let evt = event::read().unwrap();
   3709                    match evt {
   3710                        CEvent::FocusGained => {}
   3711                        CEvent::FocusLost => {}
   3712                        CEvent::Paste(_) => {}
   3713                        CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(),
   3714                        CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(),
   3715                        CEvent::Mouse(mouse_event) => {
   3716                            match mouse_event.kind {
   3717                                MouseEventKind::ScrollDown
   3718                                | MouseEventKind::ScrollUp
   3719                                | MouseEventKind::Down(_) => {
   3720                                    tx.send(Event::Input(evt)).unwrap();
   3721                                }
   3722                                _ => {}
   3723                            };
   3724                        }
   3725                    };
   3726                }
   3727                if last_tick.elapsed() >= tick_rate {
   3728                    select! {
   3729                        recv(&exit_rx1) -> _ => break,
   3730                        default => {},
   3731                    }
   3732                    last_tick = Instant::now();
   3733                }
   3734            }
   3735        });
   3736        (
   3737            Events {
   3738                rx,
   3739                exit_rx,
   3740                messages_updated_rx,
   3741            },
   3742            thread_handle,
   3743        )
   3744    }
   3745 
   3746    fn next(&self) -> Result<Event<CEvent>, crossbeam_channel::RecvError> {
   3747        select! {
   3748            recv(&self.rx) -> evt => evt,
   3749            recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick),
   3750            recv(&self.exit_rx) -> v => match v {
   3751                Ok(ExitSignal::Terminate) => Ok(Event::Terminate),
   3752                Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin),
   3753                Err(_) => Ok(Event::Terminate),
   3754            },
   3755        }
   3756    }
   3757 }
   3758 
   3759 #[cfg(test)]
   3760 mod tests {
   3761    use super::*;
   3762 
   3763    #[test]
   3764    fn gen_lines_test() {
   3765        let txt = StyledText::Styled(
   3766            tuiColor::White,
   3767            vec![
   3768                StyledText::Styled(
   3769                    tuiColor::Rgb(255, 255, 255),
   3770                    vec![
   3771                        StyledText::Text(" prmdbba pwuv💓".to_owned()),
   3772                        StyledText::Styled(
   3773                            tuiColor::Rgb(255, 255, 255),
   3774                            vec![StyledText::Styled(
   3775                                tuiColor::Rgb(0, 255, 0),
   3776                                vec![StyledText::Text("PMW".to_owned())],
   3777                            )],
   3778                        ),
   3779                        StyledText::Styled(
   3780                            tuiColor::Rgb(255, 255, 255),
   3781                            vec![StyledText::Styled(
   3782                                tuiColor::Rgb(255, 255, 255),
   3783                                vec![StyledText::Text("A".to_owned())],
   3784                            )],
   3785                        ),
   3786                        StyledText::Styled(
   3787                            tuiColor::Rgb(255, 255, 255),
   3788                            vec![StyledText::Styled(
   3789                                tuiColor::Rgb(0, 255, 0),
   3790                                vec![StyledText::Text("XOS".to_owned())],
   3791                            )],
   3792                        ),
   3793                        StyledText::Text(
   3794                            "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(),
   3795                        ),
   3796                    ],
   3797                ),
   3798                StyledText::Text(" - ".to_owned()),
   3799                StyledText::Styled(
   3800                    tuiColor::Rgb(255, 255, 255),
   3801                    vec![StyledText::Text("rytxvgs".to_owned())],
   3802                ),
   3803            ],
   3804        );
   3805        let lines = gen_lines(&txt, 71, "");
   3806        assert_eq!(lines.len(), 2);
   3807    }
   3808 }