html terminal
| -rw-r--r-- | Cargo.toml | 7 | ||||
| -rw-r--r-- | src/bot/maps.rs | 52 | ||||
| -rw-r--r-- | src/bot/mod.rs | 69 | ||||
| -rw-r--r-- | src/bot/player.rs | 36 | ||||
| -rw-r--r-- | src/bot/status.rs | 30 | ||||
| -rw-r--r-- | src/bot/voting.rs | 187 | ||||
| -rw-r--r-- | src/webhook.rs | 33 |
7 files changed, 230 insertions, 184 deletions
@@ -22,7 +22,7 @@ tokio = { version = "1.28.2", features = [ ], default-features = false } tokio-stream = "0.1.14" futures-util = "0.3.28" -serenity = { version = "0.11.5", features = [ +serenity = { version = "0.12", features = [ "builder", "client", "utils", @@ -30,7 +30,7 @@ serenity = { version = "0.11.5", features = [ "cache", "gateway", ], default-features = false } -poise = "0.5.5" +poise = { git = "https://github.com/serenity-rs/poise" } anyhow = "1.0.75" regex = { version = "1.8.4", features = ["std"], default-features = false } minify-js = "0.5.6" @@ -59,3 +59,6 @@ opt-level = 3 [profile.dev.package.fimg] opt-level = 3 + +[patch.crates-io] +serenity = { git = "https://github.com/bend-n/serenity", branch = "opt" } diff --git a/src/bot/maps.rs b/src/bot/maps.rs index b9df598..94321db 100644 --- a/src/bot/maps.rs +++ b/src/bot/maps.rs @@ -4,7 +4,6 @@ use futures_util::StreamExt; use mindus::*; use oxipng::*; use poise::serenity_prelude::*; -use std::borrow::Cow; use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; use std::time::{Duration, Instant, SystemTime}; use tokio::sync::broadcast::{self, Sender}; @@ -50,15 +49,12 @@ pub async fn autocomplete<'a>( pub async fn list(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer_or_broadcast().await; let maps = Maps::get_all(&ctx.data().stdin).await; - poise::send_reply(ctx, |m| { - m.embed(|e| { - for (k, v) in maps.iter().enumerate() { - e.field((k + 1).to_string(), v, true); - } - e.description("map list.").color(SUCCESS) - }) - }) - .await?; + let mut e = CreateEmbed::default(); + for (k, v) in maps.iter().enumerate() { + e = e.field((k + 1).to_string(), v, true); + } + e = e.description("map list.").color(SUCCESS); + poise::send_reply(ctx, poise::CreateReply::default().embed(e)).await?; Ok(()) } @@ -148,18 +144,30 @@ impl MapImage { pub async fn view(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer_or_broadcast().await; let (i, info) = MAP_IMAGE.get(&ctx.data().stdin).await?; - poise::send_reply(ctx, |m| { - m.attachment(AttachmentType::Bytes { - data: Cow::Borrowed(&i), - filename: "0.png".to_string(), - }) - .embed(|e| { - if let Some(RenderInfo { deserialization, render, compression, total, name }) = info { - e.footer(|f| f.text(format!("render of {name} took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", total.as_secs_f32(), deserialization.as_millis(), render.as_secs_f32(), compression.as_secs_f32()))); - } - e.attachment("0.png").color(SUCCESS) - }) - }) + let mut e = CreateEmbed::default(); + if let Some(RenderInfo { + deserialization, + render, + compression, + total, + name, + }) = info + { + e = e.footer(CreateEmbedFooter::new(format!( + "render of {name} took: {:.3}s (deser: {}ms, render: {:.3}s, compression: {:.3}s)", + total.as_secs_f32(), + deserialization.as_millis(), + render.as_secs_f32(), + compression.as_secs_f32() + ))); + } + e = e.attachment("0.png").color(SUCCESS); + poise::send_reply( + ctx, + poise::CreateReply::default() + .attachment(CreateAttachment::bytes(&**i, "0.png")) + .embed(e), + ) .await?; Ok(()) } diff --git a/src/bot/mod.rs b/src/bot/mod.rs index e85015f..4e960c1 100644 --- a/src/bot/mod.rs +++ b/src/bot/mod.rs @@ -17,6 +17,7 @@ use serenity::http::Http; use serenity::model::channel::Message; use std::fmt::Write; use std::fs::read_to_string; +use std::str::FromStr; use std::sync::LazyLock; use std::sync::{ atomic::{AtomicU8, Ordering}, @@ -48,8 +49,7 @@ macro_rules! send_ctx { pub const SOURCE_GUILD: u64 = 1003092764919091282; pub mod emojis { use super::SOURCE_GUILD; - use poise::serenity_prelude::Emoji; - use serenity::http::client::Http; + use poise::serenity_prelude::*; use std::sync::OnceLock; macro_rules! create { @@ -57,7 +57,7 @@ pub mod emojis { $(pub static $i: OnceLock<Emoji> = OnceLock::new();)+ pub async fn load(c: &Http) { - let all = c.get_emojis(SOURCE_GUILD).await.unwrap(); + let all = c.get_emojis(SOURCE_GUILD.into()).await.unwrap(); for e in all { match e.name.as_str() { $(stringify!([< $i:lower >])=>{let _=$i.get_or_init(||e);},)+ @@ -94,7 +94,7 @@ 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)) + Ok(ctx.guild_id().map_or(false, |i| i == GUILD)) } pub async fn discord_to_mindustry(m: &Message, c: &serenity::client::Context) -> String { @@ -103,7 +103,11 @@ pub async fn discord_to_mindustry(m: &Message, c: &serenity::client::Context) -> 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())); + at_distinct.push_str( + &u.nick_in(c, GuildId::new(GUILD)) + .await + .unwrap_or(u.name.clone()), + ); let mut m = u.mention().to_string(); if !result.contains(&m) { @@ -112,22 +116,19 @@ pub async fn discord_to_mindustry(m: &Message, c: &serenity::client::Context) -> result = result.replace(&m, &at_distinct); } - for id in &m.mention_roles { - let mention = id.mention().to_string(); + if let Some(guild_id) = m.guild_id { + 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"); - } - } + if let Some(guild) = <_ as AsRef<Cache>>::as_ref(c).guild(guild_id) { + if let Some(role) = guild.roles.get(id) { + result = result.replace(&mention, &format!("@{}", role.name)); + continue; + } + } - pub fn parse(x: &[u8]) -> u64 { - let mut n = 0; - for &b in x { - n = n * 10 + (b - b'0') as u64 + result = result.replace(&mention, "@deleted-role"); } - n } static CHANNEL: LazyLock<Regex> = LazyLock::new(|| Regex::new("<#([0-9]+)>").unwrap()); @@ -138,7 +139,7 @@ pub async fn discord_to_mindustry(m: &Message, c: &serenity::client::Context) -> &[ "#", c.http() - .get_channel(parse(m.get(1).unwrap().as_str().as_bytes())) + .get_channel(ChannelId::from_str(m.get(1).unwrap().as_str()).unwrap()) .await .unwrap() .guild() @@ -178,7 +179,7 @@ impl Bot { pub async fn spawn(stdout: broadcast::Receiver<String>, stdin: broadcast::Sender<String>) { println!("bot startup"); let tok = std::env::var("TOKEN").unwrap_or(read_to_string("token").expect("wher token")); - let f: poise::FrameworkBuilder<Data, anyhow::Error> = poise::Framework::builder() + let f = poise::Framework::<Data, anyhow::Error>::builder() .options(poise::FrameworkOptions { commands: vec![ raw(), @@ -204,18 +205,18 @@ impl Bot { event_handler: |c, e, _, d| { Box::pin(async move { match e { - poise::Event::Ready { .. } => { + FullEvent::Ready { .. } => { println!("bot ready"); emojis::load(&c.http).await; } - poise::Event::Message { new_message } => { + FullEvent::Message { new_message } => { if new_message.content.starts_with('!') || new_message.content.starts_with(PFX) || new_message.author.bot { return Ok(()); } - if CHANNEL == new_message.channel_id.0 { + if new_message.channel_id == CHANNEL { say(c, new_message, d).await?; } } @@ -226,16 +227,14 @@ impl Bot { }, on_error: |e| Box::pin(on_error(e)), prefix_options: poise::PrefixFrameworkOptions { - edit_tracker: Some(poise::EditTracker::for_timespan( + edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( std::time::Duration::from_secs(2 * 60), - )), + ))), prefix: Some(PFX.to_string()), ..Default::default() }, ..Default::default() }) - .token(tok) - .intents(GatewayIntents::all()) .setup(|ctx, _ready, framework| { Box::pin(async move { poise::builtins::register_globally(ctx, &framework.options().commands).await?; @@ -246,7 +245,8 @@ impl Bot { }) // todo: voting::fixall() auto }) - }); + }) + .build(); tokio::spawn(async move { let http = Http::new(""); let wh = std::env::var("WEBHOOK") @@ -255,7 +255,13 @@ impl Bot { SKIPPING.get_or_init(|| (wh.skip.clone(), wh.skipped.clone())); wh.link(stdout).await; }); - f.run().await.unwrap(); + ClientBuilder::new(tok, GatewayIntents::all()) + .framework(f) + .await + .unwrap() + .start() + .await + .unwrap(); } } @@ -264,7 +270,7 @@ type Context<'a> = poise::Context<'a, Data, anyhow::Error>; async fn on_error(error: poise::FrameworkError<'_, Data, anyhow::Error>) { use poise::FrameworkError::Command; match error { - Command { error, ctx } => { + Command { error, ctx, .. } => { let mut msg; { let mut chain = error.chain(); @@ -324,7 +330,8 @@ async fn raw( macro_rules! return_next { ($ctx:expr) => {{ let line = $crate::bot::get_nextblock().await; - $ctx.send(|m| m.content(line)).await?; + $ctx.send(poise::CreateReply::default().content(line)) + .await?; return Ok(()); }}; } diff --git a/src/bot/player.rs b/src/bot/player.rs index 489a4ec..9cfa9ba 100644 --- a/src/bot/player.rs +++ b/src/bot/player.rs @@ -3,6 +3,7 @@ use crate::send; use anyhow::Result; use futures_util::StreamExt; use itertools::Itertools; +use poise::serenity_prelude::*; use std::net::Ipv4Addr; use std::str::FromStr; use std::time::Instant; @@ -88,23 +89,24 @@ pub async fn autocomplete<'a>( pub async fn list(ctx: Context<'_>) -> Result<()> { let _ = ctx.defer().await; let players = Players::get_all(&ctx.data().stdin).await.unwrap().clone(); - poise::send_reply(ctx, |m| { - m.embed(|e| { - if players.is_empty() { - return e.title("no players online.").color(FAIL); - } - e.fields(players.into_iter().map(|p| { - let admins = if p.admin { - "<:admin:1182128872435749005>" - } else { - "" - }; - (p.name, admins, true) - })) - .description("currently online players.") - .color(SUCCESS) - }) - }) + poise::send_reply( + ctx, + poise::CreateReply::default().embed(if players.is_empty() { + CreateEmbed::new().title("no players online.").color(FAIL) + } else { + CreateEmbed::new() + .fields(players.into_iter().map(|p| { + let admins = if p.admin { + "<:admin:1182128872435749005>" + } else { + "" + }; + (p.name, admins, true) + })) + .description("currently online players.") + .color(SUCCESS) + }), + ) .await?; Ok(()) } diff --git a/src/bot/status.rs b/src/bot/status.rs index 852042f..d0792ec 100644 --- a/src/bot/status.rs +++ b/src/bot/status.rs @@ -2,6 +2,7 @@ use super::{get_nextblock, Context, FAIL, SUCCESS}; use crate::send_ctx; use anyhow::Result; use itertools::Itertools; +use poise::serenity_prelude::*; use std::str::FromStr; use tokio::time::{sleep, Duration}; @@ -58,7 +59,12 @@ pub async fn command(ctx: Context<'_>) -> Result<()> { send_ctx!(ctx, "status")?; macro_rules! fail { ($ctx:expr,$fail:expr) => {{ - poise::send_reply(ctx, |m| m.embed(|e| e.title("server down").color($fail))).await?; + poise::send_reply( + ctx, + poise::CreateReply::default() + .embed(CreateEmbed::new().title("server down").color($fail)), + ) + .await?; return Ok(()); }}; } @@ -69,18 +75,20 @@ pub async fn command(ctx: Context<'_>) -> Result<()> { let Some((tps, mem, pcount)) = parse(&block) else { fail!(ctx, FAIL); }; - poise::send_reply(ctx, |m| { - m.embed(|e| { - if pcount > 0 { - e.footer(|f| f.text("see /players for player list")); - } - e.title("server online") - .field("tps", tps, true) + poise::send_reply( + ctx, + poise::CreateReply::default().embed(if pcount > 0 { + CreateEmbed::new() + .title("server online") + .field("tps", format!("{tps}"), true) .field("memory use", humanize_bytes(Size::Mb(f64::from(mem))), true) - .field("players", pcount, true) + .field("players", format!("{pcount}"), true) .color(SUCCESS) - }) - }) + .footer(CreateEmbedFooter::new("see /players for player list")) + } else { + CreateEmbed::new().title("no players online").color(FAIL) + }), + ) .await?; Ok(()) } diff --git a/src/bot/voting.rs b/src/bot/voting.rs index 61b789e..83807f5 100644 --- a/src/bot/voting.rs +++ b/src/bot/voting.rs @@ -69,12 +69,19 @@ pub enum VoteData { pub type Votes = Mutex<Vec<VoteData>>; trait EmbedUtil { - fn imageor<S: ToString>(&mut self, img: Option<S>) -> &mut Self; - fn set_fields<M: IntoIterator<Item = (S, S)>, S: ToString>(&mut self, fields: M) -> &mut Self; + fn imageor<S>(self, img: Option<S>) -> Self + where + String: From<S>; + fn set_fields<M: IntoIterator<Item = (S, S)>, S: ToString>(self, fields: M) -> Self + where + String: From<S>; } impl EmbedUtil for CreateEmbed { - fn imageor<S: ToString>(&mut self, img: Option<S>) -> &mut Self { + fn imageor<S: Into<String>>(self, img: Option<S>) -> Self + where + String: From<S>, + { if let Some(iuri) = img { self.image(iuri) } else { @@ -82,9 +89,12 @@ impl EmbedUtil for CreateEmbed { } } - fn set_fields<M: IntoIterator<Item = (S, S)>, S: ToString>(&mut self, fields: M) -> &mut Self { + fn set_fields<M: IntoIterator<Item = (S, S)>, S: ToString>(mut self, fields: M) -> Self + where + String: From<S>, + { for (k, v) in fields { - self.field(k, v, false); + self = self.field(k, v, false); } self } @@ -180,27 +190,29 @@ impl VoteData { pub async fn begin(mut self, ctx: Context<'_>) -> Result<Self> { self.set_end(); let o = self.options(); - let handle = poise::send_reply(ctx, |m| { - m.embed(|e| { - e.imageor(o.image.as_ref()) - .color(SUCCESS) - .title(&o.title) - .description(format!("vote ends {}", self.end_stamp())) - .set_fields(&o.fields) - }) - .components(|c| { - c.create_action_row(|r| { - for (n, option) in o.options.iter().enumerate() { - r.create_button(|b| { - b.custom_id(format!("{}{n}", self.id())) + let handle = poise::send_reply( + ctx, + poise::CreateReply::default() + .embed( + CreateEmbed::new() + .imageor(o.image.as_ref()) + .color(SUCCESS) + .title(&o.title) + .description(format!("vote ends {}", self.end_stamp())) + .set_fields(&o.fields), + ) + .components(vec![CreateActionRow::Buttons( + o.options + .iter() + .enumerate() + .map(|(n, option)| { + CreateButton::new(format!("{}{n}", self.id())) .label(option) .style(o.styles[n]) - }); - } - r - }) - }) - }) + }) + .collect(), + )]), + ) .await?; let msg = handle.into_message().await?; self.set_reply(&ctx, msg); @@ -255,7 +267,7 @@ impl VoteData { let ctx_id_len = ctx_id.to_string().len(); let o = self.options().clone(); let timestamp = self.end_stamp(); - while let Some(press) = CollectComponentInteraction::new(ctx) + while let Some(press) = ComponentInteractionCollector::new(ctx) .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) .timeout(dead) .await @@ -276,24 +288,30 @@ impl VoteData { }; self.save_ref(ctx)?; let (_m, _) = tokio::join!( - press.create_followup_message(ctx, |m| { m.ephemeral(true).content(s) }), - press.create_interaction_response(ctx, |c| { - c.kind(InteractionResponseType::UpdateMessage) - .interaction_response_data(|m| { - m.embed(|e| { - for (option, votes) in - self.summarize(ctx, o.options.len()).iter().enumerate() - { - e.field(&o.options[option], votes, true); - } - e.imageor(o.image.as_ref()) - .color(SUCCESS) - .title(&o.title) - .set_fields(&o.fields) - .description(format!("vote ends {timestamp}")) - }) + press.create_followup( + ctx, + CreateInteractionResponseFollowup::default() + .ephemeral(true) + .content(s) + ), + press.create_response( + ctx, + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::new().embed({ + let mut e = CreateEmbed::new(); + for (option, votes) in + self.summarize(ctx, o.options.len()).iter().enumerate() + { + e = e.field(&o.options[option], votes.to_string(), true); + } + e.imageor(o.image.as_ref()) + .color(SUCCESS) + .title(&o.title) + .set_fields(&o.fields) + .description(format!("vote ends {timestamp}")) }) - }) + ) + ) ); // let m = m?; // let http = ctx.serenity_context().http.clone(); @@ -313,38 +331,41 @@ impl VoteData { let _ = std::fs::remove_file(p); } self.get_reply(ctx) - .edit(ctx, |m| { - m.embed(|e| { - for (option, votes) in self - .remove(ctx) - .summarize(ctx, o.options.len()) - .iter() - .enumerate() - { - e.field(&o.options[option], votes, true); - } - e.color(DISABLED) - .title(&o.title) - .imageor(o.image.as_ref()) - .set_fields(o.fields) - .description(format!("vote ended!")) - }) - .components(|c| { - c.set_action_row({ - let mut r = CreateActionRow::default(); - for (n, option) in o.options.iter().enumerate() { - r.create_button(|b| { - b.label(option) - .disabled(true) - .style(o.styles[n]) - .custom_id("_") - }); + .edit( + ctx, + EditMessage::default() + .embed({ + let mut e = CreateEmbed::new(); + + for (option, votes) in self + .remove(ctx) + .summarize(ctx, o.options.len()) + .iter() + .enumerate() + { + e = e.field(&o.options[option], votes.to_string(), true); } - r + e.color(DISABLED) + .title(&o.title) + .imageor(o.image.as_ref()) + .set_fields(o.fields) + .description(format!("vote ended!")) }) - }) - }) + .components(vec![CreateActionRow::Buttons( + o.options + .iter() + .enumerate() + .map(|(n, option)| { + CreateButton::new("_") + .label(option) + .disabled(true) + .style(o.styles[n]) + }) + .collect(), + )]), + ) .await?; + Ok(()) } } @@ -453,7 +474,7 @@ pub async fn create( async fn fix(ctx: &Context<'_>, data: BufReader<std::fs::File>) -> Result<()> { let mut v: BeforePushVoteData = serde_json::from_reader(data)?; - let m = ctx.http().get_message(v.cid.0, v.mid.0).await?; + let m = ctx.http().get_message(v.cid, v.mid).await?; let end = dbg!(m.timestamp.unix_timestamp()) as u64; v.reply = Some(Box::new(m)); let now = SystemTime::now() @@ -496,7 +517,11 @@ pub async fn fixall(ctx: Context<'_>) -> Result<()> { } } let msg = format!("fixed {}", futs.len()); - poise::send_reply(ctx, |m| m.content(msg).ephemeral(true)).await?; + poise::send_reply( + ctx, + poise::CreateReply::default().content(msg).ephemeral(true), + ) + .await?; future::join_all(futs).await; Ok(()) } @@ -569,11 +594,13 @@ pub async fn list(ctx: Context<'_>, #[description = "the vote title"] vote: Stri VoteData::After(_) => unreachable!(), } }; - poise::send_reply(ctx, |m| { - m.allowed_mentions(|x| x.empty_parse()).embed(|e| { + poise::send_reply( + ctx, + poise::CreateReply::default().embed({ + let mut e = CreateEmbed::default(); let mut votes: HashMap<usize, Vec<u64>> = HashMap::new(); for (user, vote) in vd.votes { - votes.entry(vote).or_default().push(user.0); + votes.entry(vote).or_default().push(user.get()); } for (vote, voters) in votes { let mut s = vec![]; @@ -581,13 +608,13 @@ pub async fn list(ctx: Context<'_>, #[description = "the vote title"] vote: Stri for person in voters { s.push(format!("<@{person}>")); } - e.field(&vd.options.options[vote], s.join("\n"), false); + e = e.field(&vd.options.options[vote], s.join("\n"), false); } e.color(SUCCESS) .title(format!("voter list for {vote}")) - .footer(|f| f.text("privacy is a illusion")) - }) - }) + .footer(CreateEmbedFooter::new("privacy is a illusion")) + }), + ) .await?; Ok(()) } diff --git a/src/webhook.rs b/src/webhook.rs index 6b04b78..dcf9f16 100644 --- a/src/webhook.rs +++ b/src/webhook.rs @@ -1,6 +1,6 @@ -use poise::serenity_prelude::Webhook as RealHook; +use poise::serenity_prelude::{Webhook as RealHook, *}; use regex::Regex; -use serenity::{builder::ExecuteWebhook, http::Http, json}; +use serenity::{builder::ExecuteWebhook, http::Http}; use std::convert::AsRef; use std::sync::{ atomic::{AtomicU8, Ordering}, @@ -30,34 +30,25 @@ impl<'a> Webhook<'a> { async fn send<F>(&self, block: F) where - for<'b> F: FnOnce(&'b mut ExecuteWebhook<'a>) -> &'b mut ExecuteWebhook<'a>, + for<'b> F: FnOnce(ExecuteWebhook) -> ExecuteWebhook, { - let mut execute_webhook = ExecuteWebhook::default(); - execute_webhook.allowed_mentions(|m| { - m.empty_parse() + let execute_webhook = ExecuteWebhook::default().allowed_mentions( + CreateAllowedMentions::default() .roles(vec![1110088946374938715, 1133416252791074877]) .users(vec![ 696196765564534825, 600014432298598400, 1173213085553660034, - ]) - }); - block(&mut execute_webhook); - - let map = json::hashmap_to_json_map(execute_webhook.0); + ]), + ); + let execute_webhook = block(execute_webhook); if let Err(e) = self - .http - .as_ref() - .execute_webhook( - self.inner.id.0, - self.inner.token.as_ref().unwrap(), - false, - &map, - ) + .inner + .execute(self.http, false, execute_webhook.clone()) .await { - println!("sending {map:#?} got error {e}."); - }; + println!("sending {execute_webhook:#?} got error {e}."); + } } async fn send_message(&self, username: &str, content: &str) { |