#![allow(dead_code)]
use url::Url;
pub type Name = String;
pub type Id = String;
#[derive(Debug, Default, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Words(#[knuffel(arguments)] Vec<String>);
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(Default, PartialEq))]
pub struct Art {
/// prominent web handle
#[knuffel(node_name)]
handle: Name,
#[knuffel(child, default)]
aliases: Words,
#[knuffel(child, default)]
styles: Words,
/// official website
#[knuffel(argument, default, str)]
web: Option<Url>,
/// its feed, and readablility score
#[knuffel(child)]
feed: Option<feed::Feed>,
/// linktr.ee, carrd.co, ... and the like
#[knuffel(property, str)]
link_list: Option<Url>,
// capital pipes
#[knuffel(child)]
pat: Option<Patreon>,
#[knuffel(child)]
kofi: Option<Kofi>,
#[knuffel(child)]
github: Option<Github>,
// 1-way feed
#[knuffel(child)]
tiny: Option<Tinyview>,
#[knuffel(child)]
fb: Option<Facebook>,
#[knuffel(child)]
go: Option<Gocomics>,
#[knuffel(child)]
tumblr: Option<Tumblr>,
#[knuffel(child)]
substack: Option<Substack>,
#[knuffel(child)]
bsky: Option<Bsky>,
#[knuffel(child)]
cohost: Option<Cohost>,
#[knuffel(child)]
da: Option<DeviantArt>,
#[knuffel(child)]
yt: Option<Youtube>,
#[knuffel(child)]
ig: Option<Insta>,
/// Huginn agent id
#[knuffel(child)]
huginn: Option<HuginnId>,
// interactive flows
#[knuffel(child)]
fedi: Option<Fedi>,
#[knuffel(child)]
twt: Option<Twitter>,
}
macro_rules! simple_account {
($site: ident, $url_template: expr, $feed_template: expr ) => {
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct $site(#[knuffel(argument)] String);
impl std::fmt::Display for $site {
fn fmt(
&self,
f: &mut std::fmt::Formatter,
) -> std::fmt::Result {
write!(f, $url_template, &self.0)
}
}
impl crate::art::feed::AsFeed for $site {
fn as_feed(&self) -> miette::Result<Url> {
use miette::IntoDiagnostic as _;
format!($feed_template, &self.0)
.parse::<Url>()
.into_diagnostic()
}
}
};
}
simple_account!(Cohost, "https://cohost.org/{}", "https://cohost.org/{}.rss");
simple_account!(
DeviantArt,
"https://deviantart.com/user/{}",
"https://backend.deviantart.com/{}"
);
simple_account!(
Gocomics,
"https://gocomics.com/{}",
"https://comicrss.com/rss/{}.rss"
);
simple_account!(Insta, "https://instagram.com/{}", "NA: {}");
simple_account!(
Substack,
"https://{}.substack.com/",
"https://{}.substack.com/rss"
);
simple_account!(Tinyview, "https://tinyview.com/{}", "NA: {}");
simple_account!(
Tumblr,
"https://{}.tumblr.com/",
"https://{}.tumblr.com/rss"
);
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
// TODO: some hostname type
pub struct Bsky(#[knuffel(argument, str)] String);
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Fedi(#[knuffel(argument, str)] Url);
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
// TODO: channel ID & rss feed
pub struct Youtube(#[knuffel(argument)] String);
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct HuginnId(#[knuffel(argument)] u16);
/// patreon profile, and monthly support amount
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Patreon {
#[knuffel(property, default)]
per_month: u8,
#[knuffel(flatten(property))]
account: Account,
}
/// ko-fi profile, and monthly support amount
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Kofi {
#[knuffel(property, default)]
per_month: u8,
#[knuffel(flatten(property))]
account: Account,
}
/// github profile, and monthly support amount
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Github {
#[knuffel(property, default)]
per_month: u8,
#[knuffel(flatten(property))]
account: Account,
}
/**
/// name-or-id common pattern
Most account handles can have
- (text) name
- (numeric) ID, eg snowflake
IDs are allocated once, and are innate.
DNS and bsky handles are hostnames, innate IDs.
TODO: hydrate name from (grand)parent node_name
*/
#[derive(Debug, Default, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Account {
/// Mutable vain handle
#[knuffel(property)]
vanity: Option<Name>,
/// Immutable humble, per-system internal identity
#[knuffel(property)]
serial: Option<Id>,
}
/// fb profile
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Facebook {
#[knuffel(flatten(property))]
account: Account,
}
/// twt/X profile
#[derive(Debug, knuffel::Decode)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Twitter {
#[knuffel(flatten(property))]
account: Account,
/// list memberships
#[knuffel(child, default)]
lists: Words,
}
mod display;
mod feed;
mod scoring;
#[cfg(test)]
mod tests {
use super::*;
use miette::IntoDiagnostic as _;
#[test]
fn aliases() -> miette::Result<()> {
let input = r#"comic { aliases "artist1" "artist2";
}"#;
assert_eq!(
vec![Art {
handle: "comic".to_string(),
aliases: Words(vec!["artist1".to_string(), "artist2".into()]),
..Default::default()
}],
knuffel::parse::<Vec<Art>>("", input)?
);
Ok(())
}
#[test]
#[ignore = "try dropping /-"]
fn default_handle() -> miette::Result<()> {
let input = r#"comic { fb /-vanity="comic"
}"#;
assert_eq!(
vec![Art {
handle: "comic".to_string(),
fb: Some(Facebook {
account: Account {
vanity: ("comic".to_string().into()),
serial: None
}
}),
..Default::default()
}],
knuffel::parse::<Vec<Art>>("", input)?
);
Ok(())
}
#[test]
fn decode_fb() -> miette::Result<()> {
let input = r#"
facebook vanity="abc"
facebook vanity=null serial="1234"
"#;
assert_eq!(
vec![
Facebook {
account: Account {
vanity: Some("abc".to_string()),
serial: None
}
},
Facebook {
account: Account {
serial: Some(1234.to_string()),
vanity: None
}
}
],
knuffel::parse::<Vec<Facebook>>("", input)?
);
Ok(())
}
#[test]
fn where_am_i() -> miette::Result<()> {
let input = r#"
hdhoang "https://hdhoang.space/" {
cohost "hdhoang"
pat vanity="hdhoang"
fb vanity="hdh0000"
fedi "https://blob.cat/hdhoang"
twt vanity="21_25" serial="2125"
} "#;
assert_eq!(
knuffel::parse::<Vec<Art>>("", input)?,
vec![Art {
handle: "hdhoang".to_string(),
web: Some(
"https://hdhoang.space/"
.parse::<url::Url>()
.into_diagnostic()?
),
cohost: Cohost("hdhoang".to_string()).into(),
fedi: Fedi(
"https://blob.cat/hdhoang"
.parse::<url::Url>()
.into_diagnostic()?
)
.into(),
pat: Some(Patreon {
per_month: 0,
account: Account {
vanity: Some("hdhoang".to_string()),
serial: None
}
}),
fb: Some(Facebook {
account: Account {
vanity: Some("hdh0000".to_string()),
serial: None
}
}),
twt: Some(Twitter {
account: Account {
vanity: Some("21_25".to_string()),
serial: Some(2125.to_string())
},
lists: Words(vec![]),
}),
..Default::default()
}]
);
Ok(())
}
}