[no description]
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..88fb223 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,285 @@ +#![feature(impl_trait_in_fn_trait_return, try_blocks, try_blocks_heterogeneous)] +mod tenor; + +use anyhow::Result; +use fimg::Image; +use fimg::WritePng; +use fimg::scale::Lanczos3; +use fimg::scale::Nearest; +use gif::Frame; +use regex::Regex; +use serenity::all::*; + +use std::sync::Arc; +use std::sync::LazyLock; + +#[tokio::main] +async fn main() { + spawn().await; +} + +async fn on_error(error: poise::FrameworkError<'_, (), anyhow::Error>) { + use poise::FrameworkError::Command; + match error { + Command { error, ctx, .. } => { + let mut msg; + { + let mut chain = error.chain(); + msg = format!("e: `{}`", chain.next().unwrap()); + for mut source in chain { + use std::fmt::Write; + write!(msg, "from: `{source}`").unwrap(); + while let Some(next) = source.source() { + write!(msg, "from: `{next}`").unwrap(); + source = next; + } + } + } + ctx.say(msg).await.unwrap(); + } + err => poise::builtins::on_error(err).await.unwrap(), + } +} + +pub async fn spawn() { + println!("bot startup"); + let tok = include_str!("../token"); + let f = poise::Framework::<(), anyhow::Error>::builder() + .options(poise::FrameworkOptions { + commands: vec![], + event_handler: |c, e, _, ()| { + Box::pin(async move { + match e { + FullEvent::Ready { .. } => { + println!("bot ready"); + } + FullEvent::Message { new_message } => { + if let Err(error) = handle_message(c, new_message).await { + eprintln!("{}", error); + } + } + _ => {} + }; + Ok(()) + }) + }, + on_error: |e| Box::pin(on_error(e)), + prefix_options: poise::PrefixFrameworkOptions { + edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( + std::time::Duration::from_secs(2 * 60), + ))), + ..Default::default() + }, + ..Default::default() + }) + .setup(|ctx, _ready, _| { + Box::pin(async move { + println!("registered"); + Ok(()) + }) + }) + .build(); + ClientBuilder::new( + tok, + GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT, + ) + .framework(f) + .await + .unwrap() + .start() + .await + .unwrap(); +} + +async fn handle_message(c: &poise::serenity_prelude::Context, new_message: &Message) -> Result<()> { + static TENOR: LazyLock<Regex> = + LazyLock::new(|| Regex::new("https?://tenor.com/view/.+-[0-9]+").unwrap()); + static DISCORD: LazyLock<Regex> = LazyLock::new(|| { + Regex::new( + r"https:\/\/(?:cdn|media)\.discordapp\.(?:com|net)\/attachments\/\d+\/\d+\/(?<name>[\w-]+)\.(?<ext>\w+)(?:\?ex=[0-9a-f]+&is=[0-9a-f]+&hm=[0-9a-f]+)?(?:&animated=true)?(?:&width=\d+)?(?:&height=\d+)?&?", + ) + .unwrap() + }); + + if new_message.author.bot { + return Ok(()); + } + if let Some(x) = TENOR.find(&new_message.content) { + let x = x.as_str(); + let (r, e) = tenor::download_url(&x).await?; + if r.media_formats.gif.dims[1] > 160 { + let r = try bikeshed anyhow::Result<()> { + let gif = e().await?; + let (dec, vec) = regif(&gif)?; + let vec = vec(dec)?; + + new_message + .channel_id + .send_message( + c, + CreateMessage::new() + .files([CreateAttachment::bytes(vec, format!("g{}.gif", r.h1_title))]), + ) + .await?; + }; + new_message.delete(c).await?; + r?; + } + } else if let Some(x) = DISCORD.captures(&new_message.content) + && let Some(url) = x.get(0) + && let Some(name) = x.name("name") + && let Some(x) = x.name("ext") + { + let dat = reqwest::get(url.as_str()).await?.bytes().await?; + if x.as_str() == "gif" { + let (dec, vec) = regif(&dat)?; + if dec.height() > 160 { + new_message + .channel_id + .send_message( + c, + CreateMessage::new() + .files([CreateAttachment::bytes( + vec(dec)?, + format!("{}.gif", name.as_str()), + )]) + .content(format!("<@{}>", new_message.author.id)), + ) + .await?; + new_message.delete(c).await?; + } + } else { + let i = if let Some(f) = image::ImageFormat::from_extension(x.as_str()) { + image::load_from_memory_with_format(&*dat, f)?.to_rgba8() + } else { + let Ok(i) = image::load_from_memory(&*dat) else { + return Ok(()); + }; + i.to_rgba8() + }; + if i.height() < 160 { + return Ok(()); + } + let i = Image::<_, 4>::build(i.width(), i.height()) + .buf(i.into_vec()) + .boxed(); + let mut to = Vec::with_capacity(1 << 10); + resize4(i).write(&mut to)?; + new_message + .channel_id + .send_message( + c, + CreateMessage::new() + .files([CreateAttachment::bytes( + to, + format!("{}.png", name.as_str()), + )]) + .content(format!("<@{}>", new_message.author.id)), + ) + .await?; + new_message.delete(c).await?; + } + } + Ok(()) +} + +fn regif( + x: &[u8], +) -> Result<( + gif::Decoder<&[u8]>, + impl FnOnce(gif::Decoder<&[u8]>) -> Result<Vec<u8>>, +)> { + let mut options = gif::DecodeOptions::new(); + options.set_color_output(gif::ColorOutput::Indexed); + + let decoder = options.read_info(x)?; + Ok((decoder, |decoder: gif::Decoder<&[u8]>| { + let mut vec = Vec::with_capacity(1 << 14); + println!("resizing gif..."); + let now = std::time::Instant::now(); + let p = decoder.global_palette(); + let nw = ((decoder.width() as f32 / decoder.height() as f32) * 152.0).round() as u16; + let mut encoder = gif::Encoder::new(&mut vec, nw, 152, p.unwrap_or(&[])).unwrap(); + encoder.set_repeat(decoder.repeat())?; + let bg = decoder.bg_color().unwrap_or(0); + let mut pf = Image::<Vec<u8>, 1>::build(decoder.width() as _, decoder.height() as _).buf( + vec![bg as u8; decoder.width() as usize * decoder.height() as usize], + ); + // let mut first = true; + for frame in decoder.into_iter() { + let frame = frame?; + // if take(&mut first) && let Some(x) = frame.transparent{ + // pf = Image::<Vec<u8>, 1>::build(pf.width(), pf.height()) + // .buf(vec![x as u8; pf.width() as usize * pf.height() as usize]); + // } + + let _pf = pf.clone(); + match frame.dispose { + gif::DisposalMethod::Background => { + pf = Image::<Vec<u8>, 1>::build(pf.width(), pf.height()) + .buf(vec![bg as u8; pf.width() as usize * pf.height() as usize]); + } + _ => {} + } + let x_ = Image::<_, 1>::build(frame.width as _, frame.height as _).buf(&*frame.buffer); + + pf.as_mut().clipping_overlay_at( + &x_, + frame.left as _, + frame.top as _, + frame.transparent, + ); + let nf = pf.scale::<Nearest>(nw as _, 152); + + let f = Frame { + width: nw, + height: 152, + buffer: std::borrow::Cow::Borrowed(nf.bytes()), + top: 0, + left: 0, + ..frame + }; + if f.dispose == gif::DisposalMethod::Previous { + pf = _pf; + } + encoder.write_frame(&f)?; + } + drop(encoder); + println!("took {:?}", now.elapsed()); + Ok(vec) + })) +} + +fn resize4(mut x: Image<Box<[u8]>, 4>) -> Image<Box<[u8]>, 4> { + let nw = ((x.width() as f32 / x.height() as f32) * 152.0).round() as u32; + x.scale::<Lanczos3>(nw, 152) +} + +impl<T: AsMut<[u8]> + AsRef<[u8]>, U: AsRef<[u8]>> OverlayAtClipping<Image<U, 1>> for Image<T, 1> { + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + fn clipping_overlay_at( + &mut self, + with: &Image<U, 1>, + x: u32, + y: u32, + t: Option<u8>, + ) -> &mut Self { + for j in 0..with.height() { + for i in 0..with.width() { + // SAFETY: i, j is in bounds. + if let Some([their_px]) = with.get_pixel(i, j) + && let Some([our_px]) = self.get_pixel_mut(i + x, j + y) + && t != Some(*their_px) + { + *our_px = *their_px; + } + } + } + self + } +} + +trait OverlayAtClipping<W> { + fn clipping_overlay_at(&mut self, with: &W, x: u32, y: u32, t: Option<u8>) -> &mut Self; +} |