use std::collections::HashMap; use actix_multipart::form::{text::Text, MultipartForm}; use actix_web::{http::StatusCode, post, HttpResponse, HttpResponseBuilder}; use color_eyre::eyre::{Context, Result}; use futures::FutureExt; use lettre::{message::SinglePart, Message}; use maud::{html, Markup, DOCTYPE}; use reqwest::Client; use tracing::{error, info}; #[derive(Debug, MultipartForm)] struct CampForm { #[multipart(rename = "first-name")] first_name: Text, #[multipart(rename = "last-name")] last_name: Text, #[multipart(rename = "parent-first-name")] parent_first_name: Text, #[multipart(rename = "parent-last-name")] parent_last_name: Text, #[multipart(rename = "birth-date")] birthdate: Text, gender: Text, street: Text, city: Text, state: Text, zip: Text, #[multipart(rename = "parent-phone")] parent_phone: Text, #[multipart(rename = "parent-email")] parent_email: Text, grade: Text, shirt: Text, allergies: Text, week: Text, registration: Text, #[multipart(rename = "health-form")] health_form: Text, } impl From<&CampForm> for HashMap { fn from(form: &CampForm) -> Self { let mut map = HashMap::new(); map.insert(63, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( 64, format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); map.insert(65, form.parent_phone.0.clone()); map.insert(66, form.parent_email.0.clone().clone()); map.insert(67, form.birthdate.0.clone()); map.insert(69, form.gender.0.clone()); map.insert(70, form.street.0.clone()); map.insert(71, form.city.0.clone()); map.insert(72, form.state.0.clone()); map.insert(73, form.zip.0.clone().to_string()); map.insert(74, form.grade.0.clone()); map.insert(75, form.week.0.clone()); map.insert(76, form.shirt.0.clone()); map.insert(77, form.registration.0.clone()); map.insert(115, form.health_form.0.clone()); map } } impl CampForm { fn build_email(&self) -> Markup { html! { (DOCTYPE) meta charset="utf-8"; html { head { title { (self.first_name.0) " " (self.last_name.0) " signed up for camp!" } style { "table { border-collapse: collapse; width: 100% }" "td, th { padding: 8px }" "td { text-align: left; width: 70%; word-wrap: break-word }" "th { text-align: right; border-right: 1px solid #ddd }" "tr { border-bottom: 1px solid #ddd }" "h1 { text-align: center }" } } body { h1 { "Camp form for " (self.first_name.0) " " (self.last_name.0) "!" } hr; table { tr { th { "Name" } td { (self.first_name.0) " " (self.last_name.0) } } tr { th { "Parent" } td { (self.parent_first_name.0) " " (self.parent_last_name.0) } } tr { th { "Birthdate" } td { (self.birthdate.0) } } tr { th { "Gender" } td { (self.gender.0) } } tr { th { "Street" } td { (self.street.0) } } tr { th { "City" } td { (self.city.0) } } tr { th { "State" } td { (self.state.0) } } tr { th { "Zip" } td { (self.zip.0) } } tr { th { "Parent Phone" } td { (self.parent_phone.0) } } tr { th { "Parent Email" } td { (self.parent_email.0) } } tr { th { "Grade" } td { (self.grade.0) } } tr { th { "Camper Allergies" } td { (self.allergies.0) } } tr { th { "T-Shirt Size" } td { (self.shirt.0) } } tr { th { "Week Choice" } td { (self.week.0) } } tr { th { "Health Form" } td { (self.health_form.0) } } tr { th { "Registration" } td { (self.registration.0) } } } } } } } fn prepare_email(&self) -> Result { let first = self.first_name.clone(); let last = self.last_name.clone(); let email_subject = format!("{} {} signed up for camp!", first, last); info!("{first} {last} signed up for camp!"); let email = self.build_email(); // let temp_file = self.get_temp_file(); // let multi = if let Some((file, path, content_type)) = temp_file { // let filebody = fs::read(path); // let content_type = // ContentType::parse(&content_type.unwrap_or(String::from("image/jpg"))).unwrap(); // let attachment = Attachment::new(file).body(filebody.unwrap(), content_type); // // info!(?attachment); // MultiPart::mixed() // .singlepart(SinglePart::html(email.into_string())) // .singlepart(attachment) // } else { // MultiPart::alternative_plain_html(String::from("Testing"), email.into_string()) // }; let singlepart = SinglePart::html(email.into_string()); Message::builder() .from( "TFC ADMIN " .parse() .unwrap(), ) .to("Chris Cochrun ".parse().unwrap()) .to("Ethan Rose ".parse().unwrap()) .subject(email_subject) .singlepart(singlepart) // .multipart(multi) .wrap_err("problemss") } } #[post("/api/camp-form")] pub async fn camp_form(MultipartForm(form): MultipartForm) -> HttpResponse { let full_name = format!("{} {}", form.first_name.0, form.last_name.0); let map = (&form).into(); let future = store_camp_form(map); actix_rt::spawn(future.map(|s| match s { Ok(_) => info!("Successfully posted to nextcloud tables"), Err(e) => log::error!("Error in posting camp data: {:?}", e), })); let email = form.prepare_email(); match email { Ok(m) => { let sent = crate::email::send_email(m); actix_rt::spawn(sent.map(|s| match s { Ok(_) => info!("Successfully sent form to email!"), Err(e) => error!("There was an erroring sending form to email: {e}"), })); } Err(e) => error!("error sending email {e}"), }; match form.health_form.0.as_str() { "now" => { info!("Sending them to fill out the health form"); HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(( "HX-Redirect", format!( "/camp-health-form/?registration={}", form.registration.0.as_str() ), )) .finish() } "later" => match form.registration.0.as_str() { "now" => { info!("Sending them to pay for registration now"); HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(( "HX-Redirect", "https://secure.myvanco.com/L-Z772/campaign/C-13JPJ", )) .finish() } "full" => { info!("Sending them to pay for the full registration now"); HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(( "HX-Redirect", "https://secure.myvanco.com/L-Z772/campaign/C-13JQE", )) .finish() } "later" => { info!("{} would like to pay later", full_name); let html = html! { div class="mt-8" { h2 { "Thank you, " (full_name) "!" } p { "Can't wait to see you at camp!" } p { class { "" } "If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option." } } }; HttpResponse::Ok().body(html.into_string()) } _ => { log::error!("Got registration error....."); let html = html! { div class="mt-8" { h2 { "Thank you, " (full_name) "!" } p { "Can't wait to see you at camp!" } p { class { "" } "If you'd like to pay for your registration go to the donate tab in the top right when you are ready and find the camp registration option." } } }; HttpResponse::Ok().body(html.into_string()) } }, _ => { log::error!("Unknown selection for health. We don't know where to send the user."); HttpResponseBuilder::new(StatusCode::IM_A_TEAPOT) .body("Unknown selection for health. We don't know where to send the user.") } } } async fn store_camp_form(map: HashMap) -> Result<()> { let request = Client::new(); let mut json = HashMap::new(); json.insert("data", map); request .post("https://staff.tfcconnection.org/apps/tables/api/1/tables/5/rows") .basic_auth("chris", Some("2VHeGxeC^Zf9KqFK^G@Pt!zu2q^6@b")) .header("OCS-APIRequest", "true") .header("Content-Type", "application/json") .json(&json) .send() .await?; Ok(()) } #[cfg(test)] mod test { use super::*; use actix_web::test; fn form() -> CampForm { CampForm { first_name: Text("Frodo".into()), last_name: Text("Braggins".into()), parent_first_name: Text("Bilbo".into()), parent_last_name: Text("Braggins".into()), birthdate: Text(String::from("1845-09-12")), gender: Text(String::from("male")), street: Text(String::from("1234 Bag End")), city: Text(String::from("The Shire")), state: Text(String::from("Hobbiton")), zip: Text(88888), parent_phone: Text(String::from("1234567898")), parent_email: Text(String::from("bilbo@hobbits.com")), grade: Text(String::from("junior")), shirt: Text(String::from("medium")), allergies: Text(String::from("Cool beans")), week: Text(String::from("1")), registration: Text(String::from("later")), health_form: Text(String::from("I guess")), } } #[test] async fn test_nc_post() { let form = form(); assert!(!form.first_name.is_empty()); let map = HashMap::from(&form); let res = store_camp_form(map).await; match res { Ok(_) => assert!(true), Err(e) => assert!(false, "Failed storing test: {e}"), } } #[test] async fn test_email() { let form = form(); assert!(!form.first_name.is_empty()); match form.prepare_email() { Ok(m) => { assert!(crate::email::send_email(m).await.is_ok()) } Err(e) => assert!(false, "Failed emailing test: {e}"), } } }