| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
pub fn bluesky(input: &str) -> Option<String> { |
| 8 |
let h = input.trim().trim_start_matches('@').trim_end_matches('/'); |
| 9 |
let h = h.strip_prefix("https://bsky.app/profile/").unwrap_or(h).to_ascii_lowercase(); |
| 10 |
(!h.is_empty() && h.len() <= 253 && h.contains('.') && h.split('.').all(label_ok)).then_some(h) |
| 11 |
} |
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
pub fn mastodon(input: &str) -> Option<String> { |
| 16 |
let s = input.trim().trim_end_matches('/'); |
| 17 |
let (user, instance) = if let Some(rest) = s.strip_prefix("https://") { |
| 18 |
let (instance, user) = rest.split_once("/@")?; |
| 19 |
(user, instance) |
| 20 |
} else { |
| 21 |
s.trim_start_matches('@').split_once('@')? |
| 22 |
}; |
| 23 |
let user_ok = !user.is_empty() && user.len() <= 64 && user.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.'); |
| 24 |
let instance = instance.to_ascii_lowercase(); |
| 25 |
let instance_ok = instance.contains('.') && instance.len() <= 253 && instance.split('.').all(label_ok); |
| 26 |
(user_ok && instance_ok).then(|| format!("{user}@{instance}")) |
| 27 |
} |
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
pub fn github(input: &str) -> Option<String> { |
| 32 |
let u = input.trim().trim_start_matches('@').trim_end_matches('/'); |
| 33 |
let u = u.strip_prefix("https://github.com/").unwrap_or(u); |
| 34 |
let ok = !u.is_empty() |
| 35 |
&& u.len() <= 39 |
| 36 |
&& u.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') |
| 37 |
&& !u.starts_with('-') |
| 38 |
&& !u.ends_with('-'); |
| 39 |
ok.then(|| u.to_string()) |
| 40 |
} |
| 41 |
|
| 42 |
fn label_ok(l: &str) -> bool { |
| 43 |
!l.is_empty() && l.len() <= 63 && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && !l.starts_with('-') && !l.ends_with('-') |
| 44 |
} |
| 45 |
|
| 46 |
pub fn bluesky_url(handle: &str) -> String { |
| 47 |
format!("https://bsky.app/profile/{handle}") |
| 48 |
} |
| 49 |
|
| 50 |
|
| 51 |
pub fn mastodon_url(address: &str) -> String { |
| 52 |
match address.split_once('@') { |
| 53 |
Some((user, instance)) => format!("https://{instance}/@{user}"), |
| 54 |
None => String::new(), |
| 55 |
} |
| 56 |
} |
| 57 |
|
| 58 |
pub fn github_url(user: &str) -> String { |
| 59 |
format!("https://github.com/{user}") |
| 60 |
} |
| 61 |
|
| 62 |
#[cfg(test)] |
| 63 |
mod tests { |
| 64 |
use super::*; |
| 65 |
|
| 66 |
#[test] |
| 67 |
fn handles_are_checked_and_normalised() { |
| 68 |
assert_eq!(bluesky("@Andy.bsky.social").as_deref(), Some("andy.bsky.social")); |
| 69 |
assert_eq!(bluesky("https://bsky.app/profile/andy.example.org/").as_deref(), Some("andy.example.org")); |
| 70 |
assert_eq!(bluesky("andy"), None, "a handle is a domain"); |
| 71 |
assert_eq!(bluesky("an dy.bsky.social"), None); |
| 72 |
assert_eq!(mastodon("@[email protected]").as_deref(), Some("[email protected]")); |
| 73 |
assert_eq!(mastodon("https://Fosstodon.org/@andy").as_deref(), Some("[email protected]")); |
| 74 |
assert_eq!(mastodon("andy"), None); |
| 75 |
assert_eq!(mastodon("andy@nodot"), None); |
| 76 |
assert_eq!(github("https://github.com/andymccall/").as_deref(), Some("andymccall")); |
| 77 |
assert_eq!(github("@andy-mccall").as_deref(), Some("andy-mccall")); |
| 78 |
assert_eq!(github("-andy"), None); |
| 79 |
assert_eq!(github("andy mccall"), None); |
| 80 |
assert_eq!(mastodon_url("[email protected]"), "https://fosstodon.org/@andy"); |
| 81 |
} |
| 82 |
} |