#![feature(path_file_prefix)] mod article; mod atom; mod blog; mod error; mod i18n; mod live; mod poetry; mod posts; mod scrobbles; mod skweets; mod templates; mod utils; use std::{collections::HashSet, time::Duration}; use i18n::set_language; use poem::http::StatusCode; use poem::i18n::unic_langid::langid; use poem::i18n::I18NResources; use poem::session::{CookieConfig, CookieSession, ServerSession}; use poem::web::cookie::{CookieKey, SameSite}; use poem::{ endpoint::EmbeddedFilesEndpoint, get, handler, listener::TcpListener, middleware::AddData, middleware::Tracing, web::{Data, Path, Query}, EndpointExt, Route, Server, }; use poem::{IntoResponse, Response}; use rand::seq::SliceRandom; use rust_embed::RustEmbed; use error::BlossomError; use serde::Deserialize; use tracing_subscriber::FmtSubscriber; use crate::article::Article; use crate::blog::Blogpost; use crate::i18n::Locale; 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>, locale: Locale) -> templates::Home { let listenbrainz_client = listenbrainz::raw::Client::new(); let (live, listenbrainz, blogposts, poems) = tokio::join!( live::get_live_status(reqwest), scrobbles::get_now_playing(&listenbrainz_client), // skweets::get_recents(&clients.skinnyverse), Blogpost::get_articles(), Poem::get_articles() ); let is_live = live.unwrap_or_default().online; let listenbrainz = listenbrainz.unwrap_or_default(); let blogposts = blogposts.unwrap_or_default(); let poems = poems.unwrap_or_default(); let poem = poems.choose(&mut rand::thread_rng()).cloned(); templates::Home { title: locale.text("title").unwrap(), is_live, listenbrainz, blogposts, poem, locale, } } // #[get("/blog/")] #[handler] async fn blogpost(Path(blogpost): Path, locale: Locale) -> Result { let blogpost = Blogpost::get_article(&blogpost).await?; Ok(templates::Blogpost { title: blogpost .subject() .unwrap_or(locale.text("untitled").unwrap().as_str()) .to_owned(), blogpost, filter_tags: HashSet::new(), locale, }) } #[derive(Deserialize)] struct FilterTags { filter: String, } // #[get("/blog?")] #[handler] async fn get_blog( filter_tags: Option>, locale: Locale, ) -> 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 { title: locale.text("title-blog").unwrap(), blogposts, tags, filter_tags: filter_hashset, locale, }) } // TODO: localize feed #[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(locale: Locale) -> templates::Contact { templates::Contact { title: locale.text("title-contact").unwrap(), locale, } } #[handler] async fn plants() -> Result<()> { Err(BlossomError::Unimplemented) } #[handler] async fn get_poem(Path(poem): Path, locale: Locale) -> Result { let poem = Poem::get_article(&poem).await?; Ok(templates::Poem { title: (&poem.title) .clone() .unwrap_or(locale.text("untitled").unwrap()), poem, jiggle: 4, locale, }) } #[handler] async fn get_poetry(locale: Locale) -> Result { let mut poems = Poem::get_articles().await?; poems.sort_by_key(|poem| poem.created_at); poems.reverse(); Ok(templates::Poetry { title: locale.text("title-poetry").unwrap(), poems, jiggle: 16, locale, }) } async fn custom_error(err: poem::Error) -> impl IntoResponse { templates::Error { title: err.to_string(), 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 resources = I18NResources::builder() .add_path("./resources") .default_language(langid!("en")) .build() .unwrap(); let session = CookieSession::new( // CookieConfig::private(CookieKey::generate()) // .name("blossom") // .domain("blos.sm") // .same_site(SameSite::Strict), CookieConfig::default(), ); 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) .data(resources) .around(set_language) .with(Tracing) .with(session) .with(AddData::new( reqwest::Client::builder() .connect_timeout(Duration::from_secs(1)) .build() .unwrap(), )); Server::new(TcpListener::bind("0.0.0.0:8000")) .run(blossom) .await }