pub mod ogp_images; pub mod contrast; pub mod statics; pub mod configuration; use configuration::ConfigError; use std::process::exit; use std::collections::HashMap; use std::fmt::{self, Display}; use actix_web::dev::HttpResponseBuilder; use actix_web::http::{header, StatusCode}; use actix_web::middleware::normalize::TrailingSlash; use actix_web::middleware::{self, Logger}; use actix_web::web::resource; use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, ResponseError, Result, post, web}; use argh; use askama::Template; use pronouns_today::user_preferences::Preference; use pronouns_today::{InstanceSettings, Pronoun}; #[cfg(feature = "ogp_images")] use image::{DynamicImage, ImageOutputFormat}; #[cfg(feature = "ogp_images")] use ogp_images::render_today; // TODO: Make this configurable const HOSTNAME: &str = "pronouns.today"; #[derive(Template)] #[template(path = "index.html")] struct IndexTemplate<'a> { name: Option, pronoun: &'a Pronoun, pronouns: Vec<(usize, &'a Pronoun)>, url: String, } fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option, url: String) -> String { IndexTemplate { name, pronoun, pronouns: settings.pronoun_list.iter().enumerate().collect(), url, } .render() .unwrap() } #[post("/")] async fn create_link( settings: web::Data, form: web::Form>, ) -> Result { let mut weights = vec![0; settings.pronoun_list.len()]; for (k, v) in form.iter() { if let Ok(i) = k.parse::() { let w = v.parse::().map_err(|_| Error::InvlaidPrefString)?; if i < weights.len() - 1 { weights[i] = w; } } } let prefs = InstanceSettings::create_preferences(&weights); let pref_string = prefs.as_prefstring(); let url = match form.get("name") { Some(name) if !name.is_empty() => format!("/{}/{}", name, pref_string), _ => format!("/{}", pref_string), }; Ok(HttpResponse::SeeOther() .header(header::LOCATION, url) .finish()) } fn form_full_url(host: &str, name: Option<&str>, prefstr: Option<&str>) -> String { ["https:/", host].into_iter() .chain(name) .chain(prefstr) .collect::>() .join("/") } /// Determine some basic information about a request /// /// Determines the name and prefstring properties, if available, and also computes the pronoun that /// should be responded using. This method is designed to facilitate creating the basic response /// pages. /// /// Both arguments should be passed directly from the caller, and the return values are, in order, /// the prefstring, if available, the user's name, if available, and the computed pronouns. fn get_request_info<'s, 'r>( settings: &'s web::Data, req: &'r HttpRequest, ) -> (Option<&'r str>, Option<&'r str>, Result<&'s Pronoun, Error>) { let prefs = req.match_info().get("prefs"); let name = req.match_info().get("name"); let pronoun = settings .select_pronouns(name, prefs) .map_err(|_| Error::InvlaidPrefString); (prefs, name, pronoun) } async fn handle_basic_request( settings: web::Data, req: HttpRequest, ) -> Result { let (prefstr, name, pronoun) = get_request_info(&settings, &req); Ok(HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(render_page( pronoun?, &settings, name.map(str::to_owned), form_full_url(HOSTNAME, name, prefstr) ))) } #[cfg(feature = "ogp_images")] async fn handle_thumbnail_request( settings: web::Data, req: HttpRequest, ) -> Result { let (_, name, pronoun) = get_request_info(&settings, &req); let mut data: Vec = Vec::with_capacity(15_000); let image = DynamicImage::ImageRgb8(render_today(pronoun?, name.unwrap_or(""))); image.write_to(&mut data, ImageOutputFormat::Png) .expect("Error encoding thumbnail to PNG"); Ok(HttpResponse::Ok() .content_type("image/png") .body(data)) } async fn not_found() -> impl Responder { HttpResponse::NotFound() .content_type("text/html; charset=utf-8") .body(include_str!("../templates/404.html")) } #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); let args: configuration::PronounsTodayArgs = argh::from_env(); match args.command { configuration::SubCommand::DumpStatics(_subargs) => { println!("Support for dumping statics not yet implemented"); Ok(()) } configuration::SubCommand::Run(subargs) => { let config = match subargs.load_config() { Ok(config) => config, Err(ConfigError::IoError(e)) => { return Err(e); } Err(ConfigError::MalformedConfig(e)) => { eprintln!("Error parsing config file:\n{}", e); exit(1001); } Err(ConfigError::ConfigCreated(path)) => { println!("A config file has been generated at {}! Please check it out and modify it to your liking, and then run this command again", path.display()); exit(1002); } }; log::info!("Starting with configuration {:?}", config); start_server(config).await } } } async fn start_server(config: configuration::Conf) -> std::io::Result<()> { // Where we binding bois let bind = format!("0.0.0.0:{}", config.port); println!("Starting pronouns-today-web on {}", &bind); HttpServer::new(move|| { let logger = Logger::default(); let app = App::new() .data(config.instance_settings.clone()) .wrap(logger) .wrap(middleware::NormalizePath::new(TrailingSlash::Trim)) .service(create_link); let app = statics::STATIC_ASSETS.iter() .fold(app, |app, asset| app.service(asset.generate_resource())); #[cfg(feature = "ogp_images")] let app = app .service(resource("/thumb.png") .to(handle_thumbnail_request)) .service(resource("/{prefs}/thumb.png") .to(handle_thumbnail_request)) .service(resource("/{name}/{prefs}/thumb.png").to(handle_thumbnail_request)); app .service(resource("/") .to(handle_basic_request)) .service(resource("/{prefs}") .to(handle_basic_request)) .service(resource("/{name}/{prefs}").to(handle_basic_request)) .default_service(web::to(not_found)) }) .bind(&bind)? .run() .await } #[derive(Template)] #[template(path = "error.html")] struct ErrorPage { msg: String, } #[derive(Debug)] enum Error { InvlaidPrefString, } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let msg = match self { &Error::InvlaidPrefString => "This URL contains an invalid pronoun preference string", }; write!(f, "{}", msg) } } impl ResponseError for Error { fn status_code(&self) -> actix_web::http::StatusCode { match self { &Error::InvlaidPrefString => StatusCode::BAD_REQUEST, } } fn error_response(&self) -> HttpResponse { HttpResponseBuilder::new(self.status_code()) .set_header(header::CONTENT_TYPE, "text/html; charset=utf-8") .body( ErrorPage { msg: self.to_string(), } .render() .unwrap(), ) } }