html terminal
-rw-r--r--.gitignore1
-rw-r--r--src/bot/bans.rs50
-rw-r--r--src/bot/mod.rs11
-rw-r--r--src/bot/player.rs4
-rw-r--r--src/webhook.rs227
5 files changed, 166 insertions, 127 deletions
diff --git a/.gitignore b/.gitignore
index aedca3a..544ef74 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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]