2023-04-21 23:39:52 +00:00
|
|
|
use magnetar_core::web_model::acct::Acct;
|
2023-10-30 22:00:46 +00:00
|
|
|
use magnetar_sdk::mmm;
|
|
|
|
use magnetar_sdk::mmm::Token;
|
2023-04-21 23:39:52 +00:00
|
|
|
use percent_encoding::percent_decode_str;
|
|
|
|
use std::borrow::Cow;
|
2023-07-07 19:22:30 +00:00
|
|
|
use thiserror::Error;
|
2023-04-21 23:39:52 +00:00
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
pub struct FediverseTag {
|
|
|
|
pub name: String,
|
|
|
|
pub host: Option<String>,
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
#[derive(Debug, Error)]
|
|
|
|
pub enum FediverseTagParseError {
|
|
|
|
#[error("Invalid char in tag: {0}")]
|
|
|
|
InvalidChar(String),
|
|
|
|
#[error("Invalid UTF-8 in tag: {0}")]
|
|
|
|
InvalidUtf8(#[from] std::str::Utf8Error),
|
|
|
|
}
|
|
|
|
|
2023-10-30 22:00:46 +00:00
|
|
|
impl From<&FediverseTagParseError> for &str {
|
|
|
|
fn from(_: &FediverseTagParseError) -> Self {
|
|
|
|
"FediverseTagParseError"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-21 23:39:52 +00:00
|
|
|
impl<S1: AsRef<str>, S2: AsRef<str>> From<(S1, Option<S2>)> for FediverseTag {
|
|
|
|
fn from((name, host): (S1, Option<S2>)) -> Self {
|
|
|
|
Self {
|
|
|
|
name: name.as_ref().to_owned(),
|
2023-04-22 00:17:24 +00:00
|
|
|
host: host.as_ref().map(S2::as_ref).map(str::to_owned),
|
2023-04-21 23:39:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<FediverseTag> for Acct {
|
|
|
|
fn from(value: FediverseTag) -> Self {
|
|
|
|
value.to_string().into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ToString for FediverseTag {
|
|
|
|
fn to_string(&self) -> String {
|
|
|
|
if let Some(ref host) = self.host {
|
|
|
|
format!("{}@{host}", self.name)
|
|
|
|
} else {
|
|
|
|
self.name.clone()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
pub fn lenient_parse_acct(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
|
2023-04-21 23:39:52 +00:00
|
|
|
lenient_parse_tag(acct.as_ref())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn split_tag_inner(tag: impl AsRef<str>) -> (String, Option<String>) {
|
|
|
|
let tag = tag.as_ref();
|
2023-04-22 00:17:24 +00:00
|
|
|
let tag = tag.strip_prefix('@').unwrap_or(tag.as_ref());
|
2023-04-21 23:39:52 +00:00
|
|
|
|
|
|
|
match tag.split_once('@') {
|
|
|
|
Some((name, host)) if name.is_empty() => (host.to_owned(), None),
|
|
|
|
Some((name, host)) => (name.to_owned(), Some(host.to_owned())),
|
|
|
|
None => (tag.to_owned(), None),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
fn validate_tag_inner((name, host): (&str, Option<&str>)) -> Result<(), FediverseTagParseError> {
|
2023-04-22 00:17:24 +00:00
|
|
|
if name
|
|
|
|
.chars()
|
|
|
|
.any(|c| !c.is_alphanumeric() && c != '-' && c != '.')
|
|
|
|
{
|
2023-07-07 19:22:30 +00:00
|
|
|
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
|
2023-04-21 23:39:52 +00:00
|
|
|
}
|
|
|
|
|
2023-04-22 00:17:24 +00:00
|
|
|
if let Some(host_str) = host {
|
2023-04-21 23:39:52 +00:00
|
|
|
if host_str
|
|
|
|
.chars()
|
|
|
|
.any(|c| c.is_control() || c.is_whitespace() || c == '/' || c == '#')
|
|
|
|
{
|
2023-07-07 19:22:30 +00:00
|
|
|
return Err(FediverseTagParseError::InvalidChar(name.to_owned()));
|
2023-04-21 23:39:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
pub fn lenient_parse_tag(tag: impl AsRef<str>) -> Result<FediverseTag, FediverseTagParseError> {
|
2023-04-21 23:39:52 +00:00
|
|
|
let (name, host) = split_tag_inner(tag);
|
|
|
|
|
|
|
|
validate_tag_inner((&name, host.as_ref().map(String::as_ref)))?;
|
|
|
|
|
|
|
|
Ok(FediverseTag { name, host })
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
pub fn lenient_parse_acct_decode(acct: &Acct) -> Result<FediverseTag, FediverseTagParseError> {
|
2023-04-21 23:39:52 +00:00
|
|
|
lenient_parse_tag_decode(acct.as_ref())
|
|
|
|
}
|
|
|
|
|
2023-07-07 19:22:30 +00:00
|
|
|
pub fn lenient_parse_tag_decode(
|
|
|
|
tag: impl AsRef<str>,
|
|
|
|
) -> Result<FediverseTag, FediverseTagParseError> {
|
2023-04-21 23:39:52 +00:00
|
|
|
let (name, host) = split_tag_inner(tag);
|
|
|
|
|
|
|
|
let name_decoded = percent_decode_str(&name).decode_utf8()?;
|
|
|
|
let host_decoded = host
|
|
|
|
.map(|host| percent_decode_str(&host).decode_utf8().map(Cow::into_owned))
|
|
|
|
.transpose()?;
|
|
|
|
|
|
|
|
validate_tag_inner((&name_decoded, host_decoded.as_deref()))?;
|
|
|
|
|
|
|
|
Ok(FediverseTag {
|
|
|
|
name: name_decoded.into_owned(),
|
|
|
|
host: host_decoded,
|
|
|
|
})
|
|
|
|
}
|
2023-10-30 22:00:46 +00:00
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum RawReaction {
|
|
|
|
Unicode(String),
|
|
|
|
Shortcode {
|
|
|
|
shortcode: String,
|
|
|
|
host: Option<String>,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_reaction(tag: impl AsRef<str>) -> Option<RawReaction> {
|
|
|
|
let tok = mmm::Context::default().parse_ui(tag.as_ref());
|
|
|
|
|
|
|
|
match tok {
|
|
|
|
Token::UnicodeEmoji(text) => Some(RawReaction::Unicode(text)),
|
|
|
|
Token::ShortcodeEmoji { shortcode, host } => {
|
|
|
|
Some(RawReaction::Shortcode { shortcode, host })
|
|
|
|
}
|
|
|
|
_ => None,
|
|
|
|
}
|
|
|
|
}
|