serenity/utils/
mod.rs

1//! A set of utilities to help with common use cases that are not required to fully use the
2//! library.
3
4#[cfg(feature = "client")]
5mod argument_convert;
6#[cfg(feature = "cache")]
7mod content_safe;
8mod custom_message;
9mod formatted_timestamp;
10mod message_builder;
11#[cfg(feature = "collector")]
12mod quick_modal;
13
14pub mod token;
15
16use std::num::NonZeroU16;
17
18#[cfg(feature = "client")]
19pub use argument_convert::*;
20#[cfg(feature = "cache")]
21pub use content_safe::*;
22pub use formatted_timestamp::*;
23#[cfg(feature = "collector")]
24pub use quick_modal::*;
25use url::Url;
26
27pub use self::custom_message::CustomMessage;
28pub use self::message_builder::{Content, ContentModifier, EmbedMessageBuilding, MessageBuilder};
29#[doc(inline)]
30pub use self::token::validate as validate_token;
31#[cfg(all(feature = "cache", feature = "model"))]
32use crate::cache::Cache;
33#[cfg(all(feature = "cache", feature = "model"))]
34use crate::http::CacheHttp;
35use crate::model::prelude::*;
36
37/// Retrieves the "code" part of an invite out of a URL.
38///
39/// # Examples
40///
41/// Two formats of [invite][`RichInvite`] codes are supported, both regardless of protocol prefix.
42/// Some examples:
43///
44/// 1. Retrieving the code from the URL `"https://discord.gg/0cDvIgU2voY8RSYL"`:
45///
46/// ```rust
47/// use serenity::utils;
48///
49/// let url = "https://discord.gg/0cDvIgU2voY8RSYL";
50///
51/// assert_eq!(utils::parse_invite(url), "0cDvIgU2voY8RSYL");
52/// ```
53///
54/// 2. Retrieving the code from the URL `"http://discord.com/invite/0cDvIgU2voY8RSYL"`:
55///
56/// ```rust
57/// use serenity::utils;
58///
59/// let url = "http://discord.com/invite/0cDvIgU2voY8RSYL";
60///
61/// assert_eq!(utils::parse_invite(url), "0cDvIgU2voY8RSYL");
62/// ```
63///
64/// [`RichInvite`]: crate::model::invite::RichInvite
65#[must_use]
66pub fn parse_invite(code: &str) -> &str {
67    let code = code.trim_start_matches("http://").trim_start_matches("https://");
68    let lower = code.to_lowercase();
69    if lower.starts_with("discord.gg/") {
70        &code[11..]
71    } else if lower.starts_with("discord.com/invite/") {
72        &code[19..]
73    } else {
74        code
75    }
76}
77
78/// Retrieves the username and discriminator out of a user tag (`name#discrim`).
79/// In order to accomodate next gen Discord usernames, this will also accept `name` style tags.
80///
81/// If the user tag is invalid, None is returned.
82///
83/// # Examples
84/// ```rust
85/// use std::num::NonZeroU16;
86///
87/// use serenity::utils::parse_user_tag;
88///
89/// assert_eq!(parse_user_tag("kangalioo#9108"), Some(("kangalioo", NonZeroU16::new(9108))));
90/// assert_eq!(parse_user_tag("kangalioo#10108"), None);
91/// assert_eq!(parse_user_tag("kangalioo"), Some(("kangalioo", None)));
92/// ```
93#[must_use]
94pub fn parse_user_tag(s: &str) -> Option<(&str, Option<NonZeroU16>)> {
95    if let Some((name, discrim)) = s.split_once('#') {
96        let discrim: u16 = discrim.parse().ok()?;
97        if discrim > 9999 {
98            return None;
99        }
100        Some((name, NonZeroU16::new(discrim)))
101    } else {
102        Some((s, None))
103    }
104}
105
106/// Retrieves an Id from a user mention.
107///
108/// If the mention is invalid, then [`None`] is returned.
109///
110/// # Examples
111///
112/// Retrieving an Id from a valid [`User`] mention:
113///
114/// ```rust
115/// use serenity::model::id::UserId;
116/// use serenity::utils::parse_username;
117///
118/// // regular username mention
119/// assert_eq!(parse_username("<@114941315417899012>"), Some(UserId::new(114941315417899012)));
120///
121/// // nickname mention
122/// assert_eq!(parse_username("<@!114941315417899012>"), Some(UserId::new(114941315417899012)));
123/// ```
124///
125/// Asserting that an invalid username or nickname mention returns [`None`]:
126///
127/// ```rust
128/// use serenity::utils::parse_username;
129///
130/// assert!(parse_username("<@1149413154aa17899012").is_none());
131/// assert!(parse_username("<@!11494131541789a90b1c2").is_none());
132/// ```
133///
134/// [`User`]: crate::model::user::User
135#[must_use]
136pub fn parse_user_mention(mention: &str) -> Option<UserId> {
137    if mention.len() < 4 {
138        return None;
139    }
140
141    let len = mention.len() - 1;
142    if mention.starts_with("<@!") {
143        mention[3..len].parse().ok()
144    } else if mention.starts_with("<@") {
145        mention[2..len].parse().ok()
146    } else {
147        None
148    }
149}
150
151#[deprecated = "use `utils::parse_user_mention` instead"]
152pub fn parse_username(mention: impl AsRef<str>) -> Option<UserId> {
153    parse_user_mention(mention.as_ref())
154}
155
156/// Retrieves an Id from a role mention.
157///
158/// If the mention is invalid, then [`None`] is returned.
159///
160/// # Examples
161///
162/// Retrieving an Id from a valid [`Role`] mention:
163///
164/// ```rust
165/// use serenity::model::id::RoleId;
166/// use serenity::utils::parse_role;
167///
168/// assert_eq!(parse_role("<@&136107769680887808>"), Some(RoleId::new(136107769680887808)));
169/// ```
170///
171/// Asserting that an invalid role mention returns [`None`]:
172///
173/// ```rust
174/// use serenity::utils::parse_role;
175///
176/// assert!(parse_role("<@&136107769680887808").is_none());
177/// ```
178///
179/// [`Role`]: crate::model::guild::Role
180#[must_use]
181pub fn parse_role_mention(mention: &str) -> Option<RoleId> {
182    if mention.len() < 4 {
183        return None;
184    }
185
186    if mention.starts_with("<@&") && mention.ends_with('>') {
187        let len = mention.len() - 1;
188        mention[3..len].parse().ok()
189    } else {
190        None
191    }
192}
193
194#[deprecated = "use `utils::parse_role_mention` instead"]
195pub fn parse_role(mention: impl AsRef<str>) -> Option<RoleId> {
196    parse_role_mention(mention.as_ref())
197}
198
199/// Retrieves an Id from a channel mention.
200///
201/// If the channel mention is invalid, then [`None`] is returned.
202///
203/// # Examples
204///
205/// Retrieving an Id from a valid [`Channel`] mention:
206///
207/// ```rust
208/// use serenity::model::id::ChannelId;
209/// use serenity::utils::parse_channel;
210///
211/// assert_eq!(parse_channel("<#81384788765712384>"), Some(ChannelId::new(81384788765712384)));
212/// ```
213///
214/// Asserting that an invalid channel mention returns [`None`]:
215///
216/// ```rust
217/// use serenity::utils::parse_channel;
218///
219/// assert!(parse_channel("<#!81384788765712384>").is_none());
220/// assert!(parse_channel("<#81384788765712384").is_none());
221/// ```
222///
223/// [`Channel`]: crate::model::channel::Channel
224#[must_use]
225pub fn parse_channel_mention(mention: &str) -> Option<ChannelId> {
226    if mention.len() < 4 {
227        return None;
228    }
229
230    if mention.starts_with("<#") && mention.ends_with('>') {
231        let len = mention.len() - 1;
232        mention[2..len].parse().ok()
233    } else {
234        None
235    }
236}
237
238#[deprecated = "use `utils::parse_channel_mention` instead"]
239pub fn parse_channel(mention: impl AsRef<str>) -> Option<ChannelId> {
240    parse_channel_mention(mention.as_ref())
241}
242
243/// Retrieves the animated state, name and Id from an emoji mention, in the form of an
244/// [`EmojiIdentifier`].
245///
246/// If the emoji usage is invalid, then [`None`] is returned.
247///
248/// # Examples
249///
250/// Ensure that a valid [`Emoji`] usage is correctly parsed:
251///
252/// ```rust
253/// use serenity::model::id::{EmojiId, GuildId};
254/// use serenity::model::misc::EmojiIdentifier;
255/// use serenity::utils::parse_emoji;
256///
257/// let emoji = parse_emoji("<:smugAnimeFace:302516740095606785>").unwrap();
258/// assert_eq!(emoji.animated, false);
259/// assert_eq!(emoji.id, EmojiId::new(302516740095606785));
260/// assert_eq!(emoji.name, "smugAnimeFace".to_string());
261/// ```
262///
263/// Asserting that an invalid emoji usage returns [`None`]:
264///
265/// ```rust
266/// use serenity::utils::parse_emoji;
267///
268/// assert!(parse_emoji("<:smugAnimeFace:302516740095606785").is_none());
269/// ```
270///
271/// [`Emoji`]: crate::model::guild::Emoji
272pub fn parse_emoji(mention: impl AsRef<str>) -> Option<EmojiIdentifier> {
273    let mention = mention.as_ref();
274
275    let len = mention.len();
276
277    if !(6..=56).contains(&len) {
278        return None;
279    }
280
281    if (mention.starts_with("<:") || mention.starts_with("<a:")) && mention.ends_with('>') {
282        let mut name = String::default();
283        let mut id = String::default();
284        let animated = &mention[1..3] == "a:";
285
286        let start = if animated { 3 } else { 2 };
287
288        for (i, x) in mention[start..].chars().enumerate() {
289            if x == ':' {
290                let from = i + start + 1;
291
292                for y in mention[from..].chars() {
293                    if y == '>' {
294                        break;
295                    }
296                    id.push(y);
297                }
298
299                break;
300            }
301            name.push(x);
302        }
303
304        id.parse().ok().map(|id| EmojiIdentifier {
305            animated,
306            id,
307            name,
308        })
309    } else {
310        None
311    }
312}
313
314/// Turns a string into a vector of string arguments, splitting by spaces, but parsing content
315/// within quotes as one individual argument.
316///
317/// # Examples
318///
319/// Parsing two quoted commands:
320///
321/// ```rust
322/// use serenity::utils::parse_quotes;
323///
324/// let command = r#""this is the first" "this is the second""#;
325/// let expected = vec!["this is the first".to_string(), "this is the second".to_string()];
326///
327/// assert_eq!(parse_quotes(command), expected);
328/// ```
329///
330/// ```rust
331/// use serenity::utils::parse_quotes;
332///
333/// let command = r#""this is a quoted command that doesn't have an ending quotation"#;
334/// let expected =
335///     vec!["this is a quoted command that doesn't have an ending quotation".to_string()];
336///
337/// assert_eq!(parse_quotes(command), expected);
338/// ```
339pub fn parse_quotes(s: impl AsRef<str>) -> Vec<String> {
340    let s = s.as_ref();
341    let mut args = vec![];
342    let mut in_string = false;
343    let mut escaping = false;
344    let mut current_str = String::default();
345
346    for x in s.chars() {
347        if in_string {
348            if x == '\\' && !escaping {
349                escaping = true;
350            } else if x == '"' && !escaping {
351                if !current_str.is_empty() {
352                    args.push(current_str);
353                }
354
355                current_str = String::default();
356                in_string = false;
357            } else {
358                current_str.push(x);
359                escaping = false;
360            }
361        } else if x == ' ' {
362            if !current_str.is_empty() {
363                args.push(current_str.clone());
364            }
365
366            current_str = String::default();
367        } else if x == '"' {
368            if !current_str.is_empty() {
369                args.push(current_str.clone());
370            }
371
372            in_string = true;
373            current_str = String::default();
374        } else {
375            current_str.push(x);
376        }
377    }
378
379    if !current_str.is_empty() {
380        args.push(current_str);
381    }
382
383    args
384}
385
386/// Discord's official domains. This is used in [`parse_webhook`] and in its corresponding test.
387const DOMAINS: &[&str] = &[
388    "discord.com",
389    "canary.discord.com",
390    "ptb.discord.com",
391    "discordapp.com",
392    "canary.discordapp.com",
393    "ptb.discordapp.com",
394];
395
396/// Parses the id and token from a webhook url. Expects a [`url::Url`] rather than a [`&str`].
397///
398/// # Examples
399///
400/// ```rust
401/// use serenity::utils;
402///
403/// let url_str = "https://discord.com/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV";
404/// let url = url_str.parse().unwrap();
405/// let (id, token) = utils::parse_webhook(&url).unwrap();
406///
407/// assert_eq!(id, 245037420704169985);
408/// assert_eq!(token, "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV");
409/// ```
410#[must_use]
411pub fn parse_webhook(url: &Url) -> Option<(WebhookId, &str)> {
412    let (webhook_id, token) = url.path().strip_prefix("/api/webhooks/")?.split_once('/')?;
413    if !["http", "https"].contains(&url.scheme())
414        || !DOMAINS.contains(&url.domain()?)
415        || !(17..=20).contains(&webhook_id.len())
416        || !(60..=68).contains(&token.len())
417    {
418        return None;
419    }
420    Some((webhook_id.parse().ok()?, token))
421}
422
423#[cfg(all(feature = "cache", feature = "model"))]
424pub(crate) fn user_has_guild_perms(
425    cache_http: impl CacheHttp,
426    guild_id: GuildId,
427    permissions: Permissions,
428) -> Result<()> {
429    if let Some(cache) = cache_http.cache() {
430        if let Some(guild) = cache.guild(guild_id) {
431            guild.require_perms(cache, permissions)?;
432        }
433    }
434    Ok(())
435}
436
437/// Tries to find a user's permissions using the cache. Unlike [`user_has_perms`], this function
438/// will return `true` even when the permissions are not in the cache.
439#[cfg(all(feature = "cache", feature = "model"))]
440#[inline]
441pub(crate) fn user_has_perms_cache(
442    cache: impl AsRef<Cache>,
443    channel_id: ChannelId,
444    required_permissions: Permissions,
445) -> Result<()> {
446    match user_perms(cache, channel_id) {
447        Ok(perms) => {
448            if perms.contains(required_permissions) {
449                Ok(())
450            } else {
451                Err(Error::Model(ModelError::InvalidPermissions {
452                    required: required_permissions,
453                    present: perms,
454                }))
455            }
456        },
457        Err(Error::Model(err)) if err.is_cache_err() => Ok(()),
458        Err(other) => Err(other),
459    }
460}
461
462#[cfg(all(feature = "cache", feature = "model"))]
463pub(crate) fn user_perms(cache: impl AsRef<Cache>, channel_id: ChannelId) -> Result<Permissions> {
464    let cache = cache.as_ref();
465
466    let Some(guild_id) = cache.channels.get(&channel_id).map(|c| *c) else {
467        return Err(Error::Model(ModelError::ChannelNotFound));
468    };
469
470    let Some(guild) = cache.guild(guild_id) else {
471        return Err(Error::Model(ModelError::GuildNotFound));
472    };
473
474    let Some(channel) = guild.channels.get(&channel_id) else {
475        return Err(Error::Model(ModelError::ChannelNotFound));
476    };
477
478    let Some(member) = guild.members.get(&cache.current_user().id) else {
479        return Err(Error::Model(ModelError::MemberNotFound));
480    };
481
482    Ok(guild.user_permissions_in(channel, member))
483}
484
485/// Calculates the Id of the shard responsible for a guild, given its Id and total number of shards
486/// used.
487///
488/// # Examples
489///
490/// Retrieve the Id of the shard for a guild with Id `81384788765712384`, using 17 shards:
491///
492/// ```rust
493/// use serenity::model::id::GuildId;
494/// use serenity::utils;
495///
496/// assert_eq!(utils::shard_id(GuildId::new(81384788765712384), 17), 7);
497/// ```
498#[inline]
499#[must_use]
500pub fn shard_id(guild_id: GuildId, shard_count: u32) -> u32 {
501    ((guild_id.get() >> 22) % (shard_count as u64)) as u32
502}
503
504#[cfg(test)]
505mod test {
506    use super::*;
507
508    #[test]
509    fn test_invite_parser() {
510        assert_eq!(parse_invite("https://discord.gg/abc"), "abc");
511        assert_eq!(parse_invite("http://discord.gg/abc"), "abc");
512        assert_eq!(parse_invite("discord.gg/abc"), "abc");
513        assert_eq!(parse_invite("DISCORD.GG/ABC"), "ABC");
514        assert_eq!(parse_invite("https://discord.com/invite/abc"), "abc");
515        assert_eq!(parse_invite("http://discord.com/invite/abc"), "abc");
516        assert_eq!(parse_invite("discord.com/invite/abc"), "abc");
517    }
518
519    #[test]
520    fn test_username_parser() {
521        assert_eq!(parse_user_mention("<@12345>").unwrap(), 12_345);
522        assert_eq!(parse_user_mention("<@!12345>").unwrap(), 12_345);
523    }
524
525    #[test]
526    fn role_parser() {
527        assert_eq!(parse_role_mention("<@&12345>").unwrap(), 12_345);
528    }
529
530    #[test]
531    fn test_channel_parser() {
532        assert_eq!(parse_channel_mention("<#12345>").unwrap(), 12_345);
533    }
534
535    #[test]
536    fn test_emoji_parser() {
537        let emoji = parse_emoji("<:name:12345>").unwrap();
538        assert_eq!(emoji.name, "name");
539        assert_eq!(emoji.id, 12_345);
540    }
541
542    #[test]
543    fn test_quote_parser() {
544        let parsed = parse_quotes("a \"b c\" d\"e f\"  g");
545        assert_eq!(parsed, ["a", "b c", "d", "e f", "g"]);
546    }
547
548    #[test]
549    fn test_webhook_parser() {
550        for domain in DOMAINS {
551            let url = format!("https://{domain}/api/webhooks/245037420704169985/ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV").parse().unwrap();
552            let (id, token) = parse_webhook(&url).unwrap();
553            assert_eq!(id, 245037420704169985);
554            assert_eq!(
555                token,
556                "ig5AO-wdVWpCBtUUMxmgsWryqgsW3DChbKYOINftJ4DCrUbnkedoYZD0VOH1QLr-S3sV"
557            );
558        }
559    }
560}