use std::{collections::HashMap, fs, io::Write}; use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; use actix_web::{post, HttpResponse}; use color_eyre::{ eyre::{eyre, Context}, Result, }; use futures::FutureExt; use lettre::{ message::{header::ContentType, Attachment, MultiPart, SinglePart}, Message, }; use maud::{html, Markup, DOCTYPE}; use reqwest::Client; use tracing::{error, info}; #[derive(Debug, MultipartForm)] struct MtForm { #[multipart(rename = "firstname")] first_name: Text, #[multipart(rename = "lastname")] last_name: Text, #[multipart(rename = "parentfirstname")] parent_first_name: Text, #[multipart(rename = "parentlastname")] parent_last_name: Text, birthdate: Text, gender: Text, street: Text, city: Text, state: Text, zip: Text, cellphone: Text, parentphone: Text, email: Text, parentemail: Text, school: Text, grade: Text, #[multipart(rename = "pastorfirstname")] pastor_first_name: Text, #[multipart(rename = "pastorlastname")] pastor_last_name: Text, #[multipart(rename = "churchattendance")] church_attendance: Text, #[multipart(rename = "tfcgroup")] tfc_group: Text, shirt: Text, trip: Text, #[multipart(rename = "tripnotes")] trip_notes: Text, #[multipart(rename = "relationship-with-jesus")] relationship_with_jesus: Text, #[multipart(rename = "testimony")] testimony: Text, #[multipart(rename = "involvement-with-group")] involvement_with_group: Text, #[multipart(rename = "reasons-for-trip-choice")] reasons: Text, strengths: Text, weaknesses: Text, #[multipart(rename = "previous-trip-info")] previous_trip_info: Text, #[multipart(rename = "attitude-torward-work")] attitude: Text, #[multipart(rename = "relevant-notes")] relevant_notes: Text, #[multipart(rename = "final-agreement")] final_agreement: Text, registration: Text, #[multipart(rename = "health-form")] health_form: Text, #[multipart(rename = "image")] file: Option, } impl From<&MtForm> for HashMap { fn from(form: &MtForm) -> Self { let mut map = HashMap::new(); map.insert(106, format!("{} {}", form.first_name.0, form.last_name.0)); map.insert( 107, format!("{} {}", form.parent_first_name.0, form.parent_last_name.0), ); map.insert(109, form.gender.0.clone()); map.insert(110, form.birthdate.0.clone()); map.insert(117, form.street.0.clone()); map.insert(118, form.city.0.clone()); map.insert(119, form.zip.0.to_string()); map.insert(120, form.state.0.clone()); map.insert(121, form.cellphone.0.clone()); map.insert(122, form.email.0.clone()); map.insert(123, form.parentphone.0.clone()); map.insert(124, form.parentemail.0.clone()); map.insert(125, form.school.0.clone()); map.insert(126, form.grade.0.clone()); map.insert( 127, format!("{} {}", form.pastor_first_name.0, form.pastor_last_name.0), ); map.insert(128, form.church_attendance.0.clone()); map.insert(129, form.tfc_group.0.clone()); map.insert(130, form.shirt.0.clone()); map.insert(131, form.trip.0.clone()); map.insert(132, form.trip_notes.0.clone()); map.insert(133, form.relationship_with_jesus.0.clone()); map.insert(134, form.testimony.0.clone()); map.insert(135, form.involvement_with_group.0.clone()); map.insert(136, form.reasons.0.clone()); map.insert(137, form.strengths.0.clone()); map.insert(138, form.weaknesses.0.clone()); map.insert(139, form.previous_trip_info.0.clone()); map.insert(140, form.attitude.0.clone()); map.insert(141, form.relevant_notes.0.clone()); map.insert(144, form.final_agreement.0.clone()); map.insert(145, form.registration.0.clone()); map } } impl MtForm { 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 mission trip!" } 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 { "Mission trip 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 { "Phone" } td { (self.cellphone.0) } } tr { th { "Parent Phone" } td { (self.parentphone.0) } } tr { th { "Email" } td { (self.email.0) } } tr { th { "Parent Email" } td { (self.parentemail.0) } } tr { th { "School" } td { (self.school.0) } } tr { th { "Grade" } td { (self.grade.0) } } tr { th { "Pastor" } td { (self.pastor_first_name.0) (self.pastor_last_name.0) } } tr { th { "Church Attendance" } td { (self.church_attendance.0) } } tr { th { "TFC Group" } td { (self.tfc_group.0) } } tr { th { "T-Shirt Size" } td { (self.shirt.0) } } tr { th { "Trip Choice" } td { (self.trip.0) } } tr { th { "Extra Trip Notes" } td { (self.trip_notes.0) } } tr { th { "Relationship with Jesus" } td { (self.relationship_with_jesus.0) } } tr { th { "Testimony" } td { (self.testimony.0) } } tr { th { "Involvement with TFC or Youth Group" } td { (self.involvement_with_group.0) } } tr { th { "Reasons for trip choice" } td { (self.reasons.0) } } tr { th { "Strengths" } td { (self.strengths.0) } } tr { th { "Weaknesses" } td { (self.weaknesses.0) } } tr { th { "Previous Trips" } td { (self.previous_trip_info.0) } } tr { th { "Attitude Torward Work" } td { (self.attitude.0) } } tr { th { "Other Relevant Info" } td { (self.relevant_notes.0) } } tr { th { "Final Agreement" } td { (self.final_agreement.0) } } tr { th { "Registration" } td { (self.registration.0) } } } } } } } async fn store_form(&self) -> Result<()> { let client = Client::new(); let map = HashMap::from(self); let mut json = HashMap::new(); json.insert("data", map); let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/9/row/757"#; let res = client .post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/9/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?; if res.status().is_success() { let res = res.text().await.unwrap(); Ok(()) } else { Err(eyre!( "Problem in storing data: {:?}", res.error_for_status() )) } } fn get_temp_file(&self) -> Option<(String, String, Option)> { let first = self.first_name.clone(); let last = self.last_name.clone(); let filename_noext = format!("{}_{}", first, last); let (file_name, content_type) = if let Some(file) = self.file.as_ref() { let content_type = file.content_type.clone().map(|m| m.to_string()); (file.file_name.to_owned(), content_type) } else { return None; }; let filename; let path = if let Some(file_name) = file_name { if let Some(ext) = file_name.rsplit('.').next() { filename = format!("{}.{}", filename_noext, ext); format!("./tmp/{}.{}", filename_noext, ext) } else { filename = String::default(); format!("./tmp/{}", file_name) } } else { filename = String::default(); String::default() }; if let Some(file) = &self.file { let file = file.file.path(); match fs::copy(file, &path) { Ok(f) => { if f <= 0 { return None; } info!(?f, "File saved successfully"); Some((filename, path, content_type.clone())) } Err(e) => { error!("{:?}: Probably a missing image", e); None } } } else { error!("Error in tempfile"); None } } fn send_email(&self) -> Result { let first = self.first_name.clone(); let last = self.last_name.clone(); let email_subject = format!("{} {} signed up for mission trip!", first, last); info!("{first} {last} signed up for mission trip!"); 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()) }; Message::builder() .from( "TFC ADMIN " .parse() .unwrap(), ) .to("Chris Cochrun ".parse().unwrap()) // .to("Ethan Rose ".parse().unwrap()) .subject(email_subject) .multipart(multi) .wrap_err("problemss") } } #[post("/api/mt-form")] pub async fn mt_form(MultipartForm(form): MultipartForm) -> HttpResponse { let name = format!("{} {}", form.first_name.0, form.last_name.0); let map = HashMap::from(&form); let store = store_form(map); actix_rt::spawn(store.map(|s| match s { Ok(_) => info!("Successfully sent form to nextcloud!"), Err(e) => error!("There was an erroring sending form to nextcloud: {e}"), })); let email = form.send_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.registration.0.as_str() { "now" => { if form.health_form.0.as_str() == "yes" { HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(("HX-Redirect", "/health-form?registration=now")) .finish() } else { HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(( "HX-Redirect", "https://secure.myvanco.com/L-Z772/campaign/C-13DM3", )) .finish() } } "later" => { if form.health_form.0.as_str() == "yes" { HttpResponse::Ok() .insert_header(("Access-Control-Expose-Headers", "*")) .insert_header(("HX-Redirect", "/health-form?registration=later")) .finish() } else { HttpResponse::Ok().body( html! { h2 { "Thank you! {}" (name)} p { "You can go to the health form " a href="/health-form" { "here" } " or you can pay for mission trip " a href="https://secure.myvanco.com/L-Z772/campaign/C-13DM3" { "here" } } } .into_string(), ) } } _ => { error!("There wasn't an option for the registration passed in."); HttpResponse::Ok().body( html! { h2 { "Thank you! {}" (name)} p { "You can go to the health form " a href="/health-form" { "here" } " or you can pay for mission trip " a href="https://secure.myvanco.com/L-Z772/campaign/C-13DM3" { "here" } } } .into_string(), ) } } } async fn store_form(map: HashMap) -> Result<()> { let client = Client::new(); // let map = HashMap::from(self); let mut json = HashMap::new(); json.insert("data", map); let link = r#"https://staff.tfcconnection.org/apps/tables/#/table/9/row/757"#; let res = client .post("https://staff.tfcconnection.org/ocs/v2.php/apps/tables/api/2/tables/9/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?; if res.status().is_success() { let res = res.text().await.unwrap(); Ok(()) } else { Err(eyre!( "Problem in storing data: {:?}", res.error_for_status() )) } } #[cfg(test)] mod test { use actix_web::test; use pretty_assertions::assert_eq; use sqlx::Connection; use tracing::debug; use super::*; fn form() -> MtForm { MtForm { first_name: Text(String::from("Frodo")), last_name: Text(String::from("Braggins")), parent_first_name: Text(String::from("Bilbo")), parent_last_name: Text(String::from("Braggins")), 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), cellphone: Text(String::from("7868889797")), parentphone: Text(String::from("1234567898")), email: Text(String::from("frodo@hobbits.com")), parentemail: Text(String::from("bilbo@hobbits.com")), school: Text(String::from("Shire High")), grade: Text(String::from("junior")), pastor_first_name: Text(String::from("Gandalf")), pastor_last_name: Text(String::from("The White")), church_attendance: Text(String::from("often")), tfc_group: Text(String::from("Northern Valley")), shirt: Text(String::from("medium")), trip: Text(String::from("Mordor")), trip_notes: Text(String::from("If it must happen, I'll do it.")), relationship_with_jesus: Text(String::from("Cool beans")), testimony: Text(String::from("Nephew of Bilbo Braggins")), involvement_with_group: Text(String::from("Good friends with Gandalf")), reasons: Text(String::from("Want an adventure")), strengths: Text(String::from("Willing, brave, small, and curious")), weaknesses: Text(String::from("Not strong, or good with weapons")), previous_trip_info: Text(String::from("The edge of Hob Hill")), attitude: Text(String::from("Willing")), relevant_notes: Text(String::from("Willing to take the ring")), final_agreement: Text(String::from("yes")), registration: Text(String::from("later")), health_form: Text(String::from("yes")), file: None, } } #[test] async fn test_nc_post() { let form = form(); assert!(!form.first_name.is_empty()); let res = form.store_form().await; match res { Ok(_) => assert!(true, "passed storing test"), Err(e) => assert!(false, "Failed storing test: {e}"), } } #[test] async fn test_email() { let mut form = form(); assert!(!form.first_name.is_empty()); match form.send_email() { Ok(_) => assert!(true, "passed emailing test"), Err(e) => assert!(false, "Failed emailing test: {e}"), } } }