ignore code case. add code 5. refactor send_sms

This commit is contained in:
2026-10-14 22:26:37 -05:00
parent 1ec94767f0
commit c36e40f825
5 changed files with 143 additions and 148 deletions
+10 -5
View File
@@ -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`).
+33 -101
View File
@@ -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<SocketAddr>,
@@ -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(())
}
+2 -28
View File
@@ -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
}
+15
View File
@@ -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
}
+70 -1
View File
@@ -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,13 +80,17 @@ struct DownloadInfo {
#[server]
async fn submit_code(code: String) -> Result<Option<SubmissionResponse>, 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<std::net::SocketAddr> =
leptos_axum::extract().await?;
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
@@ -49,6 +105,19 @@ async fn submit_code(code: String) -> Result<Option<SubmissionResponse>, ServerF
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 {