1#[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#[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#[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#[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#[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#[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
243pub 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
314pub 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
386const 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#[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#[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#[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}