use crate::{post::Post, signalboost::Person}; use chrono::prelude::*; use color_eyre::eyre::Result; use maud::{html, Markup}; use serde::{Deserialize, Serialize}; use std::{ fmt::{self, Display}, fs, path::PathBuf, sync::Arc, }; use tracing::{error, instrument}; pub mod markdown; pub mod poke; #[derive(Clone, Deserialize, Default)] pub struct Config { pub(crate) signalboost: Vec, #[serde(rename = "resumeFname")] pub(crate) resume_fname: PathBuf, #[serde(rename = "miToken")] pub(crate) mi_token: String, #[serde(rename = "jobHistory")] pub(crate) job_history: Vec, } #[derive(Clone, Deserialize, Serialize, Default)] pub struct Salary { pub amount: i32, pub per: String, pub currency: String, } impl Display for Salary { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}${}/{}", self.currency, self.amount, self.per) } } #[derive(Clone, Deserialize, Serialize, Default)] pub struct Job { pub company: String, pub title: String, #[serde(rename = "startDate")] pub start_date: String, #[serde(rename = "endDate")] pub end_date: Option, #[serde(rename = "daysWorked")] pub days_worked: Option, #[serde(rename = "daysBetween")] pub days_between: Option, pub salary: Salary, #[serde(rename = "leaveReason")] pub leave_reason: Option, } impl Job { pub fn pay_history_row(&self) -> Markup { html! { tr { td { (self.title) } td { (self.start_date) } td { (self.end_date.as_ref().unwrap_or(&"current".to_string())) } td { (if self.days_worked.is_some() { self.days_worked.as_ref().unwrap().to_string() } else { "n/a".to_string() }) } td { (self.salary) } td { (self.leave_reason.as_ref().unwrap_or(&"n/a".to_string())) } } } } } #[instrument] async fn patrons() -> Result> { let mut p = dirs::home_dir().unwrap_or(".".into()); p.push(".patreon.json"); if !p.exists() { info!("{:?} does not exist", p); return Ok(None); } let mut cli = patreon::Client::new()?; if let Err(why) = cli.refresh_token().await { error!("error getting refresh token: {}", why); } match cli.campaign().await { Ok(camp) => { let id = camp.data[0].id.clone(); match cli.pledges(id).await { Ok(users) => Ok(Some(users)), Err(why) => { error!("error getting pledges: {}", why); Ok(None) } } } Err(why) => { error!("error getting patreon campaign: {}", why); Ok(None) } } } pub const ICON: &'static str = "https://xeiaso.net/static/img/avatar.png"; pub struct State { pub cfg: Arc, pub signalboost: Vec, pub resume: String, pub blog: Vec, pub gallery: Vec, pub talks: Vec, pub everything: Vec, pub jf: jsonfeed::Feed, pub sitemap: Vec, pub patrons: Option, pub mi: mi::Client, } pub async fn init(cfg: PathBuf) -> Result { let cfg: Arc = Arc::new(serde_dhall::from_file(cfg).parse()?); let sb = cfg.signalboost.clone(); let resume = fs::read_to_string(cfg.clone().resume_fname.clone())?; let resume: String = markdown::render(cfg.clone(), &resume)?; let mi = mi::Client::new( cfg.clone().mi_token.clone(), crate::APPLICATION_NAME.to_string(), )?; let blog = crate::post::load(cfg.clone(), "blog").await?; let gallery = crate::post::load(cfg.clone(), "gallery").await?; let talks = crate::post::load(cfg.clone(), "talks").await?; let mut everything: Vec = vec![]; { let blog = blog.clone(); let gallery = gallery.clone(); let talks = talks.clone(); everything.extend(blog.iter().cloned()); everything.extend(gallery.iter().cloned()); everything.extend(talks.iter().cloned()); }; everything.sort(); everything.reverse(); let today = Utc::today(); let everything: Vec = everything .into_iter() .filter(|p| today.num_days_from_ce() >= p.date.num_days_from_ce()) .take(5) .collect(); let mut jfb = jsonfeed::Feed::builder() .title("Xe's Blog") .description("My blog posts and rants about various technology things.") .author( jsonfeed::Author::new() .name("Xe") .url("https://xeiaso.net") .avatar(ICON), ) .feed_url("https://xeiaso.net/blog.json") .user_comment("This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1") .home_page_url("https://xeiaso.net") .icon(ICON) .favicon(ICON); for post in &everything { let post = post.clone(); jfb = jfb.item(post.clone().into()); } let mut sm: Vec = vec![]; let smw = sitemap::writer::SiteMapWriter::new(&mut sm); let mut urlwriter = smw.start_urlset()?; for url in &[ "https://xeiaso.net/resume", "https://xeiaso.net/contact", "https://xeiaso.net/", "https://xeiaso.net/blog", "https://xeiaso.net/signalboost", ] { urlwriter.url(*url)?; } for post in &blog { urlwriter.url(format!("https://xeiaso.net/{}", post.link))?; } for post in &gallery { urlwriter.url(format!("https://xeiaso.net/{}", post.link))?; } for post in &talks { urlwriter.url(format!("https://xeiaso.net/{}", post.link))?; } urlwriter.end()?; Ok(State { mi, cfg, signalboost: sb, resume, blog, gallery, talks, everything, jf: jfb.build(), sitemap: sm, patrons: patrons().await?, }) } #[cfg(test)] mod tests { use color_eyre::eyre::Result; #[tokio::test] async fn init() -> Result<()> { super::init("./config.dhall".into()).await?; Ok(()) } }