html terminal
exec
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | src/bot/exec.rs | 161 | ||||
| -rw-r--r-- | src/bot/js.rs | 2 | ||||
| -rw-r--r-- | src/bot/mod.rs | 2 | ||||
| -rw-r--r-- | src/bot/status.rs | 4 | ||||
| -rw-r--r-- | src/bot/trace.rs | 3 | ||||
| -rw-r--r-- | src/bot/voting.rs | 8 | ||||
| -rw-r--r-- | src/process.rs | 2 | ||||
| -rw-r--r-- | src/webhook.rs | 6 |
9 files changed, 176 insertions, 13 deletions
@@ -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("иди к черту") == "иди к черту"); } |