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