feat: reintroduce message link embeds

Signed-off-by: seth <getchoo@tuta.io>
This commit is contained in:
seth 2023-12-04 08:22:38 -05:00
parent 604a81fb44
commit 640409f2e2
No known key found for this signature in database
GPG key ID: D31BD0D494BBEE86
6 changed files with 160 additions and 94 deletions

1
Cargo.lock generated
View file

@ -1235,6 +1235,7 @@ dependencies = [
"rand", "rand",
"redis", "redis",
"redis-macros", "redis-macros",
"regex",
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",

View file

@ -25,6 +25,7 @@ once_cell = "1.18.0"
rand = "0.8.5" rand = "0.8.5"
redis = { version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"] } redis = { version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"] }
redis-macros = "0.2.1" redis-macros = "0.2.1"
regex = "1.10.2"
reqwest = { version = "0.11.22", default-features = false, features = [ reqwest = { version = "0.11.22", default-features = false, features = [
"rustls-tls", "rustls-tls",
"json", "json",

View file

@ -0,0 +1,28 @@
use color_eyre::eyre::{eyre, Context as _, Result};
use poise::serenity_prelude::{Context, Message};
use crate::utils;
pub async fn handle(ctx: &Context, msg: &Message) -> Result<()> {
let embeds = utils::resolve_message(ctx, msg).await?;
// TOOD getchoo: actually reply to user
// ...not sure why Message doesn't give me a builder in reply() or equivalents
let our_channel = msg
.channel(ctx)
.await
.wrap_err_with(|| "Couldn't get channel from message!")?
.guild()
.ok_or_else(|| eyre!("Couldn't convert to GuildChannel!"))?;
if !embeds.is_empty() {
our_channel
.send_message(ctx, |m| {
m.set_embeds(embeds)
.allowed_mentions(|am| am.replied_user(false))
})
.await?;
}
Ok(())
}

View file

@ -6,6 +6,7 @@ use poise::{Event, FrameworkContext};
mod delete; mod delete;
mod eta; mod eta;
mod expand_link;
mod support_onboard; mod support_onboard;
pub async fn handle( pub async fn handle(
@ -26,7 +27,8 @@ pub async fn handle(
return Ok(()); return Ok(());
} }
eta::handle(ctx, new_message).await? eta::handle(ctx, new_message).await?;
expand_link::handle(ctx, new_message).await?;
} }
Event::ReactionAdd { add_reaction } => delete::handle(ctx, add_reaction).await?, Event::ReactionAdd { add_reaction } => delete::handle(ctx, add_reaction).await?,

View file

@ -1,13 +1,11 @@
use crate::Context;
use color_eyre::eyre::{eyre, Result}; use color_eyre::eyre::{eyre, Result};
use poise::serenity_prelude as serenity;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serenity::{CreateEmbed, Message};
use url::Url;
#[macro_use] #[macro_use]
mod macros; mod macros;
mod resolve_message;
pub use resolve_message::resolve as resolve_message;
/* /*
* chooses a random element from an array * chooses a random element from an array
@ -20,91 +18,3 @@ pub fn random_choice<const N: usize>(arr: [&str; N]) -> Result<String> {
Ok((*resp).to_string()) Ok((*resp).to_string())
} }
// waiting for `round_char_boundary` to stabilize
pub fn floor_char_boundary(s: &str, index: usize) -> usize {
if index >= s.len() {
s.len()
} else {
let lower_bound = index.saturating_sub(3);
let new_index = s.as_bytes()[lower_bound..=index]
.iter()
.rposition(|&b| (b as i8) >= -0x40); // b.is_utf8_char_boundary
// Can be made unsafe but whatever
lower_bound + new_index.unwrap()
}
}
pub async fn send_url_as_embed(ctx: Context<'_>, url: String) -> Result<()> {
let parsed = Url::parse(&url)?;
let title = parsed
.path_segments()
.unwrap()
.last()
.unwrap_or("image")
.replace("%20", " ");
ctx.send(|c| c.embed(|e| e.title(title).image(&url).url(url)))
.await?;
Ok(())
}
pub async fn resolve_message_to_embed(ctx: &serenity::Context, msg: &Message) -> CreateEmbed {
let truncation_point = floor_char_boundary(&msg.content, 700);
let truncated_content = if msg.content.len() <= truncation_point {
msg.content.to_string()
} else {
format!("{}...", &msg.content[..truncation_point])
};
let color = msg
.member(ctx)
.await
.ok()
.and_then(|m| m.highest_role_info(&ctx.cache))
.and_then(|(role, _)| role.to_role_cached(&ctx.cache))
.map(|role| role.colour);
let attached_image = msg
.attachments
.iter()
.filter(|a| {
a.content_type
.as_ref()
.filter(|ct| ct.contains("image/"))
.is_some()
})
.map(|a| &a.url)
.next();
let attachments_len = msg.attachments.len();
let mut embed = msg
.embeds
.first()
.map(|embed| CreateEmbed::from(embed.clone()))
.unwrap_or_default();
embed.author(|author| author.name(&msg.author.name).icon_url(&msg.author.face()));
if let Some(color) = color {
embed.color(color);
}
if let Some(attachment) = attached_image {
embed.image(attachment);
}
if attachments_len > 1 {
embed.footer(|footer| {
// yes it will say '1 attachments' no i do not care
footer.text(format!("{} attachments", attachments_len))
});
}
embed.description(truncated_content);
embed
}

View file

@ -0,0 +1,124 @@
use color_eyre::eyre::{eyre, Context as _, Result};
use log::*;
use once_cell::sync::Lazy;
use poise::serenity_prelude::{ChannelType, Colour, Context, CreateEmbed, Message};
use regex::Regex;
static MESSAGE_PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"/(https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?<serverId>\d+)\/(?<channelId>\d+)\/(?<messageId>\d+)/g;").unwrap()
});
pub fn find_first_image(msg: &Message) -> Option<String> {
msg.attachments
.iter()
.find(|a| {
a.content_type
.as_ref()
.unwrap_or(&"".to_string())
.starts_with("image/")
})
.map(|res| res.url.clone())
}
pub async fn resolve(ctx: &Context, msg: &Message) -> Result<Vec<CreateEmbed>> {
let matches = MESSAGE_PATTERN.captures_iter(&msg.content);
let mut embeds: Vec<CreateEmbed> = vec![];
for captured in matches.take(3) {
// don't leak messages from other servers
if let Some(server_id) = captured.get(0) {
let other_server: u64 = server_id.as_str().parse().unwrap_or_default();
let current_id = msg.guild_id.unwrap_or_default();
if &other_server != current_id.as_u64() {
debug!("Not resolving message of other guild.");
continue;
}
} else {
warn!("Couldn't find server_id from Discord link! Not resolving message to be safe");
continue;
}
if let Some(channel_id) = captured.get(1) {
let parsed: u64 = channel_id.as_str().parse().unwrap_or_default();
let req_channel = ctx
.cache
.channel(parsed)
.ok_or_else(|| eyre!("Couldn't get channel_id from Discord regex!"))?
.guild()
.ok_or_else(|| {
eyre!("Couldn't convert to GuildChannel from channel_id {parsed}!")
})?;
if !req_channel.is_text_based() {
debug!("Not resolving message is non-text-based channel.");
continue;
}
if req_channel.kind == ChannelType::PrivateThread {
if let Some(id) = req_channel.parent_id {
let parent = ctx.cache.guild_channel(id).ok_or_else(|| {
eyre!("Couldn't get parent channel {id} for thread {req_channel}!")
})?;
let parent_members = parent.members(ctx).await.unwrap_or_default();
if !parent_members.iter().any(|m| m.user.id == msg.author.id) {
debug!("Not resolving message for user not a part of a private thread.");
continue;
}
}
} else if req_channel
.members(ctx)
.await?
.iter()
.any(|m| m.user.id == msg.author.id)
{
debug!("Not resolving for message for user not a part of a channel");
continue;
}
let message_id: u64 = captured
.get(2)
.ok_or_else(|| eyre!("Couldn't get message_id from Discord regex!"))?
.as_str()
.parse()
.wrap_err_with(|| {
eyre!("Couldn't parse message_id from Discord regex as a MessageId!")
})?;
let original_message = req_channel.message(ctx, message_id).await?;
let mut embed = CreateEmbed::default();
embed
.author(|a| {
a.name(original_message.author.tag())
.icon_url(original_message.author.default_avatar_url())
})
.color(Colour::BLITZ_BLUE)
.timestamp(original_message.timestamp)
.footer(|f| f.text(format!("#{}", req_channel.name)))
.description(format!(
"{}\n\n[Jump to original message]({})",
original_message.content,
original_message.link()
));
if !original_message.attachments.is_empty() {
embed.fields(original_message.attachments.iter().map(|a| {
(
"Attachments".to_string(),
format!("[{}]({})", a.filename, a.url),
false,
)
}));
if let Some(image) = find_first_image(msg) {
embed.image(image);
}
}
embeds.push(embed);
}
}
Ok(embeds)
}