smol bot
user app
bendn 2024-06-13
parent 6926db3 · commit fdeeb42
-rw-r--r--Cargo.toml6
-rw-r--r--src/bot/help_eval_ru.md4
-rw-r--r--src/bot/map.rs171
-rw-r--r--src/bot/mod.rs122
-rw-r--r--src/bot/schematic.rs143
-rw-r--r--src/main.rs2
6 files changed, 323 insertions, 125 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3a6b45c..c0d6f18 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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]