use clap::Parser; use std::collections::HashMap; use std::error::Error; #[macro_use] extern crate rocket; use reqwest::{Client, StatusCode}; use rocket::response::content::RawHtml; use rocket::serde::json::Json; mod cohost_account; mod cohost_posts; mod syndication; mod webfinger; use cohost_account::{CohostAccount, COHOST_ACCOUNT_API_URL}; use cohost_posts::{cohost_posts_api_url, CohostPostsPage}; use webfinger::CohostWebfingerResource; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// The base URL for the corobel instance #[clap(short, long, required = true)] domain: String, /// The base URL for the corobel instance #[clap(short, long, default_value_t = default_base_url() )] base_url: String, } fn default_base_url() -> String { "/".into() } fn user_agent() -> String { format!( "{}/{} (RSS feed converter) on {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), &ARGS.domain ) } static ARGS: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| Args::parse()); #[get("/")] fn index() -> RawHtml<&'static str> { RawHtml(include_str!("../static/index.html")) } #[derive(Responder)] #[response(content_type = "application/rss+xml")] struct RssResponse { inner: String, } #[derive(Responder)] #[response(content_type = "text/plain")] enum ErrorResponse { #[response(status = 404)] NotFound(String), #[response(status = 500)] InternalError(String), } #[get("//feed.rss?")] async fn syndication_rss_route( project: &str, page: Option, ) -> Result { let page = page.unwrap_or(0); let project_url = format!("{}{}", COHOST_ACCOUNT_API_URL, project); let posts_url = cohost_posts_api_url(project, page); let client = match Client::builder().user_agent(user_agent()).build() { Ok(v) => v, Err(e) => { let err = format!("Couldn't build a reqwest client: {:?}", e); eprintln!("{}", err); return Err(ErrorResponse::InternalError(err)); } }; eprintln!("making request to {}", project_url); let project_data: CohostAccount = match client.get(project_url).send().await { Ok(v) => match v.status() { StatusCode::OK => match v.json::().await { Ok(a) => a, Err(e) => { let err = format!("Couldn't deserialize Cohost project '{}': {:?}", project, e); eprintln!("{}", err); return Err(ErrorResponse::InternalError(err)); } }, // TODO NORA: Handle possible redirects s => { let err = format!( "Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", project, s ); eprintln!("{}", err); return Err(ErrorResponse::NotFound(err)); } }, Err(e) => { let err = format!( "Error making request to Cohost for project '{}': {:?}", project, e ); eprintln!("{}", err); return Err(ErrorResponse::InternalError(err)); } }; eprintln!("making request to {}", posts_url); match client.get(posts_url).send().await { Ok(v) => match v.status() { StatusCode::OK => match v.json::().await { Ok(page_data) => { return Ok(RssResponse { inner: syndication::channel_for_posts_page( project, page, project_data, page_data, ) .to_string(), }); } Err(e) => { let err = format!( "Couldn't deserialize Cohost posts page for '{}': {:?}", project, e ); eprintln!("{}", err); return Err(ErrorResponse::InternalError(err)); } }, // TODO NORA: Handle possible redirects s => { let err = format!("Didn't receive status code 200 for posts for Cohost project '{}'; got {:?} instead.", page, s); eprintln!("{}", err); return Err(ErrorResponse::NotFound(err)); } }, Err(e) => { let err = format!( "Error making request to Cohost for posts for project '{}': {:?}", project, e ); eprintln!("{}", err); return Err(ErrorResponse::InternalError(err)); } }; } #[get("/.well-known/webfinger?")] async fn webfinger_route(params: HashMap) -> Option> { if params.len() != 1 { eprintln!( "Too may or too few parameters. Expected 1, got {}", params.len() ); return None; } let client = match Client::builder().user_agent(user_agent()).build() { Ok(v) => v, Err(e) => { let err = format!("Couldn't build a reqwest client: {:?}", e); eprintln!("{}", err); return None; } }; if let Some(param) = params.iter().next() { let url = format!("{}{}", COHOST_ACCOUNT_API_URL, param.0); eprintln!("making request to {}", url); match client.get(url).send().await { Ok(v) => { match v.status() { StatusCode::OK => match v.json::().await { Ok(_v) => { return Some(Json(CohostWebfingerResource::new( param.0.as_str(), &ARGS.domain, &ARGS.base_url, ))); } Err(e) => { eprintln!("Couldn't deserialize Cohost project '{}': {:?}", param.0, e); } }, // TODO NORA: Handle possible redirects s => { eprintln!("Didn't receive status code 200 for Cohost project '{}'; got {:?} instead.", param.0, s); return None; } } } Err(e) => { eprintln!( "Error making request to Cohost for project '{}': {:?}", param.0, e ); return None; } }; } None } #[rocket::main] async fn main() -> Result<(), Box> { // Set up the global config once_cell::sync::Lazy::force(&ARGS); let _rocket = rocket::build() .mount( &ARGS.base_url, routes![index, webfinger_route, syndication_rss_route], ) .ignite() .await? .launch() .await?; Ok(()) }