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, +        }) +    } +} | 
