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:
| M | CMDS.md | | | 8 | ++++++++ |
| M | README.md | | | 29 | +++++++++++++++-------------- |
| M | src/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(¶ms).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,
}
}
}