#![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))])
.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 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;
}