2021-10-25 15:43:52 +00:00
|
|
|
pub mod ogp_images;
|
2021-10-25 17:07:14 +00:00
|
|
|
pub mod contrast;
|
2021-10-25 15:43:52 +00:00
|
|
|
|
2021-10-24 05:24:56 +00:00
|
|
|
use std::collections::HashMap;
|
2021-10-24 08:44:07 +00:00
|
|
|
use std::fmt::{self, Display};
|
2021-10-24 05:24:56 +00:00
|
|
|
|
2021-10-24 08:44:07 +00:00
|
|
|
use actix_web::dev::HttpResponseBuilder;
|
|
|
|
use actix_web::http::{header, StatusCode};
|
|
|
|
use actix_web::middleware::normalize::TrailingSlash;
|
|
|
|
use actix_web::middleware::{self, Logger};
|
2021-10-25 18:53:22 +00:00
|
|
|
use actix_web::web::resource;
|
2021-10-25 18:13:39 +00:00
|
|
|
use actix_web::{App, HttpRequest, HttpResponse, HttpServer, Responder, ResponseError, Result, post, web};
|
2021-10-24 05:24:56 +00:00
|
|
|
use askama::Template;
|
|
|
|
use pronouns_today::user_preferences::Preference;
|
|
|
|
use pronouns_today::{InstanceSettings, Pronoun};
|
|
|
|
|
2021-10-25 18:53:22 +00:00
|
|
|
#[cfg(feature = "ogp_images")]
|
|
|
|
use image::{DynamicImage, ImageOutputFormat};
|
|
|
|
#[cfg(feature = "ogp_images")]
|
|
|
|
use ogp_images::render_today;
|
|
|
|
|
2021-10-25 19:48:41 +00:00
|
|
|
// TODO: Make this configurable
|
|
|
|
const HOSTNAME: &str = "pronouns.today";
|
|
|
|
|
2021-10-24 05:24:56 +00:00
|
|
|
#[derive(Template)]
|
|
|
|
#[template(path = "index.html")]
|
|
|
|
struct IndexTemplate<'a> {
|
2021-10-24 08:44:07 +00:00
|
|
|
name: Option<String>,
|
2021-10-24 09:34:02 +00:00
|
|
|
pronoun: &'a Pronoun,
|
2021-10-24 05:24:56 +00:00
|
|
|
pronouns: Vec<(usize, &'a Pronoun)>,
|
2021-10-25 19:48:41 +00:00
|
|
|
url: String,
|
2021-10-24 05:24:56 +00:00
|
|
|
}
|
|
|
|
|
2021-10-25 19:48:41 +00:00
|
|
|
fn render_page(pronoun: &Pronoun, settings: &InstanceSettings, name: Option<String>, url: String) -> String {
|
2021-10-24 05:24:56 +00:00
|
|
|
IndexTemplate {
|
2021-10-24 08:44:07 +00:00
|
|
|
name,
|
2021-10-24 09:34:02 +00:00
|
|
|
pronoun,
|
2021-10-24 05:24:56 +00:00
|
|
|
pronouns: settings.pronoun_list.iter().enumerate().collect(),
|
2021-10-25 19:48:41 +00:00
|
|
|
url,
|
2021-10-24 05:24:56 +00:00
|
|
|
}
|
|
|
|
.render()
|
|
|
|
.unwrap()
|
|
|
|
}
|
2021-10-24 00:44:08 +00:00
|
|
|
|
2021-10-24 05:24:56 +00:00
|
|
|
#[post("/")]
|
|
|
|
async fn create_link(
|
|
|
|
settings: web::Data<InstanceSettings>,
|
2021-10-24 05:46:57 +00:00
|
|
|
form: web::Form<HashMap<String, String>>,
|
2021-10-24 05:24:56 +00:00
|
|
|
) -> Result<impl Responder> {
|
2021-10-24 08:44:07 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-10-24 05:24:56 +00:00
|
|
|
let prefs = InstanceSettings::create_preferences(&weights);
|
2021-10-24 08:44:07 +00:00
|
|
|
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),
|
|
|
|
};
|
2021-10-24 05:24:56 +00:00
|
|
|
Ok(HttpResponse::SeeOther()
|
2021-10-24 08:44:07 +00:00
|
|
|
.header(header::LOCATION, url)
|
2021-10-24 05:24:56 +00:00
|
|
|
.finish())
|
2021-10-24 00:44:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-25 19:48:41 +00:00
|
|
|
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("/")
|
|
|
|
}
|
|
|
|
|
2021-10-25 18:13:39 +00:00
|
|
|
/// 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");
|
2021-10-24 08:44:07 +00:00
|
|
|
let pronoun = settings
|
2021-10-25 18:13:39 +00:00
|
|
|
.select_pronouns(name, prefs)
|
|
|
|
.map_err(|_| Error::InvlaidPrefString);
|
|
|
|
(prefs, name, pronoun)
|
2021-10-24 08:44:07 +00:00
|
|
|
}
|
|
|
|
|
2021-10-25 18:13:39 +00:00
|
|
|
async fn handle_basic_request(
|
2021-10-24 05:24:56 +00:00
|
|
|
settings: web::Data<InstanceSettings>,
|
2021-10-25 18:13:39 +00:00
|
|
|
req: HttpRequest,
|
2021-10-24 00:44:08 +00:00
|
|
|
) -> Result<impl Responder> {
|
|
|
|
|
2021-10-25 19:48:41 +00:00
|
|
|
let (prefstr, name, pronoun) = get_request_info(&settings, &req);
|
2021-10-25 18:13:39 +00:00
|
|
|
|
2021-10-24 05:24:56 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
2021-10-24 08:44:07 +00:00
|
|
|
.content_type("text/html; charset=utf-8")
|
2021-10-25 19:48:41 +00:00
|
|
|
.body(render_page(
|
|
|
|
pronoun?,
|
|
|
|
&settings,
|
|
|
|
name.map(str::to_owned),
|
|
|
|
form_full_url(HOSTNAME, name, prefstr)
|
|
|
|
)))
|
2021-10-24 00:44:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-25 18:53:22 +00:00
|
|
|
#[cfg(feature = "ogp_images")]
|
|
|
|
async fn handle_thumbnail_request(
|
2021-10-24 05:24:56 +00:00
|
|
|
settings: web::Data<InstanceSettings>,
|
2021-10-25 18:53:22 +00:00
|
|
|
req: HttpRequest,
|
2021-10-24 00:44:08 +00:00
|
|
|
) -> Result<impl Responder> {
|
2021-10-25 18:53:22 +00:00
|
|
|
|
|
|
|
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");
|
|
|
|
|
2021-10-24 05:24:56 +00:00
|
|
|
Ok(HttpResponse::Ok()
|
2021-10-25 18:53:22 +00:00
|
|
|
.content_type("image/png")
|
|
|
|
.body(data))
|
2021-10-24 00:44:08 +00:00
|
|
|
}
|
|
|
|
|
2021-10-24 08:44:07 +00:00
|
|
|
async fn not_found() -> impl Responder {
|
|
|
|
HttpResponse::NotFound()
|
|
|
|
.content_type("text/html; charset=utf-8")
|
|
|
|
.body(include_str!("../templates/404.html"))
|
|
|
|
}
|
|
|
|
|
2021-10-24 00:44:08 +00:00
|
|
|
#[actix_web::main]
|
|
|
|
async fn main() -> std::io::Result<()> {
|
2021-10-24 05:24:56 +00:00
|
|
|
env_logger::init();
|
2021-10-25 10:41:23 +00:00
|
|
|
println!("Starting pronouns-today-web on 127.0.0.1:8080");
|
2021-10-24 05:24:56 +00:00
|
|
|
HttpServer::new(|| {
|
|
|
|
let logger = Logger::default();
|
2021-10-25 18:53:22 +00:00
|
|
|
let app = App::new()
|
2021-10-24 05:24:56 +00:00
|
|
|
.data(InstanceSettings::default())
|
|
|
|
.wrap(logger)
|
2021-10-25 18:53:22 +00:00
|
|
|
.wrap(middleware::NormalizePath::new(TrailingSlash::Trim));
|
|
|
|
|
|
|
|
#[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
|
2021-10-25 18:13:39 +00:00
|
|
|
.service(resource("/") .to(handle_basic_request))
|
|
|
|
.service(resource("/{prefs}") .to(handle_basic_request))
|
|
|
|
.service(resource("/{name}/{prefs}").to(handle_basic_request))
|
2021-10-24 05:24:56 +00:00
|
|
|
.service(create_link)
|
2021-10-24 08:44:07 +00:00
|
|
|
.default_service(web::to(not_found))
|
2021-10-24 05:24:56 +00:00
|
|
|
})
|
2021-10-26 01:11:40 +00:00
|
|
|
.bind("0.0.0.0:8080")?
|
2021-10-24 05:24:56 +00:00
|
|
|
.run()
|
|
|
|
.await
|
2021-10-24 00:44:08 +00:00
|
|
|
}
|
2021-10-24 08:44:07 +00:00
|
|
|
|
|
|
|
#[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(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|