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