#![feature(path_file_prefix)] mod article; mod atom; mod blog; mod error; mod live; mod poetry; mod posts; mod scrobbles; mod skweets; mod templates; mod utils; use std::{collections::HashSet, time::Duration}; use poem::http::StatusCode; use poem::{ endpoint::EmbeddedFilesEndpoint, get, handler, listener::TcpListener, middleware::AddData, middleware::Tracing, web::{Data, Path, Query}, EndpointExt, Route, Server, }; use poem::{IntoResponse, Response}; use rust_embed::RustEmbed; use error::BlossomError; use serde::Deserialize; use tracing_subscriber::FmtSubscriber; use crate::article::Article; use crate::blog::Blogpost; use crate::poetry::Poem; use crate::posts::Post; type Result = std::result::Result; #[derive(RustEmbed)] #[folder = "static/"] struct Static; #[handler] async fn home(Data(reqwest): Data<&reqwest::Client>) -> templates::Home { let listenbrainz_client = listenbrainz::raw::Client::new(); let (live, listenbrainz, blogposts) = tokio::join!( live::get_live_status(reqwest), scrobbles::get_now_playing(&listenbrainz_client), // skweets::get_recents(&clients.skinnyverse), Blogpost::get_articles() ); let is_live = live.unwrap_or_default().online; let listenbrainz = listenbrainz.unwrap_or_default(); let blogposts = blogposts.unwrap_or_default(); templates::Home { is_live, listenbrainz, blogposts, } } // #[get("/blog/")] #[handler] async fn blogpost(Path(blogpost): Path) -> Result { let blogpost = Blogpost::get_article(&blogpost).await?; Ok(templates::Blogpost { blogpost, filter_tags: HashSet::new(), }) } #[derive(Deserialize)] struct FilterTags { filter: String, } // #[get("/blog?")] #[handler] async fn get_blog(filter_tags: Option>) -> Result { let mut blogposts = Blogpost::get_articles().await?; let tags: Vec = posts::Post::get_tags(&blogposts) .into_iter() .map(|tag| tag.to_owned()) .collect(); let mut filter_hashset: HashSet = HashSet::new(); if let Some(Query(FilterTags { filter })) = filter_tags { filter_hashset.insert(filter); blogposts = posts::Post::filter_by_tags(blogposts, &filter_hashset); } Ok(templates::Blog { blogposts, tags, filter_tags: filter_hashset, }) } #[handler] async fn feed() -> Result { let blogposts: Vec> = Blogpost::get_articles() .await? .into_iter() .map(|bp| Box::new(bp) as Box) .collect(); let poems: Vec> = Poem::get_articles() .await? .into_iter() .map(|poem| Box::new(poem) as Box) .collect(); let mut posts = Vec::new(); posts.extend(blogposts); posts.extend(poems); // TODO: i18n let context = atom::Context { page_title: "celeste's hard drive".to_owned(), page_url: "https://en.blos.sm".to_owned(), self_url: "https://en.blos.sm/feed".to_owned(), lang: "en".to_owned(), }; let feed = atom::atom(context, posts).await; let feed: String = String::from_utf8(feed.write_to(Vec::new())?)?; Ok(Response::builder() .status(StatusCode::OK) .content_type("application/atom+xml") .body(feed)) } #[handler] async fn contact() -> templates::Contact { templates::Contact } #[handler] async fn plants() -> Result<()> { Err(BlossomError::Unimplemented) } #[handler] async fn get_poem(Path(poem): Path) -> Result { let poem = Poem::get_article(&poem).await?; Ok(templates::Poem { poem, jiggle: 4 }) } #[handler] async fn get_poetry() -> Result { let mut poems = Poem::get_articles().await?; poems.sort_by_key(|poem| poem.created_at); poems.reverse(); Ok(templates::Poetry { poems, jiggle: 16 }) } async fn custom_error(err: poem::Error) -> impl IntoResponse { templates::Error { status: err.status(), message: err.to_string(), } .with_status(err.status()) } #[tokio::main] async fn main() -> std::result::Result<(), std::io::Error> { let subscriber = FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed"); // let mut skinny_data = mastodon_async::Data::default(); // skinny_data.base = Cow::from("https://skinnyver.se"); let blossom = Route::new() .at("/", get(home)) .at("/blog", get(get_blog)) .at("/blog/:blogpost", get(blogpost)) .at("/poetry", get(get_poetry)) .at("/poetry/:poem", get(get_poem)) .at("/feed", get(feed)) .at("/contact", get(contact)) .at("/plants", get(plants)) .nest("/static/", EmbeddedFilesEndpoint::::new()) .catch_all_error(custom_error) .with(Tracing) .with(AddData::new( reqwest::Client::builder() .connect_timeout(Duration::from_secs(1)) .build() .unwrap(), )); Server::new(TcpListener::bind("0.0.0.0:3000")) .run(blossom) .await }