#![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::i18n::unic_langid::langid;
use poem::i18n::{I18NResources, Locale};
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::poetry::Poem;
use crate::posts::Post;
type Result<T> = std::result::Result<T, BlossomError>;
#[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/<blogpost>")]
#[handler]
async fn blogpost(Path(blogpost): Path<String>, locale: Locale) -> Result<templates::Blogpost> {
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?<filter>")]
#[handler]
async fn get_blog(
filter_tags: Option<Query<FilterTags>>,
locale: Locale,
) -> Result<templates::Blog> {
let mut blogposts = Blogpost::get_articles().await?;
let tags: Vec<String> = posts::Post::get_tags(&blogposts)
.into_iter()
.map(|tag| tag.to_owned())
.collect();
let mut filter_hashset: HashSet<String> = 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<Response> {
let blogposts: Vec<Box<dyn Post + Send + Sync>> = Blogpost::get_articles()
.await?
.into_iter()
.map(|bp| Box::new(bp) as Box<dyn Post + Send + Sync>)
.collect();
let poems: Vec<Box<dyn Post + Send + Sync>> = Poem::get_articles()
.await?
.into_iter()
.map(|poem| Box::new(poem) as Box<dyn Post + Send + Sync>)
.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<String>, locale: Locale) -> Result<templates::Poem> {
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<templates::Poetry> {
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 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::<Static>::new())
.catch_all_error(custom_error)
.data(resources)
.with(Tracing)
.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
}