html terminal
bendn 2024-01-21
parent 00aba89 · commit aad9468
-rw-r--r--Cargo.toml1
-rw-r--r--src/bot/exec.rs161
-rw-r--r--src/bot/js.rs2
-rw-r--r--src/bot/mod.rs2
-rw-r--r--src/bot/status.rs4
-rw-r--r--src/bot/trace.rs3
-rw-r--r--src/bot/voting.rs8
-rw-r--r--src/process.rs2
-rw-r--r--src/webhook.rs6
9 files changed, 176 insertions, 13 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 506b52d..cf0b622 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ tokio = { version = "1.28.2", features = [
"rt",
"parking_lot",
"time",
+ "process",
], default-features = false }
tokio-stream = "0.1.14"
futures-util = "0.3.28"
diff --git a/src/bot/exec.rs b/src/bot/exec.rs
new file mode 100644
index 0000000..8528a8c
--- /dev/null
+++ b/src/bot/exec.rs
@@ -0,0 +1,161 @@
+use super::{Context, Result};
+use emoji::named::*;
+use poise::serenity_prelude::*;
+use std::process::ExitStatus;
+use tokio::sync::broadcast::{channel, error::TryRecvError as ChannelE};
+use tokio::sync::oneshot::{channel as oneshot, error::TryRecvError as OneE};
+
+pub async fn sysadmin(c: Context<'_>) -> Result<bool> {
+ let c = c.author_member().await.ok_or(anyhow::anyhow!("dang"))?;
+ Ok(sys_ck(&c))
+}
+
+pub fn sys_ck(c: &Member) -> bool {
+ c.user.name == "bendn" || c.roles.contains(&RoleId::new(1113997024220696606))
+}
+
+#[poise::command(
+ slash_command,
+ check = "sysadmin",
+ category = "System Administration",
+ default_member_permissions = "ADMINISTRATOR",
+ guild_only
+)]
+/// Executes any command. Please no `rm -rf /*`.
+/// Executes in a shell, so you can >|& as you desire.
+pub async fn exec(
+ c: Context<'_>,
+ #[description = "command to run in bash"] command: String,
+) -> Result<()> {
+ #[derive(Debug, Clone)]
+ enum Payload {
+ Stdout(String),
+ Stderr(String),
+ Exit(ExitStatus),
+ }
+ let (otx, mut orx) = channel::<Payload>(16);
+ let (ttx, mut trx) = oneshot::<()>();
+ let cc = command.clone();
+ tokio::task::spawn(async move {
+ let mut c = tokio::process::Command::new("bash")
+ .arg("-c")
+ .arg(cc)
+ .env("FORCE_COLOR", "1")
+ .current_dir(env!("HOME"))
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .spawn()
+ .unwrap();
+ let mut stdout = c.stdout.take().unwrap();
+ let mut o = Box::new([0; 1 << 20]);
+ let mut stderr = c.stderr.take().unwrap();
+ loop {
+ match trx.try_recv() {
+ Err(e) => match e {
+ OneE::Closed => _ = c.kill().await,
+ OneE::Empty => {}
+ },
+ Ok(()) => c.kill().await.unwrap(),
+ }
+ tokio::select! {
+ n = tokio::io::AsyncReadExt::read(&mut stdout, &mut *o) => {
+ let n = n.unwrap();
+ if n != 0 {
+ let string = String::from_utf8_lossy(&o[..n]).into_owned();
+ otx.send(Payload::Stdout(string)).unwrap();
+ }
+ },
+ () = tokio::time::sleep(tokio::time::Duration::from_millis(50)) => (),
+ };
+ tokio::select! {
+ n = tokio::io::AsyncReadExt::read(&mut stderr, &mut *o) => {
+ let n = n.unwrap();
+ if n != 0 {
+ let string = String::from_utf8_lossy(&o[..n]).into_owned();
+ otx.send(Payload::Stderr(string)).unwrap();
+ }
+ },
+ () = tokio::time::sleep(tokio::time::Duration::from_millis(50)) => (),
+ };
+ if let Ok(Some(n)) = c.try_wait() {
+ otx.send(Payload::Exit(n)).unwrap();
+ break;
+ }
+ tokio::time::sleep(tokio::time::Duration::from_millis(25)).await;
+ }
+ });
+ let h = poise::send_reply(
+ c,
+ poise::CreateReply::default()
+ .content("<a:lod:1198459385790337164>\u{200b}")
+ .components(vec![CreateActionRow::Buttons(vec![CreateButton::new(
+ format!("{}_ctrl_c", c.id()),
+ )
+ .label("C^c")
+ .style(ButtonStyle::Danger)
+ .emoji(CANCEL.parse::<ReactionType>().unwrap())])]),
+ )
+ .await?;
+ let mut dat = String::new();
+ let mut ttx = Some(ttx);
+ let cc = c.serenity_context().clone();
+ let cid = c.id();
+ let k = tokio::spawn(async move {
+ while let Some(x) = tokio::select! {
+ x =async { ComponentInteractionCollector::new(&cc)
+ .filter(move |press| press.data.custom_id.starts_with(&cid.to_string()))
+ .timeout(std::time::Duration::from_secs(60 * 60)).await }=> x,
+ () = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => None,
+ } {
+ if sys_ck(x.member.as_ref().unwrap()) {
+ _ = ttx.take().unwrap().send(());
+ } else {
+ x.create_followup(
+ &cc,
+ CreateInteractionResponseFollowup::default()
+ .ephemeral(true)
+ .content("not admin"),
+ )
+ .await
+ .unwrap();
+ }
+ }
+ });
+
+ loop {
+ match orx.try_recv() {
+ Ok(Payload::Stdout(x) | Payload::Stderr(x)) => {
+ dat.push_str(&x);
+ if dat.len() > 1900 {
+ let mut i = dat.len() - 1900;
+ while !dat.is_char_boundary(i) {
+ i -= 1;
+ }
+ dat.drain(0..i);
+ }
+ h.edit(
+ c,
+ poise::CreateReply::default().content(format!("```ansi\n{dat}\n```")),
+ )
+ .await?;
+ }
+ Ok(Payload::Exit(x)) => {
+ let e = if x.success() { OK } else { CANCEL };
+ h.edit(
+ c,
+ poise::CreateReply::default()
+ .content(format!("{e} ```ansi\n{dat}\n```"))
+ .components(vec![]),
+ )
+ .await?;
+ k.abort();
+ break;
+ }
+ Err(ChannelE::Empty | ChannelE::Lagged(_)) => {}
+ Err(ChannelE::Closed) => panic!(),
+ }
+ tokio::time::sleep(std::time::Duration::from_millis(200)).await;
+ }
+
+ Ok(())
+}
diff --git a/src/bot/js.rs b/src/bot/js.rs
index fa7cbbe..1601319 100644
--- a/src/bot/js.rs
+++ b/src/bot/js.rs
@@ -4,7 +4,7 @@ use std::sync::LazyLock;
fn parse_js(from: &str) -> Result<String> {
static RE: LazyLock<Regex> =
- LazyLock::new(|| Regex::new(r#"```(js|javascript)?([^`]+)```"#).unwrap());
+ LazyLock::new(|| Regex::new(r"```(js|javascript)?([^`]+)```").unwrap());
let mat = RE
.captures(from)
.ok_or(anyhow::anyhow!(r#"no code found (use \`\`\`js...\`\`\`."#))?;
diff --git a/src/bot/mod.rs b/src/bot/mod.rs
index 2713f42..46df650 100644
--- a/src/bot/mod.rs
+++ b/src/bot/mod.rs
@@ -1,6 +1,7 @@
mod admin;
mod bans;
mod config;
+mod exec;
mod js;
mod lb;
pub mod maps;
@@ -213,6 +214,7 @@ impl Bot {
rules::del(),
trace::trace(),
lb::lb(),
+ exec::exec(),
start(),
end(),
help(),
diff --git a/src/bot/status.rs b/src/bot/status.rs
index 308ae42..ffc62cf 100644
--- a/src/bot/status.rs
+++ b/src/bot/status.rs
@@ -42,7 +42,7 @@ pub fn humanize_bytes<T: Into<Size>>(bytes: T) -> String {
return "0 B".to_string();
}
- let base = size.log10() / UNIT.log10();
+ let base = size.log(UNIT);
let result = format!("{:.1}", UNIT.powf(base - base.floor()),)
.trim_end_matches(".0")
@@ -70,7 +70,7 @@ pub async fn command(ctx: Context<'_>) -> Result<()> {
}
let block = tokio::select! {
block = get_nextblock() => block,
- _ = sleep(Duration::from_secs(5)) => fail!(ctx, FAIL),
+ () = sleep(Duration::from_secs(5)) => fail!(ctx, FAIL),
};
let Some((tps, mem, pcount)) = parse(&block) else {
fail!(ctx, FAIL);
diff --git a/src/bot/trace.rs b/src/bot/trace.rs
index f514169..174a871 100644
--- a/src/bot/trace.rs
+++ b/src/bot/trace.rs
@@ -43,8 +43,7 @@ pub async fn trace(
.map(|x| x.roles.clone())
.unwrap_or(vec![])
.iter()
- .find(|&&x| x == 1133416252791074877)
- .is_some(),
+ .any(|&x| x == 1133416252791074877),
_ => unreachable!(),
};
let mut r = poise::CreateReply::default().ephemeral(authorized);
diff --git a/src/bot/voting.rs b/src/bot/voting.rs
index 83807f5..5783f70 100644
--- a/src/bot/voting.rs
+++ b/src/bot/voting.rs
@@ -227,8 +227,8 @@ impl VoteData {
Self::After(y) => {
re = ctx.data().vote_data.lock().unwrap();
match re.get_mut(y.index).unwrap() {
- VoteData::Before(x) => x,
- VoteData::After(_) => unreachable!(),
+ Self::Before(x) => x,
+ Self::After(_) => unreachable!(),
}
}
};
@@ -349,7 +349,7 @@ impl VoteData {
.title(&o.title)
.imageor(o.image.as_ref())
.set_fields(o.fields)
- .description(format!("vote ended!"))
+ .description("vote ended!".to_string())
})
.components(vec![CreateActionRow::Buttons(
o.options
@@ -404,7 +404,7 @@ where
self.as_ref()
.split('|')
.map(|s| {
- use ButtonStyle::*;
+ use ButtonStyle::{Danger, Primary, Secondary, Success};
match s.trim().to_lowercase().as_str() {
// "blue" => Primary,
"gray" => Secondary,
diff --git a/src/process.rs b/src/process.rs
index f9774fa..b856c9d 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -59,7 +59,7 @@ impl Process {
let string = {
let n = tokio::select! {
n = self.inner.read(&mut stdout) => n.unwrap(),
- _ = sleep(Duration::from_millis(100)) => continue,
+ () = sleep(Duration::from_millis(100)) => continue,
};
String::from_utf8_lossy(&strip_ansi_escapes::strip(&stdout[..n])).into_owned()
};
diff --git a/src/webhook.rs b/src/webhook.rs
index dcf9f16..c67b93d 100644
--- a/src/webhook.rs
+++ b/src/webhook.rs
@@ -107,7 +107,7 @@ impl<'a> Webhook<'a> {
}
}
-#[derive(PartialEq, Debug, Clone)]
+#[derive(PartialEq, Eq, Debug, Clone)]
pub enum Message {
Join { player: String },
Left { player: String },
@@ -193,7 +193,7 @@ pub fn mindustry_to_discord(s: &str) -> String {
pub fn unify(s: &str) -> String {
s.chars()
- .filter(|&c| c < '\u{f80}' || c > '\u{107f}')
+ .filter(|&c| !('\u{f80}'..='\u{107f}').contains(&c))
.collect()
}
@@ -254,5 +254,5 @@ fn style() {
#[test]
fn test_unify() {
assert!(unify("grassྱྊၔ") == "grass");
- assert!(unify("иди к черту") == "иди к черту")
+ assert!(unify("иди к черту") == "иди к черту");
}