PronounsToday/web/src/main.rs

251 lines
7.5 KiB
Rust

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<String>,
pronoun: &'a Pronoun,
pronouns: Vec<(usize, &'a Pronoun)>,
url: String,
}
fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option<String>, url: String) -> String {
IndexTemplate {
name,
pronoun,
pronouns: settings.pronoun_list.iter().enumerate().collect(),
url,
}
.render()
.unwrap()
}
#[post("/")]
async fn create_link(
settings: web::Data<InstanceSettings>,
form: web::Form<HashMap<String, String>>,
) -> Result<impl Responder> {
let mut weights = vec![0; settings.pronoun_list.len()];
for (k, v) in form.iter() {
if let Ok(i) = k.parse::<usize>() {
let w = v.parse::<u8>().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::<Vec<&str>>()
.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<InstanceSettings>,
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<InstanceSettings>,
req: HttpRequest,
) -> Result<impl Responder> {
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<InstanceSettings>,
req: HttpRequest,
) -> Result<impl Responder> {
let (_, name, pronoun) = get_request_info(&settings, &req);
let mut data: Vec<u8> = 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(),
)
}
}