[no description]
much more reliable
| -rw-r--r-- | rustfmt.toml | 7 | ||||
| -rw-r--r-- | src/main.rs | 309 | ||||
| -rw-r--r-- | src/tenor.rs | 3 |
3 files changed, 203 insertions, 116 deletions
diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..13d8a30 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,7 @@ +max_width = 80 +format_strings = true +group_imports = "StdExternalCrate" +imports_granularity = "Module" +match_arm_blocks = false +style_edition = "2024" +use_small_heuristics = "Max" diff --git a/src/main.rs b/src/main.rs index 6440132..77a5fe5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,23 @@ -#![feature(impl_trait_in_fn_trait_return, try_blocks, try_blocks_heterogeneous)] +#![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::Image; -use fimg::WritePng; -use fimg::scale::Lanczos3; -use fimg::scale::Nearest; +use fimg::scale::{Lanczos3, Nearest}; +use fimg::{Image, WritePng}; use gif::Frame; use regex::Regex; use serenity::all::*; -use std::sync::Arc; -use std::sync::LazyLock; - #[tokio::main] async fn main() { spawn().await; @@ -53,11 +58,32 @@ pub async fn spawn() { FullEvent::Ready { .. } => { println!("bot ready"); } - FullEvent::Message { new_message } => { - if let Err(error) = handle_message(c, new_message).await { - eprintln!("{}", error); - } - } + 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 + )) + .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(()) @@ -72,7 +98,7 @@ pub async fn spawn() { }, ..Default::default() }) - .setup(|ctx, _ready, _| { + .setup(|_, _, _| { Box::pin(async move { println!("registered"); Ok(()) @@ -91,97 +117,70 @@ pub async fn spawn() { .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() - }); - +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+)(?<tracking>\?[a-z]+=[0-9a-f]+(?:&[a-z]+=[0-9a-f]+)*)?", + ) + .unwrap() +}); +async fn handle_message( + c: &poise::serenity_prelude::Context, + new_message: &Message, +) -> Result<Option<CreateAttachment>> { if new_message.author.bot { - return Ok(()); + return Ok(None); } - 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))]) - .content(format!("<@{}>", new_message.author.id)), - ) - .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 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?, + ); + } } - 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?; + } 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; } } - Ok(()) + 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( @@ -199,30 +198,46 @@ fn regif( 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(); + 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 bg = decoder.bg_color().map(|x| x as u8); // fuck bg color + let mut pf = Image::<Vec<u8>, 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 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]); + // if std::mem::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]); + .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); + let x_ = Image::<_, 1>::build(frame.width as _, frame.height as _) + .buf(&*frame.buffer); pf.as_mut().clipping_overlay_at( &x_, @@ -256,9 +271,10 @@ fn resize4(mut x: Image<Box<[u8]>, 4>) -> Image<Box<[u8]>, 4> { x.scale::<Lanczos3>(nw, 152) } -impl<T: AsMut<[u8]> + AsRef<[u8]>, U: AsRef<[u8]>> OverlayAtClipping<Image<U, 1>> for Image<T, 1> { +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>, @@ -268,7 +284,6 @@ impl<T: AsMut<[u8]> + AsRef<[u8]>, U: AsRef<[u8]>> OverlayAtClipping<Image<U, 1> ) -> &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) @@ -282,5 +297,69 @@ impl<T: AsMut<[u8]> + AsRef<[u8]>, U: AsRef<[u8]>> OverlayAtClipping<Image<U, 1> } trait OverlayAtClipping<W> { - fn clipping_overlay_at(&mut self, with: &W, x: u32, y: u32, t: Option<u8>) -> &mut Self; + fn clipping_overlay_at( + &mut self, + with: &W, + x: u32, + y: u32, + t: Option<u8>, + ) -> &mut Self; +} + +async fn retenor(u: &str) -> anyhow::Result<Option<CreateAttachment>> { + 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<CreateAttachment> { + 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<Option<CreateAttachment>> { + 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) } diff --git a/src/tenor.rs b/src/tenor.rs index 0cf79af..aad5933 100644 --- a/src/tenor.rs +++ b/src/tenor.rs @@ -136,7 +136,8 @@ pub async fn download_url( PurpleResult, impl FnOnce() -> impl Future<Output = anyhow::Result<Vec<u8>>>, )> { - let response = get(dbg!(url)).await.context("h")?; + println!("GET {url}"); + let response = get(url).await.context("h")?; let html_content = response.text().await.context("ah")?; let document = Html::parse_document(&html_content); let selector = Selector::parse("#store-cache").unwrap(); |