main.rs (109868B)
1 mod util; 2 mod lechatphp; 3 mod bhc; 4 5 use anyhow::{anyhow, Context}; 6 use log; 7 use log::LevelFilter; 8 use log4rs::append::file::FileAppender; 9 use log4rs::encode::pattern::PatternEncoder; 10 use log4rs; 11 use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; 12 use clap::Parser; 13 use clipboard::ClipboardContext; 14 use clipboard::ClipboardProvider; 15 use colors_transform::{Color, Rgb}; 16 use crossbeam_channel::{self, after, select}; 17 use crossterm::event; 18 use crossterm::event::Event as CEvent; 19 use crossterm::event::{MouseEvent, MouseEventKind}; 20 use crossterm::{ 21 event::{DisableMouseCapture, EnableMouseCapture, KeyCode, KeyEvent, KeyModifiers}, 22 execute, 23 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, 24 }; 25 use lazy_static::lazy_static; 26 use linkify::LinkFinder; 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 rodio::{source::Source, Decoder, OutputStream}; 33 use select::document::Document; 34 use select::predicate::{Attr, Name}; 35 use serde_derive::{Deserialize, Serialize}; 36 use std::collections::HashMap; 37 use std::io::Cursor; 38 use std::io::{self, Write}; 39 use std::sync::{Arc, MutexGuard}; 40 use std::sync::Mutex; 41 use std::thread; 42 use std::time::Duration; 43 use std::time::Instant; 44 use reqwest; 45 use reqwest::redirect::Policy; 46 use textwrap; 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 crate::lechatphp::LoginErr; 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 BHCLI_BLOG_URL: &str = 69 "http://dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/bhcli"; 70 const BAN_IMPOSTERS: bool = true; 71 const SERVER_DOWN_500_ERR: &str = "500 Internal Server Error, server down"; 72 const SERVER_DOWN_ERR: &str = "502 Bad Gateway, server down"; 73 const KICKED_ERR: &str = "You have been kicked"; 74 const REG_ERR: &str = "This nickname is a registered member"; 75 const NICKNAME_ERR: &str = "Invalid nickname"; 76 const CAPTCHA_WG_ERR: &str = "Wrong Captcha"; 77 const CAPTCHA_FAILED_SOLVE_ERR: &str = "Failed solve captcha"; 78 const CAPTCHA_USED_ERR: &str = "Captcha already used or timed out"; 79 const UNKNOWN_ERR: &str = "Unknown error"; 80 const N0TR1V: &str = "n0tr1v"; 81 const STUXNET: &str = "STUXNET"; 82 const FAGGOT: &str = "faggot"; 83 const DNMX_URL: &str = "http://hxuzjtocnzvv5g2rtg2bhwkcbupmk7rclb6lly3fo4tvqkk5oyrv3nid.onion"; 84 85 lazy_static! { 86 static ref META_REFRESH_RGX: Regex = Regex::new(r#"url='([^']+)'"#).unwrap(); 87 static ref SESSION_RGX: Regex = Regex::new(r#"session=([^&]+)"#).unwrap(); 88 static ref COLOR_RGX: Regex = Regex::new(r#"color:\s*([#\w]+)\s*;"#).unwrap(); 89 static ref COLOR1_RGX: Regex = Regex::new(r#"^#([0-9A-Fa-f]{6})$"#).unwrap(); 90 static ref PM_RGX: Regex = Regex::new(r#"^/pm ([^\s]+) (.*)"#).unwrap(); 91 static ref KICK_RGX: Regex = Regex::new(r#"^/(?:kick|k) ([^\s]+)\s?(.*)"#).unwrap(); 92 static ref UNKICK_RGX: Regex = Regex::new(r#"^/(?:unkick|uk) (.+)$"#).unwrap(); 93 static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap(); 94 static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap(); 95 static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap(); 96 static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap(); 97 static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap(); 98 static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap(); 99 static ref NEW_COLOR_RGX: Regex = Regex::new(r#"^/color\s(.*)$"#).unwrap(); 100 } 101 102 fn default_empty_str() -> String { 103 "".to_string() 104 } 105 106 #[derive(Debug, Serialize, Deserialize)] 107 struct Profile { 108 username: String, 109 password: String, 110 #[serde(default = "default_empty_str")] 111 url: String, 112 #[serde(default = "default_empty_str")] 113 date_format: String, 114 #[serde(default = "default_empty_str")] 115 page_php: String, 116 #[serde(default = "default_empty_str")] 117 members_tag: String, 118 #[serde(default = "default_empty_str")] 119 keepalive_send_to: String, 120 } 121 122 #[derive(Default, Debug, Serialize, Deserialize)] 123 struct MyConfig { 124 dkf_api_key: Option<String>, 125 profiles: HashMap<String, Profile>, 126 } 127 128 #[derive(Parser)] 129 #[command(name = "bhcli")] 130 #[command(author = "n0tr1v <n0tr1v@protonmail.com>")] 131 #[command(version = "0.0.1")] 132 struct Opts { 133 #[arg(long, env = "DKF_API_KEY")] 134 dkf_api_key: Option<String>, 135 #[arg(short, long, env = "BHC_USERNAME")] 136 username: Option<String>, 137 #[arg(short, long, env = "BHC_PASSWORD")] 138 password: Option<String>, 139 #[arg(short, long, env = "BHC_MANUAL_CAPTCHA")] 140 manual_captcha: bool, 141 #[arg(short, long, env = "BHC_GUEST_COLOR")] 142 guest_color: Option<String>, 143 #[arg(short, long, env = "BHC_REFRESH_RATE", default_value = "5")] 144 refresh_rate: u64, 145 #[arg(long, env = "BHC_MAX_LOGIN_RETRY", default_value = "5")] 146 max_login_retry: isize, 147 #[arg(long)] 148 url: Option<String>, 149 #[arg(long)] 150 page_php: Option<String>, 151 #[arg(long)] 152 datetime_fmt: Option<String>, 153 #[arg(long)] 154 members_tag: Option<String>, 155 #[arg(short, long)] 156 dan: bool, 157 #[arg( 158 short, 159 long, 160 env = "BHC_PROXY_URL", 161 default_value = "socks5h://127.0.0.1:9050" 162 )] 163 socks_proxy_url: String, 164 #[arg(long)] 165 no_proxy: bool, 166 #[arg(long, env = "DNMX_USERNAME")] 167 dnmx_username: Option<String>, 168 #[arg(long, env = "DNMX_PASSWORD")] 169 dnmx_password: Option<String>, 170 #[arg(short = 'c', long, default_value = "default")] 171 profile: String, 172 } 173 174 struct LeChatPHPConfig { 175 url: String, 176 datetime_fmt: String, 177 page_php: String, 178 keepalive_send_to: Option<String>, 179 members_tag: String, 180 staffs_tag: String, 181 } 182 183 impl LeChatPHPConfig { 184 fn new_black_hat_chat_config() -> Self { 185 Self { 186 url: "http://blkhatjxlrvc5aevqzz5t6kxldayog6jlx5h7glnu44euzongl4fh5ad.onion".to_owned(), 187 datetime_fmt: "%m-%d %H:%M:%S".to_owned(), 188 page_php: "index.php".to_owned(), 189 keepalive_send_to: Some("0".to_owned()), 190 members_tag: "[M] ".to_owned(), 191 staffs_tag: "[Staff] ".to_owned(), 192 } 193 } 194 195 fn new_dans_chat_config() -> Self { 196 Self { 197 url: "http://danschat356lctri3zavzh6fbxg2a7lo6z3etgkctzzpspewu7zdsaqd.onion".to_owned(), 198 datetime_fmt: "%d-%m %H:%M:%S".to_owned(), 199 page_php: "chat.php".to_owned(), 200 keepalive_send_to: None, 201 members_tag: "[Members] ".to_owned(), 202 staffs_tag: "[Staff] ".to_owned(), 203 } 204 } 205 } 206 207 struct BaseClient { 208 username: String, 209 password: String, 210 } 211 212 struct LeChatPHPClient { 213 #[allow(unused)] 214 chat_type: ClientType, 215 base_client: BaseClient, 216 guest_color: String, 217 client: Client, 218 session: Option<String>, 219 config: LeChatPHPConfig, 220 last_key_event: Option<KeyCode>, 221 manual_captcha: bool, 222 refresh_rate: u64, 223 max_login_retry: isize, 224 225 is_muted: Arc<Mutex<bool>>, 226 show_sys: bool, 227 display_guest_view: bool, 228 display_hidden_msgs: bool, 229 tx: crossbeam_channel::Sender<PostType>, 230 rx: Arc<Mutex<crossbeam_channel::Receiver<PostType>>>, 231 232 color_tx: crossbeam_channel::Sender<()>, 233 color_rx: Arc<Mutex<crossbeam_channel::Receiver<()>>>, 234 } 235 236 impl LeChatPHPClient { 237 fn run_forever(&mut self) { 238 let max_retry = self.max_login_retry; 239 let mut attempt = 0; 240 loop { 241 match self.login() { 242 Err(e) => { 243 match e { 244 LoginErr::KickedErr | LoginErr::RegErr | LoginErr::NicknameErr | LoginErr::UnknownErr => { 245 log::error!("{}", e); 246 break; 247 }, 248 LoginErr::CaptchaFailedSolveErr => { 249 log::error!("{}", e); 250 continue; 251 }, 252 LoginErr::CaptchaWgErr | LoginErr::CaptchaUsedErr => {}, 253 LoginErr::ServerDownErr | LoginErr::ServerDown500Err => { 254 log::error!("{}", e); 255 }, 256 LoginErr::Reqwest(err) => { 257 if err.is_connect() { 258 log::error!("{}\nIs tor proxy enabled ?", err); 259 break; 260 } else if err.is_timeout() { 261 log::error!("timeout: {}", err); 262 } else { 263 log::error!("{}", err); 264 } 265 }, 266 } 267 }, 268 Ok(()) => { 269 attempt = 0; 270 match self.get_msgs() { 271 Ok(ExitSignal::NeedLogin) => {} 272 Ok(ExitSignal::Terminate) => return, 273 Err(e) => log::error!("{:?}", e), 274 } 275 }, 276 } 277 attempt += 1; 278 if max_retry > 0 && attempt > max_retry { 279 break; 280 } 281 self.session = None; 282 let retry_in = Duration::from_secs(2); 283 let mut msg = format!("retry login in {:?}, attempt: {}", retry_in, attempt); 284 if max_retry > 0 { 285 msg += &format!("/{}", max_retry); 286 } 287 println!("{}", msg); 288 thread::sleep(retry_in); 289 } 290 } 291 292 fn start_keepalive_thread( 293 &self, 294 exit_rx: crossbeam_channel::Receiver<ExitSignal>, 295 last_post_rx: crossbeam_channel::Receiver<()>, 296 ) -> thread::JoinHandle<()> { 297 let tx = self.tx.clone(); 298 let send_to = self.config.keepalive_send_to.clone(); 299 thread::spawn(move || loop { 300 let clb = || { 301 tx.send(PostType::Post("<keepalive>".to_owned(), send_to.clone())).unwrap(); 302 tx.send(PostType::DeleteLast).unwrap(); 303 }; 304 let timeout = after(Duration::from_secs(60 * 75)); 305 select! { 306 // Whenever we send a message to chat server, 307 // we will receive a message on this channel 308 // and reset the timer for next keepalive. 309 recv(&last_post_rx) -> _ => {}, 310 recv(&exit_rx) -> _ => return, 311 recv(&timeout) -> _ => clb(), 312 } 313 }) 314 } 315 316 // Thread that POST to chat server 317 fn start_post_msg_thread( 318 &self, 319 exit_rx: crossbeam_channel::Receiver<ExitSignal>, 320 last_post_tx: crossbeam_channel::Sender<()>, 321 ) -> thread::JoinHandle<()> { 322 let client = self.client.clone(); 323 let rx = Arc::clone(&self.rx); 324 let full_url = format!("{}/{}", &self.config.url, &self.config.page_php); 325 let session = self.session.clone().unwrap(); 326 let url = format!("{}?action=post&session={}", &full_url, &session); 327 thread::spawn(move || loop { 328 // select! macro fucks all the LSP, therefore the code gymnastic here 329 let clb = |v: Result<PostType, crossbeam_channel::RecvError>| { 330 match v { 331 Ok(post_type_recv) => post_msg(&client, post_type_recv, &full_url, session.clone(), &url, &last_post_tx), 332 Err(_) => return, 333 } 334 }; 335 let rx = rx.lock().unwrap(); 336 select! { 337 recv(&exit_rx) -> _ => return, 338 recv(&rx) -> v => clb(v), 339 } 340 }) 341 } 342 343 // Thread that update messages every "refresh_rate" 344 fn start_get_msgs_thread( 345 &self, 346 sig: &Arc<Mutex<Sig>>, 347 messages: &Arc<Mutex<Vec<Message>>>, 348 users: &Arc<Mutex<Users>>, 349 messages_updated_tx: crossbeam_channel::Sender<()>, 350 ) -> thread::JoinHandle<()> { 351 let client = self.client.clone(); 352 let messages = Arc::clone(&messages); 353 let users = Arc::clone(&users); 354 let session = self.session.clone().unwrap(); 355 let username = self.base_client.username.clone(); 356 let refresh_rate = self.refresh_rate.clone(); 357 let base_url = self.config.url.clone(); 358 let page_php = self.config.page_php.clone(); 359 let datetime_fmt = self.config.datetime_fmt.clone(); 360 let is_muted = Arc::clone(&self.is_muted); 361 let exit_rx = sig.lock().unwrap().clone(); 362 let sig = Arc::clone(sig); 363 let tx = self.tx.clone(); 364 let members_tag = self.config.members_tag.clone(); 365 thread::spawn(move || loop { 366 let (_stream, stream_handle) = OutputStream::try_default().unwrap(); 367 let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); 368 let mut should_notify = false; 369 370 if let Err(err) = get_msgs(&client, &base_url, &page_php, &session, &username, &tx, &users, &sig, 371 &messages_updated_tx, &members_tag, &datetime_fmt, &messages, &mut should_notify) { 372 log::error!("{}", err); 373 }; 374 375 let muted = { *is_muted.lock().unwrap() }; 376 if should_notify && !muted { 377 if let Err(err) = stream_handle.play_raw(source.convert_samples()) { 378 log::error!("{}", err); 379 } 380 } 381 382 let timeout = after(Duration::from_secs(refresh_rate)); 383 select! { 384 recv(&exit_rx) -> _ => return, 385 recv(&timeout) -> _ => {}, 386 } 387 }) 388 } 389 390 fn get_msgs(&mut self) -> anyhow::Result<ExitSignal> { 391 let terminate_signal: ExitSignal; 392 393 let messages: Arc<Mutex<Vec<Message>>> = Arc::new(Mutex::new(Vec::new())); 394 let users: Arc<Mutex<Users>> = Arc::new(Mutex::new(Users::default())); 395 396 // Create default app state 397 let mut app = App::default(); 398 399 // Each threads gets a clone of the receiver. 400 // When someone calls ".signal", all threads receive it, 401 // and knows that they have to terminate. 402 let sig = Arc::new(Mutex::new(Sig::new())); 403 404 let (messages_updated_tx, messages_updated_rx) = crossbeam_channel::unbounded(); 405 let (last_post_tx, last_post_rx) = crossbeam_channel::unbounded(); 406 407 let h1 = self.start_keepalive_thread(sig.lock().unwrap().clone(), last_post_rx); 408 let h2 = self.start_post_msg_thread(sig.lock().unwrap().clone(), last_post_tx); 409 let h3 = self.start_get_msgs_thread(&sig, &messages, &users, messages_updated_tx); 410 411 // Terminal initialization 412 let mut stdout = io::stdout(); 413 enable_raw_mode().unwrap(); 414 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; 415 let backend = CrosstermBackend::new(stdout); 416 let mut terminal = Terminal::new(backend)?; 417 418 // Setup event handlers 419 let (events, h4) = Events::with_config(Config { 420 messages_updated_rx, 421 exit_rx: sig.lock().unwrap().clone(), 422 tick_rate: Duration::from_millis(250), 423 }); 424 425 loop { 426 app.is_muted = *self.is_muted.lock().unwrap(); 427 app.show_sys = self.show_sys; 428 app.display_guest_view = self.display_guest_view; 429 app.display_hidden_msgs = self.display_hidden_msgs; 430 app.members_tag = self.config.members_tag.clone(); 431 app.staffs_tag = self.config.staffs_tag.clone(); 432 433 // Draw UI 434 terminal.draw(|f| { 435 draw_terminal_frame(f, &mut app, &messages, &users, &self.base_client.username); 436 })?; 437 438 // Handle input 439 match self.handle_input(&events, &mut app, &messages, &users) { 440 Err(ExitSignal::Terminate) => { 441 terminate_signal = ExitSignal::Terminate; 442 sig.lock().unwrap().signal(&terminate_signal); 443 break; 444 } 445 Err(ExitSignal::NeedLogin) => { 446 terminate_signal = ExitSignal::NeedLogin; 447 sig.lock().unwrap().signal(&terminate_signal); 448 break; 449 } 450 Ok(_) => continue, 451 }; 452 } 453 454 // Cleanup before leaving 455 disable_raw_mode()?; 456 execute!( 457 terminal.backend_mut(), 458 LeaveAlternateScreen, 459 DisableMouseCapture 460 )?; 461 terminal.show_cursor()?; 462 terminal.clear()?; 463 terminal.set_cursor(0, 0)?; 464 465 h1.join().unwrap(); 466 h2.join().unwrap(); 467 h3.join().unwrap(); 468 h4.join().unwrap(); 469 470 Ok(terminate_signal) 471 } 472 473 fn post_msg(&self, post_type: PostType) -> anyhow::Result<()> { 474 self.tx.send(post_type)?; 475 Ok(()) 476 } 477 478 fn login(&mut self) -> Result<(), LoginErr> { 479 // If we provided a session, skip login process 480 if self.session.is_some() { 481 return Ok(()); 482 } 483 self.session = Some(lechatphp::login( 484 &self.client, &self.config.url, &self.config.page_php, &self.base_client.username, 485 &self.base_client.password, &self.guest_color, self.manual_captcha)?); 486 Ok(()) 487 } 488 489 fn logout(&mut self) -> anyhow::Result<()> { 490 if let Some(session) = &self.session { 491 lechatphp::logout(&self.client, &self.config.url, &self.config.page_php, &session)?; 492 self.session = None; 493 } 494 Ok(()) 495 } 496 497 fn start_cycle(&self, color_only: bool) { 498 let username = self.base_client.username.clone(); 499 let tx = self.tx.clone(); 500 let color_rx = Arc::clone(&self.color_rx); 501 thread::spawn(move || { 502 let mut idx = 0; 503 let colors = vec![ 504 "#ff3366", "#ff6633", "#FFCC33", "#33FF66", "#33FFCC", "#33CCFF", "#3366FF", 505 "#6633FF", "#CC33FF", "#efefef", 506 ]; 507 loop { 508 let color_rx = color_rx.lock().unwrap(); 509 let timeout = after(Duration::from_millis(5200)); 510 select! { 511 recv(&color_rx) -> _ => break, 512 recv(&timeout) -> _ => {} 513 } 514 idx = (idx + 1) % colors.len(); 515 let color = colors[idx].to_owned(); 516 if !color_only { 517 let name = format!("{}{}", username, random_string(14)); 518 log::error!("New name : {}", name); 519 tx.send(PostType::Profile(color, name)).unwrap(); 520 } else { 521 tx.send(PostType::NewColor(color)).unwrap(); 522 } 523 // tx.send(PostType::Post("!up".to_owned(), Some(username.clone()))) 524 // .unwrap(); 525 // tx.send(PostType::DeleteLast).unwrap(); 526 } 527 let msg = PostType::Profile("#90ee90".to_owned(), username); 528 tx.send(msg).unwrap(); 529 }); 530 } 531 532 fn handle_input(&mut self, events: &Events, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>) -> Result<(), ExitSignal> { 533 match events.next() { 534 Ok(Event::NeedLogin) => return Err(ExitSignal::NeedLogin), 535 Ok(Event::Terminate) => return Err(ExitSignal::Terminate), 536 Ok(Event::Input(evt)) => self.handle_event(app, messages, users, evt), 537 _ => Ok(()), 538 } 539 } 540 541 fn handle_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, event: event::Event) -> Result<(), ExitSignal> { 542 match event { 543 event::Event::Resize(_cols, _rows) => Ok(()), 544 event::Event::FocusGained => Ok(()), 545 event::Event::FocusLost => Ok(()), 546 event::Event::Paste(_) => Ok(()), 547 event::Event::Key(key_event) => self.handle_key_event(app, messages, users, key_event), 548 event::Event::Mouse(mouse_event) => self.handle_mouse_event(app, mouse_event), 549 } 550 } 551 552 fn handle_key_event(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>, users: &Arc<Mutex<Users>>, key_event: KeyEvent) -> Result<(), ExitSignal> { 553 if app.input_mode != InputMode::Normal { 554 self.last_key_event = None; 555 } 556 match app.input_mode { 557 InputMode::LongMessage => self.handle_long_message_mode_key_event(app, key_event, messages), 558 InputMode::Normal => self.handle_normal_mode_key_event(app, key_event, messages), 559 InputMode::Editing | 560 InputMode::EditingErr => self.handle_editing_mode_key_event(app, key_event, users), 561 } 562 } 563 564 fn handle_long_message_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> Result<(), ExitSignal> { 565 match key_event { 566 KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } | 567 KeyEvent { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, .. } => self.handle_long_message_mode_key_event_esc(app), 568 KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_long_message_mode_key_event_ctrl_d(app, messages), 569 _ => {} 570 } 571 Ok(()) 572 } 573 574 fn handle_normal_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, messages: &Arc<Mutex<Vec<Message>>>) -> Result<(), ExitSignal> { 575 match key_event { 576 KeyEvent { code: KeyCode::Char('/'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_slash(app), 577 KeyEvent { code: KeyCode::Char('j'), modifiers: KeyModifiers::NONE, .. } | 578 KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_down(app), 579 KeyEvent { code: KeyCode::Char('k'), modifiers: KeyModifiers::NONE, .. } | 580 KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_up(app), 581 KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_enter(app, messages), 582 KeyEvent { code: KeyCode::Backspace, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_backspace(app, messages), 583 KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::NONE, .. } | 584 KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_normal_mode_key_event_yank(app), 585 KeyEvent { code: KeyCode::Char('Y'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_yank_link(app), 586 KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_debug(app), 587 KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_debug2(app), 588 KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_toggle_mute(), 589 KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_toggle_sys(), 590 KeyEvent { code: KeyCode::Char('G'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_toggle_guest_view(), 591 KeyEvent { code: KeyCode::Char('H'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_toggle_hidden(), 592 KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_input_mode(app), 593 KeyEvent { code: KeyCode::Char('Q'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_logout()?, 594 KeyEvent { code: KeyCode::Char('q'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_exit()?, 595 KeyEvent { code: KeyCode::Char('t'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_tag(app), 596 KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_pm(app), 597 KeyEvent { code: KeyCode::Char('k'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_normal_mode_key_event_kick(app), 598 KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::CONTROL, .. } | 599 KeyEvent { code: KeyCode::PageUp, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_page_up(app), 600 KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::CONTROL, .. } | 601 KeyEvent { code: KeyCode::PageDown, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_page_down(app), 602 KeyEvent { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_esc(app), 603 KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::SHIFT, .. } => self.handle_normal_mode_key_event_shift_u(app), 604 KeyEvent { code: KeyCode::Char('g'), modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_g(app), 605 _ => {} 606 } 607 self.last_key_event = Some(key_event.code); 608 Ok(()) 609 } 610 611 fn handle_editing_mode_key_event(&mut self, app: &mut App, key_event: KeyEvent, users: &Arc<Mutex<Users>>) -> Result<(), ExitSignal> { 612 app.input_mode = InputMode::Editing; 613 match key_event { 614 KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_enter(app)?, 615 KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_tab(app, users), 616 KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_c(app), 617 KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_a(app), 618 KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_e(app), 619 KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_f(app), 620 KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_b(app), 621 KeyEvent { code: KeyCode::Char('v'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_editing_mode_key_event_ctrl_v(app), 622 KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_left(app), 623 KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_right(app), 624 KeyEvent { code: KeyCode::Down, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_down(app), 625 KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, .. } | 626 KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::SHIFT, .. } => self.handle_editing_mode_key_event_shift_c(app, c), 627 KeyEvent { code: KeyCode::Backspace, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_backspace(app), 628 KeyEvent { code: KeyCode::Delete, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_delete(app), 629 KeyEvent { code: KeyCode::Esc, modifiers: KeyModifiers::NONE, .. } => self.handle_editing_mode_key_event_esc(app), 630 _ => {} 631 } 632 Ok(()) 633 } 634 635 fn handle_long_message_mode_key_event_esc(&mut self, app: &mut App) { 636 app.long_message = None; 637 app.input_mode = InputMode::Normal; 638 } 639 640 fn handle_long_message_mode_key_event_ctrl_d(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>) { 641 if let Some(idx) = app.items.state.selected() { 642 if let Some(item) = app.items.items.get(idx) { 643 self.post_msg(PostType::Clean(item.date.to_owned(), item.text.text())).unwrap(); 644 let mut messages = messages.lock().unwrap(); 645 if let Some(pos) = messages.iter().position(|m| m.date == item.date && m.text == item.text) { 646 messages[pos].hide = !messages[pos].hide; 647 } 648 app.long_message = None; 649 app.input_mode = InputMode::Normal; 650 } 651 } 652 } 653 654 fn handle_normal_mode_key_event_up(&mut self, app: &mut App) { 655 app.items.previous() 656 } 657 658 fn handle_normal_mode_key_event_down(&mut self, app: &mut App) { 659 app.items.next() 660 } 661 662 fn handle_normal_mode_key_event_slash(&mut self, app: &mut App) { 663 app.items.unselect(); 664 app.input = "/".to_owned(); 665 app.input_idx = app.input.width(); 666 app.input_mode = InputMode::Editing; 667 } 668 669 fn handle_normal_mode_key_event_enter(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>) { 670 if let Some(idx) = app.items.state.selected() { 671 if let Some(item) = app.items.items.get(idx) { 672 // If we have a filter, <enter> will "jump" to the message 673 if app.filter != "" { 674 let idx = messages 675 .lock().unwrap() 676 .iter() 677 .enumerate() 678 .find(|(_, e)| e.date == item.date) 679 .map(|(i, _)| i); 680 app.clear_filter(); 681 app.items.state.select(idx); 682 return; 683 } 684 app.long_message = Some(item.clone()); 685 app.input_mode = InputMode::LongMessage; 686 } 687 } 688 } 689 690 fn handle_normal_mode_key_event_backspace(&mut self, app: &mut App, messages: &Arc<Mutex<Vec<Message>>>) { 691 if let Some(idx) = app.items.state.selected() { 692 if let Some(item) = app.items.items.get(idx) { 693 let mut messages = messages.lock().unwrap(); 694 if let Some(pos) = messages.iter().position(|m| m.date == item.date && m.text == item.text) { 695 if item.deleted { 696 messages.remove(pos); 697 } else { 698 messages[pos].hide = !messages[pos].hide; 699 } 700 } 701 } 702 } 703 } 704 705 fn handle_normal_mode_key_event_yank(&mut self, app: &mut App) { 706 if let Some(idx) = app.items.state.selected() { 707 if let Some(item) = app.items.items.get(idx) { 708 if let Some(upload_link) = &item.upload_link { 709 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 710 let mut out = format!("{}{}", self.config.url, upload_link); 711 if let Some((_, _, msg)) = 712 get_message(&item.text, &self.config.members_tag) 713 { 714 out = format!("{} {}", msg, out); 715 } 716 ctx.set_contents(out).unwrap(); 717 } else if let Some((_, _, msg)) = 718 get_message(&item.text, &self.config.members_tag) 719 { 720 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 721 ctx.set_contents(msg).unwrap(); 722 } 723 } 724 } 725 } 726 727 fn handle_normal_mode_key_event_yank_link(&mut self, app: &mut App) { 728 if let Some(idx) = app.items.state.selected() { 729 if let Some(item) = app.items.items.get(idx) { 730 if let Some(upload_link) = &item.upload_link { 731 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 732 let out = format!("{}{}", self.config.url, upload_link); 733 ctx.set_contents(out).unwrap(); 734 } else if let Some((_, _, msg)) = get_message(&item.text, &self.config.members_tag) { 735 let finder = LinkFinder::new(); 736 let links: Vec<_> = finder.links(msg.as_str()).collect(); 737 if let Some(link) = links.get(0) { 738 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 739 ctx.set_contents(link.as_str().to_owned()).unwrap(); 740 } 741 } 742 } 743 } 744 } 745 746 fn handle_normal_mode_key_event_debug(&mut self, app: &mut App) { 747 if let Some(idx) = app.items.state.selected() { 748 if let Some(item) = app.items.items.get(idx) { 749 log::error!("{:?}", item.text.text()); 750 } 751 } 752 } 753 754 fn handle_normal_mode_key_event_debug2(&mut self, app: &mut App) { 755 if let Some(idx) = app.items.state.selected() { 756 if let Some(item) = app.items.items.get(idx) { 757 log::error!("{:?} {:?}", item.text, item.upload_link); 758 } 759 } 760 } 761 762 fn handle_normal_mode_key_event_toggle_mute(&mut self) { 763 let mut is_muted = self.is_muted.lock().unwrap(); 764 *is_muted = !*is_muted; 765 } 766 767 fn handle_normal_mode_key_event_toggle_sys(&mut self) { 768 self.show_sys = !self.show_sys; 769 } 770 771 fn handle_normal_mode_key_event_toggle_guest_view(&mut self) { 772 self.display_guest_view = !self.display_guest_view; 773 } 774 775 776 fn handle_normal_mode_key_event_g(&mut self, app: &mut App) { 777 // Handle "gg" key combination 778 if self.last_key_event == Some(KeyCode::Char('g')) { 779 app.items.select_top(); 780 self.last_key_event = None; 781 } 782 } 783 784 fn handle_normal_mode_key_event_toggle_hidden(&mut self) { 785 self.display_hidden_msgs = !self.display_hidden_msgs; 786 } 787 788 fn handle_normal_mode_key_event_input_mode(&mut self, app: &mut App) { 789 app.input_mode = InputMode::Editing; 790 app.items.unselect(); 791 } 792 793 fn handle_normal_mode_key_event_logout(&mut self) -> Result<(), ExitSignal> { 794 self.logout().unwrap(); 795 return Err(ExitSignal::Terminate); 796 } 797 798 fn handle_normal_mode_key_event_exit(&mut self) -> Result<(), ExitSignal> { 799 return Err(ExitSignal::Terminate); 800 } 801 802 fn handle_normal_mode_key_event_tag(&mut self, app: &mut App) { 803 if let Some(idx) = app.items.state.selected() { 804 let text = &app.items.items.get(idx).unwrap().text; 805 if let Some(username) = get_username( 806 &self.base_client.username, 807 &text, 808 &self.config.members_tag, 809 ) { 810 if text.text().starts_with(&app.members_tag) { 811 app.input = format!("/m @{} ", username); 812 } else { 813 app.input = format!("@{} ", username); 814 } 815 app.input_idx = app.input.width(); 816 app.input_mode = InputMode::Editing; 817 app.items.unselect(); 818 } 819 } 820 } 821 822 fn handle_normal_mode_key_event_pm(&mut self, app: &mut App) { 823 if let Some(idx) = app.items.state.selected() { 824 if let Some(username) = get_username( 825 &self.base_client.username, 826 &app.items.items.get(idx).unwrap().text, 827 &self.config.members_tag, 828 ) { 829 app.input = format!("/pm {} ", username); 830 app.input_idx = app.input.width(); 831 app.input_mode = InputMode::Editing; 832 app.items.unselect(); 833 } 834 } 835 } 836 837 fn handle_normal_mode_key_event_kick(&mut self, app: &mut App) { 838 if let Some(idx) = app.items.state.selected() { 839 if let Some(username) = get_username( 840 &self.base_client.username, 841 &app.items.items.get(idx).unwrap().text, 842 &self.config.members_tag, 843 ) { 844 app.input = format!("/kick {} ", username); 845 app.input_idx = app.input.width(); 846 app.input_mode = InputMode::Editing; 847 app.items.unselect(); 848 } 849 } 850 } 851 852 fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) { 853 if let Some(idx) = app.items.state.selected() { 854 app.items.state.select(idx.checked_sub(10).or(Some(0))); 855 } else { 856 app.items.next(); 857 } 858 } 859 860 fn handle_normal_mode_key_event_page_down(&mut self, app: &mut App) { 861 if let Some(idx) = app.items.state.selected() { 862 let wanted_idx = idx + 10; 863 let max_idx = app.items.items.len() - 1; 864 let new_idx = std::cmp::min(wanted_idx, max_idx); 865 app.items.state.select(Some(new_idx)); 866 } else { 867 app.items.next(); 868 } 869 } 870 871 fn handle_normal_mode_key_event_esc(&mut self, app: &mut App) { 872 app.items.unselect(); 873 } 874 875 fn handle_normal_mode_key_event_shift_u(&mut self, app: &mut App) { 876 app.items.state.select(Some(0)); 877 } 878 879 fn handle_editing_mode_key_event_enter(&mut self, app: &mut App) -> Result<(), ExitSignal> { 880 if FIND_RGX.is_match(&app.input) { 881 return Ok(()); 882 } 883 884 let input: String = app.input.drain(..).collect(); 885 app.input_idx = 0; 886 if input == "/dl" { 887 // Delete last message 888 self.post_msg(PostType::DeleteLast).unwrap(); 889 } else if let Some(captures) = DLX_RGX.captures(&input) { 890 // Delete the last X messages 891 let x: usize = captures.get(1).unwrap().as_str().parse().unwrap(); 892 for _ in 0..x { 893 self.post_msg(PostType::DeleteLast).unwrap(); 894 } 895 } else if input == "/dall" { 896 // Delete all messages 897 self.post_msg(PostType::DeleteAll).unwrap(); 898 } else if input == "/cycles" { 899 self.color_tx.send(()).unwrap(); 900 } else if input == "/cycle1" { 901 self.start_cycle(true); 902 } else if input == "/cycle2" { 903 self.start_cycle(false); 904 } else if input == "/kall" { 905 // Kick all guests 906 let username = "s _".to_owned(); 907 let msg = "".to_owned(); 908 self.post_msg(PostType::Kick(msg, username)).unwrap(); 909 } else if input.starts_with("/m ") { 910 // Send message to "members" section 911 let msg = remove_prefix(&input, "/m ").to_owned(); 912 let to = Some(SEND_TO_MEMBERS.to_owned()); 913 self.post_msg(PostType::Post(msg, to)).unwrap(); 914 app.input = "/m ".to_owned(); 915 app.input_idx = app.input.width() 916 } else if input.starts_with("/a ") { 917 // Send message to "admin" section 918 let msg = remove_prefix(&input, "/a ").to_owned(); 919 let to = Some(SEND_TO_ADMINS.to_owned()); 920 self.post_msg(PostType::Post(msg, to)).unwrap(); 921 app.input = "/a ".to_owned(); 922 app.input_idx = app.input.width() 923 } else if input.starts_with("/s ") { 924 // Send message to "staff" section 925 let msg = remove_prefix(&input, "/s ").to_owned(); 926 let to = Some(SEND_TO_STAFFS.to_owned()); 927 self.post_msg(PostType::Post(msg, to)).unwrap(); 928 app.input = "/s ".to_owned(); 929 app.input_idx = app.input.width() 930 } else if let Some(captures) = PM_RGX.captures(&input) { 931 // Send PM message 932 let username = &captures[1]; 933 let msg = captures[2].to_owned(); 934 let to = Some(username.to_owned()); 935 self.post_msg(PostType::Post(msg, to)).unwrap(); 936 app.input = format!("/pm {} ", username); 937 app.input_idx = app.input.width() 938 } else if let Some(captures) = NEW_NICKNAME_RGX.captures(&input) { 939 // Change nickname 940 let new_nickname = captures[1].to_owned(); 941 self.post_msg(PostType::NewNickname(new_nickname)).unwrap(); 942 } else if input == "/inc" { 943 self.post_msg(PostType::Incognito).unwrap(); 944 } else if let Some(captures) = NEW_COLOR_RGX.captures(&input) { 945 // Change color 946 let new_color = captures[1].to_owned(); 947 self.post_msg(PostType::NewColor(new_color)).unwrap(); 948 } else if let Some(captures) = KICK_RGX.captures(&input) { 949 // Kick a user 950 let username = captures[1].to_owned(); 951 let msg = captures[2].to_owned(); 952 self.post_msg(PostType::Kick(msg, username)).unwrap(); 953 } else if let Some(captures) = UNKICK_RGX.captures(&input) { 954 // Unkick a user 955 let username = captures[1].to_owned(); 956 self.post_msg(PostType::Unkick(username)).unwrap(); 957 } else if let Some(captures) = IGNORE_RGX.captures(&input) { 958 // Ignore a user 959 let username = captures[1].to_owned(); 960 self.post_msg(PostType::Ignore(username)).unwrap(); 961 } else if let Some(captures) = UNIGNORE_RGX.captures(&input) { 962 // Unignore a user 963 let username = captures[1].to_owned(); 964 self.post_msg(PostType::Unignore(username)).unwrap(); 965 } else if let Some(captures) = UPLOAD_RGX.captures(&input) { 966 // Upload a file 967 let file_path = captures[1].to_owned(); 968 let send_to = match captures.get(2) { 969 Some(to_match) => match to_match.as_str() { 970 "members" => SEND_TO_MEMBERS, 971 "staffs" => SEND_TO_STAFFS, 972 "admins" => SEND_TO_ADMINS, 973 _ => SEND_TO_ALL, 974 }, 975 None => SEND_TO_ALL, 976 } 977 .to_owned(); 978 let msg = match captures.get(3) { 979 Some(msg_match) => msg_match.as_str().to_owned(), 980 None => "".to_owned(), 981 }; 982 self.post_msg(PostType::Upload(file_path, send_to, msg)) 983 .unwrap(); 984 } else { 985 if input.starts_with("/") && !input.starts_with("/me ") { 986 app.input_idx = input.len(); 987 app.input = input; 988 app.input_mode = InputMode::EditingErr; 989 } else { 990 // Send normal message 991 self.post_msg(PostType::Post(input, None)).unwrap(); 992 } 993 } 994 Ok(()) 995 } 996 997 fn handle_editing_mode_key_event_tab(&mut self, app: &mut App, users: &Arc<Mutex<Users>>) { 998 let (p1, p2) = app.input.split_at(app.input_idx); 999 if p2 == "" || p2.chars().nth(0) == Some(' ') { 1000 let mut parts: Vec<&str> = p1.split(" ").collect(); 1001 if let Some(user_prefix) = parts.pop() { 1002 let mut should_autocomplete = false; 1003 let mut prefix = ""; 1004 if parts.len() == 1 1005 && ((parts[0] == "/kick" || parts[0] == "/k") 1006 || (parts[0] == "/unkick" || parts[0] == "/uk") 1007 || parts[0] == "/pm" 1008 || parts[0] == "/ignore" 1009 || parts[0] == "/unignore") 1010 { 1011 should_autocomplete = true; 1012 } else if user_prefix.starts_with("@") { 1013 should_autocomplete = true; 1014 prefix = "@"; 1015 } 1016 if should_autocomplete { 1017 let user_prefix_norm = remove_prefix(user_prefix, prefix); 1018 let user_prefix_norm_len = user_prefix_norm.len(); 1019 if let Some(name) = autocomplete_username(users, user_prefix_norm) { 1020 let complete_name = format!("{}{}", prefix, name); 1021 parts.push(complete_name.as_str()); 1022 let p2 = p2.trim_start(); 1023 if p2 != "" { 1024 parts.push(p2); 1025 } 1026 app.input = parts.join(" "); 1027 app.input_idx += name.len() - user_prefix_norm_len; 1028 } 1029 } 1030 } 1031 } 1032 } 1033 1034 fn handle_editing_mode_key_event_ctrl_c(&mut self, app: &mut App) { 1035 app.clear_filter(); 1036 app.input = "".to_owned(); 1037 app.input_idx = 0; 1038 app.input_mode = InputMode::Normal; 1039 } 1040 1041 fn handle_editing_mode_key_event_ctrl_a(&mut self, app: &mut App) { 1042 app.input_idx = 0; 1043 } 1044 1045 fn handle_editing_mode_key_event_ctrl_e(&mut self, app: &mut App) { 1046 app.input_idx = app.input.width(); 1047 } 1048 1049 fn handle_editing_mode_key_event_ctrl_f(&mut self, app: &mut App) { 1050 if let Some(idx) = app.input.chars().skip(app.input_idx).position(|c| c == ' ') { 1051 app.input_idx = std::cmp::min(app.input_idx + idx + 1, app.input.width()); 1052 } else { 1053 app.input_idx = app.input.width(); 1054 } 1055 } 1056 1057 fn handle_editing_mode_key_event_ctrl_b(&mut self, app: &mut App) { 1058 if let Some(idx) = app.input_idx.checked_sub(2) { 1059 let tmp = app 1060 .input 1061 .chars() 1062 .take(idx) 1063 .collect::<String>() 1064 .chars() 1065 .rev() 1066 .collect::<String>(); 1067 if let Some(idx) = tmp.chars().position(|c| c == ' ') { 1068 app.input_idx = std::cmp::max(tmp.width() - idx, 0); 1069 } else { 1070 app.input_idx = 0; 1071 } 1072 } 1073 } 1074 1075 fn handle_editing_mode_key_event_ctrl_v(&mut self, app: &mut App) { 1076 let mut ctx: ClipboardContext = ClipboardProvider::new().unwrap(); 1077 if let Ok(clipboard) = ctx.get_contents() { 1078 let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); 1079 app.input.insert_str(byte_position, &clipboard); 1080 app.input_idx += clipboard.width(); 1081 } 1082 } 1083 1084 fn handle_editing_mode_key_event_left(&mut self, app: &mut App) { 1085 if app.input_idx > 0 { 1086 app.input_idx -= 1; 1087 } 1088 } 1089 1090 fn handle_editing_mode_key_event_right(&mut self, app: &mut App) { 1091 if app.input_idx < app.input.width() { 1092 app.input_idx += 1; 1093 } 1094 } 1095 1096 fn handle_editing_mode_key_event_down(&mut self, app: &mut App) { 1097 app.input_mode = InputMode::Normal; 1098 app.items.next(); 1099 } 1100 1101 fn handle_editing_mode_key_event_shift_c(&mut self, app: &mut App, c: char) { 1102 let byte_position = byte_pos(&app.input, app.input_idx).unwrap(); 1103 app.input.insert(byte_position, c); 1104 1105 app.input_idx += 1; 1106 app.update_filter(); 1107 } 1108 1109 fn handle_editing_mode_key_event_backspace(&mut self, app: &mut App) { 1110 if app.input_idx > 0 { 1111 app.input_idx -= 1; 1112 app.input = remove_at(&app.input, app.input_idx); 1113 app.update_filter(); 1114 } 1115 } 1116 1117 fn handle_editing_mode_key_event_delete(&mut self, app: &mut App) { 1118 if app.input_idx > 0 && app.input_idx == app.input.width() { 1119 app.input_idx -= 1; 1120 } 1121 app.input = remove_at(&app.input, app.input_idx); 1122 app.update_filter(); 1123 } 1124 1125 fn handle_editing_mode_key_event_esc(&mut self, app: &mut App) { 1126 app.input_mode = InputMode::Normal; 1127 } 1128 1129 fn handle_mouse_event( 1130 &mut self, 1131 app: &mut App, 1132 mouse_event: MouseEvent, 1133 ) -> Result<(), ExitSignal> { 1134 match mouse_event.kind { 1135 MouseEventKind::ScrollDown => app.items.next(), 1136 MouseEventKind::ScrollUp => app.items.previous(), 1137 _ => {} 1138 } 1139 Ok(()) 1140 } 1141 } 1142 1143 // Give a char index, return the byte position 1144 fn byte_pos(v: &str, idx: usize) -> Option<usize> { 1145 let mut b = 0; 1146 let mut chars = v.chars(); 1147 for _ in 0..idx { 1148 if let Some(c) = chars.next() { 1149 b += c.len_utf8(); 1150 } else { 1151 return None; 1152 } 1153 } 1154 Some(b) 1155 } 1156 1157 // Remove the character at idx (utf-8 aware) 1158 fn remove_at(v: &str, idx: usize) -> String { 1159 v.chars() 1160 .enumerate() 1161 .flat_map(|(i, c)| { 1162 if i == idx { 1163 return None; 1164 } 1165 Some(c) 1166 }) 1167 .collect::<String>() 1168 } 1169 1170 // Autocomplete any username 1171 fn autocomplete_username(users: &Arc<Mutex<Users>>, prefix: &str) -> Option<String> { 1172 let users = users.lock().unwrap(); 1173 let all_users = users.all(); 1174 let prefix_lower = prefix.to_lowercase(); 1175 let filtered = all_users.iter().find(|(_, name)| name.to_lowercase().starts_with(&prefix_lower)); 1176 Some(filtered?.1.to_owned()) 1177 } 1178 1179 fn set_profile_base_info( 1180 client: &Client, 1181 full_url: &str, 1182 params: &mut Vec<(&str, String)>, 1183 ) -> anyhow::Result<()> { 1184 params.extend(vec![("action", "profile".to_owned())]); 1185 let profile_resp = client.post(full_url).form(¶ms).send()?; 1186 let profile_resp_txt = profile_resp.text().unwrap(); 1187 let doc = Document::from(profile_resp_txt.as_str()); 1188 let bold = doc.find(Attr("id", "bold")).next().unwrap(); 1189 let italic = doc.find(Attr("id", "italic")).next().unwrap(); 1190 let small = doc.find(Attr("id", "small")).next().unwrap(); 1191 if bold.attr("checked").is_some() { 1192 params.push(("bold", "on".to_owned())); 1193 } 1194 if italic.attr("checked").is_some() { 1195 params.push(("italic", "on".to_owned())); 1196 } 1197 if small.attr("checked").is_some() { 1198 params.push(("small", "on".to_owned())); 1199 } 1200 if doc.find(Attr("name", "incognito")).next().and_then(|incognito| incognito.attr("checked")).is_some() { 1201 params.push(("incognito", "on".to_owned())); 1202 } 1203 let font_select = doc.find(Attr("name", "font")).next().unwrap(); 1204 let font = font_select.find(Name("option")).find_map(|el| { 1205 if el.attr("selected").is_some() { 1206 return Some(el.attr("value").unwrap()); 1207 } 1208 None 1209 }); 1210 params.push(("font", font.unwrap_or("").to_owned())); 1211 Ok(()) 1212 } 1213 1214 enum RetryErr { 1215 Retry, 1216 Exit, 1217 } 1218 1219 fn retry_fn<F>(mut clb: F) where F: FnMut() -> anyhow::Result<RetryErr> { 1220 loop { 1221 match clb() { 1222 Ok(RetryErr::Retry) => continue, 1223 Ok(RetryErr::Exit) => return, 1224 Err(err) => { 1225 log::error!("{}", err); 1226 continue; 1227 }, 1228 } 1229 } 1230 } 1231 1232 fn post_msg(client: &Client, post_type_recv: PostType, full_url: &str, session: String, url: &str, last_post_tx: &crossbeam_channel::Sender<()>) { 1233 let mut should_reset_keepalive_timer = false; 1234 retry_fn(|| -> anyhow::Result<RetryErr> { 1235 let post_type = post_type_recv.clone(); 1236 let resp_text = client.get(url).send()?.text()?; 1237 let doc = Document::from(resp_text.as_str()); 1238 let nc = doc.find(Attr("name", "nc")).next().context("nc not found")?; 1239 let nc_value = nc.attr("value").context("nc value not found")?.to_owned(); 1240 let postid = doc.find(Attr("name", "postid")).next().context("failed to get postid")?; 1241 let postid_value = postid.attr("value").context("failed to get postid value")?.to_owned(); 1242 let mut params: Vec<(&str, String)> = vec![ 1243 ("lang", LANG.to_owned()), 1244 ("nc", nc_value.to_owned()), 1245 ("session", session.clone()), 1246 ]; 1247 1248 if let PostType::Clean(date, text) = post_type { 1249 if let Err(e) = delete_message(&client, full_url, &mut params, date, text) { 1250 log::error!("failed to delete message: {:?}", e); 1251 return Ok(RetryErr::Retry); 1252 } 1253 return Ok(RetryErr::Exit); 1254 } 1255 1256 let mut req = client.post(full_url); 1257 let mut form: Option<multipart::Form> = None; 1258 1259 match post_type { 1260 PostType::Post(msg, send_to) => { 1261 should_reset_keepalive_timer = true; 1262 params.extend(vec![ 1263 ("action", "post".to_owned()), 1264 ("postid", postid_value.to_owned()), 1265 ("multi", "on".to_owned()), 1266 ("message", msg), 1267 ("sendto", send_to.unwrap_or(SEND_TO_ALL.to_owned())), 1268 ]); 1269 } 1270 PostType::NewNickname(new_nickname) => { 1271 set_profile_base_info(&client, full_url, &mut params)?; 1272 params.extend(vec![ 1273 ("do", "save".to_owned()), 1274 ("timestamps", "on".to_owned()), 1275 ("newnickname", new_nickname), 1276 ]); 1277 } 1278 PostType::Incognito => { 1279 set_profile_base_info(&client, full_url, &mut params)?; 1280 if let Some(pos) = params.iter().position(|(key, _)| *key == "incognito") { 1281 params.remove(pos); 1282 } else { 1283 params.push(("incognito", "on".to_owned())); 1284 } 1285 params.extend(vec![ 1286 ("do", "save".to_owned()), 1287 ("timestamps", "on".to_owned()), 1288 ]); 1289 } 1290 PostType::NewColor(new_color) => { 1291 set_profile_base_info(&client, full_url, &mut params)?; 1292 params.extend(vec![ 1293 ("do", "save".to_owned()), 1294 ("timestamps", "on".to_owned()), 1295 ("colour", new_color), 1296 ]); 1297 } 1298 PostType::Ignore(username) => { 1299 set_profile_base_info(&client, full_url, &mut params)?; 1300 params.extend(vec![ 1301 ("do", "save".to_owned()), 1302 ("timestamps", "on".to_owned()), 1303 ("ignore", username), 1304 ]); 1305 } 1306 PostType::Unignore(username) => { 1307 set_profile_base_info(&client, full_url, &mut params)?; 1308 params.extend(vec![ 1309 ("do", "save".to_owned()), 1310 ("timestamps", "on".to_owned()), 1311 ("unignore", username), 1312 ]); 1313 } 1314 PostType::Profile(new_color, new_nickname) => { 1315 set_profile_base_info(&client, full_url, &mut params)?; 1316 params.extend(vec![ 1317 ("do", "save".to_owned()), 1318 ("timestamps", "on".to_owned()), 1319 ("colour", new_color), 1320 ("newnickname", new_nickname), 1321 ]); 1322 } 1323 PostType::Kick(msg, send_to) => { 1324 params.extend(vec![ 1325 ("action", "post".to_owned()), 1326 ("postid", postid_value.to_owned()), 1327 ("message", msg), 1328 ("sendto", send_to), 1329 ("kick", "kick".to_owned()), 1330 ("what", "purge".to_owned()), 1331 ]); 1332 } 1333 PostType::Unkick(username) => { 1334 params.extend(vec![ 1335 ("action", "admin".to_owned()), 1336 ("session", session.clone()), 1337 ("lang", LANG.to_owned()), 1338 ("nc", nc_value.to_owned()), 1339 ("do", "sessions".to_owned()), 1340 ("logout", "1".to_owned()), 1341 ("nick", username), 1342 ]); 1343 } 1344 PostType::DeleteLast | PostType::DeleteAll => { 1345 params.extend(vec![("action", "delete".to_owned())]); 1346 if let PostType::DeleteAll = post_type { 1347 params.extend(vec![ 1348 ("sendto", SEND_TO_ALL.to_owned()), 1349 ("confirm", "yes".to_owned()), 1350 ("what", "all".to_owned()), 1351 ]); 1352 } else { 1353 params.extend(vec![ 1354 ("sendto", "".to_owned()), 1355 ("what", "last".to_owned()), 1356 ]); 1357 } 1358 } 1359 PostType::Upload(file_path, send_to, msg) => { 1360 form = Some( 1361 match multipart::Form::new() 1362 .text("lang", LANG.to_owned()) 1363 .text("nc", nc_value.to_owned()) 1364 .text("session", session.clone()) 1365 .text("action", "post".to_owned()) 1366 .text("postid", postid_value.to_owned()) 1367 .text("message", msg) 1368 .text("sendto", send_to.to_owned()) 1369 .text("what", "purge".to_owned()) 1370 .file("file", file_path) { 1371 Ok(f) => f, 1372 Err(e) => { 1373 log::error!("{:?}", e); 1374 return Ok(RetryErr::Exit); 1375 }, 1376 } 1377 ); 1378 } 1379 PostType::Clean(_, _) => {} 1380 } 1381 1382 if let Some(form_content) = form { 1383 req = req.multipart(form_content); 1384 } else { 1385 req = req.form(¶ms); 1386 } 1387 if let Err(err) = req.send() { 1388 log::error!("{:?}", err.to_string()); 1389 if err.is_timeout() { 1390 return Ok(RetryErr::Retry); 1391 } 1392 } 1393 return Ok(RetryErr::Exit); 1394 }); 1395 if should_reset_keepalive_timer { 1396 last_post_tx.send(()).unwrap(); 1397 } 1398 } 1399 1400 fn parse_date(date: &str, datetime_fmt: &str) -> NaiveDateTime { 1401 let now = Utc::now(); 1402 let date_fmt = format!("%Y-{}", datetime_fmt); 1403 NaiveDateTime::parse_from_str(format!("{}-{}", now.year(), date).as_str(), date_fmt.as_str()).unwrap() 1404 } 1405 1406 fn get_msgs( 1407 client: &Client, 1408 base_url: &str, 1409 page_php: &str, 1410 session: &str, 1411 username: &str, 1412 tx: &crossbeam_channel::Sender<PostType>, 1413 users: &Arc<Mutex<Users>>, 1414 sig: &Arc<Mutex<Sig>>, 1415 messages_updated_tx: &crossbeam_channel::Sender<()>, 1416 members_tag:&str, 1417 datetime_fmt: &str, 1418 messages: &Arc<Mutex<Vec<Message>>>, 1419 should_notify: &mut bool) -> anyhow::Result<()> 1420 { 1421 let url = format!("{}/{}?action=view&session={}&lang={}", base_url, page_php, session, LANG); 1422 let resp_text = client.get(url).send()?.text()?; 1423 let resp_text = resp_text.replace("<br>", "\n"); 1424 let doc = Document::from(resp_text.as_str()); 1425 let new_messages = match extract_messages(&doc) { 1426 Ok(messages) => messages, 1427 Err(_) => { 1428 // Failed to get messages, probably need re-login 1429 sig.lock().unwrap().signal(&ExitSignal::NeedLogin); 1430 return Ok(()); 1431 } 1432 }; 1433 { 1434 let messages = messages.lock().unwrap(); 1435 process_new_messages(&new_messages, &messages, datetime_fmt, members_tag, username, should_notify, tx, sig); 1436 // Build messages vector. Tag deleted messages. 1437 update_messages(new_messages, messages, datetime_fmt); 1438 // Notify new messages has arrived. 1439 // This ensure that we redraw the messages on the screen right away. 1440 // Otherwise, the screen would not redraw until a keyboard event occurs. 1441 messages_updated_tx.send(()).unwrap(); 1442 } 1443 { 1444 let mut users = users.lock().unwrap(); 1445 ban_imposters(&tx, &username, &users); 1446 *users = extract_users(&doc); 1447 } 1448 Ok(()) 1449 } 1450 1451 fn process_new_messages( 1452 new_messages: &Vec<Message>, messages: &MutexGuard<Vec<Message>>, 1453 datetime_fmt: &str, members_tag: &str, username: &str, should_notify: &mut bool, 1454 tx: &crossbeam_channel::Sender<PostType>, 1455 sig: &Arc<Mutex<Sig>>) { 1456 if let Some(last_known_msg) = messages.get(0) { 1457 let last_known_msg_parsed_dt = parse_date(&last_known_msg.date, datetime_fmt); 1458 let filtered = new_messages 1459 .iter() 1460 .filter(|new_msg| last_known_msg_parsed_dt <= parse_date(&new_msg.date, datetime_fmt) && !(new_msg.date == last_known_msg.date && last_known_msg.text == new_msg.text)); 1461 for new_msg in filtered { 1462 if let Some((from, to_opt, msg)) = get_message(&new_msg.text, &members_tag) { 1463 // Process new messages 1464 if username == N0TR1V { 1465 n0tr1v_only_process_msg(&from, &msg, &tx, &sig); 1466 } 1467 // Notify when tagged 1468 if msg.contains(format!("@{}", &username).as_str()) { 1469 *should_notify = true; 1470 } 1471 // Notify when PM is received 1472 if let Some(to) = to_opt { 1473 if to == username && msg != "!up" { 1474 *should_notify = true; 1475 } 1476 } 1477 } 1478 } 1479 } 1480 } 1481 1482 fn update_messages(new_messages: Vec<Message>, mut messages: MutexGuard<Vec<Message>>, datetime_fmt: &str) { 1483 let mut old_msg_ptr = 0; 1484 for new_msg in new_messages.into_iter() { 1485 loop { 1486 if let Some(old_msg) = messages.get_mut(old_msg_ptr) { 1487 let new_parsed_dt = parse_date(&new_msg.date, datetime_fmt); 1488 let parsed_dt = parse_date(&old_msg.date, datetime_fmt); 1489 if new_parsed_dt < parsed_dt { 1490 old_msg.deleted = true; 1491 old_msg_ptr += 1; 1492 continue; 1493 } 1494 if new_parsed_dt == parsed_dt { 1495 if old_msg.text != new_msg.text { 1496 let mut found = false; 1497 let mut x = 0; 1498 loop { 1499 x += 1; 1500 if let Some(old_msg) = messages.get(old_msg_ptr + x) { 1501 let parsed_dt = parse_date(&old_msg.date, datetime_fmt); 1502 if new_parsed_dt == parsed_dt { 1503 if old_msg.text == new_msg.text { 1504 found = true; 1505 break; 1506 } 1507 continue; 1508 } 1509 } 1510 break; 1511 } 1512 if !found { 1513 messages.insert(old_msg_ptr, new_msg); 1514 old_msg_ptr += 1; 1515 } 1516 } 1517 old_msg_ptr += 1; 1518 break; 1519 } 1520 } 1521 messages.insert(old_msg_ptr, new_msg); 1522 old_msg_ptr += 1; 1523 break; 1524 } 1525 } 1526 messages.truncate(1000); 1527 } 1528 1529 fn n0tr1v_only_process_msg(from: &str, msg: &str, tx: &crossbeam_channel::Sender<PostType>, sig: &Arc<Mutex<Sig>>) { 1530 // !bhcli filters 1531 if msg == "!bhcli" { 1532 let msg = format!("@{} -> {}", from, BHCLI_BLOG_URL).to_owned(); 1533 tx.send(PostType::Post(msg, None)).unwrap(); 1534 } else if msg == "/logout" && from == STUXNET { 1535 log::error!("forced logout by {}", from); 1536 sig.lock().unwrap().signal(&ExitSignal::Terminate); 1537 return; 1538 } 1539 // Auto kick spammers 1540 if from != N0TR1V && from != FAGGOT { 1541 if msg.contains(FAGGOT) && (msg.contains("pedo") || msg.contains("child")) { 1542 let msg = "spam".to_owned(); 1543 let username_to_kick = from.to_owned(); 1544 tx.send(PostType::Kick(msg, username_to_kick)).unwrap(); 1545 } 1546 } 1547 } 1548 1549 fn delete_message( 1550 client: &Client, 1551 full_url: &str, 1552 params: &mut Vec<(&str, String)>, 1553 date: String, 1554 text: String, 1555 ) -> anyhow::Result<()> { 1556 params.extend(vec![ 1557 ("action", "admin".to_owned()), 1558 ("do", "clean".to_owned()), 1559 ("what", "choose".to_owned()), 1560 ]); 1561 let clean_resp_txt = client.post(full_url).form(¶ms).send()?.text()?; 1562 let doc = Document::from(clean_resp_txt.as_str()); 1563 let nc = doc.find(Attr("name", "nc")).next().context("nc not found")?; 1564 let nc_value = nc.attr("value").context("nc value not found")?.to_owned(); 1565 let msgs = extract_messages(&doc)?; 1566 if let Some(msg) = msgs.iter().find(|m| m.date == date && m.text.text() == text) { 1567 let msg_id = msg.id.context("msg id not found")?; 1568 params.extend(vec![ 1569 ("nc", nc_value.to_owned()), 1570 ("what", "selected".to_owned()), 1571 ("mid[]", format!("{}", msg_id)), 1572 ]); 1573 client.post(full_url).form(¶ms).send()?; 1574 } 1575 Ok(()) 1576 } 1577 1578 fn ban_imposters(tx: &crossbeam_channel::Sender<PostType>, account_username: &str, users: &Users) { 1579 if BAN_IMPOSTERS { 1580 if users.admin.len() == 0 && (users.staff.len() == 0 || account_username == N0TR1V) { 1581 let n0tr1v_rgx = Regex::new(r#"n[o0]tr[1il][vy]"#).unwrap(); // o 0 | 1 i l | v y 1582 let molester_rgx = Regex::new(r#"m[o0][1l][e3][s5$]t[e3]r"#).unwrap(); 1583 let rapist_rgx = Regex::new(r#"r[a4]p[i1l]st"#).unwrap(); 1584 let hitler_rgx = Regex::new(r#"h[i1l]t[l1]er"#).unwrap(); 1585 let himmler_rgx = Regex::new(r#"h[i1]m+l[e3]r"#).unwrap(); 1586 let mengele_rgx = Regex::new(r#"m[e3]ng[e3]l[e3]"#).unwrap(); 1587 let goebbels_rgx = Regex::new(r#"g[o0][e|3]b+[e3]ls"#).unwrap(); 1588 let heydrich_rgx = Regex::new(r#"h[e3]ydr[i1]ch"#).unwrap(); 1589 let globocnik_rgx = Regex::new(r#"gl[o0]b[o0]cn[i1l]k"#).unwrap(); 1590 let dirlewanger_rgx = Regex::new(r#"d[i1]rl[e3]wang[e3]r"#).unwrap(); 1591 let jeckeln_rgx = Regex::new(r#"j[e3]ck[e3]ln"#).unwrap(); 1592 let kramer_rgx = Regex::new(r#"kram[e3]r"#).unwrap(); 1593 let blobel_rgx = Regex::new(r#"bl[o0]b[e3]l"#).unwrap(); 1594 let stangl_rgx = Regex::new(r#"stangl"#).unwrap(); 1595 for (_color, username) in &users.guests { 1596 let lower_name = username.to_lowercase(); 1597 // Names that anyone using bhcli will ban 1598 if n0tr1v_rgx.is_match(&lower_name) || lower_name.contains("pedo") { 1599 let msg = "forbidden name".to_owned(); 1600 let username = username.to_owned(); 1601 tx.send(PostType::Kick(msg, username)).unwrap(); 1602 } 1603 // Names that only "n0tr1v" will ban 1604 if account_username == N0TR1V { 1605 if lower_name.contains("fuck") 1606 || lower_name.contains("nigger") 1607 || lower_name.contains("nigga") 1608 || lower_name.contains("chink") 1609 || lower_name.contains("atomwaffen") 1610 || lower_name.contains("altright") 1611 || hitler_rgx.is_match(&lower_name) 1612 || goebbels_rgx.is_match(&lower_name) 1613 || himmler_rgx.is_match(&lower_name) 1614 || mengele_rgx.is_match(&lower_name) 1615 || heydrich_rgx.is_match(&lower_name) 1616 || globocnik_rgx.is_match(&lower_name) 1617 || dirlewanger_rgx.is_match(&lower_name) 1618 || jeckeln_rgx.is_match(&lower_name) 1619 || kramer_rgx.is_match(&lower_name) 1620 || blobel_rgx.is_match(&lower_name) 1621 || stangl_rgx.is_match(&lower_name) 1622 || rapist_rgx.is_match(&lower_name) 1623 || molester_rgx.is_match(&lower_name) 1624 { 1625 let msg = "forbidden name".to_owned(); 1626 let username = username.to_owned(); 1627 tx.send(PostType::Kick(msg, username)).unwrap(); 1628 } 1629 } 1630 } 1631 } 1632 } 1633 } 1634 1635 impl ChatClient { 1636 fn new(params: Params) -> Self { 1637 let mut c = new_default_le_chat_php_client(params.clone()); 1638 match params.chat_type { 1639 ClientType::Custom => { 1640 c.config.url = params.url.unwrap_or("".to_owned()); 1641 c.config.page_php = params.page_php.unwrap_or("chat.php".to_owned()); 1642 c.config.datetime_fmt = params.datetime_fmt.unwrap_or("%m-%d %H:%M:%S".to_owned()); 1643 c.config.members_tag = params.members_tag.unwrap_or("[M] ".to_owned()); 1644 c.config.keepalive_send_to = None; 1645 }, 1646 ClientType::Dan => { 1647 c.config = LeChatPHPConfig::new_dans_chat_config(); 1648 }, 1649 ClientType::BHC => { 1650 c.config = LeChatPHPConfig::new_black_hat_chat_config(); 1651 } 1652 } 1653 Self { 1654 le_chat_php_client: c, 1655 } 1656 } 1657 1658 fn run_forever(&mut self) { 1659 self.le_chat_php_client.run_forever(); 1660 } 1661 } 1662 1663 fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient { 1664 let (color_tx, color_rx) = crossbeam_channel::unbounded(); 1665 let (tx, rx) = crossbeam_channel::unbounded(); 1666 LeChatPHPClient { 1667 chat_type: params.chat_type, 1668 base_client: BaseClient { 1669 username: params.username, 1670 password: params.password, 1671 }, 1672 max_login_retry: params.max_login_retry, 1673 guest_color: params.guest_color, 1674 session: None, 1675 last_key_event: None, 1676 client: params.client, 1677 manual_captcha: params.manual_captcha, 1678 refresh_rate: params.refresh_rate, 1679 config: LeChatPHPConfig::new_black_hat_chat_config(), 1680 is_muted: Arc::new(Mutex::new(false)), 1681 show_sys: false, 1682 display_guest_view: false, 1683 display_hidden_msgs: false, 1684 tx, 1685 rx: Arc::new(Mutex::new(rx)), 1686 color_tx, 1687 color_rx: Arc::new(Mutex::new(color_rx)), 1688 } 1689 } 1690 1691 struct ChatClient { 1692 le_chat_php_client: LeChatPHPClient, 1693 } 1694 1695 #[derive(Debug, Clone)] 1696 struct Params { 1697 chat_type: ClientType, 1698 url: Option<String>, 1699 page_php: Option<String>, 1700 datetime_fmt: Option<String>, 1701 members_tag: Option<String>, 1702 username: String, 1703 password: String, 1704 guest_color: String, 1705 client: Client, 1706 manual_captcha: bool, 1707 refresh_rate: u64, 1708 max_login_retry: isize, 1709 } 1710 1711 #[derive(Clone)] 1712 enum ExitSignal { 1713 Terminate, 1714 NeedLogin, 1715 } 1716 struct Sig { 1717 tx: crossbeam_channel::Sender<ExitSignal>, 1718 rx: crossbeam_channel::Receiver<ExitSignal>, 1719 nb_rx: usize, 1720 } 1721 1722 impl Sig { 1723 fn new() -> Self { 1724 let (tx, rx) = crossbeam_channel::unbounded(); 1725 let nb_rx = 0; 1726 Self { tx, rx, nb_rx } 1727 } 1728 1729 fn clone(&mut self) -> crossbeam_channel::Receiver<ExitSignal> { 1730 self.nb_rx += 1; 1731 self.rx.clone() 1732 } 1733 1734 fn signal(&self, signal: &ExitSignal) { 1735 for _ in 0..self.nb_rx { 1736 self.tx.send(signal.clone()).unwrap(); 1737 } 1738 } 1739 } 1740 1741 fn trim_newline(s: &mut String) { 1742 if s.ends_with('\n') { 1743 s.pop(); 1744 if s.ends_with('\r') { 1745 s.pop(); 1746 } 1747 } 1748 } 1749 1750 fn get_guest_color(wanted: Option<String>) -> String { 1751 match wanted.as_deref() { 1752 Some("beige") => "F5F5DC", 1753 Some("blue-violet") => "8A2BE2", 1754 Some("brown") => "A52A2A", 1755 Some("cyan") => "00FFFF", 1756 Some("sky-blue") => "00BFFF", 1757 Some("gold") => "FFD700", 1758 Some("gray") => "808080", 1759 Some("green") => "008000", 1760 Some("hot-pink") => "FF69B4", 1761 Some("light-blue") => "ADD8E6", 1762 Some("light-green") => "90EE90", 1763 Some("lime-green") => "32CD32", 1764 Some("magenta") => "FF00FF", 1765 Some("olive") => "808000", 1766 Some("orange") => "FFA500", 1767 Some("orange-red") => "FF4500", 1768 Some("red") => "FF0000", 1769 Some("royal-blue") => "4169E1", 1770 Some("see-green") => "2E8B57", 1771 Some("sienna") => "A0522D", 1772 Some("silver") => "C0C0C0", 1773 Some("tan") => "D2B48C", 1774 Some("teal") => "008080", 1775 Some("violet") => "EE82EE", 1776 Some("white") => "FFFFFF", 1777 Some("yellow") => "FFFF00", 1778 Some("yellow-green") => "9ACD32", 1779 Some(other) => COLOR1_RGX 1780 .captures(other) 1781 .map_or("", |captures| captures.get(1).map_or("", |m| m.as_str())), 1782 None => "", 1783 } 1784 .to_owned() 1785 } 1786 1787 fn get_tor_client(socks_proxy_url: &str, no_proxy: bool) -> Client { 1788 let ua = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0"; 1789 let mut builder = reqwest::blocking::ClientBuilder::new() 1790 .redirect(Policy::none()) 1791 .cookie_store(true) 1792 .user_agent(ua); 1793 if !no_proxy { 1794 let proxy = reqwest::Proxy::all(socks_proxy_url).unwrap(); 1795 builder = builder.proxy(proxy); 1796 } 1797 builder.build().unwrap() 1798 } 1799 1800 fn ask_username(username: Option<String>) -> String { 1801 username.unwrap_or_else(|| { 1802 print!("username: "); 1803 let mut username_input = String::new(); 1804 io::stdout().flush().unwrap(); 1805 io::stdin().read_line(&mut username_input).unwrap(); 1806 trim_newline(&mut username_input); 1807 username_input 1808 }) 1809 } 1810 1811 fn ask_password(password: Option<String>) -> String { 1812 password.unwrap_or_else(|| { 1813 rpassword::prompt_password("Password: ").unwrap() 1814 }) 1815 } 1816 1817 #[derive(Debug, Clone, PartialEq)] 1818 enum ClientType { 1819 BHC, 1820 Dan, 1821 Custom, 1822 } 1823 1824 #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 1825 #[serde(rename_all = "camelCase")] 1826 pub struct DkfNotifierResp { 1827 #[serde(rename = "NewMessageSound")] 1828 pub new_message_sound: bool, 1829 #[serde(rename = "TaggedSound")] 1830 pub tagged_sound: bool, 1831 #[serde(rename = "PmSound")] 1832 pub pm_sound: bool, 1833 #[serde(rename = "InboxCount")] 1834 pub inbox_count: i64, 1835 #[serde(rename = "LastMessageCreatedAt")] 1836 pub last_message_created_at: String, 1837 } 1838 1839 fn start_dkf_notifier(client: &Client, dkf_api_key: &str) { 1840 let client = client.clone(); 1841 let dkf_api_key = dkf_api_key.to_owned(); 1842 let mut last_known_date = Utc::now(); 1843 thread::spawn(move || loop { 1844 let (_stream, stream_handle) = OutputStream::try_default().unwrap(); 1845 let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); 1846 1847 let params: Vec<(&str, String)> = vec![( 1848 "last_known_date", 1849 last_known_date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true), 1850 )]; 1851 let right_url = format!("{}/api/v1/chat/1/notifier", DKF_URL); 1852 if let Ok(resp) = client 1853 .post(right_url) 1854 .form(¶ms) 1855 .header("DKF_API_KEY", &dkf_api_key) 1856 .send() 1857 { 1858 if let Ok(txt) = resp.text() { 1859 if let Ok(v) = serde_json::from_str::<DkfNotifierResp>(&txt) { 1860 if v.pm_sound || v.tagged_sound { 1861 stream_handle.play_raw(source.convert_samples()).unwrap(); 1862 } 1863 last_known_date = DateTime::parse_from_rfc3339(&v.last_message_created_at) 1864 .unwrap() 1865 .with_timezone(&Utc); 1866 } 1867 } 1868 } 1869 thread::sleep(Duration::from_secs(5)); 1870 }); 1871 } 1872 1873 // Start thread that looks for new emails on DNMX every minutes. 1874 fn start_dnmx_mail_notifier(client: &Client, username: &str, password: &str) { 1875 let params: Vec<(&str, &str)> = vec![("login_username", username), ("secretkey", password)]; 1876 let login_url = format!("{}/src/redirect.php", DNMX_URL); 1877 client.post(login_url).form(¶ms).send().unwrap(); 1878 1879 let client_clone = client.clone(); 1880 thread::spawn(move || loop { 1881 let (_stream, stream_handle) = OutputStream::try_default().unwrap(); 1882 let source = Decoder::new_mp3(Cursor::new(SOUND1)).unwrap(); 1883 1884 let right_url = format!("{}/src/right_main.php", DNMX_URL); 1885 if let Ok(resp) = client_clone.get(right_url).send() { 1886 let mut nb_mails = 0; 1887 let doc = Document::from(resp.text().unwrap().as_str()); 1888 if let Some(table) = doc.find(Name("table")).nth(7) { 1889 table.find(Name("tr")).skip(1).for_each(|n| { 1890 if let Some(td) = n.find(Name("td")).nth(2) { 1891 if td.find(Name("b")).nth(0).is_some() { 1892 nb_mails += 1; 1893 } 1894 } 1895 }); 1896 } 1897 if nb_mails > 0 { 1898 log::error!("{} new mails", nb_mails); 1899 stream_handle.play_raw(source.convert_samples()).unwrap(); 1900 } 1901 } 1902 thread::sleep(Duration::from_secs(60)); 1903 }); 1904 } 1905 1906 fn main() -> anyhow::Result<()> { 1907 let mut opts: Opts = Opts::parse(); 1908 1909 // Configs file 1910 if let Ok(config_path) = confy::get_configuration_file_path("bhcli", None) { 1911 println!("Config path: {:?}", config_path); 1912 } 1913 if let Ok(cfg) = confy::load::<MyConfig>("bhcli", None) { 1914 if opts.dkf_api_key.is_none() { 1915 opts.dkf_api_key = cfg.dkf_api_key; 1916 } 1917 if let Some(default_profile) = cfg.profiles.get(&opts.profile) { 1918 if opts.username.is_none() { 1919 opts.username = Some(default_profile.username.clone()); 1920 opts.password = Some(default_profile.password.clone()); 1921 } 1922 } 1923 } 1924 1925 let logfile = FileAppender::builder() 1926 .encoder(Box::new(PatternEncoder::new("{d} {l} {t} - {m}{n}"))) 1927 .build("bhcli.log")?; 1928 1929 let config = log4rs::config::Config::builder() 1930 .appender(log4rs::config::Appender::builder().build("logfile", Box::new(logfile))) 1931 .build(log4rs::config::Root::builder() 1932 .appender("logfile") 1933 .build(LevelFilter::Error))?; 1934 1935 log4rs::init_config(config)?; 1936 1937 let client = get_tor_client(&opts.socks_proxy_url, opts.no_proxy); 1938 1939 // If dnmx username is set, start mail notifier thread 1940 if let Some(dnmx_username) = opts.dnmx_username { 1941 start_dnmx_mail_notifier(&client, &dnmx_username, &opts.dnmx_password.unwrap()) 1942 } 1943 1944 if let Some(dkf_api_key) = &opts.dkf_api_key { 1945 start_dkf_notifier(&client, dkf_api_key); 1946 } 1947 1948 let guest_color = get_guest_color(opts.guest_color); 1949 let username = ask_username(opts.username); 1950 let password = ask_password(opts.password); 1951 1952 let chat_type = if opts.url.is_some() { 1953 ClientType::Custom 1954 } else if opts.dan { 1955 ClientType::Dan 1956 } else { 1957 ClientType::BHC 1958 }; 1959 1960 let params = Params { 1961 chat_type, 1962 url: opts.url, 1963 page_php: opts.page_php, 1964 datetime_fmt: opts.datetime_fmt, 1965 members_tag: opts.members_tag, 1966 username, 1967 password, 1968 guest_color, 1969 client: client.clone(), 1970 manual_captcha: opts.manual_captcha, 1971 refresh_rate: opts.refresh_rate, 1972 max_login_retry: opts.max_login_retry, 1973 }; 1974 1975 ChatClient::new(params).run_forever(); 1976 1977 Ok(()) 1978 } 1979 1980 #[derive(Debug, Clone)] 1981 enum PostType { 1982 Post(String, Option<String>), // Message, SendTo 1983 Kick(String, String), // Message, Username 1984 Unkick(String), // Username 1985 Upload(String, String, String), // FilePath, SendTo, Message 1986 DeleteLast, // DeleteLast 1987 DeleteAll, // DeleteAll 1988 Incognito, // Toggle incognito 1989 NewNickname(String), // NewUsername 1990 NewColor(String), // NewColor 1991 Profile(String, String), // NewColor, NewUsername 1992 Ignore(String), // Username 1993 Unignore(String), // Username 1994 Clean(String, String), // Clean message 1995 } 1996 1997 // Get username of other user (or ours if it's the only one) 1998 fn get_username(own_username: &str, root: &StyledText, members_tag: &str) -> Option<String> { 1999 match get_message(root, members_tag) { 2000 Some((from, Some(to), _)) => { 2001 if from == own_username { 2002 return Some(to); 2003 } 2004 return Some(from); 2005 } 2006 Some((from, None, _)) => { 2007 return Some(from); 2008 } 2009 _ => return None, 2010 } 2011 } 2012 2013 // Extract "from"/"to"/"message content" from a "StyledText" 2014 fn get_message(root: &StyledText, members_tag: &str) -> Option<(String, Option<String>, String)> { 2015 if let StyledText::Styled(_, children) = root { 2016 let msg = children.get(0)?.text(); 2017 match children.get(children.len() - 1)? { 2018 StyledText::Styled(_, children) => { 2019 let from = match children.get(children.len() - 1)? { 2020 StyledText::Text(t) => t.to_owned(), 2021 _ => return None, 2022 }; 2023 return Some((from, None, msg)); 2024 }, 2025 StyledText::Text(t) => { 2026 if t == &members_tag { 2027 let from = match children.get(children.len() - 2)? { 2028 StyledText::Styled(_, children) => match children.get(children.len() - 1)? { 2029 StyledText::Text(t) => t.to_owned(), 2030 _ => return None, 2031 }, 2032 _ => return None, 2033 }; 2034 return Some((from, None, msg)); 2035 } else if t == "[" { 2036 let from = match children.get(children.len() - 2)? { 2037 StyledText::Styled(_, children) => match children.get(children.len() - 1)? { 2038 StyledText::Text(t) => t.to_owned(), 2039 _ => return None, 2040 }, 2041 _ => return None, 2042 }; 2043 let to = match children.get(2)? { 2044 StyledText::Styled(_, children) => match children.get(children.len() - 1)? { 2045 StyledText::Text(t) => Some(t.to_owned()), 2046 _ => return None, 2047 }, 2048 _ => return None, 2049 }; 2050 return Some((from, to, msg)); 2051 } 2052 }, 2053 _ => return None, 2054 } 2055 } 2056 return None; 2057 } 2058 2059 #[derive(Debug, PartialEq, Clone)] 2060 enum MessageType { 2061 UserMsg, 2062 SysMsg, 2063 } 2064 2065 #[derive(Debug, PartialEq, Clone)] 2066 struct Message { 2067 id: Option<usize>, 2068 typ: MessageType, 2069 date: String, 2070 upload_link: Option<String>, 2071 text: StyledText, 2072 deleted: bool, // Either or not a message was deleted on the chat 2073 hide: bool, // Either ot not to hide a specific message 2074 } 2075 2076 impl Message { 2077 fn new(id: Option<usize>, typ: MessageType, date: String, upload_link: Option<String>, text: StyledText) -> Self { 2078 Self { 2079 id, 2080 typ, 2081 date, 2082 upload_link, 2083 text, 2084 deleted: false, 2085 hide: false, 2086 } 2087 } 2088 } 2089 2090 #[derive(Debug, PartialEq, Clone)] 2091 enum StyledText { 2092 Styled(tuiColor, Vec<StyledText>), 2093 Text(String), 2094 None, 2095 } 2096 2097 impl StyledText { 2098 fn walk<F>(&self, mut clb: F) 2099 where 2100 F: FnMut(&StyledText), 2101 { 2102 let mut v: Vec<&StyledText> = vec![self]; 2103 loop { 2104 if let Some(e) = v.pop() { 2105 clb(e); 2106 if let StyledText::Styled(_, children) = e { 2107 v.extend(children); 2108 } 2109 continue; 2110 } 2111 break; 2112 } 2113 } 2114 2115 fn text(&self) -> String { 2116 let mut s = String::new(); 2117 self.walk(|n| { 2118 if let StyledText::Text(t) = n { 2119 s += t; 2120 } 2121 }); 2122 s 2123 } 2124 2125 // Return a vector of each text parts & what color it should be 2126 fn colored_text(&self) -> Vec<(tuiColor, String)> { 2127 let mut out: Vec<(tuiColor, String)> = vec![]; 2128 let mut v: Vec<(tuiColor, &StyledText)> = vec![(tuiColor::White, self)]; 2129 loop { 2130 if let Some((el_color, e)) = v.pop() { 2131 match e { 2132 StyledText::Styled(tui_color, children) => { 2133 for child in children { 2134 v.push((*tui_color, child)); 2135 } 2136 } 2137 StyledText::Text(t) => { 2138 out.push((el_color, t.to_owned())); 2139 } 2140 StyledText::None => {} 2141 } 2142 continue; 2143 } 2144 break; 2145 } 2146 out 2147 } 2148 } 2149 2150 fn parse_color(color_str: &str) -> tuiColor { 2151 let mut color = tuiColor::White; 2152 if color_str == "red" { 2153 return tuiColor::Red; 2154 } 2155 if let Ok(rgb) = Rgb::from_hex_str(color_str) { 2156 color = tuiColor::Rgb( 2157 rgb.get_red() as u8, 2158 rgb.get_green() as u8, 2159 rgb.get_blue() as u8, 2160 ); 2161 } 2162 color 2163 } 2164 2165 fn process_node(e: select::node::Node, mut color: tuiColor) -> (StyledText, Option<String>) { 2166 match e.data() { 2167 select::node::Data::Element(_, _) => { 2168 let mut upload_link: Option<String> = None; 2169 match e.name() { 2170 Some("span") => { 2171 if let Some(style) = e.attr("style") { 2172 if let Some(captures) = COLOR_RGX.captures(style) { 2173 let color_match = captures.get(1).unwrap().as_str(); 2174 color = parse_color(color_match); 2175 } 2176 } 2177 }, 2178 Some("font") => { 2179 if let Some(color_str) = e.attr("color") { 2180 color = parse_color(color_str); 2181 } 2182 }, 2183 Some("a") => { 2184 color = tuiColor::White; 2185 if let (Some("attachement"), Some(href)) = (e.attr("class"), e.attr("href")) { 2186 upload_link = Some(href.to_owned()); 2187 } 2188 }, 2189 Some("style") => { 2190 return (StyledText::None, None); 2191 }, 2192 _ => {}, 2193 } 2194 let mut children_texts: Vec<StyledText> = vec![]; 2195 let children = e.children(); 2196 for child in children { 2197 let (st, ul) = process_node(child, color); 2198 if ul.is_some() { 2199 upload_link = ul; 2200 } 2201 children_texts.push(st); 2202 } 2203 children_texts.reverse(); 2204 (StyledText::Styled(color, children_texts), upload_link) 2205 } 2206 select::node::Data::Text(t) => (StyledText::Text(t.to_string()), None), 2207 select::node::Data::Comment(_) => (StyledText::None, None), 2208 } 2209 } 2210 2211 struct Users { 2212 admin: Vec<(tuiColor, String)>, 2213 staff: Vec<(tuiColor, String)>, 2214 members: Vec<(tuiColor, String)>, 2215 guests: Vec<(tuiColor, String)>, 2216 } 2217 2218 impl Default for Users { 2219 fn default() -> Self { 2220 Self { 2221 admin: Default::default(), 2222 staff: Default::default(), 2223 members: Default::default(), 2224 guests: Default::default(), 2225 } 2226 } 2227 } 2228 2229 impl Users { 2230 fn all(&self) -> Vec<&(tuiColor, String)> { 2231 let mut out = Vec::new(); 2232 out.extend(&self.admin); 2233 out.extend(&self.staff); 2234 out.extend(&self.members); 2235 out.extend(&self.guests); 2236 out 2237 } 2238 2239 // fn is_guest(&self, name: &str) -> bool { 2240 // self.guests.iter().find(|(_, username)| username == name).is_some() 2241 // } 2242 } 2243 2244 fn extract_users(doc: &Document) -> Users { 2245 let mut users = Users::default(); 2246 2247 if let Some(chatters) = doc.find(Attr("id", "chatters")).next() { 2248 if let Some(tr) = chatters.find(Name("tr")).next() { 2249 let mut th_count = 0; 2250 for e in tr.children() { 2251 if let select::node::Data::Element(_, _) = e.data() { 2252 if e.name() == Some("th") { 2253 th_count += 1; 2254 continue; 2255 } 2256 for user_span in e.find(Name("span")) { 2257 if let Some(user_style) = user_span.attr("style") { 2258 if let Some(captures) = COLOR_RGX.captures(user_style) { 2259 if let Some(color_match) = captures.get(1) { 2260 let color = color_match.as_str().to_owned(); 2261 let tui_color = parse_color(&color); 2262 let username = user_span.text(); 2263 match th_count { 2264 1 => users.admin.push((tui_color, username)), 2265 2 => users.staff.push((tui_color, username)), 2266 3 => users.members.push((tui_color, username)), 2267 4 => users.guests.push((tui_color, username)), 2268 _ => {} 2269 } 2270 } 2271 } 2272 } 2273 } 2274 } 2275 } 2276 } 2277 } 2278 users 2279 } 2280 2281 fn remove_suffix<'a>(s: &'a str, suffix: &str) -> &'a str { 2282 s.strip_suffix(suffix).unwrap_or(s) 2283 } 2284 2285 fn remove_prefix<'a>(s: &'a str, prefix: &str) -> &'a str { 2286 s.strip_prefix(prefix).unwrap_or(s) 2287 } 2288 2289 fn extract_messages(doc: &Document) -> anyhow::Result<Vec<Message>> { 2290 let msgs = doc 2291 .find(Attr("id", "messages")) 2292 .next() 2293 .ok_or(anyhow!("failed to get messages div"))? 2294 .find(Attr("class", "msg")) 2295 .filter_map(|tag| { 2296 let mut id: Option<usize> = None; 2297 if let Some(checkbox) = tag.find(Name("input")).next() { 2298 let id_value: usize = checkbox.attr("value").unwrap().parse().unwrap(); 2299 id = Some(id_value); 2300 } 2301 if let Some(date_node) = tag.find(Name("small")).next() { 2302 if let Some(msg_span) = tag.find(Name("span")).next() { 2303 let date = remove_suffix(&date_node.text(), " - ").to_owned(); 2304 let typ = match msg_span.attr("class") { 2305 Some("usermsg") => MessageType::UserMsg, 2306 Some("sysmsg") => MessageType::SysMsg, 2307 _ => return None, 2308 }; 2309 let (text, upload_link) = process_node(msg_span, tuiColor::White); 2310 return Some(Message::new(id, typ, date, upload_link, text)); 2311 } 2312 } 2313 None 2314 }) 2315 .collect::<Vec<_>>(); 2316 Ok(msgs) 2317 } 2318 2319 fn draw_terminal_frame( 2320 f: &mut Frame<CrosstermBackend<io::Stdout>>, 2321 app: &mut App, 2322 messages: &Arc<Mutex<Vec<Message>>>, 2323 users: &Arc<Mutex<Users>>, 2324 username: &str, 2325 ) { 2326 if app.long_message.is_none() { 2327 let hchunks = Layout::default() 2328 .direction(Direction::Horizontal) 2329 .constraints([Constraint::Min(1), Constraint::Length(25)].as_ref()) 2330 .split(f.size()); 2331 2332 { 2333 let chunks = Layout::default() 2334 .direction(Direction::Vertical) 2335 .constraints( 2336 [ 2337 Constraint::Length(1), 2338 Constraint::Length(3), 2339 Constraint::Min(1), 2340 ] 2341 .as_ref(), 2342 ) 2343 .split(hchunks[0]); 2344 2345 render_help_txt(f, app, chunks[0], username); 2346 render_textbox(f, app, chunks[1]); 2347 render_messages(f, app, chunks[2], messages); 2348 render_users(f, hchunks[1], users); 2349 } 2350 } else { 2351 let hchunks = Layout::default() 2352 .direction(Direction::Horizontal) 2353 .constraints([Constraint::Min(1)]) 2354 .split(f.size()); 2355 { 2356 render_long_message(f, app, hchunks[0]); 2357 } 2358 } 2359 } 2360 2361 fn gen_lines(msg_txt: &StyledText, w: usize, line_prefix: &str) -> Vec<Vec<(tuiColor, String)>> { 2362 let txt = msg_txt.text(); 2363 let wrapped = textwrap::fill(&txt, w); 2364 let splits = wrapped.split("\n").collect::<Vec<&str>>(); 2365 let mut new_lines: Vec<Vec<(tuiColor, String)>> = Vec::new(); 2366 let mut ctxt = msg_txt.colored_text(); 2367 ctxt.reverse(); 2368 let mut ptr = 0; 2369 let mut split_idx = 0; 2370 let mut line: Vec<(tuiColor, String)> = Vec::new(); 2371 let mut first_in_line = true; 2372 loop { 2373 if let Some((color, mut txt)) = ctxt.pop() { 2374 txt = txt.replace("\n", ""); 2375 if let Some(split) = splits.get(split_idx) { 2376 if let Some(chr) = txt.chars().next() { 2377 if chr == ' ' && first_in_line { 2378 let skipped: String = txt.chars().skip(1).collect(); 2379 txt = skipped; 2380 } 2381 } 2382 2383 let remain = split.len() - ptr; 2384 if txt.len() <= remain { 2385 ptr += txt.len(); 2386 line.push((color, txt)); 2387 first_in_line = false; 2388 } else { 2389 line.push((color, txt[0..remain].to_owned())); 2390 new_lines.push(line.clone()); 2391 line.clear(); 2392 line.push((tuiColor::White, line_prefix.to_owned())); 2393 ctxt.push((color, txt[(remain)..].to_owned())); 2394 ptr = 0; 2395 split_idx += 1; 2396 first_in_line = true; 2397 } 2398 } 2399 } else { 2400 new_lines.push(line); 2401 break; 2402 } 2403 } 2404 new_lines 2405 } 2406 2407 fn render_long_message(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) { 2408 if let Some(m) = &app.long_message { 2409 let new_lines = gen_lines(&m.text, (r.width - 2) as usize, ""); 2410 2411 let mut rows = vec![]; 2412 for line in new_lines.into_iter() { 2413 let spans_vec: Vec<Span> = line 2414 .into_iter() 2415 .map(|(color, txt)| Span::styled(txt, Style::default().fg(color))) 2416 .collect(); 2417 rows.push(Spans::from(spans_vec)); 2418 } 2419 2420 let messages_list_items = vec![ListItem::new(rows)]; 2421 2422 let messages_list = List::new(messages_list_items) 2423 .block(Block::default().borders(Borders::ALL).title("")) 2424 .highlight_style( 2425 Style::default() 2426 .bg(tuiColor::Rgb(50, 50, 50)) 2427 .add_modifier(Modifier::BOLD), 2428 ); 2429 2430 f.render_widget(messages_list, r); 2431 } 2432 } 2433 2434 fn render_help_txt(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect, curr_user: &str) { 2435 let (mut msg, style) = match app.input_mode { 2436 InputMode::Normal => ( 2437 vec![ 2438 Span::raw("Press "), 2439 Span::styled("q", Style::default().add_modifier(Modifier::BOLD)), 2440 Span::raw(" to exit, "), 2441 Span::styled("i", Style::default().add_modifier(Modifier::BOLD)), 2442 Span::raw(" to start editing."), 2443 ], 2444 Style::default(), 2445 ), 2446 InputMode::Editing | InputMode::EditingErr => ( 2447 vec![ 2448 Span::raw("Press "), 2449 Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)), 2450 Span::raw(" to stop editing, "), 2451 Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)), 2452 Span::raw(" to record the message"), 2453 ], 2454 Style::default(), 2455 ), 2456 InputMode::LongMessage => (vec![], Style::default()), 2457 }; 2458 msg.extend(vec![Span::raw(format!(" | {}", curr_user))]); 2459 if app.is_muted { 2460 let fg = tuiColor::Red; 2461 let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); 2462 msg.extend(vec![Span::raw(" | "), Span::styled("muted", style)]); 2463 } else { 2464 let fg = tuiColor::LightGreen; 2465 let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); 2466 msg.extend(vec![Span::raw(" | "), Span::styled("not muted", style)]); 2467 } 2468 if app.display_guest_view { 2469 let fg = tuiColor::LightGreen; 2470 let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); 2471 msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]); 2472 } else { 2473 let fg = tuiColor::Gray; 2474 let style = Style::default().fg(fg); 2475 msg.extend(vec![Span::raw(" | "), Span::styled("G", style)]); 2476 } 2477 if app.display_hidden_msgs { 2478 let fg = tuiColor::LightGreen; 2479 let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); 2480 msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]); 2481 } else { 2482 let fg = tuiColor::Gray; 2483 let style = Style::default().fg(fg); 2484 msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]); 2485 } 2486 let mut text = Text::from(Spans::from(msg)); 2487 text.patch_style(style); 2488 let help_message = Paragraph::new(text); 2489 f.render_widget(help_message, r); 2490 } 2491 2492 fn render_textbox(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &mut App, r: Rect) { 2493 let w = (r.width - 3) as usize; 2494 let str = app.input.clone(); 2495 let mut input_str = str.as_str(); 2496 let mut overflow = 0; 2497 if app.input_idx >= w { 2498 overflow = std::cmp::max(app.input.width() - w, 0); 2499 input_str = &str[overflow..]; 2500 } 2501 let input = Paragraph::new(input_str) 2502 .style(match app.input_mode { 2503 InputMode::LongMessage => Style::default(), 2504 InputMode::Normal => Style::default(), 2505 InputMode::Editing => Style::default().fg(tuiColor::Yellow), 2506 InputMode::EditingErr => Style::default().fg(tuiColor::Red), 2507 }) 2508 .block(Block::default().borders(Borders::ALL).title("Input")); 2509 f.render_widget(input, r); 2510 match app.input_mode { 2511 InputMode::LongMessage => {} 2512 InputMode::Normal => 2513 // Hide the cursor. `Frame` does this by default, so we don't need to do anything here 2514 {} 2515 2516 InputMode::Editing | InputMode::EditingErr => { 2517 // Make the cursor visible and ask tui-rs to put it at the specified coordinates after rendering 2518 f.set_cursor( 2519 // Put cursor past the end of the input text 2520 r.x + app.input_idx as u16 - overflow as u16 + 1, 2521 // Move one line down, from the border to the input line 2522 r.y + 1, 2523 ) 2524 } 2525 } 2526 } 2527 2528 fn render_messages( 2529 f: &mut Frame<CrosstermBackend<io::Stdout>>, 2530 app: &mut App, 2531 r: Rect, 2532 messages: &Arc<Mutex<Vec<Message>>>, 2533 ) { 2534 // Messages 2535 app.items.items.clear(); 2536 let messages = messages.lock().unwrap(); 2537 let messages_list_items: Vec<ListItem> = messages 2538 .iter() 2539 .filter_map(|m| { 2540 if !app.display_hidden_msgs && m.hide { 2541 return None; 2542 } 2543 // Simulate a guest view (remove "PMs" and "Members chat" messages) 2544 if app.display_guest_view { 2545 // TODO: this is not efficient at all 2546 let text = m.text.text(); 2547 if text.starts_with(&app.members_tag) || text.starts_with(&app.staffs_tag) { 2548 return None; 2549 } 2550 if let Some((_, Some(_), _)) = get_message(&m.text, &app.members_tag) { 2551 return None; 2552 } 2553 } 2554 2555 if app.filter != "" { 2556 if !m.text.text().to_lowercase().contains(&app.filter.to_lowercase()) { 2557 return None; 2558 } 2559 } 2560 2561 app.items.items.push(m.clone()); 2562 2563 let new_lines = gen_lines(&m.text, (r.width - 20) as usize, " ".repeat(17).as_str()); 2564 2565 let mut rows = vec![]; 2566 let date_style = match (m.deleted, m.hide) { 2567 (false, true) => Style::default().fg(tuiColor::Gray), 2568 (false, _) => Style::default().fg(tuiColor::DarkGray), 2569 (true, _) => Style::default().fg(tuiColor::Red), 2570 }; 2571 let mut spans_vec = vec![Span::styled(m.date.clone(), date_style)]; 2572 let show_sys_sep = app.show_sys && m.typ == MessageType::SysMsg; 2573 let sep = if show_sys_sep { " * " } else { " - " }; 2574 spans_vec.push(Span::raw(sep)); 2575 for (idx, line) in new_lines.into_iter().enumerate() { 2576 // Spams can take your whole screen, so we limit to 5 lines. 2577 if idx >= 5 { 2578 spans_vec.push(Span::styled( 2579 " […]", 2580 Style::default().fg(tuiColor::White), 2581 )); 2582 rows.push(Spans::from(spans_vec)); 2583 break; 2584 } 2585 for (color, txt) in line { 2586 spans_vec.push(Span::styled(txt, Style::default().fg(color))); 2587 } 2588 rows.push(Spans::from(spans_vec.clone())); 2589 spans_vec.clear(); 2590 } 2591 2592 let style = match (m.deleted, m.hide) { 2593 (true, _) => Style::default().bg(tuiColor::Rgb(30, 0, 0)), 2594 (_, true) => Style::default().bg(tuiColor::Rgb(20, 20, 20)), 2595 _ => Style::default(), 2596 }; 2597 Some(ListItem::new(rows).style(style)) 2598 }) 2599 .collect(); 2600 2601 let messages_list = List::new(messages_list_items) 2602 .block(Block::default().borders(Borders::ALL).title("Messages")) 2603 .highlight_style( 2604 Style::default() 2605 .bg(tuiColor::Rgb(50, 50, 50)) 2606 .add_modifier(Modifier::BOLD), 2607 ); 2608 f.render_stateful_widget(messages_list, r, &mut app.items.state) 2609 } 2610 2611 fn render_users(f: &mut Frame<CrosstermBackend<io::Stdout>>, r: Rect, users: &Arc<Mutex<Users>>) { 2612 // Users lists 2613 let users = users.lock().unwrap(); 2614 let mut users_list: Vec<ListItem> = vec![]; 2615 let mut users_types: Vec<(&Vec<(tuiColor, String)>, &str)> = Vec::new(); 2616 users_types.push((&users.admin, "-- Admin --")); 2617 users_types.push((&users.staff, "-- Staff --")); 2618 users_types.push((&users.members, "-- Members --")); 2619 users_types.push((&users.guests, "-- Guests --")); 2620 for (users, label) in users_types.into_iter() { 2621 users_list.push(ListItem::new(Span::raw(label))); 2622 for (tui_color, username) in users.iter() { 2623 let span = Span::styled(username, Style::default().fg(*tui_color)); 2624 users_list.push(ListItem::new(span)); 2625 } 2626 } 2627 let users = List::new(users_list).block(Block::default().borders(Borders::ALL).title("Users")); 2628 f.render_widget(users, r); 2629 } 2630 2631 fn random_string(n: usize) -> String { 2632 let s: Vec<u8> = thread_rng().sample_iter(&Alphanumeric).take(n).collect(); 2633 std::str::from_utf8(&s).unwrap().to_owned() 2634 } 2635 2636 #[derive(PartialEq)] 2637 enum InputMode { 2638 LongMessage, 2639 Normal, 2640 Editing, 2641 EditingErr, 2642 } 2643 2644 /// App holds the state of the application 2645 struct App { 2646 /// Current value of the input box 2647 input: String, 2648 input_idx: usize, 2649 /// Current input mode 2650 input_mode: InputMode, 2651 is_muted: bool, 2652 show_sys: bool, 2653 display_guest_view: bool, 2654 display_hidden_msgs: bool, 2655 items: StatefulList<Message>, 2656 filter: String, 2657 members_tag: String, 2658 staffs_tag: String, 2659 long_message: Option<Message>, 2660 } 2661 2662 impl Default for App { 2663 fn default() -> App { 2664 App { 2665 input: String::new(), 2666 input_idx: 0, 2667 input_mode: InputMode::Normal, 2668 is_muted: false, 2669 show_sys: false, 2670 display_guest_view: false, 2671 display_hidden_msgs: false, 2672 items: StatefulList::new(), 2673 filter: "".to_owned(), 2674 members_tag: "".to_owned(), 2675 staffs_tag: "".to_owned(), 2676 long_message: None, 2677 } 2678 } 2679 } 2680 2681 impl App { 2682 fn update_filter(&mut self) { 2683 if let Some(captures) = FIND_RGX.captures(&self.input) { 2684 // Find 2685 self.filter = captures.get(1).map_or("", |m| m.as_str()).to_owned(); 2686 } 2687 } 2688 2689 fn clear_filter(&mut self) { 2690 if FIND_RGX.is_match(&self.input) { 2691 self.filter = "".to_owned(); 2692 self.input = "".to_owned(); 2693 self.input_idx = 0; 2694 } 2695 } 2696 } 2697 2698 pub enum Event<I> { 2699 Input(I), 2700 Tick, 2701 Terminate, 2702 NeedLogin, 2703 } 2704 2705 /// A small event handler that wrap termion input and tick events. Each event 2706 /// type is handled in its own thread and returned to a common `Receiver` 2707 struct Events { 2708 messages_updated_rx: crossbeam_channel::Receiver<()>, 2709 exit_rx: crossbeam_channel::Receiver<ExitSignal>, 2710 rx: crossbeam_channel::Receiver<Event<CEvent>>, 2711 } 2712 2713 #[derive(Debug, Clone)] 2714 struct Config { 2715 pub exit_rx: crossbeam_channel::Receiver<ExitSignal>, 2716 pub messages_updated_rx: crossbeam_channel::Receiver<()>, 2717 pub tick_rate: Duration, 2718 } 2719 2720 impl Events { 2721 fn with_config(config: Config) -> (Events, thread::JoinHandle<()>) { 2722 let (tx, rx) = crossbeam_channel::unbounded(); 2723 let tick_rate = config.tick_rate; 2724 let exit_rx = config.exit_rx; 2725 let messages_updated_rx = config.messages_updated_rx; 2726 let exit_rx1 = exit_rx.clone(); 2727 let thread_handle = thread::spawn(move || { 2728 let mut last_tick = Instant::now(); 2729 loop { 2730 // poll for tick rate duration, if no events, sent tick event. 2731 let timeout = tick_rate 2732 .checked_sub(last_tick.elapsed()) 2733 .unwrap_or_else(|| Duration::from_secs(0)); 2734 if event::poll(timeout).unwrap() { 2735 let evt = event::read().unwrap(); 2736 match evt { 2737 CEvent::FocusGained => {}, 2738 CEvent::FocusLost => {}, 2739 CEvent::Paste(_) => {}, 2740 CEvent::Resize(_, _) => tx.send(Event::Input(evt)).unwrap(), 2741 CEvent::Key(_) => tx.send(Event::Input(evt)).unwrap(), 2742 CEvent::Mouse(mouse_event) => { 2743 match mouse_event.kind { 2744 MouseEventKind::ScrollDown 2745 | MouseEventKind::ScrollUp 2746 | MouseEventKind::Down(_) => { 2747 tx.send(Event::Input(evt)).unwrap(); 2748 } 2749 _ => {} 2750 }; 2751 } 2752 }; 2753 } 2754 if last_tick.elapsed() >= tick_rate { 2755 select! { 2756 recv(&exit_rx1) -> _ => break, 2757 default => {}, 2758 } 2759 last_tick = Instant::now(); 2760 } 2761 } 2762 }); 2763 ( 2764 Events { 2765 rx, 2766 exit_rx, 2767 messages_updated_rx, 2768 }, 2769 thread_handle, 2770 ) 2771 } 2772 2773 fn next(&self) -> Result<Event<CEvent>, crossbeam_channel::RecvError> { 2774 select! { 2775 recv(&self.rx) -> evt => evt, 2776 recv(&self.messages_updated_rx) -> _ => Ok(Event::Tick), 2777 recv(&self.exit_rx) -> v => match v { 2778 Ok(ExitSignal::Terminate) => Ok(Event::Terminate), 2779 Ok(ExitSignal::NeedLogin) => Ok(Event::NeedLogin), 2780 Err(_) => Ok(Event::Terminate), 2781 }, 2782 } 2783 } 2784 } 2785 2786 #[cfg(test)] 2787 mod tests { 2788 use super::*; 2789 2790 #[test] 2791 fn gen_lines_test() { 2792 let txt = StyledText::Styled( 2793 tuiColor::White, 2794 vec![ 2795 StyledText::Styled( 2796 tuiColor::Rgb(255, 255, 255), 2797 vec![ 2798 StyledText::Text(" prmdbba pwuv💓".to_owned()), 2799 StyledText::Styled( 2800 tuiColor::Rgb(255, 255, 255), 2801 vec![StyledText::Styled( 2802 tuiColor::Rgb(0, 255, 0), 2803 vec![StyledText::Text("PMW".to_owned())], 2804 )], 2805 ), 2806 StyledText::Styled( 2807 tuiColor::Rgb(255, 255, 255), 2808 vec![StyledText::Styled( 2809 tuiColor::Rgb(255, 255, 255), 2810 vec![StyledText::Text("A".to_owned())], 2811 )], 2812 ), 2813 StyledText::Styled( 2814 tuiColor::Rgb(255, 255, 255), 2815 vec![StyledText::Styled( 2816 tuiColor::Rgb(0, 255, 0), 2817 vec![StyledText::Text("XOS".to_owned())], 2818 )], 2819 ), 2820 StyledText::Text( 2821 "pqb a mavx pkj fhsoeycg oruzb asd lk ruyaq re lheot mbnrw ".to_owned(), 2822 ), 2823 ], 2824 ), 2825 StyledText::Text(" - ".to_owned()), 2826 StyledText::Styled( 2827 tuiColor::Rgb(255, 255, 255), 2828 vec![StyledText::Text("rytxvgs".to_owned())], 2829 ), 2830 ], 2831 ); 2832 let lines = gen_lines(&txt, 71, ""); 2833 assert_eq!(lines.len(), 2); 2834 } 2835 }