Skip to main content

babyrite/
preview.rs

1//! Message preview module.
2//!
3//! This module provides functionality for parsing Discord message links
4//! and generating previews of the linked messages.
5
6use 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
13/// Regex pattern for matching Discord message links.
14///
15/// Supports production, PTB, and Canary Discord URLs.
16pub 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/// Parsed IDs from a Discord message link.
21#[derive(serde::Deserialize, Debug)]
22pub struct MessageLinkIDs {
23    /// The guild ID from the message link.
24    pub guild_id: GuildId,
25    /// The channel ID from the message link.
26    pub channel_id: ChannelId,
27    /// The message ID from the message link.
28    pub message_id: MessageId,
29}
30
31/// A preview containing the message and its channel.
32#[derive(serde::Deserialize, Debug)]
33pub struct Preview {
34    /// The message to preview.
35    pub message: Message,
36    /// The channel containing the message.
37    pub channel: GuildChannel,
38}
39
40/// Errors that can occur when generating a preview.
41#[derive(thiserror::Error, Debug)]
42pub enum PreviewError {
43    /// Failed to retrieve channel information from cache.
44    #[error("Failed to retrieve from cache.")]
45    Cache,
46    /// The target channel is marked as NSFW.
47    #[error("NSFW content previews are not permitted, but the channel is marked as NSFW.")]
48    Nsfw,
49    /// The target channel is private or a private thread.
50    #[error("The channel is a private channel or private thread.")]
51    Permission,
52    /// An error occurred while communicating with Discord.
53    #[allow(clippy::enum_variant_names)]
54    #[error(transparent)]
55    SerenityError(#[from] serenity::Error),
56}
57
58impl MessageLinkIDs {
59    /// Parses a Discord message link from text.
60    ///
61    /// Returns `Some(MessageLinkIDs)` if a valid message link is found,
62    /// otherwise returns `None`.
63    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    /// Retrieves a preview for the given message link.
96    ///
97    /// Fetches the message and channel information, validating that
98    /// the channel is not NSFW and is publicly accessible.
99    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}