diff options
Diffstat (limited to '')
-rw-r--r-- | src/error.rs | 22 | ||||
-rw-r--r-- | src/main.rs | 13 | ||||
-rw-r--r-- | src/posts/mod.rs | 141 |
3 files changed, 159 insertions, 17 deletions
diff --git a/src/error.rs b/src/error.rs index 890a148..81cadd1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,6 +5,10 @@ pub enum BlossomError { Reqwest(Status, #[response(ignore)] reqwest::Error), ListenBrainz(Status, #[response(ignore)] listenbrainz::Error), Skinnyverse(Status, #[response(ignore)] mastodon_async::Error), + Chrono(Status, #[response(ignore)] chrono::ParseError), + Io(Status, #[response(ignore)] std::io::Error), + Deserialization(Status, #[response(ignore)] toml::de::Error), + NoMetadata(Status), Unimplemented(Status), } @@ -25,3 +29,21 @@ impl From<mastodon_async::Error> for BlossomError { BlossomError::Skinnyverse(Status::new(500), e) } } + +impl From<chrono::ParseError> for BlossomError { + fn from(e: chrono::ParseError) -> Self { + BlossomError::Chrono(Status::new(500), e) + } +} + +impl From<std::io::Error> for BlossomError { + fn from(e: std::io::Error) -> Self { + BlossomError::Io(Status::new(500), e) + } +} + +impl From<toml::de::Error> for BlossomError { + fn from(e: toml::de::Error) -> Self { + BlossomError::Deserialization(Status::new(500), e) + } +} diff --git a/src/main.rs b/src/main.rs index 57d4973..dd2bd34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,20 +26,23 @@ extern crate rocket; #[get("/")] async fn home(clients: &State<Clients>) -> Template { - let (live, listenbrainz, skweets) = tokio::join!( + let (live, listenbrainz, skweets, blogposts) = tokio::join!( live::get_live_status(&clients.reqwest), scrobbles::get_now_playing(&clients.listenbrainz), - skweets::get_recents(&clients.skinnyverse) + skweets::get_recents(&clients.skinnyverse), + posts::get_blogposts() ); let is_live = live.unwrap_or_default().online; let listenbrainz = listenbrainz.unwrap_or_default(); let skweets = skweets.unwrap_or_default(); + let blogposts = blogposts.unwrap_or_default(); Template::render( "home", context! { - is_live, - listenbrainz, - skweets, + is_live, + listenbrainz, + skweets, + blogposts, }, ) } diff --git a/src/posts/mod.rs b/src/posts/mod.rs index b0c3749..b188ee0 100644 --- a/src/posts/mod.rs +++ b/src/posts/mod.rs @@ -1,6 +1,14 @@ +use async_trait::async_trait; use chrono::{DateTime, Utc}; -use std::path::Path; +use markdown::{mdast::Node, Constructs, ParseOptions}; +use rocket::http::Status; +use serde::{ser::SerializeStruct, Deserialize, Serialize}; +use tokio::fs; +use tokio::io::AsyncReadExt; +use crate::{error::BlossomError, Result}; + +#[derive(Serialize, Debug)] enum PostType { Article, Note, @@ -12,22 +20,131 @@ enum TextFormat { Html, } -pub struct Post<T: Content> { - id: i64, +#[derive(Debug)] +pub struct Post<D: Content> { + // id: i64, + subject: Option<String>, created_at: DateTime<Utc>, updated_at: Option<DateTime<Utc>>, - post_type: PostType, - media: Option<Vec<Box<Path>>>, - content: Option<T>, tags: Vec<String>, + post_type: PostType, + data: D, +} + +impl<D: Content> Serialize for Post<D> { + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + let mut state = serializer.serialize_struct("Post", 5)?; + state.serialize_field("subject", &self.subject)?; + state.serialize_field("created_at", &self.created_at.to_string())?; + state.serialize_field( + "updated_at", + &self.updated_at.and_then(|time| Some(time.to_string())), + )?; + state.serialize_field("tags", &self.tags)?; + state.serialize_field("post_type", &self.post_type)?; + state.end() + } } +pub async fn get_blogposts() -> Result<Vec<Post<Article>>> { + let mut blogposts: Vec<Post<Article>> = Vec::new(); + let mut articles_dir = fs::read_dir("./articles").await?; + while let Some(dir) = articles_dir.next_entry().await? { + let mut file_path = dir.path(); + file_path.push(dir.file_name()); + let file_path = file_path.with_extension("md"); + println!("{:?}", file_path); + let blogpost: Article = Article { + path: file_path.to_str().unwrap_or_default().to_owned(), + }; + let blogpost = Post::try_from(blogpost).await.unwrap(); + println!("{:?}", blogpost); + blogposts.push(blogpost); + } + Ok(blogposts) +} + +#[async_trait] pub trait Content { - fn render(&self) -> String; + async fn render(&self) -> Result<String>; } -// impl<T> Post<T> { -// // renders as internal html (must sanitize) -// fn new(type: PostType, ) -> Post<T> { -// } -// } +#[derive(Debug)] +pub struct Article { + path: String, +} + +impl Article { + async fn tree(&self) -> Result<Node> { + let mut file = fs::File::open(&self.path).await?; + let mut buf = String::new(); + file.read_to_string(&mut buf).await?; + Ok(markdown::to_mdast( + &buf, + &ParseOptions { + constructs: Constructs { + frontmatter: true, + ..Constructs::default() + }, + ..ParseOptions::default() + }, + ) + .unwrap()) + } + + async fn frontmatter(&self) -> Result<String> { + let tree = self.tree().await?; + let children = tree.children(); + if let Some(children) = children { + if let Some(toml) = children.into_iter().find_map(|el| match el { + Node::Toml(toml) => Some(toml.value.to_owned()), + _ => None, + }) { + return Ok(toml); + }; + } + Err(BlossomError::NoMetadata(Status::new(500))) + } +} + +#[async_trait] +impl Content for Article { + async fn render(&self) -> Result<String> { + let mut file = fs::File::open(&self.path).await?; + let mut buf = String::new(); + file.read_to_string(&mut buf).await?; + Ok(markdown::to_html(&buf)) + } +} + +#[derive(Deserialize)] +struct ArticleMetadata { + title: String, + created_at: String, + updated_at: Option<String>, + tags: Vec<String>, +} + +impl Post<Article> { + async fn try_from(article: Article) -> Result<Post<Article>> { + let metadata = article.frontmatter().await?; + let metadata: ArticleMetadata = toml::from_str(&metadata)?; + let updated_at = if let Some(updated_at) = metadata.updated_at { + Some(updated_at.parse::<DateTime<Utc>>()?) + } else { + None + }; + + Ok(Post { + subject: Some(metadata.title), + created_at: metadata.created_at.parse::<DateTime<Utc>>()?, + updated_at, + tags: metadata.tags, + post_type: PostType::Article, + data: article, + }) + } +} |