aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: 8e31fee4a715528bdf5e815351b66785d1c73619 (plain) (tree)
1
2
3
4
5
6
7
8
9


                             

         
          
         
         
           
          

              


              

                                                
                       
                           
                                    


                                                                




                                    
                        


                               
                                   
                           
                          
 
                        
                       
                                      
 

                            
                        
                        
                       
 
                                                      
 


                     
 
          
                                                                                         
                                                               
                                                              

                                                         
                                                      

                                 
      
                                                  
                                                        
                                                  

                                                              
                     
                                             


                     
             
               




                             
                                                                                                
                                                           
                            



                                                                 

                                    
               
      

 


                      

 

                           



                                           
                                                        
                                                             



                                                             


                                                                            
     
                        
                                                  


                                    
               
      

 
                      

                                     












                                                                              







                                                       
                                                                      



                                             

 
          




                                                        

 


                                    

 
          
                                                                                        
                                               







                                                         


          
                                                                  


                                                





                                                    

 

                                                              
                               





                                 
              
                                                            


                                                                                                    

                                                             
 

                                            
                                        


                  







                                                       



                                             

                                           



                                                                 
                                      
                        
                             
                      
                      

                                      


                                                        
           
 
                                                  

                     
 
#![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<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 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::<Static>::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
}