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",
|
"rand",
|
||||||
"redis",
|
"redis",
|
||||||
"redis-macros",
|
"redis-macros",
|
||||||
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
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 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?,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
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