feat: reintroduce message link embeds
Signed-off-by: seth <getchoo@tuta.io>
This commit is contained in:
parent
604a81fb44
commit
640409f2e2
6 changed files with 160 additions and 94 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1235,6 +1235,7 @@ dependencies = [
|
|||
"rand",
|
||||
"redis",
|
||||
"redis-macros",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
|
@ -25,6 +25,7 @@ once_cell = "1.18.0"
|
|||
rand = "0.8.5"
|
||||
redis = { version = "0.23.3", features = ["tokio-comp", "tokio-rustls-comp"] }
|
||||
redis-macros = "0.2.1"
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.11.22", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
"json",
|
||||
|
|
28
src/handlers/event/expand_link.rs
Normal file
28
src/handlers/event/expand_link.rs
Normal 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(())
|
||||
}
|
|
@ -6,6 +6,7 @@ use poise::{Event, FrameworkContext};
|
|||
|
||||
mod delete;
|
||||
mod eta;
|
||||
mod expand_link;
|
||||
mod support_onboard;
|
||||
|
||||
pub async fn handle(
|
||||
|
@ -26,7 +27,8 @@ pub async fn handle(
|
|||
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?,
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use crate::Context;
|
||||
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use poise::serenity_prelude as serenity;
|
||||
use rand::seq::SliceRandom;
|
||||
use serenity::{CreateEmbed, Message};
|
||||
use url::Url;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod resolve_message;
|
||||
|
||||
pub use resolve_message::resolve as resolve_message;
|
||||
|
||||
/*
|
||||
* 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())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
124
src/utils/resolve_message.rs
Normal file
124
src/utils/resolve_message.rs
Normal 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)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue