diff --git a/README.md b/README.md index 346b52f..4406b01 100644 --- a/README.md +++ b/README.md @@ -62,11 +62,16 @@ recipient 1 and `CHALLENGE_1_SECOND` to recipient 2. The latter two are sent literally as written. When offline, the planned messages are logged instead. TextBee acceptance means a message was queued, not necessarily delivered. -Code 2 uses the former manual code value (`Qj4eAn`). On successful submission, -it sends a notification to `RECIPIENT_SELF` while the server is live (and logs -the notification instead while offline). The result page displays -`NEXT_STEP_TEXT` from `src/codes/code2.rs`; replace its TODO text with the next -step when ready. +Code 2 uses the former manual code value (`QJ4EAN`). Code values are case +insensitive. On successful submission, code 2 sends a notification to +`RECIPIENT_SELF` while the server is live (and logs the notification instead +while offline). The result page displays `NEXT_STEP_TEXT` from +`src/codes/code2.rs`; replace its TODO text with the next step when ready. + +Code 5 uses the code value defined in `src/codes/code5.rs`. A successful +submission sends a notification to `RECIPIENT_SELF` while the server is live +(and logs the notification instead while offline), and displays the completion +message without providing a download. The robot submits code 1 with `POST /event/buzz` and a JSON body containing `delay_ms` (a non-negative integer) and `secret` (the value of `CODE1_SECRET`). diff --git a/src/codes/code1.rs b/src/codes/code1.rs index f474a10..f6a1c54 100644 --- a/src/codes/code1.rs +++ b/src/codes/code1.rs @@ -10,46 +10,39 @@ const ACCEPTANCE_START_SECONDS: u32 = 11 * 60 * 60; const ACCEPTANCE_END_SECONDS: u32 = 13 * 60 * 60 + 30 * 60; const SELF_NOTIFICATION: &str = "Code 1 has been submitted while the server is live."; const CHALLENGE_1_FIRST: &str = "SECDED-13\nPARITY MSB...LSB"; -const CHALLENGE_1_SECOND: &str = "0x144A -0x17AE -0x17AA -0x07A0 +const CHALLENGE_1_SECOND: &str = "0x164A +0x13AA +0x13AA +0x0580 0x079E -0x1751 +0x1351 0x02FD -0x02FD -0x163F -0x067A -0x062B -0x122C -0x12FA -0x1667 -0x0606 -0x179B -0x164D -0x16F9 -0x027E -0x0629 +0x03FD +0x161F +0x047E +0x042B 0x1E2C +0x12F2 +0x16E7 +0x0606 +0x1799 +0x064C +0x1679 +0x0676 +0x062B +0x062C 0x07B3 0x12FA -0x121F -0x0676 -0x1667 -0x02FC -0x0F9E -0x07AD -0x061C -0x1267 -0x064D -0x16AA -0x02F5 -0x0505 -0x065B -0x1329 -0x1E2C -0x1484 -0x1671"; +0x160F +0x067E +0x0667 +0x02FD +0x0D05 +0x14F1 +0x1129 +0x048E +0x1084 +0x04FF"; pub async fn buzz_event_handler( ConnectInfo(address): ConnectInfo, @@ -140,75 +133,14 @@ async fn on_submit(live: bool, mut delta_ms: i64) -> Result<(), String> { return Ok(()); } - let secrets = crate::secrets::runtime(); - let client = reqwest::Client::new(); - - send_textbee_sms( - &client, - &secrets.textbee_api_key, - secrets.textbee_device_id.as_deref(), - &[&secrets.recipient_self], - SELF_NOTIFICATION, - ) - .await?; - send_textbee_sms( - &client, - &secrets.textbee_api_key, - secrets.textbee_device_id.as_deref(), - &[&secrets.recipient_1, &secrets.recipient_2], + super::send_textbee_sms(&[super::SmsRecipient::SelfRecipient], SELF_NOTIFICATION).await?; + super::send_textbee_sms( + &[super::SmsRecipient::First, super::SmsRecipient::Second], &congrats_message, ) .await?; - send_textbee_sms( - &client, - &secrets.textbee_api_key, - secrets.textbee_device_id.as_deref(), - &[&secrets.recipient_1], - CHALLENGE_1_FIRST, - ) - .await?; - send_textbee_sms( - &client, - &secrets.textbee_api_key, - secrets.textbee_device_id.as_deref(), - &[&secrets.recipient_2], - CHALLENGE_1_SECOND, - ) - .await?; - - Ok(()) -} - -async fn send_textbee_sms( - client: &reqwest::Client, - api_key: &str, - device_id: Option<&str>, - recipients: &[&str], - message: &str, -) -> Result<(), String> { - let mut payload = serde_json::json!({ - "message": message, - "recipients": recipients, - }); - if let Some(device_id) = device_id { - payload["deviceId"] = serde_json::Value::String(device_id.to_string()); - } - - let response = client - .post("https://api.textbee.dev/api/v1/gateway/send-sms") - .header("x-api-key", api_key) - .json(&payload) - .send() - .await - .map_err(|error| format!("TextBee request failed: {error}"))?; - - let status = response.status(); - if !status.is_success() { - let response_body = response.text().await.map_err(|error| { - format!("TextBee returned HTTP {status}; unable to read error: {error}") - })?; - return Err(format!("TextBee returned HTTP {status}: {response_body}")); - } + super::send_textbee_sms(&[super::SmsRecipient::First], CHALLENGE_1_FIRST).await?; + super::send_textbee_sms(&[super::SmsRecipient::Second], CHALLENGE_1_SECOND).await?; Ok(()) } diff --git a/src/codes/code2.rs b/src/codes/code2.rs index 956b60f..3ac956d 100644 --- a/src/codes/code2.rs +++ b/src/codes/code2.rs @@ -1,5 +1,5 @@ pub const ID: &str = "code2"; -pub const CODE: &str = "Qj4eAn"; +pub const CODE: &str = "QJ4EAN"; pub const SUCCESS_MESSAGE: &str = "Congratulations on completing step 2."; const SELF_NOTIFICATION: &str = "Code 2 has been submitted."; @@ -10,31 +10,5 @@ pub(super) async fn on_submit(live: bool) -> Result<(), String> { return Ok(()); } - let secrets = crate::secrets::runtime(); - - let mut payload = serde_json::json!({ - "message": SELF_NOTIFICATION, - "recipients": [&secrets.recipient_self], - }); - if let Some(device_id) = &secrets.textbee_device_id { - payload["deviceId"] = serde_json::Value::String(device_id.clone()); - } - - let response = reqwest::Client::new() - .post("https://api.textbee.dev/api/v1/gateway/send-sms") - .header("x-api-key", &secrets.textbee_api_key) - .json(&payload) - .send() - .await - .map_err(|error| format!("TextBee request failed: {error}"))?; - - let status = response.status(); - if !status.is_success() { - let response_body = response.text().await.map_err(|error| { - format!("TextBee returned HTTP {status}; unable to read error: {error}") - })?; - return Err(format!("TextBee returned HTTP {status}: {response_body}")); - } - - Ok(()) + super::send_textbee_sms(&[super::SmsRecipient::SelfRecipient], SELF_NOTIFICATION).await } diff --git a/src/codes/code5.rs b/src/codes/code5.rs new file mode 100644 index 0000000..3389752 --- /dev/null +++ b/src/codes/code5.rs @@ -0,0 +1,15 @@ +pub const ID: &str = "code5"; +pub const CODE: &str = "V7MK2P"; +pub const SUCCESS_MESSAGE: &str = + "Congradulations on completing challenge 5. We will be in touch"; + +const SELF_NOTIFICATION: &str = "Code 5 has been submitted."; + +pub(super) async fn on_submit(live: bool) -> Result<(), String> { + if !live { + leptos::logging::log!("Code 5 SMS (not sent; server is offline): {SELF_NOTIFICATION}"); + return Ok(()); + } + + super::send_textbee_sms(&[super::SmsRecipient::SelfRecipient], SELF_NOTIFICATION).await +} diff --git a/src/codes/mod.rs b/src/codes/mod.rs index 2a057cb..120386a 100644 --- a/src/codes/mod.rs +++ b/src/codes/mod.rs @@ -9,9 +9,61 @@ use leptos_router::{ pub mod code1; #[cfg(feature = "ssr")] pub mod code2; +#[cfg(feature = "ssr")] +pub mod code5; pub mod history; +#[cfg(feature = "ssr")] +#[derive(Clone, Copy)] +pub(super) enum SmsRecipient { + SelfRecipient, + First, + Second, +} + +#[cfg(feature = "ssr")] +pub(super) async fn send_textbee_sms( + recipients: &[SmsRecipient], + message: &str, +) -> Result<(), String> { + let secrets = crate::secrets::runtime(); + let recipients: Vec<&str> = recipients + .iter() + .map(|recipient| match recipient { + SmsRecipient::SelfRecipient => secrets.recipient_self.as_str(), + SmsRecipient::First => secrets.recipient_1.as_str(), + SmsRecipient::Second => secrets.recipient_2.as_str(), + }) + .collect(); + + let mut payload = serde_json::json!({ + "message": message, + "recipients": recipients, + }); + if let Some(device_id) = &secrets.textbee_device_id { + payload["deviceId"] = serde_json::Value::String(device_id.clone()); + } + + let response = reqwest::Client::new() + .post("https://api.textbee.dev/api/v1/gateway/send-sms") + .header("x-api-key", &secrets.textbee_api_key) + .json(&payload) + .send() + .await + .map_err(|error| format!("TextBee request failed: {error}"))?; + + let status = response.status(); + if !status.is_success() { + let response_body = response.text().await.map_err(|error| { + format!("TextBee returned HTTP {status}; unable to read error: {error}") + })?; + return Err(format!("TextBee returned HTTP {status}: {response_body}")); + } + + Ok(()) +} + #[derive(Clone, serde::Deserialize, serde::Serialize)] struct SubmissionResponse { message: String, @@ -28,27 +80,44 @@ struct DownloadInfo { #[server] async fn submit_code(code: String) -> Result, ServerFnError> { - if code != crate::codes::code2::CODE { + if !code.eq_ignore_ascii_case(crate::codes::code2::CODE) + && !code.eq_ignore_ascii_case(crate::codes::code5::CODE) + { return Ok(None); } let live = crate::manage::server_live_state(); let axum::extract::ConnectInfo(address): axum::extract::ConnectInfo = leptos_axum::extract().await?; - history::record_successful_submission(address.ip(), live, crate::codes::code2::ID)?; - crate::codes::code2::on_submit(live) - .await - .map_err(ServerFnError::new)?; - Ok(Some(SubmissionResponse { - message: crate::codes::code2::SUCCESS_MESSAGE.to_string(), - download: Some(DownloadInfo { - href: "/challenge3.txt".to_string(), - filename: "challenge3.txt".to_string(), - title: "Download challenge 3".to_string(), - description: "TXT file".to_string(), - }), - })) + if code.eq_ignore_ascii_case(crate::codes::code2::CODE) { + history::record_successful_submission(address.ip(), live, crate::codes::code2::ID)?; + crate::codes::code2::on_submit(live) + .await + .map_err(ServerFnError::new)?; + + Ok(Some(SubmissionResponse { + message: crate::codes::code2::SUCCESS_MESSAGE.to_string(), + download: Some(DownloadInfo { + href: "/challenge3.txt".to_string(), + filename: "challenge3.txt".to_string(), + title: "Download challenge 3".to_string(), + description: "TXT file".to_string(), + }), + })) + } else if code.eq_ignore_ascii_case(crate::codes::code5::CODE) { + history::record_successful_submission(address.ip(), live, crate::codes::code5::ID)?; + crate::codes::code5::on_submit(live) + .await + .map_err(ServerFnError::new)?; + + Ok(Some(SubmissionResponse { + message: crate::codes::code5::SUCCESS_MESSAGE.to_string(), + download: None, + })) + } else { + Ok(None) + } } fn encode_path_segment(value: &str) -> String {