1use once_cell::sync::Lazy;
7use regex::Regex;
8use serenity::all::{ChannelId, ChannelType, Context, GuildChannel, GuildId, Message, MessageId};
9use url::Url;
10
11use crate::cache::CacheArgs;
12
13pub static MESSAGE_LINK_REGEX: Lazy<Regex> = Lazy::new(|| {
17 Regex::new(r"https://(?:ptb\.|canary\.)?discord\.com/channels/(\d+)/(\d+)/(\d+)").unwrap()
18});
19
20#[derive(serde::Deserialize, Debug)]
22pub struct MessageLinkIDs {
23 pub guild_id: GuildId,
25 pub channel_id: ChannelId,
27 pub message_id: MessageId,
29}
30
31#[derive(serde::Deserialize, Debug)]
33pub struct Preview {
34 pub message: Message,
36 pub channel: GuildChannel,
38}
39
40#[derive(thiserror::Error, Debug)]
42pub enum PreviewError {
43 #[error("Failed to retrieve from cache.")]
45 Cache,
46 #[error("NSFW content previews are not permitted, but the channel is marked as NSFW.")]
48 Nsfw,
49 #[error("The channel is a private channel or private thread.")]
51 Permission,
52 #[allow(clippy::enum_variant_names)]
54 #[error(transparent)]
55 SerenityError(#[from] serenity::Error),
56}
57
58impl MessageLinkIDs {
59 pub fn parse(text: &str) -> Option<MessageLinkIDs> {
64 if !MESSAGE_LINK_REGEX.is_match(text) {
65 return None;
66 }
67
68 match MESSAGE_LINK_REGEX.captures(text) {
69 Some(captures) => {
70 let url = Url::parse(captures.get(0)?.as_str()).ok()?;
71
72 if !matches!(
73 url.domain(),
74 Some("discord.com") | Some("canary.discord.com") | Some("ptb.discord.com")
75 ) {
76 return None;
77 }
78
79 let guild_id = GuildId::new(captures.get(1)?.as_str().parse().ok()?);
80 let channel_id = ChannelId::new(captures.get(2)?.as_str().parse().ok()?);
81 let message_id = MessageId::new(captures.get(3)?.as_str().parse().ok()?);
82
83 Some(MessageLinkIDs {
84 guild_id,
85 channel_id,
86 message_id,
87 })
88 }
89 _ => None,
90 }
91 }
92}
93
94impl Preview {
95 pub async fn get(args: MessageLinkIDs, ctx: &Context) -> Result<Preview, PreviewError> {
100 let caches = CacheArgs {
101 guild_id: args.guild_id,
102 channel_id: args.channel_id,
103 };
104
105 let channel = caches.get(ctx).await.map_err(|_| PreviewError::Cache)?;
106
107 if channel.nsfw {
108 return Err(PreviewError::Nsfw);
109 }
110
111 if matches!(
112 channel.kind,
113 ChannelType::Private | ChannelType::PrivateThread
114 ) {
115 return Err(PreviewError::Permission);
116 }
117
118 let message = channel.message(&ctx.http, args.message_id).await?;
119 Ok(Preview { message, channel })
120 }
121}