html terminal
exposable logs
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | src/bot/bans.rs | 50 | ||||
| -rw-r--r-- | src/bot/mod.rs | 11 | ||||
| -rw-r--r-- | src/bot/player.rs | 4 | ||||
| -rw-r--r-- | src/webhook.rs | 227 |
5 files changed, 166 insertions, 127 deletions
@@ -4,3 +4,4 @@ html/ # vote data **.vd Cargo.lock +webhook diff --git a/src/bot/bans.rs b/src/bot/bans.rs index 89266c6..aa11440 100644 --- a/src/bot/bans.rs +++ b/src/bot/bans.rs @@ -9,7 +9,7 @@ use crate::{return_next, send_ctx}; required_permissions = "ADMINISTRATOR", default_member_permissions = "ADMINISTRATOR" )] -/// ban a player by uuid and ip +/// ban a ingame player by uuid and ip pub async fn add( ctx: Context<'_>, #[description = "player to ban"] @@ -29,6 +29,54 @@ pub async fn add( #[poise::command( slash_command, category = "Control", + required_permissions = "ADMINISTRATOR", + default_member_permissions = "ADMINISTRATOR" +)] +/// kick somebody off the server +pub async fn kick( + ctx: Context<'_>, + #[description = "player to ban"] + #[autocomplete = "player::autocomplete"] + player: String, +) -> Result<()> { + let _ = ctx.defer().await; + let player = Players::find(&ctx.data().stdin, player) + .await + .unwrap() + .unwrap(); + send_ctx!(ctx, "kick {}", player.uuid)?; // FIXME + return_next!(ctx) +} + +#[poise::command( + slash_command, + category = "Control", + rename = "ban", + required_permissions = "ADMINISTRATOR", + default_member_permissions = "ADMINISTRATOR" +)] +/// ban a player by uuid and/or ip +pub async fn add_raw( + ctx: Context<'_>, + #[description = "uuid of player to ban"] uuid: Option<String>, + #[description = "ip address of player to ban"] ip: Option<String>, +) -> Result<()> { + let _ = ctx.defer().await; + if uuid.is_none() && ip.is_none() { + anyhow::bail!("what are you banning? yourself?") + } + if let Some(uuid) = uuid { + send_ctx!(ctx, "ban id {}", uuid)?; + } + if let Some(ip) = ip { + send_ctx!(ctx, "ban ip {}", ip)?; + } + return_next!(ctx) +} + +#[poise::command( + slash_command, + category = "Control", rename = "unban", default_member_permissions = "ADMINISTRATOR", required_permissions = "ADMINISTRATOR" diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 45c9935..15acd71 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -64,6 +64,8 @@ impl Bot { say(), bans::add(), bans::remove(), + bans::add_raw(), + bans::kick(), admin::add(), admin::remove(), js::run(), @@ -84,7 +86,7 @@ impl Bot { on_error: |e| Box::pin(on_error(e)), prefix_options: poise::PrefixFrameworkOptions { edit_tracker: Some(poise::EditTracker::for_timespan( - std::time::Duration::from_secs(2 * 60), + std::time::Duration::from_secs(5 * 60), )), prefix: Some(PFX.to_string()), ..Default::default() @@ -104,10 +106,11 @@ impl Bot { // todo: voting::fixall() auto }) }); - tokio::spawn(async move { let http = Http::new(""); - let mut wh = Webhook::new(&http, &std::env::var("WEBHOOK").expect("no webhook!")).await; + let wh = std::env::var("WEBHOOK") + .unwrap_or(read_to_string("webhook").expect("wher webhook")); + let mut wh = Webhook::new(&http, &wh).await; SKIPPING.get_or_init(|| (wh.skip.clone(), wh.skipped.clone())); wh.link(stdout).await; }); @@ -209,7 +212,7 @@ async fn say(ctx: Context<'_>, #[description = "Message"] message: String) -> Re return_next!(ctx) } -fn strip_colors(from: &str) -> String { +pub fn strip_colors(from: &str) -> String { let mut result = String::new(); result.reserve(from.len()); let mut level: u8 = 0; diff --git a/src/bot/player.rs b/src/bot/player.rs index 9039485..5055944 100644 --- a/src/bot/player.rs +++ b/src/bot/player.rs @@ -60,7 +60,7 @@ async fn get_players(stdin: &broadcast::Sender<String>) -> Result<Vec<Player>> { } else if line.is_empty() { continue; } - if let Some((first, uuid, ip)) = line.split('/').collect_tuple() { + if let Some((first, uuid, ip)) = line.split('|').collect_tuple() { if let Some((admin, name)) = first.split_once(' ') { players.push(Player { admin: admin == "[A]", @@ -84,7 +84,7 @@ pub async fn autocomplete<'a>( .map(|p| p.name) } -#[poise::command(slash_command, prefix_command, category = "Info", rename = "players")] +#[poise::command(slash_command, category = "Info", rename = "players")] /// lists the currently online players. pub async fn list(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer().await; diff --git a/src/webhook.rs b/src/webhook.rs index ab71b90..d524f97 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -8,7 +8,7 @@ use std::sync::{ Arc, LazyLock, }; use tokio::sync::broadcast::{self, error::TryRecvError}; -use tokio::time::{sleep, Duration, Instant}; +use tokio::time::{sleep, Duration}; pub struct Webhook<'a> { pub skipped: broadcast::Sender<String>, @@ -52,33 +52,19 @@ impl<'a> Webhook<'a> { } async fn send_message(&self, username: &str, content: &str) { + define_print!("webhook"); + output!("{username}: {content}"); self.send(|m| m.username(username).content(content)).await; } pub async fn link(&mut self, mut stdout: broadcast::Receiver<String>) { define_print!("webhook"); - let mut last: Option<Instant> = None; - let mut feed: Vec<String> = vec![]; loop { let out = stdout.try_recv(); - let now = Instant::now(); match out { Err(e) => match e { TryRecvError::Closed => fail!("closed"), - TryRecvError::Lagged(_) => continue, - TryRecvError::Empty => { - if let Some(earlier) = last { - let since = now.duration_since(earlier).as_secs(); - if since > 1 || feed.len() > 15 { - last.take(); - self.flush::<MindustryStyle>(feed).await; - feed = vec![]; - flush!(); - } - } - sleep(Duration::from_millis(20)).await; - continue; - } + _ => sleep(Duration::from_millis(20)).await, }, Ok(m) => { if self @@ -94,108 +80,86 @@ impl<'a> Webhook<'a> { continue; } for line in m.lines() { - let line = line.to_string(); - input!("{line}"); - feed.push(line); + self.push(line).await; } - last = Some(now); } } sleep(Duration::from_millis(20)).await; } } - pub async fn flush<Style: OutputStyle>(&self, feed: Vec<String>) { - let mut current: Option<String> = None; - let mut message: Option<String> = None; - let mut unnamed: Option<String> = None; - for line in feed { - let line: String = Style::fix(line); - if let Some((name, msg)) = Style::split(&line) { - if let Some(n) = current.as_ref() { - if n == &name { - message.madd_panic(&msg); - continue; - } - let message = message.take().unwrap(); - self.send_message(n, &message).await; - current.take(); - } - current = Some(name.to_owned()); - message = Some(msg.to_owned()); - // interrupt - if let Some(msg) = unnamed.take() { - self.send_message("server", &msg).await; - } - continue; + pub async fn push(&self, msg: &str) { + match get(msg) { + Some(Message::Chat { player, content }) => { + self.send_message(&player, &content).await; } - unnamed.madd(unify(&line)); - } - // finish - if let Some(n) = current.as_ref() { - let message = message.take().unwrap(); - self.send_message(n, &message).await; - } - if let Some(msg) = unnamed.as_ref() { - self.send_message("server", msg).await; + Some(Message::Join { player }) => { + self.send_message(&player, "<has joined the game>").await; + } + Some(Message::Left { player }) => { + self.send_message(&player, "<has left the game>").await; + } + Some(Message::Load { map }) => { + self.send_message("server", &format!("loading map {map}")) + .await; + } + _ => return, } } } -/// functions ordered by call order -pub trait OutputStyle { - /// first step - fn fix(raw_line: String) -> String { - raw_line - } - /// get the user and the content (none for no user) - fn split(line: &str) -> Option<(String, String)>; -} - -macro_rules! s { - ($line:expr, $e:ident) => { - $line.starts_with(stringify!($e)) - }; - ($line:expr, $e:expr) => { - $line.starts_with($e) - }; +#[derive(PartialEq, Debug, Clone)] +pub enum Message { + Join { player: String }, + Left { player: String }, + Chat { player: String, content: String }, + Load { map: String }, } -macro_rules! tern { - ($predicate:expr, $true: expr, $false: expr) => {{ - if $predicate { - $true - } else { - $false - } - }}; -} -pub struct MindustryStyle; -impl OutputStyle for MindustryStyle { - fn split(line: &str) -> Option<(String, String)> { - if s!(line, [' ', '\t']) || s!(line, "at") || s!(line, "Lost command socket connection") { - return None; - } +fn get(line: &str) -> Option<Message> { + macro_rules! s { + ($line: expr, $($e:expr),+ $(,)?) => { + $( + $line.starts_with($e) || + )+ false + }; + } + if s!(line, [' ', '\t'], "at", "Lost command socket connection") { + return None; + } - if let Some((u, c)) = line.split(": ").map(unify).collect_tuple() { - let u = u.trim_start_matches('<'); - let c = c.trim_end_matches('>'); - if !(u.is_empty() || c.is_empty()) { - return Some((u.to_owned(), c.to_owned())); - } + static HAS_UUID: LazyLock<Regex> = + LazyLock::new(|| Regex::new(r"[a-zA-Z0-9+/]{22}==").unwrap()); + + if let Some((u, c)) = line.split(": ").map(unify).collect_tuple() { + let u = u.trim_start_matches('<'); + let c = c.trim_end_matches('>'); + if !(u.is_empty() || c.is_empty() || HAS_UUID.is_match(c)) { + return Some(Message::Chat { + player: u.to_owned(), + content: c.to_owned(), + }); } + } - static REGEX: LazyLock<Regex> = LazyLock::new(|| { - Regex::new(r"(.+) has (dis)?connected. \[([a-zA-Z0-9+/]+==)\]").unwrap() + static JOINAGE: LazyLock<Regex> = LazyLock::new(|| { + Regex::new(r"(.+) has (dis)?connected. \[([a-zA-Z0-9+/]{22}==)\]").unwrap() + }); + if let Some(captures) = JOINAGE.captures(line) { + let player = unify(captures.get(1).unwrap().as_str()); + return Some(if captures.get(2).is_some() { + Message::Left { player } + } else { + Message::Join { player } + }); + } + static MAP_LOAD: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"Loading map (.+)").unwrap()); + if let Some(captures) = MAP_LOAD.captures(line) { + return Some(Message::Load { + map: captures.get(1).unwrap().as_str().to_string(), }); - if let Some(captures) = REGEX.captures(line) { - let player = unify(captures.get(1).unwrap().as_str()); - let prefix = tern!(captures.get(2).is_some(), "left", "joined"); - let uuid = captures.get(3).unwrap().as_str(); - return Some((player, format!("{prefix} ({uuid})"))); - } - None } + None } pub fn unify(s: &str) -> String { @@ -226,32 +190,55 @@ impl Madd for Option<String> { #[test] fn style() { macro_rules! test_line { - ($line:expr) => { - let line = $line.to_string(); // no fixing done! - let got = MindustryStyle::split(&line); - assert!(got == None, "got {got:?}, expected None"); + ($line:literal) => { + let got = get($line); + assert_eq!(got, None); }; - ($line:expr, $name: expr, $content: expr) => { - let line = $line.to_string(); - let got = MindustryStyle::split(&line); - assert!( - got == Some(($name.into(), $content.into())), - "got {got:?}, expected ({}, {})", - $name, - $content - ); + ($line:literal, $what:expr) => { + let got = get($line); + assert_eq!(got, Some($what)); }; } //unnamed test_line!("undefined"); test_line!("Lost command socket connection: localhost/127.0.0.1:6859"); //named - test_line!("abc: hi", "abc", "hi"); - test_line!("<a: /help>", "a", "/help"); - test_line!("a has connected. [abc==]", "a", "joined (abc==)"); - test_line!("a has disconnected. [abc==] (closed)", "a", "left (abc==)"); - test_line!("a: :o", "a", ":o"); - test_line!("a:b: :o", "a:b", ":o"); + test_line!( + "abc: hi", + Message::Chat { + player: "abc".into(), + content: "hi".into() + } + ); + test_line!( + "<a: /help>", + Message::Chat { + player: "a".into(), + content: "/help".into() + } + ); + test_line!( + "a has connected. [+41521zhHB8321xAbXYedw==]", + Message::Join { player: "a".into() } + ); + test_line!( + "a has disconnected. [+41521zhHB8321xAbXYedw==] (closed)", + Message::Left { player: "a".into() } + ); + test_line!( + "a: :o", + Message::Chat { + player: "a".into(), + content: ":o".into() + } + ); + test_line!( + "a:b: :o", + Message::Chat { + player: "a:b".into(), + content: ":o".into() + } + ); } #[test] |