#![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<T> = std::result::Result<T, BlossomError>;
#[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/<blogpost>")]
#[handler]
async fn blogpost(Path(blogpost): Path<String>) -> Result<templates::Blogpost> {
let blogpost = Blogpost::get_article(&blogpost).await?;
Ok(templates::Blogpost {
blogpost,
filter_tags: HashSet::new(),
})
}
#[derive(Deserialize)]
struct FilterTags {
filter: String,
}
// #[get("/blog?<filter>")]
#[handler]
async fn get_blog(filter_tags: Option<Query<FilterTags>>) -> 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 {
blogposts,
tags,
filter_tags: filter_hashset,
})
}
#[handler]
async fn feed() -> Result<Response> {
let posts = Blogpost::get_articles().await?;
// 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<String>) -> Result<templates::Poem> {
let poem = Poem::get_article(&poem).await?;
Ok(templates::Poem { poem, jiggle: 4 })
}
#[handler]
async fn get_poetry() -> Result<templates::Poetry> {
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::<Static>::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
}