smol bot
user app
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/bot/help_eval_ru.md | 4 | ||||
| -rw-r--r-- | src/bot/map.rs | 171 | ||||
| -rw-r--r-- | src/bot/mod.rs | 122 | ||||
| -rw-r--r-- | src/bot/schematic.rs | 143 | ||||
| -rw-r--r-- | src/main.rs | 2 |
6 files changed, 323 insertions, 125 deletions
@@ -19,7 +19,9 @@ serenity = { version = "0.12", features = [ "gateway", "model", ], default-features = false } -poise = { git = "https://github.com/serenity-rs/poise" } +poise = { git = "https://github.com/fgardt/poise", features = [ + "unstable", +], branch = "feat/user_apps" } anyhow = "1.0.75" regex = { version = "1.8.4", features = ["std"], default-features = false } btparse = "0.1.1" @@ -54,4 +56,4 @@ opt-level = 3 debug-assertions = false [patch.crates-io] -serenity = { git = "https://github.com/bend-n/serenity", branch = "opt" } +serenity = { git = "https://github.com/serenity-rs/serenity" } diff --git a/src/bot/help_eval_ru.md b/src/bot/help_eval_ru.md index 3f0a368..c3e7550 100644 --- a/src/bot/help_eval_ru.md +++ b/src/bot/help_eval_ru.md @@ -1,10 +1,10 @@ ## как использовать `}eval` <:micro_processor:1165059281087889479> -type``` +писать``` }eval ```arm print "xd" ``` -```for the bot to evaluate your чтобы бот оценил ваш MLOG +```чтобы бот оценил ваш MLOG у вас будет доступ к одному большому дисплею. максимальное количество инструкций ограничено 52789849. diff --git a/src/bot/map.rs b/src/bot/map.rs index 3ea848b..4188fc9 100644 --- a/src/bot/map.rs +++ b/src/bot/map.rs @@ -1,72 +1,121 @@ use anyhow::Result; use mindus::{data::map::ReadError, *}; -use poise::serenity_prelude::*; -use std::time::Instant; +use poise::{serenity_prelude::*, CreateReply}; +use std::{ + ops::ControlFlow, + time::{Duration, Instant}, +}; use super::{strip_colors, SUCCESS}; -pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { - let auth = msg.author_nick(c).await.unwrap_or(msg.author.name.clone()); - for a in &msg.attachments { - if a.filename.ends_with("msav") { - let s = a.download().await?; - let then = Instant::now(); +fn string((x, f): (ReadError, &str)) -> String { + match x { + ReadError::Decompress(_) | ReadError::Header(_) => { + format!("not a map.") + } + ReadError::NoSuchBlock(b) => { + format!("couldnt find block `{b}`. error originates from `{f}`") + } + ReadError::Version(v) => { + format!( + "unsupported version: `{v}`. supported versions: `7`. error originates from `{f}`", + ) + } + ReadError::Read(r) => { + format!("failed to read map. error: `{r}`. originates from `{f}`") + } + ReadError::ReadState(r) => { + format!("failed to read dyn data in map. error: `{r}`. originates from `{f}`") + } + } +} + +pub async fn download(a: &Attachment) -> Result<(Result<Map, (ReadError, &str)>, Duration)> { + let s = a.download().await?; + let then = Instant::now(); - macro_rules! dang { - ($fmt:literal $(, $args:expr)+) => {{ - msg.reply_ping(c, format!($fmt $(, $args)+)).await?; - return Ok(()); - }}; - } - // could ignore, but i think if you have a msav, you dont want to ignore failures. - let m = match Map::deserialize(&mut mindus::data::DataRead::new(&s)) { - Ok(m) => m, - Err(ReadError::Decompress(_) | ReadError::Header(_)) => { - dang!("`{}` is not a map.", a.filename) - } - Err(ReadError::NoSuchBlock(b)) => dang!( - "couldnt find block `{b}`. error originates from `{}`", - a.filename - ), - Err(ReadError::Version(v)) => { - dang!( - "unsupported version: `{v}`. supported versions: `7`. error originates from `{}`", - a.filename - ) - } - Err(ReadError::Read(r)) => { - dang!( - "failed to read map. error: `{r}`. originates from `{}`", - a.filename - ) - } - Err(ReadError::ReadState(r)) => { - dang!( - "failed to read dyn data in map. error: `{r}`. originates from `{}`", - a.filename - ) - } - }; - let t = msg.channel_id.start_typing(&c.http); - let deser_took = then.elapsed(); - let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); - let (render_took, compression_took, total, png) = - tokio::task::spawn_blocking(move || { - let render_took = Instant::now(); - let i = m.render(); - let render_took = render_took.elapsed(); - let compression_took = Instant::now(); - let png = super::png(i); - let compression_took = compression_took.elapsed(); - let total = then.elapsed(); - (render_took, compression_took, total, png) - }) - .await?; - t.stop(); - msg.channel_id.send_message(c,CreateMessage::new().add_file(CreateAttachment::bytes(png,"map.png")).embed(CreateEmbed::new().title(&name).footer(CreateEmbedFooter::new(format!("render of {name} (requested by {auth}) took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deser_took.as_millis(), render_took.as_secs_f32(), compression_took.as_secs_f32()))).attachment("map.png").color(SUCCESS))).await?; - return Ok(()); + // could ignore, but i think if you have a msav, you dont want to ignore failures. + Ok(( + Map::deserialize(&mut mindus::data::DataRead::new(&s)).map_err(|x| (x, &*a.filename)), + then.elapsed(), + )) +} + +pub async fn scour(m: &Message) -> Result<Option<(Result<Map, (ReadError, &str)>, Duration)>> { + for a in &m.attachments { + if a.filename.ends_with("msav") { + return Ok(Some(download(a).await?)); } } + Ok(None) +} + +pub async fn reply(a: &Attachment) -> Result<ControlFlow<CreateReply, String>> { + let (m, deser_took) = match download(a).await? { + (Err(e), _) => return Ok(ControlFlow::Continue(string(e))), + (Ok(m), deser_took) => (m, deser_took), + }; + let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); + let ( + Timings { + deser_took, + render_took, + compression_took, + total, + }, + png, + ) = render(m, deser_took).await; + Ok(ControlFlow::Break(CreateReply::default().attachment(CreateAttachment::bytes(png,"map.png")).embed(CreateEmbed::new().title(&name).footer(CreateEmbedFooter::new(format!("render of {name} took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deser_took.as_millis(), render_took.as_secs_f32(), compression_took.as_secs_f32()))).attachment("map.png").color(SUCCESS)))) +} +struct Timings { + deser_took: Duration, + render_took: Duration, + compression_took: Duration, + total: Duration, +} +async fn render(m: Map, deser_took: Duration) -> (Timings, Vec<u8>) { + tokio::task::spawn_blocking(move || { + let render_took = Instant::now(); + let i = m.render(); + let render_took = render_took.elapsed(); + let compression_took = Instant::now(); + let png = super::png(i); + let compression_took = compression_took.elapsed(); + let total = deser_took + render_took + compression_took; + ( + Timings { + deser_took, + render_took, + compression_took, + total, + }, + png, + ) + }) + .await + .unwrap() +} + +pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { + let auth = msg.author_nick(c).await.unwrap_or(msg.author.name.clone()); + let (m, deser_took) = match scour(msg).await? { + None => return Ok(()), + Some((Err(e), _)) => return Ok(drop(msg.reply(c, string(e)).await?)), + Some((Ok(m), deser_took)) => (m, deser_took), + }; + let t = msg.channel_id.start_typing(&c.http); + let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); + let ( + Timings { + deser_took, + render_took, + compression_took, + total, + }, + png, + ) = render(m, deser_took).await; + t.stop(); + msg.channel_id.send_message(c,CreateMessage::new().add_file(CreateAttachment::bytes(png,"map.png")).embed(CreateEmbed::new().title(&name).footer(CreateEmbedFooter::new(format!("render of {name} (requested by {auth}) took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deser_took.as_millis(), render_took.as_secs_f32(), compression_took.as_secs_f32()))).attachment("map.png").color(SUCCESS))).await?; Ok(()) } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 747b24d..82b15cd 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -7,7 +7,7 @@ use anyhow::Result; use dashmap::DashMap; use mindus::data::DataWrite; use mindus::Serializable; -use poise::serenity_prelude::*; +use poise::{serenity_prelude::*, CreateReply}; use serenity::futures::StreamExt; use std::collections::HashSet; use std::fmt::Write; @@ -330,7 +330,7 @@ impl Bot { std::env::var("TOKEN").unwrap_or_else(|_| read_to_string("token").expect("wher token")); let f = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![logic::run(), ping(), help(), scour(), search::search(), search::find(), search::file(), tag()], + commands: vec![logic::run(), ping(), help(), scour(), search::search(), search::find(), search::file(), tag(), render(), render_file(), render_message()], event_handler: |c, e, _, d| { Box::pin(async move { match e { @@ -519,7 +519,7 @@ impl Bot { }) .setup(|ctx, _ready, _| { Box::pin(async move { - poise::builtins::register_globally(ctx, &[logic::run(), help(), ping()]).await?; + poise::builtins::register_globally(ctx, &[logic::run(), help(), ping(), render(), render_file(), render_message()]).await?; poise::builtins::register_in_guild(ctx, &[tag(), search::search(), scour(), search::find(), search::file()], 925674713429184564.into()).await?; println!("registered"); let tracker = Arc::new(DashMap::new()); @@ -644,7 +644,11 @@ pub fn strip_colors(from: &str) -> String { result } -#[poise::command(slash_command)] +#[poise::command( + slash_command, + install_context = "User", + interaction_context = "BotDm|PrivateChannel" +)] pub async fn help( ctx: Context<'_>, #[description = "command to show help about"] @@ -699,7 +703,11 @@ pub fn png(p: fimg::Image<Vec<u8>, 3>) -> Vec<u8> { .unwrap() } -#[poise::command(slash_command)] +#[poise::command( + slash_command, + install_context = "Guild|User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] /// Pong! pub async fn ping(c: Context<'_>) -> Result<()> { use emoji::named::*; @@ -723,3 +731,107 @@ pub async fn ping(c: Context<'_>) -> Result<()> { .await?; Ok(()) } + +#[poise::command( + slash_command, + install_context = "User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] +/// Renders base64 schematic. +pub async fn render(c: Context<'_>, #[description = "schematic, base64"] s: String) -> Result<()> { + let Ok(s) = schematic::from_b64(&s) else { + poise::send_reply( + c, + CreateReply::default() + .content("schem broken / not schem") + .ephemeral(true), + ) + .await?; + return Ok(()); + }; + poise::send_reply( + c, + schematic::reply( + s, + &c.author().name, + &c.author().avatar_url().unwrap_or(CAT.to_string()), + ) + .await?, + ) + .await?; + Ok(()) +} + +#[poise::command( + slash_command, + install_context = "User", + interaction_context = "Guild|BotDm|PrivateChannel" +)] +/// Renders map/msch schematic. +pub async fn render_file( + c: Context<'_>, + #[description = "map / schematic, msch"] s: Attachment, +) -> Result<()> { + _ = c.defer().await; + + let Some(s) = schematic::from_attachments(std::slice::from_ref(&s)).await? else { + match map::reply(&s).await? { + ControlFlow::Break(x) => return Ok(drop(poise::send_reply(c, x).await?)), + ControlFlow::Continue(e) if e != "not a map." => { + return Ok(drop(poise::say_reply(c, e).await?)) + } + ControlFlow::Continue(_) => (), + }; + poise::send_reply( + c, + CreateReply::default() + .content("no schem found") + .ephemeral(true), + ) + .await?; + return Ok(()); + }; + poise::send_reply( + c, + schematic::reply( + s, + &c.author().name, + &c.author().avatar_url().unwrap_or(CAT.to_string()), + ) + .await?, + ) + .await?; + Ok(()) +} + +#[poise::command( + context_menu_command = "Render schematic", + install_context = "User", + interaction_context = "Guild|PrivateChannel" +)] +/// Renders schematic inside a message. +pub async fn render_message(c: Context<'_>, m: Message) -> Result<()> { + let Some(s) = schematic::from((&m.content, &m.attachments)).await? else { + poise::send_reply( + c, + CreateReply::default() + .content("no schem found") + .ephemeral(true), + ) + .await?; + return Ok(()); + }; + poise::send_reply( + c, + schematic::reply( + s, + &m.author_nick(c) + .await + .unwrap_or_else(|| m.author.name.clone()), + &m.author.avatar_url().unwrap_or(CAT.to_string()), + ) + .await?, + ) + .await?; + Ok(()) +} diff --git a/src/bot/schematic.rs b/src/bot/schematic.rs index 8e809e5..9eb4d2b 100644 --- a/src/bot/schematic.rs +++ b/src/bot/schematic.rs @@ -3,7 +3,7 @@ use base64::Engine; use logos::Logos; use mindus::data::DataRead; use mindus::*; -use poise::serenity_prelude::*; +use poise::{serenity_prelude::*, CreateReply}; use regex::Regex; use std::ops::ControlFlow; use std::sync::LazyLock; @@ -59,67 +59,98 @@ pub async fn from_attachments(attchments: &[Attachment]) -> Result<Option<Schem> Ok(None) } +pub async fn reply(v: Schem, author: &str, avatar: &str) -> Result<CreateReply> { + let name = emoji::mindustry::to_discord(&strip_colors(v.tags.get("name").unwrap())); + let vclone = v.clone(); + let p = tokio::task::spawn_blocking(move || to_png(&vclone)).await?; + println!("rend {name}"); + Ok(CreateReply::default() + .attachment(CreateAttachment::bytes(p, "image.png")) + .embed({ + let mut e = CreateEmbed::new() + .attachment("image.png") + .author(CreateEmbedAuthor::new(author).icon_url(avatar)); + if let Some(tags) = tags(&v) { + e = e.field("tags", tags, true); + } + if let Some(v) = v + .tags + .get("description") + .map(|t| emoji::mindustry::to_discord(&strip_colors(t))) + { + e = e.description(v); + } + e.field("req", cost(&v), true) + .title(name.clone()) + .color(SUCCESS) + })) +} + +fn tags(v: &Schem) -> Option<String> { + v.tags.get("labels").map(|tags| { + decode_tags(tags) + .iter() + .map(String::as_str) + .intersperse(" | ") + .fold(String::new(), |acc, x| acc + x) + }) +} + +fn cost(v: &Schem) -> String { + let mut s = String::new(); + for (i, n) in v.compute_total_cost().0.iter() { + if n == 0 { + continue; + } + write!(s, "{} {n} ", emoji::mindustry::item(i)).unwrap(); + } + s +} + +pub async fn send( + m: Msg, + c: &serenity::client::Context, + v: Schem, +) -> Result<(poise::serenity_prelude::Message, std::string::String, Schem)> { + let name = emoji::mindustry::to_discord(&strip_colors(v.tags.get("name").unwrap())); + println!("deser {name}"); + let vclone = v.clone(); + let p = tokio::task::spawn_blocking(move || to_png(&vclone)).await?; + println!("rend {name}"); + let msg = CreateMessage::new() + .add_file(CreateAttachment::bytes(p, "image.png")) + .embed({ + let mut e = CreateEmbed::new() + .attachment("image.png") + .author(CreateEmbedAuthor::new(m.author).icon_url(m.avatar)); + if let Some(tags) = tags(&v) { + e = e.field("tags", tags, true); + } + if let Some(v) = v + .tags + .get("description") + .map(|t| emoji::mindustry::to_discord(&strip_colors(t))) + { + e = e.description(v); + } + e.field("req", cost(&v), true) + .title(name.clone()) + .color(SUCCESS) + }); + let h = m.channel.send_message(c, msg).await?; + Ok((h, name, v)) +} + pub async fn with( m: Msg, c: &serenity::client::Context, labels: Option<String>, ) -> Result<ControlFlow<(Message, String, Schem), ()>> { - let author = m.author; - let send = |v: Schem| async move { - let name = emoji::mindustry::to_discord(&strip_colors(v.tags.get("name").unwrap())); - println!("deser {name}"); - let vclone = v.clone(); - let p = tokio::task::spawn_blocking(move || to_png(&vclone)).await?; - println!("rend {name}"); - anyhow::Ok(( - m.channel - .send_message( - c, - CreateMessage::new() - .add_file(CreateAttachment::bytes(p, "image.png")) - .embed({ - let mut e = CreateEmbed::new() - .attachment("image.png") - .author(CreateEmbedAuthor::new(author).icon_url(m.avatar)); - if let Some(tags) = v.tags.get("labels") { - e = e.field( - "tags", - decode_tags(tags) - .iter() - .map(String::as_str) - .intersperse(" | ") - .fold(String::new(), |acc, x| acc + x), - true, - ); - } - if let Some(v) = v - .tags - .get("description") - .map(|t| emoji::mindustry::to_discord(&strip_colors(t))) - { - e = e.description(v); - } - let mut s = String::new(); - for (i, n) in v.compute_total_cost().0.iter() { - if n == 0 { - continue; - } - write!(s, "{} {n} ", emoji::mindustry::item(i)).unwrap(); - } - e.field("req", s, true).title(name.clone()).color(SUCCESS) - }), - ) - .await?, - name, - v, - )) - }; - if let Ok(Some(mut v)) = from((&m.content, &m.attachments)).await { labels.map(|x| { v.schem.tags.insert("labels".into(), x); }); - return Ok(ControlFlow::Break(send(v).await?)); + return Ok(ControlFlow::Break(send(m, c, v).await?)); } Ok(ControlFlow::Continue(())) @@ -145,6 +176,10 @@ pub fn from_msg(msg: &str) -> Result<Option<Schem>> { .unwrap() .as_str() .trim(); + Ok(Some(from_b64(schem_text)?)) +} + +pub fn from_b64(schem_text: &str) -> Result<Schem> { let mut buff = vec![0; schem_text.len() / 4 * 3 + 1]; let s = base64::engine::general_purpose::STANDARD .decode_slice(schem_text.as_bytes(), &mut buff) @@ -153,7 +188,7 @@ pub fn from_msg(msg: &str) -> Result<Option<Schem>> { buff.truncate(n_out); Schematic::deserialize(&mut DataRead::new(&buff)).map_err(anyhow::Error::from) })?; - Ok(Some(Schem { schem: s })) + Ok(Schem { schem: s }) } fn decode_tags(tags: &str) -> Vec<String> { diff --git a/src/main.rs b/src/main.rs index 1d620e2..ea43871 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![feature(lazy_cell, let_chains, iter_intersperse, if_let_guard, const_mut_refs)] +#![feature(let_chains, iter_intersperse, if_let_guard, const_mut_refs)] use std::{sync::OnceLock, time::Instant}; #[macro_use] |