bhcli

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

commit 27b1d77043b7977c5656ebdf03f3f93e19f6fe18
parent 6dfd840f0d9bc41a8bd69f25d830a2101fd5058b
Author: Dasho <git@dasho.dev>
Date:   Mon, 28 Jul 2025 07:08:01 +0100

Adds message deletion functionality

Adds a 'clean mode' which is toggled by Shift+C, which when active, will allow the user (if staff) to delete the selected message with `x`.

Diffstat:
MCMDS.md | 8++++++++
MREADME.md | 29+++++++++++++++--------------
Msrc/main.rs | 125++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
3 files changed, 144 insertions(+), 18 deletions(-)

diff --git a/CMDS.md b/CMDS.md @@ -17,12 +17,14 @@ - `/dl` delete last message - `/dlN` delete last N messages (e.g. `/dl5`) - `/dall` delete all messages +- `/delete <id>` delete message with id - `/u <path> [@target] [msg]` upload file ## Keyboard shortcuts - `Ctrl+k` prefill `/kick <username>` for selected message - `Ctrl+b` prefill `/ban <username>` for selected message - `Ctrl+Shift+B` prefill `/ban "<username>"` for selected message +- `x` prefill `/delete <id>` for selected message - `t` tag selected user - `p` pm selected user - `y` or `Ctrl+C` copy selected message @@ -30,5 +32,11 @@ - `m` toggle notifications - `Shift+G` guest view - `Shift+M` members view +- `Shift+V` toggle staff view +- `Shift+C` toggle clean mode +- - `x` to delete selected message when in clean mode +- `Shift+H` toggle hidden messages +- `Shift+T` translate text to English +- `Ctrl+A` prefill `/pm <master> /m ` or `/m ` (if no master account is set) - `Ctrl+D`/`PageDown` scroll down - `Ctrl+U`/`PageUp` scroll up diff --git a/README.md b/README.md @@ -18,20 +18,20 @@ Pre-buit binaries can be found on the [official website](http://git.dkforestseea - Sound notifications when tagged/pmmed - Private messages `/pm username message` -- Kick someone `/kick username message` | `/k username message` +- Kick someone `/kick username message` | `/k username message` (Members +) - Delete last message `/dl` - Delete last X message `/dl5` will delete the last 5 messages - Delete all messages `/dall` - Ignore someone `/ignore username` - Unignore someone `/unignore username` -- Ban a username and kick `/ban username` -- Ban a username exactly `/ban "username"` -- Filter messages containing text `/filter text` -- List banned usernames `/banlist` -- List exact banned usernames `/banexactlist` -- List filtered message terms `/filterlist` -- Unban a username `/unban username` -- Remove a message filter `/unfilter text` +- (Dasho) Ban a username and kick `/ban username` (Members +) +- (Dasho) Ban a username exactly `/ban "username"` (Members +) +- (Dasho) Filter messages containing text `/filter text` (Members +) +- (Dasho) List banned usernames `/banlist` (Members +) +- (Dasho) List exact banned usernames `/banexactlist` (Members +) +- (Dasho) List filtered message terms `/filterlist` (Members +) +- (Dasho) Unban a username `/unban username` (Members +) +- (Dasho) Remove a message filter `/unfilter text` (Members +) - Toggle notifications sound `m` - Toggle a "guest" view, by filtering out PMs and "Members chat" `shift+G` - Toggle a "members" view, by filtering out PMs and "Guest chat" `shift+M` @@ -40,9 +40,9 @@ Pre-buit binaries can be found on the [official website](http://git.dkforestseea - Copy the first link in a message to clipboard `shift+Y` - Directly tag author of selected message `t` will prefil the input with `@username ` - Directly private message author of selected message `p` will prefil the input with `/pm username ` -- Shortcut to kick author of selected message `ctrl+k` will prefill with `/pm <master> #kick username` if a master account is set, otherwise `/kick username ` -- Shortcut to ban author of selected message `ctrl+b` will prefill with `/pm <master> #ban username` if a master account is set, otherwise `/ban username ` -- Use `ctrl+m` to prefill the input with `/pm <master> /m ` when a master account is set, or `/m ` when none is configured +- (Dasho) Shortcut to kick author of selected message `ctrl+k` will prefill with `/pm <master> #kick username` if a master account is set, otherwise `/kick username ` +- (Dasho) Shortcut to ban author of selected message `ctrl+b` will prefill with `/pm <master> #ban username` if a master account is set, otherwise `/ban username ` (Again, only useful for members+ users) +- (Dasho) Use `ctrl+a` to prefill the input with `/pm <master> /m ` when a master account is set, or `/m ` when none is configured (Again, only useful for members+ users) - captcha is displayed directly in terminal 10 times the real size - Upload file `/u C:\path\to\file.png @username message` (@username is optional) `@members` for members group - `<tab>` to autocomplete usernames while typing @@ -118,8 +118,8 @@ To automatically login when starting the application, you can put the following [profiles.default] username = "username" password = "password" -alt_account = "myAlt" -master_account = "myMain" +alt_account = "myAlt" # Optional, only for members+ (Dasho) +master_account = "myMain" # Optional, only for members+ (Dasho) ``` ## Custom Commands @@ -141,6 +141,7 @@ The configuration is stored using `confy`. On Linux this is usually `~/.config/bhcli/bhcli.toml`. You can edit this file to preload profiles, create custom commands and maintain filters. +For members+ users, you can set the `alt_account` and `master_account` fields in the config file. (Dasho) To manually add or remove banned usernames or message filters you can edit the `bad_usernames`, `bad_exact_usernames` and `bad_messages` arrays in this file: diff --git a/src/main.rs b/src/main.rs @@ -86,6 +86,7 @@ lazy_static! { static ref IGNORE_RGX: Regex = Regex::new(r#"^/ignore ([^\s]+)"#).unwrap(); static ref UNIGNORE_RGX: Regex = Regex::new(r#"^/unignore ([^\s]+)"#).unwrap(); static ref DLX_RGX: Regex = Regex::new(r#"^/dl([\d]+)$"#).unwrap(); + static ref DELETE_RGX: Regex = Regex::new(r#"^/delete (\d+)"#).unwrap(); static ref UPLOAD_RGX: Regex = Regex::new(r#"^/u\s([^\s]+)\s?(?:@([^\s]+)\s)?(.*)$"#).unwrap(); static ref FIND_RGX: Regex = Regex::new(r#"^/f\s(.*)$"#).unwrap(); static ref NEW_NICKNAME_RGX: Regex = Regex::new(r#"^/nick\s(.*)$"#).unwrap(); @@ -263,6 +264,7 @@ struct LeChatPHPClient { display_pm_only: bool, display_staff_view: bool, display_master_pm_view: bool, + clean_mode: bool, } impl LeChatPHPClient { @@ -502,6 +504,7 @@ impl LeChatPHPClient { app.display_pm_only = self.display_pm_only; app.display_staff_view = self.display_staff_view; app.display_master_pm_view = self.display_master_pm_view; + app.clean_mode = self.clean_mode; app.alt_account = self.alt_account.clone(); app.master_account = self.master_account.clone(); app.members_tag = self.config.members_tag.clone(); @@ -730,6 +733,9 @@ impl LeChatPHPClient { } } else if input == "/dall" { self.post_msg(PostType::DeleteAll).unwrap(); + } else if let Some(captures) = DELETE_RGX.captures(input) { + let msg_id = captures.get(1).unwrap().as_str().to_owned(); + self.post_msg(PostType::Delete(msg_id)).unwrap(); } else if input == "/cycles" { self.color_tx.send(()).unwrap(); } else if input == "/cycle1" { @@ -1055,6 +1061,13 @@ impl LeChatPHPClient { } => self.handle_normal_mode_key_event_k(app, 5), KeyEvent { code: KeyCode::Enter, + modifiers, + .. + } if modifiers.contains(KeyModifiers::CONTROL) => { + self.handle_normal_mode_key_event_member_pm(app) + } + KeyEvent { + code: KeyCode::Enter, modifiers: KeyModifiers::NONE, .. } => self.handle_normal_mode_key_event_enter(app, messages), @@ -1134,6 +1147,11 @@ impl LeChatPHPClient { .. } => self.handle_normal_mode_key_event_toggle_v_view(), KeyEvent { + code: KeyCode::Char('C'), + modifiers: KeyModifiers::SHIFT, + .. + } => self.handle_normal_mode_key_event_shift_c(messages), + KeyEvent { code: KeyCode::Char('H'), modifiers: KeyModifiers::SHIFT, .. @@ -1164,7 +1182,7 @@ impl LeChatPHPClient { .. } => self.handle_normal_mode_key_event_pm(app), KeyEvent { - code: KeyCode::Char('m'), + code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL, .. } => self.handle_normal_mode_key_event_member_pm(app), @@ -1191,6 +1209,11 @@ impl LeChatPHPClient { .. } => self.handle_normal_mode_key_event_warn(app), KeyEvent { + code: KeyCode::Char('x'), + modifiers: KeyModifiers::NONE, + .. + } => self.handle_normal_mode_key_event_delete(app, messages), + KeyEvent { code: KeyCode::Char('T'), modifiers: KeyModifiers::SHIFT, .. @@ -1618,6 +1641,31 @@ impl LeChatPHPClient { } } + fn handle_normal_mode_key_event_shift_c( + &mut self, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + if self.clean_mode { + self.clean_mode = false; + return; + } + if let Some(session) = &self.session { + match fetch_clean_messages( + &self.client, + &self.config.url, + &self.config.page_php, + session, + ) { + Ok(msgs) => { + let mut lock = messages.lock().unwrap(); + *lock = msgs; + self.clean_mode = true; + } + Err(e) => log::error!("failed to load clean view: {}", e), + } + } + } + fn handle_normal_mode_key_event_g(&mut self, app: &mut App) { // Handle "gg" key combination if self.last_key_event == Some(KeyCode::Char('g')) { @@ -1812,6 +1860,29 @@ impl LeChatPHPClient { } } } + + fn handle_normal_mode_key_event_delete( + &mut self, + app: &mut App, + messages: &Arc<Mutex<Vec<Message>>>, + ) { + if let Some(idx) = app.items.state.selected() { + if let Some(id) = app.items.items.get(idx).and_then(|m| m.id) { + if self.clean_mode { + self.post_msg(PostType::Delete(id.to_string())).unwrap(); + if let Ok(mut msgs) = messages.lock() { + msgs.retain(|m| m.id != Some(id)); + } + app.items.unselect(); + } else { + app.input = format!("/delete {}", id); + app.input_idx = app.input.width(); + app.input_mode = InputMode::Editing; + app.items.unselect(); + } + } + } + } fn handle_normal_mode_key_event_page_up(&mut self, app: &mut App) { if let Some(idx) = app.items.state.selected() { app.items.state.select(idx.checked_sub(10).or(Some(0))); @@ -2726,6 +2797,35 @@ fn delete_message( Ok(()) } +fn fetch_clean_messages( + client: &Client, + base_url: &str, + page_php: &str, + session: &str, +) -> anyhow::Result<Vec<Message>> { + let full_url = format!("{}/{}", base_url, page_php); + let url = format!("{}?action=post&session={}", full_url, session); + let resp_text = client.get(&url).send()?.text()?; + let doc = Document::from(resp_text.as_str()); + let nc = doc + .find(Attr("name", "nc")) + .next() + .context("nc not found")?; + let nc_value = nc.attr("value").context("nc value not found")?.to_owned(); + let params = vec![ + ("lang", LANG.to_owned()), + ("nc", nc_value), + ("session", session.to_owned()), + ("action", "admin".to_owned()), + ("do", "clean".to_owned()), + ("what", "choose".to_owned()), + ]; + let clean_resp_txt = client.post(&full_url).form(&params).send()?.text()?; + let doc = Document::from(clean_resp_txt.as_str()); + let msgs = extract_messages(&doc)?; + Ok(msgs) +} + impl ChatClient { fn new(params: Params) -> Self { // println!("session[2026] : {:?}",params.session); @@ -2788,6 +2888,7 @@ fn new_default_le_chat_php_client(params: Params) -> LeChatPHPClient { display_pm_only: false, display_staff_view: false, display_master_pm_view: false, + clean_mode: false, } } @@ -3750,6 +3851,16 @@ fn render_help_txt( let style = Style::default().fg(fg); msg.extend(vec![Span::raw(" | "), Span::styled("H", style)]); } + + if app.clean_mode { + let fg = tuiColor::LightGreen; + let style = Style::default().fg(fg).add_modifier(Modifier::BOLD); + msg.extend(vec![Span::raw(" | "), Span::styled("C", style)]); + } else { + let fg = tuiColor::Gray; + let style = Style::default().fg(fg); + msg.extend(vec![Span::raw(" | "), Span::styled("C", style)]); + } let mut text = Text::from(Spans::from(msg)); text.patch_style(style); let help_message = Paragraph::new(text); @@ -3804,9 +3915,12 @@ fn render_messages( let messages_list_items: Vec<ListItem> = messages .iter() .filter_map(|m| { - if !app.display_hidden_msgs && m.hide { - return None; - } + if app.clean_mode { + // In clean mode show all messages + } else { + if !app.display_hidden_msgs && m.hide { + return None; + } // Simulate a guest view (remove "PMs" and "Members chat" messages) if app.display_guest_view { // TODO: this is not efficient at all @@ -3869,6 +3983,7 @@ fn render_messages( return None; } } + } app.items.items.push(m.clone()); @@ -3977,6 +4092,7 @@ struct App { display_pm_only: bool, display_staff_view: bool, display_master_pm_view: bool, + clean_mode: bool, } impl Default for App { @@ -4031,6 +4147,7 @@ impl Default for App { display_pm_only: false, display_staff_view: false, display_master_pm_view: false, + clean_mode: false, } } }