html terminal
remove /draw_schematic
| -rw-r--r-- | src/bot/js.rs | 3 | ||||
| -rw-r--r-- | src/bot/maps.rs | 4 | ||||
| -rw-r--r-- | src/bot/mod.rs | 139 | ||||
| -rw-r--r-- | src/bot/schematic.rs | 123 | ||||
| -rw-r--r-- | src/bot/status.rs | 2 | ||||
| -rw-r--r-- | src/bot/usage.md | 4 | ||||
| -rw-r--r-- | src/bot/voting.rs | 2 |
7 files changed, 164 insertions, 113 deletions
diff --git a/src/bot/js.rs b/src/bot/js.rs index 0b67e33..bd11c18 100644 --- a/src/bot/js.rs +++ b/src/bot/js.rs @@ -33,7 +33,8 @@ fn parse_js(from: &str) -> Result<String> { required_permissions = "ADMINISTRATOR", category = "Control", track_edits, - rename = "js" + rename = "js", + check = "crate::bot::in_guild" )] /// run arbitrary javascript pub async fn run( diff --git a/src/bot/maps.rs b/src/bot/maps.rs index 89e363c..55b32da 100644 --- a/src/bot/maps.rs +++ b/src/bot/maps.rs @@ -45,7 +45,7 @@ pub async fn autocomplete<'a>( .map(ToString::to_string) } -#[poise::command(slash_command, prefix_command, category = "Info", rename = "maps")] +#[poise::command(slash_command, category = "Info", rename = "maps")] /// lists the maps. pub async fn list(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer_or_broadcast().await; @@ -142,7 +142,7 @@ impl MapImage { } } -#[poise::command(slash_command, prefix_command, category = "Info")] +#[poise::command(slash_command, category = "Info")] /// look at the current game. pub async fn view(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer_or_broadcast().await; diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 6912448..0dd57a5 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -12,7 +12,9 @@ use crate::webhook::Webhook; use anyhow::Result; use maps::Maps; +use poise::serenity_prelude::GuildId; use serenity::http::Http; +use serenity::model::channel::Message; use serenity::prelude::*; use std::fmt::Write; use std::fs::read_to_string; @@ -44,15 +46,76 @@ macro_rules! send_ctx { }; } -#[cfg(not(debug_assertions))] +const SOURCE_GUILD: u64 = 1003092764919091282; +const ARROW_EMOJI: u64 = 1142290560275718194; const PFX: &str = ">"; #[cfg(debug_assertions)] -const PFX: &str = "-"; +const GUILD: u64 = SOURCE_GUILD; +#[cfg(debug_assertions)] +const CHANNEL: u64 = 1003092765581787279; +#[cfg(not(debug_assertions))] +const GUILD: u64 = 1110086242177142854; +#[cfg(not(debug_assertions))] +const CHANNEL: u64 = 1142100900442296441; const SUCCESS: (u8, u8, u8) = (34, 139, 34); const FAIL: (u8, u8, u8) = (255, 69, 0); const DISABLED: (u8, u8, u8) = (112, 128, 144); +pub async fn in_guild(ctx: Context<'_>) -> Result<bool> { + Ok(ctx.guild_id().map_or(false, |i| i.0 == GUILD)) +} + +pub async fn safe(m: &Message, c: &serenity::client::Context) -> String { + let mut result = m.content.clone(); + + for u in &m.mentions { + let mut at_distinct = String::with_capacity(33); + at_distinct.push('@'); + at_distinct.push_str(&u.nick_in(c, GuildId(GUILD)).await.unwrap_or(u.name.clone())); + + let mut m = u.mention().to_string(); + if !result.contains(&m) { + m.insert(2, '!'); + } + result = result.replace(&m, &at_distinct); + } + + for id in &m.mention_roles { + let mention = id.mention().to_string(); + + if let Some(role) = id.to_role_cached(&c) { + result = result.replace(&mention, &["@", &role.name].concat()); + } else { + result = result.replace(&mention, "@deleted-role"); + } + } + result +} + +pub async fn say(c: &serenity::client::Context, m: &Message, d: &Data) -> Result<()> { + let n = m + .author_nick(&c.http) + .await + .unwrap_or_else(|| m.author.name.replace("ggfenguin", "eris")); + for l in safe(m, c).await.lines() { + if send!( + d.stdin, + "say [royal] [coral][[[scarlet]{n}[coral]]:[white] {l}" + ) + .is_err() + { + return Ok(()); + }; + } + m.react( + &c.http, + c.http.get_emoji(SOURCE_GUILD, ARROW_EMOJI).await.unwrap(), + ) + .await?; + Ok(()) +} + pub struct Bot; impl Bot { pub async fn spawn(stdout: broadcast::Receiver<String>, stdin: broadcast::Sender<String>) { @@ -77,8 +140,6 @@ impl Bot { voting::create(), voting::fixall(), voting::list(), - schematic::draw(), - schematic::context_draw(), start(), end(), help(), @@ -90,35 +151,17 @@ impl Bot { println!("bot ready"); } poise::Event::Message { new_message } => { - if [1142100900442296441, 1003092765581787279] - .contains(new_message.channel_id.as_u64()) - && !new_message.content.starts_with('!') - && !new_message.content.starts_with(PFX) - && !new_message.author.bot + if new_message.content.starts_with('!') + || new_message.content.starts_with(PFX) + || new_message.author.bot { - if send!( - d.stdin, - "say [royal][] [scarlet][[{}]:[] {}", - new_message - .author_nick(&c.http) - .await - .unwrap_or_else(|| new_message.author.name.clone()), - new_message.content_safe(&c.cache).replace('\n', "; ") - ) - .is_err() - { - return Ok(()); - }; - new_message - .react( - &c.http, - c.http - .get_emoji(1003092764919091282, 1142290560275718194) - .await - .unwrap(), - ) - .await - .unwrap(); + return Ok(()); + } + if schematic::with(new_message, c).await?.is_break() { + return Ok(()); + } + if CHANNEL == new_message.channel_id.0 { + say(c, new_message, d).await?; } } _ => {} @@ -141,7 +184,14 @@ impl Bot { .setup(|ctx, _ready, framework| { Box::pin(async move { println!("registering"); - poise::builtins::register_globally(ctx, &framework.options().commands).await?; + poise::builtins::register_in_guild( + ctx, + &framework.options().commands[..18], + GuildId(GUILD), + ) + .await?; + poise::builtins::register_globally(ctx, &framework.options().commands[18..]) + .await?; println!("registered"); Ok(Data { stdin, @@ -206,6 +256,7 @@ async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) { #[poise::command( prefix_command, + check = "crate::bot::in_guild", required_permissions = "ADMINISTRATOR", default_member_permissions = "ADMINISTRATOR", category = "Control", @@ -305,14 +356,18 @@ pub async fn help( #[autocomplete = "poise::builtins::autocomplete_command"] command: Option<String>, ) -> Result<()> { - poise::builtins::help( - ctx, - command.as_deref(), - poise::builtins::HelpConfiguration { - extra_text_at_bottom: "Mindustry server management bot", - ..Default::default() - }, - ) - .await?; + if in_guild(ctx).await.unwrap() { + poise::builtins::help( + ctx, + command.as_deref(), + poise::builtins::HelpConfiguration { + extra_text_at_bottom: "Mindustry server management bot", + ..Default::default() + }, + ) + .await?; + } else { + ctx.say(include_str!("usage.md")).await?; + } Ok(()) } diff --git a/src/bot/schematic.rs b/src/bot/schematic.rs index 41e6cd8..370e2cb 100644 --- a/src/bot/schematic.rs +++ b/src/bot/schematic.rs @@ -1,48 +1,67 @@ -use super::{strip_colors, Context, SUCCESS}; use anyhow::{anyhow, Result}; -use mindus::data::{DataRead, DataWrite}; +use mindus::data::DataRead; use mindus::*; use oxipng::*; use poise::serenity_prelude::*; use regex::Regex; -use std::borrow::Cow; -use std::path::Path; use std::sync::LazyLock; +use std::{borrow::Cow, ops::ControlFlow}; + +use super::{strip_colors, SUCCESS}; static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(```)?(\n)?([^`]+)(\n)?(```)?").unwrap()); -#[poise::command(context_menu_command = "Render schematic", category = "Info")] -/// draw schematic. -pub async fn context_draw(ctx: Context<'_>, msg: Message) -> Result<()> { - let _ = ctx.defer_or_broadcast().await; - - if let Some(a) = msg.attachments.get(0) - && let Some(e) = Path::new(&a.filename).extension() - && e == "msch" { +pub async fn from_attachments(attchments: &[Attachment]) -> Result<Option<Schematic<'_>>> { + for a in attchments { + if a.filename.ends_with("msch") { let s = a.download().await?; let mut s = DataRead::new(&s); - let s = Schematic::deserialize(&mut s).map_err(|e| anyhow!("invalid schematic: {e} with file {}", a.filename))?; - return send(&ctx, &s, false).await; + let Ok(s) = Schematic::deserialize(&mut s) else { + continue; + }; + return Ok(Some(s)); + } } - draw_impl(ctx, &msg.content, false).await + Ok(None) } -#[poise::command( - prefix_command, - slash_command, - category = "Info", - rename = "draw_schematic", - track_edits -)] -/// draw schematic. -pub async fn draw(ctx: Context<'_>, schematic: String) -> Result<()> { - let _ = ctx.defer_or_broadcast().await; - draw_impl(ctx, &schematic, true).await +pub async fn with(m: &Message, c: &serenity::client::Context) -> Result<ControlFlow<(), ()>> { + let send = |v| async move { + let p = to_png(&v); + let author = m.author_nick(c).await.unwrap_or(m.author.name.clone()); + m.channel_id + .send_message(c, |m| { + m.add_file(AttachmentType::Bytes { + data: Cow::Owned(p), + filename: "image.png".to_string(), + }) + .embed(|e| { + e.attachment("image.png"); + if let Some(d) = v.tags.get("description") { + e.description(d); + } + e.title(strip_colors(v.tags.get("name").unwrap())) + .footer(|f| f.text(format!("requested by {author}",))) + .color(SUCCESS) + }) + }) + .await?; + anyhow::Ok(()) + }; + + if let Ok(Some(v)) = from_attachments(&m.attachments).await { + send(v).await?; + return Ok(ControlFlow::Break(())); + } + if let Ok(v) = from_msg(&m.content) { + send(v).await?; + return Ok(ControlFlow::Break(())); + } + Ok(ControlFlow::Continue(())) } -async fn send(ctx: &Context<'_>, s: &Schematic<'_>, send_schematic: bool) -> Result<()> { - let n = strip_colors(s.tags.get("name").unwrap()); +pub fn to_png(s: &Schematic<'_>) -> Vec<u8> { let p = s.render(); let p = RawImage::new( p.width(), @@ -54,51 +73,23 @@ async fn send(ctx: &Context<'_>, s: &Schematic<'_>, send_schematic: bool) -> Res p.buffer, ) .unwrap(); - let p = p - .create_optimized_png(&oxipng::Options { - filter: indexset! { RowFilter::None }, - bit_depth_reduction: false, - color_type_reduction: false, - palette_reduction: false, - grayscale_reduction: false, - ..oxipng::Options::from_preset(0) - }) - .unwrap(); - poise::send_reply(*ctx, |m| { - if send_schematic { - let mut out = DataWrite::default(); - s.serialize(&mut out).unwrap(); - m.attachment(AttachmentType::Bytes { - data: Cow::Owned(out.consume()), - filename: "schem.msch".to_string(), - }); - } - m.attachment(AttachmentType::Bytes { - data: Cow::Owned(p), - filename: "image.png".to_string(), - }) - .embed(|e| { - if let Some(d) = s.tags.get("description") { - e.description(d); - } - if send_schematic { - e.attachment("schem.msch"); - } - e.title(n).attachment("image.png").color(SUCCESS) - }) + p.create_optimized_png(&oxipng::Options { + filter: indexset! { RowFilter::None }, + bit_depth_reduction: false, + color_type_reduction: false, + palette_reduction: false, + grayscale_reduction: false, + ..oxipng::Options::from_preset(0) }) - .await?; - Ok(()) + .unwrap() } -async fn draw_impl(ctx: Context<'_>, msg: &str, send_schematic: bool) -> Result<()> { +pub fn from_msg<'l>(msg: &str) -> Result<Schematic<'l>> { let schem_text = RE .captures(msg) .ok_or(anyhow!("couldnt find schematic"))? .get(3) .unwrap() .as_str(); - let s = Schematic::deserialize_base64(schem_text) - .map_err(|e| anyhow!("schematic deserializatiion failed: {e}"))?; - send(&ctx, &s, send_schematic).await + Ok(Schematic::deserialize_base64(schem_text)?) } diff --git a/src/bot/status.rs b/src/bot/status.rs index d8cd443..852042f 100644 --- a/src/bot/status.rs +++ b/src/bot/status.rs @@ -51,7 +51,7 @@ pub fn humanize_bytes<T: Into<Size>>(bytes: T) -> String { [&result, SUFFIX[base.floor() as usize]].join(" ") } -#[poise::command(prefix_command, slash_command, category = "Info", rename = "status")] +#[poise::command(slash_command, category = "Info", rename = "status")] /// server status. pub async fn command(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer_or_broadcast().await; diff --git a/src/bot/usage.md b/src/bot/usage.md new file mode 100644 index 0000000..625ec42 --- /dev/null +++ b/src/bot/usage.md @@ -0,0 +1,4 @@ +## Usage + +upload a file, with a msch extension (eg `24tpi_imp.msch`) and a schematic preview will be generated. +you may instead upload a message containing a base64 encoded schematic.
\ No newline at end of file diff --git a/src/bot/voting.rs b/src/bot/voting.rs index 2de5c96..61b789e 100644 --- a/src/bot/voting.rs +++ b/src/bot/voting.rs @@ -556,7 +556,7 @@ pub async fn fixall(ctx: Context<'_>) -> Result<()> { // voters -#[poise::command(prefix_command, slash_command, category = "Discord", rename = "votes")] +#[poise::command(slash_command, category = "Discord", rename = "votes")] pub async fn list(ctx: Context<'_>, #[description = "the vote title"] vote: String) -> Result<()> { let vd = { let buf = ctx.data().vote_data.lock().unwrap(); |