html terminal
better banning
bendn 2023-06-15
parent 1119f97 · commit bd452ad
-rw-r--r--Cargo.lock29
-rw-r--r--Cargo.toml5
-rw-r--r--src/bot/maps.rs41
-rw-r--r--src/bot/mod.rs (renamed from src/bot.rs)202
-rw-r--r--src/bot/player.rs85
-rw-r--r--src/main.rs1
-rw-r--r--src/server.rs2
-rw-r--r--src/webhook.rs1
8 files changed, 249 insertions, 117 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ac6b57a..53f5511 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -726,12 +726,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
-name = "heck"
-version = "0.4.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
-
-[[package]]
name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1092,7 +1086,6 @@ dependencies = [
"regex",
"serenity",
"strip-ansi-escapes",
- "strum",
"tokio",
"tokio-stream",
]
@@ -1643,28 +1636,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
-name = "strum"
-version = "0.24.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
-dependencies = [
- "strum_macros",
-]
-
-[[package]]
-name = "strum_macros"
-version = "0.24.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
-dependencies = [
- "heck",
- "proc-macro2",
- "quote",
- "rustversion",
- "syn 1.0.109",
-]
-
-[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index ea8f15f..78cba10 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,11 +39,6 @@ serenity = { version = "0.11.5", features = [
], default-features = false }
poise = "0.5.5"
anyhow = "1.0.71"
-strum = { version = "0.24.1", features = [
- "strum_macros",
- "derive",
- "std",
-], default-features = false }
regex = { version = "1.8.4", features = ["std"], default-features = false }
minify-js = "0.4.3"
diff --git a/src/bot/maps.rs b/src/bot/maps.rs
new file mode 100644
index 0000000..ba330d7
--- /dev/null
+++ b/src/bot/maps.rs
@@ -0,0 +1,41 @@
+use super::{get_nextblock, strip_colors, Context};
+use crate::send;
+use futures_util::StreamExt;
+use tokio::sync::broadcast;
+use tokio::sync::OnceCell;
+
+pub struct Maps;
+impl Maps {
+ pub async fn find(map: &str, stdin: &broadcast::Sender<String>) -> usize {
+ Self::get_all(stdin)
+ .await
+ .iter()
+ .position(|r| r == map)
+ .unwrap()
+ }
+
+ pub async fn get_all(stdin: &broadcast::Sender<String>) -> &Vec<String> {
+ static MAPS: OnceCell<Vec<String>> = OnceCell::const_new();
+ MAPS.get_or_init(|| async move {
+ send!(stdin, "maps").unwrap();
+ let res = get_nextblock().await;
+ let mut vec = vec![];
+ for line in res.lines() {
+ if let Some((_, name)) = line.split_once(':') {
+ vec.push(strip_colors(name));
+ }
+ }
+ vec
+ })
+ .await
+ }
+}
+
+pub async fn autocomplete<'a>(
+ ctx: Context<'a>,
+ partial: &'a str,
+) -> impl futures::Stream<Item = String> + 'a {
+ futures::stream::iter(Maps::get_all(&ctx.data().stdin).await)
+ .filter(move |name| futures::future::ready(name.starts_with(partial)))
+ .map(|name| name.to_string())
+}
diff --git a/src/bot.rs b/src/bot/mod.rs
index e7de0f4..760a290 100644
--- a/src/bot.rs
+++ b/src/bot/mod.rs
@@ -1,35 +1,62 @@
+mod maps;
+mod player;
+
use crate::webhook::Webhook;
use anyhow::Result;
-use futures_util::StreamExt;
+use maps::Maps;
use minify_js::TopLevelMode;
+use player::Players;
use regex::Regex;
use serenity::http::Http;
use serenity::prelude::*;
use std::fs::read_to_string;
use std::sync::{Arc, Mutex, OnceLock};
-use strum::{AsRefStr, EnumString, EnumVariantNames, VariantNames};
use tokio::sync::broadcast;
-use tokio::sync::OnceCell as TokLock;
+
pub struct Data {
stdin: broadcast::Sender<String>,
}
static SKIPPING: OnceLock<(Arc<Mutex<u8>>, broadcast::Sender<String>)> = OnceLock::new();
+#[macro_export]
+macro_rules! send {
+ ($e:expr, $fmt:literal $(, $args:expr)* $(,)?) => {
+ $e.send(format!($fmt $(, $args)*))
+ };
+}
+
+macro_rules! send_ctx {
+ ($e:expr,$fmt:literal $(, $args:expr)* $(,)?) => {
+ $e.data().stdin.send(format!($fmt $(, $args)*))
+ };
+}
+
pub struct Bot;
impl Bot {
- pub async fn new(stdout: broadcast::Receiver<String>, stdin: broadcast::Sender<String>) {
+ pub async fn spawn(stdout: broadcast::Receiver<String>, stdin: broadcast::Sender<String>) {
let tok = std::env::var("TOKEN").unwrap_or(read_to_string("token").expect("wher token"));
let f: poise::FrameworkBuilder<Data, anyhow::Error> = poise::Framework::builder()
.options(poise::FrameworkOptions {
- commands: vec![raw(), say(), ban(), js(), maps(), start(), end(), help()],
+ commands: vec![
+ raw(),
+ say(),
+ ban(),
+ unban(),
+ js(),
+ maps(),
+ players(),
+ start(),
+ end(),
+ help(),
+ ],
prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(">".to_string()),
..Default::default()
},
..Default::default()
})
- .token(&tok)
+ .token(tok)
.intents(GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT)
.setup(|ctx, _ready, framework| {
Box::pin(async move {
@@ -56,7 +83,7 @@ type Context<'a> = poise::Context<'a, Data, anyhow::Error>;
#[poise::command(
prefix_command,
required_permissions = "USE_SLASH_COMMANDS",
- category = "ADMINISTRATION"
+ category = "Control"
)]
/// send a raw command to the server
async fn raw(
@@ -65,7 +92,7 @@ async fn raw(
#[rest]
cmd: String,
) -> Result<()> {
- ctx.data().stdin.send(cmd)?;
+ send_ctx!(ctx, "{cmd}")?;
println!("sent");
Ok(())
}
@@ -90,49 +117,49 @@ async fn get_nextblock() -> String {
.unwrap_or("._?".to_string())
}
-#[poise::command(slash_command, category = "ADMINISTRATION")]
+#[poise::command(slash_command, category = "Control")]
/// say something as the server
async fn say(ctx: Context<'_>, #[description = "Message"] message: String) -> Result<()> {
- let _ = ctx.defer();
+ let _ = ctx.defer().await;
ctx.data().stdin.send(format!("say {message}"))?;
return_next!(ctx)
}
-#[derive(EnumString, EnumVariantNames, AsRefStr)]
-#[strum(serialize_all = "snake_case")]
-enum BanType {
- Id,
- Ip,
- Name,
-}
-
-async fn autocomplete_ban<'a>(
- _ctx: Context<'_>,
- partial: &'a str,
-) -> impl futures::Stream<Item = String> + 'a {
- futures::stream::iter(BanType::VARIANTS)
- .filter(move |name| futures::future::ready(name.starts_with(partial)))
- .map(|name| name.to_string())
+#[poise::command(slash_command, category = "Control")]
+/// ban a player by uuid and ip
+async fn ban(
+ 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, "ban ip {}", player.ip)?;
+ send_ctx!(ctx, "ban id {}", player.uuid)?;
+ return_next!(ctx)
}
-#[poise::command(slash_command, category = "ADMINISTRATION")]
-/// ban a player
-async fn ban(
+#[poise::command(slash_command, category = "Control")]
+/// unban a player by uuid or ip
+async fn unban(
ctx: Context<'_>,
- #[autocomplete = "autocomplete_ban"] ban_type: BanType,
- #[description = "Player (id/ip/name)"] player: String,
+ #[description = "Player id/ip"]
+ #[rename = "ip_or_id"]
+ player: String,
) -> Result<()> {
- let _ = ctx.defer();
- ctx.data()
- .stdin
- .send(format!("ban {} {player}", ban_type.as_ref()))?;
+ let _ = ctx.defer().await;
+ send_ctx!(ctx, "unban {}", player)?;
return_next!(ctx)
}
#[poise::command(
prefix_command,
required_permissions = "USE_SLASH_COMMANDS",
- category = "ADMINISTRATION",
+ category = "Control",
track_edits
)]
/// run arbitrary javascript
@@ -150,12 +177,12 @@ async fn js(
.ok_or(anyhow::anyhow!(r#"no code found (use \`\`\`js...\`\`\`."#))?;
let script = mat.get(2).unwrap().as_str();
let mut out = vec![];
- let script = if let Err(_) = minify_js::minify(TopLevelMode::Global, script.into(), &mut out) {
+ let script = if minify_js::minify(TopLevelMode::Global, script.into(), &mut out).is_err() {
std::borrow::Cow::from(script.replace('\n', ";")) // xd
} else {
String::from_utf8_lossy(&out)
};
- ctx.data().stdin.send(format!("js {script}"))?;
+ send_ctx!(ctx, "js {script}")?;
let line = get_nextblock().await;
ctx.send(|m| m.content(line)).await?;
Ok(())
@@ -177,81 +204,92 @@ fn strip_colors(from: &str) -> String {
result
}
-static MAPS: TokLock<Vec<String>> = TokLock::const_new();
-async fn get_maps(stdin: &broadcast::Sender<String>) -> &Vec<String> {
- MAPS.get_or_init(|| async move {
- stdin.send(format!("listmaps")).unwrap();
- let res = get_nextblock().await;
- let mut vec = vec![];
- for line in res.lines() {
- if let Some((_, name)) = line.split_once(':') {
- vec.push(strip_colors(name));
- }
- }
- vec
- })
- .await
-}
-
-#[poise::command(slash_command, required_permissions = "USE_SLASH_COMMANDS")]
+#[poise::command(
+ slash_command,
+ required_permissions = "USE_SLASH_COMMANDS",
+ category = "Control"
+)]
/// lists the maps.
pub async fn maps(ctx: Context<'_>) -> Result<()> {
- let _ = ctx.defer();
- let maps = get_maps(&ctx.data().stdin).await;
+ let _ = ctx.defer().await;
+ let maps = Maps::get_all(&ctx.data().stdin).await;
poise::send_reply(ctx, |m| {
m.embed(|e| {
for (k, v) in maps.iter().enumerate() {
e.field((k + 1).to_string(), v, true);
}
- e.description("map list.")
+ e.description("map list.").color((34, 139, 34))
})
})
.await?;
Ok(())
}
-async fn autocomplete_map<'a>(
- ctx: Context<'a>,
- partial: &'a str,
-) -> impl futures::Stream<Item = String> + 'a {
- futures::stream::iter(get_maps(&ctx.data().stdin).await)
- .filter(move |name| futures::future::ready(name.starts_with(partial)))
- .map(|name| name.to_string())
-}
-
-async fn mapi(map: &str, stdin: &broadcast::Sender<String>) -> usize {
- get_maps(stdin).await.iter().position(|r| r == map).unwrap()
+#[poise::command(
+ slash_command,
+ required_permissions = "USE_SLASH_COMMANDS",
+ category = "Control"
+)]
+/// lists the currently online players.
+pub async fn players(ctx: Context<'_>) -> Result<()> {
+ let _ = ctx.defer().await;
+ let players = Players::get_all(&ctx.data().stdin).await.unwrap().clone();
+ poise::send_reply(ctx, |m| {
+ m.embed(|e| {
+ if players.is_empty() {
+ return e.title("no players online.").color((255, 69, 0));
+ }
+ e.fields(players.into_iter().map(|p| {
+ (
+ p.name,
+ format!("{id}, {ip}", id = p.uuid, ip = p.ip)
+ + if p.admin { " [A]" } else { "" },
+ true,
+ )
+ }));
+ e.description("currently online players.")
+ .color((255, 165, 0))
+ })
+ })
+ .await?;
+ Ok(())
}
-#[poise::command(slash_command, required_permissions = "USE_SLASH_COMMANDS")]
+#[poise::command(
+ slash_command,
+ required_permissions = "USE_SLASH_COMMANDS",
+ category = "Control"
+)]
/// start the game.
pub async fn start(
ctx: Context<'_>,
#[description = "the map"]
- #[autocomplete = "autocomplete_map"]
+ #[autocomplete = "maps::autocomplete"]
map: String,
) -> Result<()> {
- let _ = ctx.defer();
- ctx.data()
- .stdin
- .send(format!("plague {}", mapi(&map, &ctx.data().stdin).await))
- .unwrap();
+ let _ = ctx.defer().await;
+ send_ctx!(ctx, "host {}", Maps::find(&map, &ctx.data().stdin).await)?;
return_next!(ctx)
}
-#[poise::command(slash_command, required_permissions = "USE_SLASH_COMMANDS")]
+#[poise::command(
+ slash_command,
+ category = "Control",
+ required_permissions = "USE_SLASH_COMMANDS"
+)]
/// end the game.
pub async fn end(
ctx: Context<'_>,
#[description = "the map to go to"]
- #[autocomplete = "autocomplete_map"]
+ #[autocomplete = "maps::autocomplete"]
map: String,
) -> Result<()> {
- let _ = ctx.defer();
- ctx.data()
- .stdin
- .send(format!("endplague {}", mapi(&map, &ctx.data().stdin).await))
- .unwrap();
+ let _ = ctx.defer().await;
+ send_ctx!(
+ ctx,
+ "gameover {}",
+ Maps::find(&map, &ctx.data().stdin).await
+ )?;
return_next!(ctx)
}
diff --git a/src/bot/player.rs b/src/bot/player.rs
new file mode 100644
index 0000000..590f9e4
--- /dev/null
+++ b/src/bot/player.rs
@@ -0,0 +1,85 @@
+use super::{get_nextblock, strip_colors, Context};
+use crate::send;
+use anyhow::Result;
+use futures_util::StreamExt;
+use std::net::Ipv4Addr;
+use std::str::FromStr;
+use std::time::Instant;
+use tokio::sync::{broadcast, MappedMutexGuard, Mutex, MutexGuard};
+
+#[derive(Clone, Debug)]
+pub struct Player {
+ pub admin: bool,
+ pub name: String,
+ pub uuid: String,
+ pub ip: Ipv4Addr,
+}
+
+static PLAYERS: Mutex<(Vec<Player>, Option<Instant>)> = Mutex::const_new((vec![], None));
+
+async fn update(
+ stdin: &broadcast::Sender<String>,
+) -> Result<MutexGuard<(Vec<Player>, Option<Instant>)>> {
+ let mut lock = PLAYERS.lock().await;
+ if lock.1.is_none() || lock.1.unwrap().elapsed().as_millis() > 500 {
+ lock.0 = get_players(stdin).await?;
+ lock.1 = Some(Instant::now());
+ }
+ Ok(lock)
+}
+pub struct Players {}
+impl Players {
+ pub async fn get_all(
+ stdin: &broadcast::Sender<String>,
+ ) -> Result<MappedMutexGuard<Vec<Player>>> {
+ {
+ Ok(MutexGuard::map(update(stdin).await?, |(p, _)| p))
+ }
+ }
+
+ pub async fn find(
+ stdin: &broadcast::Sender<String>,
+ name: String,
+ ) -> Result<Option<MappedMutexGuard<Player>>> {
+ Ok(MutexGuard::try_map(update(stdin).await?, |(p, _)| {
+ p.iter_mut().find(|x| x.name == name)
+ })
+ .ok())
+ }
+}
+
+async fn get_players(stdin: &broadcast::Sender<String>) -> Result<Vec<Player>> {
+ let mut players = vec![];
+ send!(stdin, "players")?;
+ let recv = get_nextblock().await;
+ for line in recv.lines() {
+ if line.starts_with("No") {
+ break;
+ } else if line.is_empty() {
+ continue;
+ }
+ let split = line.split('/').collect::<Vec<&str>>();
+ if split.len() != 3 {
+ continue;
+ }
+ if let Some((admin, name)) = split[0].split_once(' ') {
+ players.push(Player {
+ admin: admin == "[A]",
+ name: strip_colors(name),
+ uuid: split[1].to_owned(),
+ ip: Ipv4Addr::from_str(split[2]).unwrap(),
+ })
+ }
+ }
+ Ok(players)
+}
+
+pub async fn autocomplete<'a>(
+ ctx: Context<'a>,
+ partial: &'a str,
+) -> impl futures::Stream<Item = String> + 'a {
+ let x = Players::get_all(&ctx.data().stdin).await.unwrap().clone();
+ futures::stream::iter(x)
+ .filter(move |p| futures::future::ready(p.name.starts_with(partial)))
+ .map(|p| p.name)
+}
diff --git a/src/main.rs b/src/main.rs
index 48ce3d6..0510033 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,7 @@ use std::str::FromStr;
#[macro_use]
mod logging;
+#[macro_use]
mod bot;
mod process;
mod server;
diff --git a/src/server.rs b/src/server.rs
index 6e0a274..9f8dbd1 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -92,7 +92,7 @@ impl Server {
.unwrap()
});
let mut process_handle = proc.input(stdin).with_state(&state).link();
- Bot::new(state.stdout_plain.subscribe(), state.stdin.clone()).await;
+ Bot::spawn(state.stdout_plain.subscribe(), state.stdin.clone()).await;
tokio::select! {
_ = (&mut server_handle) => process_handle.abort(),
_ = (&mut process_handle) => server_handle.abort(),
diff --git a/src/webhook.rs b/src/webhook.rs
index 6412a8f..7ebc069 100644
--- a/src/webhook.rs
+++ b/src/webhook.rs
@@ -27,6 +27,7 @@ impl<'a> Webhook<'a> {
for<'b> F: FnOnce(&'b mut ExecuteWebhook<'a>) -> &'b mut ExecuteWebhook<'a>,
{
let mut execute_webhook = ExecuteWebhook::default();
+ execute_webhook.allowed_mentions(|m| m.empty_parse());
block(&mut execute_webhook);
let map = json::hashmap_to_json_map(execute_webhook.0);