diff options
Diffstat (limited to 'comments/src/main.rs')
-rw-r--r-- | comments/src/main.rs | 227 |
1 files changed, 0 insertions, 227 deletions
diff --git a/comments/src/main.rs b/comments/src/main.rs deleted file mode 100644 index 50f568f..0000000 --- a/comments/src/main.rs +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Comments generator - * POST /comment - submits a comment - * - * GET /form - generate html comment form - * GET /comment - gets html fragment with comments - * both accept a query parameter ?page=<url> - */ - -use askama::Template; -use axum::{ - extract::{Query, Form, State}, - response::{Redirect,Html}, - http::StatusCode, - routing::get, - Router, -}; -use lettre::{ - AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor, - transport::smtp::authentication::Credentials -}; -use serde::Deserialize; -use sqlx::{ - postgres::{PgPool, PgPoolOptions}, - types::{ - time::OffsetDateTime, - uuid::Uuid, - }, -}; -use std::{net::SocketAddr, time::Duration}; - - -// Useful global thingies -#[derive(Clone)] -struct Ctx { - pool: PgPool, - base_path: String, - mail_opts: Option<MailCtx>, -} - -#[derive(Clone)] -struct MailCtx { - notification_address: String, - smtp_server: String, - smtp_credentials: Credentials, -} - -#[tokio::main] -async fn main() { - let base_path = std::env::var("BASE_PATH") - .unwrap_or_else(|_| "/api".to_string()); - - let db_connection_str = std::env::var("DATABASE_URL") - .unwrap_or_else(|_| "postgres://comments@localhost/comments".to_string()); - - let notification_address = std::env::var("NOTIFICATION_ADDRESS").ok(); - let mail_opts = notification_address.map(|na| { - let smtp_server = std::env::var("SMTP_SERVER").expect("SMTP_SERVER not supplied!"); - let smtp_username = std::env::var("SMTP_USERNAME").expect("SMTP_USERNAME not supplied!"); - let smtp_password = std::env::var("SMTP_PASSWORD").expect("SMTP_PASSWORD not supplied!"); - MailCtx{ - smtp_server: smtp_server, - smtp_credentials: Credentials::new(smtp_username, smtp_password), - notification_address: na, - } - }); - - let pool = PgPoolOptions::new() - .max_connections(5) - .connect_timeout(Duration::from_secs(3)) - .connect(&db_connection_str) - .await - .expect("can't connect to database"); - sqlx::migrate!() - .run(&pool) - .await - .expect("failed to run migrations"); - - let ctx = Ctx {pool, base_path: base_path.clone(), mail_opts}; - - let app = Router::new() - .nest(&base_path, Router::new() - .route( - "/form", - get(get_form), - ) - .route( - "/comment", - get(get_comments).post(post_comments), - ) - .with_state(ctx)); - - let addr = SocketAddr::from(([127, 0, 0, 1], 5678)); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -#[derive(Deserialize)] -struct UrlQuery { - url: String -} - -#[derive(Template)] -#[template(path = "form.html")] -struct CommentForm { - url: String, - capcha_question: String, - capcha_id: Uuid, - base_path: String, -} - -async fn get_form( - State(ctx): State<Ctx>, - Query(uq): Query<UrlQuery> -) -> Result<Html<String>, (StatusCode, String)> { - let capcha = sqlx::query!("select id, question from capchas order by random() limit 1") - .fetch_one(&ctx.pool) - .await - .map_err(internal_error)?; - let c = CommentForm{url: uq.url, capcha_question: capcha.question, capcha_id: capcha.id, base_path: ctx.base_path}; - let res = c.render().map_err(internal_error)?; - Ok(Html(res)) -} - -#[derive(Template)] -#[template(path = "comments.html")] -struct Comments { - comments: Vec<Comment>, -} -struct Comment { - author: String, - comment: String, - ts: OffsetDateTime, -} - -async fn get_comments( - State(ctx): State<Ctx>, - Query(uq): Query<UrlQuery>) -> Result<Html<String>, (StatusCode,String)> { - let comments = sqlx::query!("select author,comment,ts from comments where url = $1 order by ts", uq.url) - .fetch_all(&ctx.pool) - .await - .map_err(internal_error)?; - let render_comments: Vec<Comment> = comments.into_iter().map(|comment| { - Comment { - author: comment.author, - comment: comment.comment, - ts: comment.ts, - } - }).collect(); - let c = Comments{comments: render_comments}; - let res = c.render().map_err(internal_error)?; - Ok(Html(res)) -} - -#[derive(Deserialize)] -struct PostComment { - url: String, - author: String, - comment: String, - capcha_id: String, - capcha_answer: String, -} - -struct CapchaAnswer { - answer:String -} - -#[derive(Template)] -#[template(path = "notification.txt")] -struct Notification<'a> { - url: &'a str, - comment: &'a str, - author: &'a str, -} - -async fn post_comments( - State(ctx): State<Ctx>, - Form(post_comment): Form<PostComment>) -> Result<Redirect,(StatusCode,String)> { - let capcha_id: Uuid = post_comment.capcha_id.parse() - .map_err(|_| {(StatusCode::BAD_REQUEST, "Invalid capcha_id".to_string())})?; - let ans: CapchaAnswer = sqlx::query_as!(CapchaAnswer, "select answer from capchas where id = $1", capcha_id) - .fetch_one(&ctx.pool) - .await - .map_err(internal_error)?; - if post_comment.capcha_answer != ans.answer { - return Err((StatusCode::BAD_REQUEST, "Capcha was wrong!".to_string())); - } - sqlx::query!("insert into comments(url,author,comment) values($1, $2, $3)", post_comment.url, post_comment.author, post_comment.comment) - .execute(&ctx.pool) - .await - .map_err(internal_error)?; - - if let Some(mail_opts) = ctx.mail_opts { - let mail_body = Notification{ - url: &post_comment.url, - author: &post_comment.author, - comment: &post_comment.comment - }.render().unwrap(); - let email = Message::builder() - // TODO should be a config value - .from("Comments Service <comments@mfashby.net>".parse().unwrap()) - .to(mail_opts.notification_address.parse().unwrap()) - .subject(format!("New comment on {}", post_comment.url)) - .body(mail_body) - .unwrap(); - let mailer: AsyncSmtpTransport<Tokio1Executor> = - AsyncSmtpTransport::<Tokio1Executor>::relay(&mail_opts.smtp_server) - .unwrap() - .credentials(mail_opts.smtp_credentials) - .build(); - match mailer.send(email).await { - Ok(_) => (), - Err(e) => eprintln!("Could not send email: {:?}", e), - } - }; - - Ok(Redirect::temporary(&post_comment.url)) -} - -fn internal_error<E>(err: E) -> (StatusCode, String) -where - E: std::error::Error, -{ - (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()) -} |