#![allow(incomplete_features)] #![feature( impl_trait_in_fn_trait_return, try_blocks, try_blocks_heterogeneous, deref_patterns )] mod tenor; use std::borrow::Cow; use std::mem::transmute; use std::sync::{Arc, LazyLock}; use anyhow::Result; use fimg::scale::{Lanczos3, Nearest}; use fimg::{Image, WritePng}; use gif::Frame; use regex::Regex; use serenity::all::*; #[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 } => match handle_message(c, new_message).await { Ok(Some(x)) => { let (a, b) = futures::future::join( new_message.delete(c), new_message.channel_id.send_message( c, CreateMessage::new() .content(format!( "<@{}>", new_message.author.id )) .allowed_mentions( CreateAllowedMentions::new( ) .empty_roles() .empty_users(), ) .flags(MessageFlags::SUPPRESS_NOTIFICATIONS) .add_file(x), ), ) .await; if let Err(e) = a { eprintln!("FAIL {e}"); } if let Err(e) = b { eprintln!("FAIL {e}"); } } Ok(None) => {} Err(e) => eprintln!("FAIL {e}"), }, _ => {} }; 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(|_, _, _| { Box::pin(async move { println!("registered"); Ok(()) }) }) .build(); ClientBuilder::new( tok, GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT, ) .framework(f) .await .unwrap() .start() .await .unwrap(); } static TENOR: LazyLock = LazyLock::new(|| Regex::new("https?://tenor.com/view/.+-[0-9]+").unwrap()); static DISCORD: LazyLock = LazyLock::new(|| { Regex::new( r"https:\/\/(?:cdn|media)\.discordapp\.(?:com|net)\/attachments\/\d+\/\d+\/(?[\w-]+)\.(?\w+)(?\?[a-z]+=[0-9a-f]+(?:&[a-z]+=[0-9a-f]+)*)?", ) .unwrap() }); async fn handle_message( c: &poise::serenity_prelude::Context, new_message: &Message, ) -> Result> { if new_message.author.bot { return Ok(None); } let mut new_message = Cow::Borrowed(new_message); if let Some(x) = DISCORD.captures(&new_message.content) { if x.name("tracking").is_none() { if new_message.embeds.is_empty() { new_message = Cow::Owned( c.http .get_message(new_message.channel_id, new_message.id) .await?, ); if new_message.embeds.is_empty() { tokio::time::sleep(tokio::time::Duration::from_secs(10)) .await; new_message = Cow::Owned( c.http .get_message(new_message.channel_id, new_message.id) .await?, ); } } } else if let Some(url) = x.get(0) && let Some(name) = x.name("name") && let Some(ext) = x.name("ext") { return rediscord(url.into(), ext.into(), name.into()).await; } } if let Some(x) = retenor(&new_message.content).await? { return Ok(Some(x)); } for embed in new_message.embeds.clone() { let Some(u) = embed.url else { continue }; if let Some(x) = embed.provider && let Some("Tenor") = x.name && let Some(x) = retenor(&u).await? { return Ok(Some(x)); } else if let Some(x) = embed.image.or(embed.thumbnail.map(|x| unsafe { transmute(x) })) && x.height.is_none_or(|x| x > 160) && let Some(cap) = DISCORD.captures(&x.url) && let Some(name) = cap.name("name") && let Some(ext) = cap.name("ext") { return rediscord(&x.url, ext.as_str(), name.as_str()).await; } } Ok(None) } fn regif( x: &[u8], ) -> Result<( gif::Decoder<&[u8]>, impl FnOnce(gif::Decoder<&[u8]>) -> Result>, )> { 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().map(|x| x as u8); // fuck bg color let mut pf = Image::, 1>::build( decoder.width() as _, decoder.height() as _, ) .buf(vec![ bg.unwrap_or(0); decoder.width() as usize * decoder.height() as usize ]); // let mut first = true; for frame in decoder.into_iter() { let frame = frame?; // if std::mem::take(&mut first) // && let Some(x) = frame.transparent // { // pf = Image::, 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::, 1>::build(pf.width(), pf.height()) .buf(vec![ frame.transparent.or(bg).unwrap_or(0); 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::(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, 4>) -> Image, 4> { let nw = ((x.width() as f32 / x.height() as f32) * 152.0).round() as u32; x.scale::(nw, 152) } impl + AsRef<[u8]>, U: AsRef<[u8]>> OverlayAtClipping> for Image { #[inline] fn clipping_overlay_at( &mut self, with: &Image, x: u32, y: u32, t: Option, ) -> &mut Self { for j in 0..with.height() { for i in 0..with.width() { 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 { fn clipping_overlay_at( &mut self, with: &W, x: u32, y: u32, t: Option, ) -> &mut Self; } async fn retenor(u: &str) -> anyhow::Result> { if let Some(x) = TENOR.captures(&u) { let (r, e) = tenor::download_url(&x.get(0).unwrap().as_str()).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)?; CreateAttachment::bytes(vec, format!("g{}.gif", r.h1_title)) }; return r.map(Some); } } Ok(None) } async fn rediscord( u: &str, ext: &str, name: &str, ) -> anyhow::Result> { println!("GET {u:?}"); let rq = reqwest::get(u).await?.error_for_status()?; let dat = rq.bytes().await?; if &*dat == b"This content is no longer available" { eprintln!("FAIL funny gif"); return Ok(None); } if ext == "gif" { let (dec, vec) = regif(&dat)?; if dec.height() > 160 { return Ok(Some(CreateAttachment::bytes( vec(dec)?, format!("{name}.gif"), ))); } } else { let i = if let Some(f) = image::ImageFormat::from_extension(ext) { image::load_from_memory_with_format(&*dat, f)?.to_rgba8() } else { let Ok(i) = image::load_from_memory(&*dat) else { return Ok(None); }; i.to_rgba8() }; if i.height() < 160 { return Ok(None); } 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)?; return Ok(Some(CreateAttachment::bytes(to, format!("{name}.png")))); } Ok(None) }