aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar cel 🌸 <cel@blos.sm>2023-06-21 19:32:30 +0100
committerLibravatar cel 🌸 <cel@blos.sm>2023-06-21 19:32:30 +0100
commit14db008af73d25cc92ea6c0b1f3b1e0ef67920d1 (patch)
treebe9eecf46dd7dc01686e26b88e27f98d8e9c7fdd /src
parente0cd7838c350cb9814d8285cad04648f32b3db59 (diff)
downloadblossom-14db008af73d25cc92ea6c0b1f3b1e0ef67920d1.tar.gz
blossom-14db008af73d25cc92ea6c0b1f3b1e0ef67920d1.tar.bz2
blossom-14db008af73d25cc92ea6c0b1f3b1e0ef67920d1.zip
implement foundation for posts
Diffstat (limited to 'src')
-rw-r--r--src/error.rs22
-rw-r--r--src/main.rs13
-rw-r--r--src/posts/mod.rs141
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,
+ })
+ }
+}