smol bot
| -rw-r--r-- | Cargo.toml | 9 | ||||
| -rw-r--r-- | build.rs | 6 | ||||
| -rw-r--r-- | html-src/index.html | 15 | ||||
| -rw-r--r-- | src/bot/map.rs | 136 | ||||
| -rw-r--r-- | src/bot/mod.rs | 34 | ||||
| -rw-r--r-- | src/bot/sorter.rs | 13 | ||||
| -rw-r--r-- | src/bot/usage.md | 1 | ||||
| -rw-r--r-- | src/main.rs | 1 |
8 files changed, 151 insertions, 64 deletions
@@ -1,7 +1,7 @@ [package] name = "plent" version = "0.1.0" -edition = "2021" +edition = "2024" [dependencies] paste = "1.0.12" @@ -19,7 +19,7 @@ serenity = { version = "0.12", features = [ "gateway", "model", ], default-features = false } -poise = { git = "https://github.com/fgardt/poise", branch = "feat/user_apps" } +poise = { git = "https://github.com/serenity-rs/poise", branch = "current" } anyhow = "1.0.75" regex = { version = "1.8.4", features = ["std"], default-features = false } mindus = { version = "5.0.7", features = [], default-features = false } @@ -62,6 +62,7 @@ kv = "0.24.0" sled = { version = "0.34.7", features = ["compression"] } remapper = { version = "0.1.0", path = "../remapper" } implicit-fn = "0.1.0" +sql = "0.4.3" [features] server = ["axum"] @@ -72,9 +73,9 @@ emojib = { git = "https://github.com/Apricot-Conservation-Project/emoji", featur ], package = "emoji" } [profile.release] -# strip = true +strip = true lto = "thin" -debug = 2 +# debug = 2 [profile.dev.package.mindus] opt-level = 3 @@ -35,9 +35,9 @@ fn main() -> std::io::Result<()> { emojib::load(); - for path in fs::read_dir("html-src")? { - process(path.unwrap().path().file_name().unwrap())?; - } + // for path in fs::read_dir("html-src")? { + // process(path.unwrap().path().file_name().unwrap())?; + // } println!("cargo:rerun-if-changed=html-src/"); println!("cargo:rerun-if-changed=build.rs"); Ok(()) diff --git a/html-src/index.html b/html-src/index.html index 4befc53..653b49a 100644 --- a/html-src/index.html +++ b/html-src/index.html @@ -2,6 +2,16 @@ <html lang="en"> <head> + <!-- Google tag (gtag.js) --> + <script async src="https://www.googletagmanager.com/gtag/js?id=G-M97NB6WZZF"></script> + <script> + window.dataLayer = window.dataLayer || []; + function gtag() { dataLayer.push(arguments); } + gtag('js', new Date()); + + gtag('config', 'G-M97NB6WZZF'); + </script> + <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="shortcut icon" href="https://apricotalliance.org/schems/favicon.ico" /> @@ -203,6 +213,11 @@ </div> </div> + <a target="_blank" href="https://discord.gg/SsDYC4kKbb" class="link"><img class="link" + src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MCIgaGVpZ2h0PSI3MCIgdmlld0JveD0iMCAwIDE3NSAxNzUiPjxjaXJjbGUgY3g9Ijg3LjUiIGN5PSI4Ny41IiByPSI4Ny41IiBzdHlsZT0iZmlsbDojM2UzYjNiO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyOC4zNDY1O3N0cm9rZS1saW5lam9pbjpiZXZlbDtmaWxsLW9wYWNpdHk6MSIvPjxwYXRoIGQ9Ik0xMzEuNjMgNDcuMzlhMTA1LjE1IDEwNS4xNSAwIDAgMC0yNi4yMy04LjA3IDcyLjA2IDcyLjA2IDAgMCAwLTMuMzYgNi44MyA5Ny42OCA5Ny42OCAwIDAgMC0yOS4xMSAwIDcyLjM3IDcyLjM3IDAgMCAwLTMuMzYtNi44MyAxMDUuODkgMTA1Ljg5IDAgMCAwLTI2LjI1IDguMDljLTE2LjYgMjQuNTYtMjEuMSA0OC41MS0xOC44NSA3Mi4xMmExMDUuNzMgMTA1LjczIDAgMCAwIDMyLjE3IDE2LjE1IDc3LjcgNzcuNyAwIDAgMCA2Ljg5LTExLjExIDY4LjQyIDY4LjQyIDAgMCAxLTEwLjg1LTUuMThjLjkxLS42NiAxLjgtMS4zNCAyLjY2LTJhNzUuNTcgNzUuNTcgMCAwIDAgNjQuMzIgMGMuODcuNzEgMS43NiAxLjM5IDIuNjYgMmE2OC42OCA2OC42OCAwIDAgMS0xMC44NyA1LjE5IDc3IDc3IDAgMCAwIDYuODkgMTEuMSAxMDUuMjUgMTA1LjI1IDAgMCAwIDMyLjE5LTE2LjE0YzIuNjQtMjcuMzgtNC41MS01MS4xMS0xOC45LTcyLjE1em0tNjUuMjUgNTcuNjJjLTYuMjcgMC0xMS40NS01LjY5LTExLjQ1LTEyLjY5IDAtNyA1LTEyLjc0IDExLjQzLTEyLjc0czExLjU3IDUuNzQgMTEuNDYgMTIuNzRjLS4xMSA3LTUuMDUgMTIuNjktMTEuNDQgMTIuNjl6bTQyLjI0IDBjLTYuMjggMC0xMS40NC01LjY5LTExLjQ0LTEyLjY5IDAtNyA1LTEyLjc0IDExLjQ0LTEyLjc0IDYuNDQgMCAxMS41NCA1Ljc0IDExLjQzIDEyLjc0LS4xMSA3LTUuMDQgMTIuNjktMTEuNDMgMTIuNjl6IiBzdHlsZT0iZmlsbDojNTg2NWYyIi8+PC9zdmc+" + alt="discord"> + </a> + <div id="grid"> <script type="module" src="/schems/index.js"></script> <script> diff --git a/src/bot/map.rs b/src/bot/map.rs index 76023e0..c790eae 100644 --- a/src/bot/map.rs +++ b/src/bot/map.rs @@ -1,12 +1,13 @@ use anyhow::Result; use mindus::{data::map::ReadError, *}; -use poise::{serenity_prelude::*, CreateReply}; +use poise::{CreateReply, serenity_prelude::*}; use std::{ ops::ControlFlow, time::{Duration, Instant}, }; - -use super::{strip_colors, SUCCESS}; +use tokio::task::JoinError; +const BENDN: ChannelId = ChannelId::new(1149866218057117747); +use super::{SUCCESS, strip_colors}; fn string((x, f): (ReadError, &str)) -> String { match x { @@ -30,18 +31,24 @@ fn string((x, f): (ReadError, &str)) -> String { } } -pub async fn download(a: &Attachment) -> Result<(Result<Map, (ReadError, &str)>, Duration)> { - let s = a.download().await?; +pub async fn download( + a: &Attachment, +) -> Result<(Result<(Map, Box<[u8]>), (ReadError, &str)>, Duration)> { + let s = a.download().await?.into_boxed_slice(); let then = Instant::now(); // 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)), + Map::deserialize(&mut mindus::data::DataRead::new(&s)) + .map_err(|x| (x, &*a.filename)) + .map(|x| (x, s)), then.elapsed(), )) } -pub async fn scour(m: &Message) -> Result<Option<(Result<Map, (ReadError, &str)>, Duration)>> { +pub async fn scour( + m: &Message, +) -> Result<Option<(Result<(Map, Box<[u8]>), (ReadError, &str)>, Duration)>> { for a in &m.attachments { if a.filename.ends_with("msav") { return Ok(Some(download(a).await?)); @@ -50,12 +57,27 @@ pub async fn scour(m: &Message) -> Result<Option<(Result<Map, (ReadError, &str)> Ok(None) } -pub async fn reply(a: &Attachment) -> Result<ControlFlow<CreateReply, String>> { - let (m, deser_took) = match download(a).await? { +pub async fn reply( + c: super::Context<'_>, + a: &Attachment, +) -> Result<ControlFlow<CreateReply, String>> { + let ((m, b), deser_took) = match download(a).await? { (Err(e), _) => return Ok(ControlFlow::Continue(string(e))), (Ok(m), deser_took) => (m, deser_took), }; - let (a, e) = embed(m, deser_took).await; + let (a, e) = match embed(m, deser_took).await { + Ok(x) => x, + Err(e) => { + BENDN.send_files( + &c, + [CreateAttachment::bytes(b, "map.msav")], + CreateMessage::new().content(format!("<@696196765564534825> failure: {e}")), + ); + return Ok(ControlFlow::Break(CreateReply::default().content( + "there was a problem. i have notified bendn about this issue.", + ))); + } + }; Ok(ControlFlow::Break( CreateReply::default().attachment(a).embed(e), )) @@ -67,33 +89,29 @@ struct Timings { 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() +fn render(m: Map, deser_took: Duration) -> (Timings, Vec<u8>) { + 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, + ) } pub async fn find( msg: &Message, c: &serenity::client::Context, -) -> Result<Option<(String, Map, Duration)>> { +) -> Result<Option<(String, (Map, Box<[u8]>), Duration)>> { match scour(msg).await? { None => Ok(None), Some((Err(e), _)) => { @@ -108,12 +126,37 @@ pub async fn find( } } +#[implicit_fn::implicit_fn] pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { - let Some((_auth, m, deser_took)) = find(msg, c).await? else { + let Some((_auth, (m, b), deser_took)) = find(msg, c).await? else { return Ok(()); }; let t = msg.channel_id.start_typing(&c.http); - let (png, embed) = embed(m, deser_took).await; + let (png, embed) = match embed(m, deser_took).await { + Ok(x) => x, + Err(e) => { + use crate::emoji::named::*; + + BENDN + .send_files( + &c, + [CreateAttachment::bytes(b, "file.msch")], + CreateMessage::new().content(format!( + "<@696196765564534825> panic `{e}` in {}\n", + msg.guild(&c.cache).map_or( + msg.guild_id + .map(_.get().to_string()) + .unwrap_or(format!("dms with {}", msg.author.name)), + _.name.clone(), + ) + )), + ) + .await?; + msg.reply(c, format!("{CANCEL} there was an error while rendering this map.\nthis issue has been reported to bendn, who will hopefully take a look eventually.")) + .await?; + return Ok(()); + } + }; t.stop(); super::data::push_j(serde_json::json! {{ "locale": msg.author.locale.as_deref().unwrap_or("no locale"), @@ -129,7 +172,7 @@ pub async fn with(msg: &Message, c: &serenity::client::Context) -> Result<()> { Ok(()) } -async fn embed(m: Map, deser_took: Duration) -> (CreateAttachment, CreateEmbed) { +async fn embed(m: Map, deser_took: Duration) -> Result<(CreateAttachment, CreateEmbed), JoinError> { let name = strip_colors(m.tags.get("name").or(m.tags.get("mapname")).unwrap()); let d = strip_colors(m.tags.get("description").map(|x| &**x).unwrap_or("?")); let f = if m.width == m.height { @@ -137,8 +180,8 @@ async fn embed(m: Map, deser_took: Duration) -> (CreateAttachment, CreateEmbed) } else { format!("{}×{}", m.height, m.width) }; - let (timings, png) = render(m, deser_took).await; - ( + let (timings, png) = tokio::task::spawn_blocking(move || render(m, deser_took)).await?; + Ok(( CreateAttachment::bytes(png, "map.png"), CreateEmbed::new() .title(&name) @@ -149,7 +192,7 @@ async fn embed(m: Map, deser_took: Duration) -> (CreateAttachment, CreateEmbed) ))) .attachment("map.png") .color(SUCCESS), - ) + )) } #[poise::command( @@ -160,11 +203,24 @@ async fn embed(m: Map, deser_took: Duration) -> (CreateAttachment, CreateEmbed) /// Renders map inside a message. pub async fn render_message(c: super::Context<'_>, m: Message) -> Result<()> { super::log(&c); - let Some((_auth, m, deser_took)) = find(&m, c.serenity_context()).await? else { + let Some((_auth, (m, b), deser_took)) = find(&m, c.serenity_context()).await? else { poise::say_reply(c, "no map").await?; return Ok(()); }; - let (png, embed) = embed(m, deser_took).await; + let (png, embed) = match embed(m, deser_took).await { + Ok(x) => x, + Err(e) => { + BENDN.send_files( + &c, + [CreateAttachment::bytes(b, "map.msav")], + CreateMessage::new().content(format!("<@696196765564534825> failure: {e}")), + ); + c.say("there was a problem. i have notified bendn about this issue.") + .await?; + return Ok(()); + } + }; + poise::send_reply(c, CreateReply::default().attachment(png).embed(embed)).await?; Ok(()) } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index 1044e87..519b552 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -188,10 +188,10 @@ async fn del(c: &serenity::prelude::Context,& Ch{ d:dir, repo: git, ..}: &Ch, de static HOOK: OnceLock<Webhook> = OnceLock::new(); pub async fn hookup(c: &impl AsRef<Http>) { - let v = Webhook::from_url(c, { + let v = Webhook::from_url(c, &std::env::var("WEBHOOK") .unwrap_or_else(|_| read_to_string("webhook").expect("wher webhook")) - }) + ) .await .unwrap(); HOOK.get_or_init(|| v); @@ -229,6 +229,18 @@ async fn handle_message( .unwrap_or(new_message.author.name.clone()); let post = EXTRA.get(&new_message.channel_id.get()).map(|x| x.clone()); let (dir, l, repo) = sep(SPECIAL.get(&new_message.channel_id.get()).or(post.as_ref())); + if new_message.author.id.get()==OWNER&&new_message.content.contains("meow"){ + new_message.author + .direct_message( + c, + poise::serenity_prelude::CreateMessage::new().content(format!( + "your match `meow` was satisfied on message ```\n{}\n``` {}", + new_message.content.replace('`', "`"), + new_message.link() + )), + ) + .await?; + } let m = Msg { author: who.clone(), locale: new_message.author.locale.clone().unwrap_or("unknown locale".to_string()), @@ -318,6 +330,7 @@ impl Bot { FullEvent::Ready { .. } => { println!("bot ready"); while SEEN.lock().await.len() < 5 { + tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } let mut x = SEEN.lock().await.clone().into_iter().collect::<Vec<_>>(); @@ -868,6 +881,10 @@ pub fn png(p: fimg::Image<Vec<u8>, 3>) -> Vec<u8> { )] /// Pong! pub async fn ping(c: Context<'_>) -> Result<()> { + // let p = Timestamp::now() + // .signed_duration_since(*c.created_at()) + // .to_std()? + // .as_millis() as _; log(&c); use emoji::named::*; let m = memory_stats::memory_stats().unwrap().physical_mem as f32 / (1 << 20) as f32; @@ -881,13 +898,8 @@ pub async fn ping(c: Context<'_>) -> Result<()> { // let m = (m / 0.1) + 0.5; // let m = m.floor() * 0.1; c.reply(format!( - "pong!\n{DISCORD}{RIGHT}: {} — {HOST}: {m:.1}MiB - <:stopwatch:1361892467510870167><:world_processor:1307657404128690268> {util:.0}% — <:up:1307658579251167302><:time:1361892343199957022> {}", - humantime::format_duration(Duration::from_millis( - Timestamp::now() - .signed_duration_since(*c.created_at()) - .to_std()? - .as_millis() as _ - )), + "pong!\n{DISCORD}{RIGHT}: {}ms — {HOST}: {m:.1}MiB - <:stopwatch:1361892467510870167><:world_processor:1307657404128690268> {util:.0}% — <:up:1307658579251167302><:time:1361892343199957022> {}", + c.ping().await.as_millis(), humantime::format_duration(Duration::from_secs( Instant::now() .duration_since(*super::START.get().unwrap()) @@ -937,7 +949,7 @@ pub async fn render_file( _ = c.defer().await; let Some(s) = schematic::from_attachments(std::slice::from_ref(&s)).await? else { - match map::reply(&s).await? { + match map::reply(c, &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?)) @@ -1048,4 +1060,4 @@ pub async fn schembrowser_instructions(c: Context<'_>) -> Result<()> { ) .await?; Ok(()) -} +}
\ No newline at end of file diff --git a/src/bot/sorter.rs b/src/bot/sorter.rs index 8826b97..59617ec 100644 --- a/src/bot/sorter.rs +++ b/src/bot/sorter.rs @@ -1,9 +1,9 @@ use anyhow::Result; use atools::prelude::*; use block::SORTER; -use fimg::{indexed::IndexedImage, Image}; +use fimg::{Image, indexed::IndexedImage}; use mindus::*; -use poise::{serenity_prelude::*, ChoiceParameter}; +use poise::{ChoiceParameter, serenity_prelude::*}; use remapper::pal; #[derive(ChoiceParameter)] @@ -92,14 +92,15 @@ fn sort( let (width, height) = (x.width() as usize, x.height() as usize); let mut s = Schematic::new(width, height); + let q = unsafe { quant.raw() }; let pixels = (0..width) .flat_map(|x_| (0..height).map(move |y| (x_, y))) - .filter_map(|(x_, y_)| { - match unsafe { quant.raw().buffer() }[(height - y_ - 1) * width + x_] { + .filter_map( + |(x_, y_)| match q.buffer()[(height - y_ - 1) * width + x_] { 0 => None, x => Some(((x_, y_), x - 1)), - } - }); + }, + ); for ((x, y), i) in pixels { s.set( x, diff --git a/src/bot/usage.md b/src/bot/usage.md index f091955..31b348f 100644 --- a/src/bot/usage.md +++ b/src/bot/usage.md @@ -9,5 +9,6 @@ commands: - `eval`: executes mlog. see `/help eval` for more info. - `sorter`: creates sorter representations of images. +- `mapper`: creates map representations of images. bugs to be reported [here](https://github.com/bend-n/mindus/issues/new), or ping <@696196765564534825>. diff --git a/src/main.rs b/src/main.rs index a3d36be..cd236b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![allow(incomplete_features)] #![feature( + try_blocks, let_chains, generic_const_exprs, iter_intersperse, |