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