use crate::types::{ChannelId, EmojiId, RoleId, UserId, WebhookId};
pub fn parse_user_mention(s: &str) -> Option<UserId> {
let inner = strip_wrapping(s, "<@", ">")?;
let digits = inner.strip_prefix('!').unwrap_or(inner);
digits.parse().ok()
}
pub fn parse_role_mention(s: &str) -> Option<RoleId> {
let digits = strip_wrapping(s, "<@&", ">")?;
digits.parse().ok()
}
pub fn parse_channel_mention(s: &str) -> Option<ChannelId> {
let digits = strip_wrapping(s, "<#", ">")?;
digits.parse().ok()
}
pub fn parse_emoji(s: &str) -> Option<(String, EmojiId, bool)> {
let (animated, inner) = if s.starts_with("<a:") { (true, strip_wrapping(s, "<a:", ">")?) } else { (false, strip_wrapping(s, "<:", ">")?) };
let mut parts = inner.splitn(2, ':');
let name = parts.next()?.to_string();
let id: EmojiId = parts.next()?.parse().ok()?;
Some((name, id, animated))
}
pub fn parse_invite(s: &str) -> Option<String> {
let s = s.trim();
for prefix in &["https://discord.gg/", "https://discord.com/invite/", "http://discord.gg/"] {
if let Some(code) = s.strip_prefix(prefix) {
let code = code.trim_end_matches('/');
if is_valid_invite_code(code) {
return Some(code.to_string());
}
}
}
if is_valid_invite_code(s) {
return Some(s.to_string());
}
None
}
pub fn parse_user_tag(s: &str) -> Option<(String, String)> {
let hash = s.rfind('#')?;
let username = s[..hash].to_string();
let disc = &s[hash + 1..];
if disc.len() == 4 && disc.chars().all(|c| c.is_ascii_digit()) {
Some((username, disc.to_string()))
} else {
None
}
}
pub fn parse_webhook_url(url: &str) -> Option<(WebhookId, String)> {
const PREFIX: &str = "https://discord.com/api/webhooks/";
let rest = url.trim().strip_prefix(PREFIX)?;
let mut parts = rest.splitn(2, '/');
let id: WebhookId = parts.next()?.parse().ok()?;
let token = parts.next()?.trim_end_matches('/').to_string();
if token.is_empty() {
None
} else {
Some((id, token))
}
}
pub fn parse_user_id(s: &str) -> Option<UserId> {
s.trim().parse().ok()
}
pub fn parse_channel_id(s: &str) -> Option<ChannelId> {
s.trim().parse().ok()
}
pub fn parse_role_id(s: &str) -> Option<RoleId> {
s.trim().parse().ok()
}
fn strip_wrapping<'a>(s: &'a str, prefix: &str, suffix: &str) -> Option<&'a str> {
s.strip_prefix(prefix)?.strip_suffix(suffix)
}
fn is_valid_invite_code(s: &str) -> bool {
let len = s.len();
(2..=30).contains(&len) && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_mention() {
let id = parse_user_mention("<@123456789012345678>").unwrap();
assert_eq!(id.get(), 123456789012345678);
}
#[test]
fn user_nick_mention() {
let id = parse_user_mention("<@!123456789012345678>").unwrap();
assert_eq!(id.get(), 123456789012345678);
}
#[test]
fn role_mention() {
let id = parse_role_mention("<@&987654321098765432>").unwrap();
assert_eq!(id.get(), 987654321098765432);
}
#[test]
fn channel_mention() {
let id = parse_channel_mention("<#111222333444555666>").unwrap();
assert_eq!(id.get(), 111222333444555666);
}
#[test]
fn emoji_static() {
let (name, id, animated) = parse_emoji("<:wave:749054660769218631>").unwrap();
assert_eq!(name, "wave");
assert_eq!(id.get(), 749054660769218631);
assert!(!animated);
}
#[test]
fn emoji_animated() {
let (name, _id, animated) = parse_emoji("<a:dance:749054660769218631>").unwrap();
assert_eq!(name, "dance");
assert!(animated);
}
#[test]
fn invite_url() {
assert_eq!(parse_invite("https://discord.gg/rust"), Some("rust".to_string()));
assert_eq!(parse_invite("https://discord.com/invite/rust"), Some("rust".to_string()));
assert_eq!(parse_invite("rust"), Some("rust".to_string()));
}
#[test]
fn user_tag() {
let (name, disc) = parse_user_tag("Alice#1234").unwrap();
assert_eq!(name, "Alice");
assert_eq!(disc, "1234");
}
#[test]
fn webhook_url() {
let url = "https://discord.com/api/webhooks/123456789012345678/abc-TOKEN";
let (id, token) = parse_webhook_url(url).unwrap();
assert_eq!(id.get(), 123456789012345678);
assert_eq!(token, "abc-TOKEN");
}
#[test]
fn invalid_mention_returns_none() {
assert!(parse_user_mention("not a mention").is_none());
assert!(parse_role_mention("<@123>").is_none()); assert!(parse_channel_mention("").is_none());
}
}