fix: extensive ui improvements
This commit is contained in:
parent
691394445a
commit
6ba64d29a9
17 changed files with 684 additions and 142 deletions
|
|
@ -19,6 +19,12 @@ pub struct Question {
|
|||
answered_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct QuestionStats {
|
||||
asked: i64,
|
||||
answered: i64,
|
||||
}
|
||||
|
||||
pub async fn get_questions(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<Question>>, StatusCode> {
|
||||
|
|
@ -51,6 +57,29 @@ pub async fn get_questions(
|
|||
Ok(Json(questions))
|
||||
}
|
||||
|
||||
pub async fn get_question_stats(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<QuestionStats>, StatusCode> {
|
||||
let db = state
|
||||
.db
|
||||
.lock()
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let asked: i64 = db
|
||||
.query_row("SELECT COUNT(*) FROM questions", [], |row| row.get(0))
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
let answered: i64 = db
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM questions WHERE answer IS NOT NULL",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Json(QuestionStats { asked, answered }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SubmitQuestion {
|
||||
question: String,
|
||||
|
|
@ -191,10 +220,12 @@ pub struct MtaHookResponse {
|
|||
}
|
||||
|
||||
fn webhook_secret_matches(headers: &HeaderMap, expected_secret: &str) -> bool {
|
||||
let expected_secret = expected_secret.trim();
|
||||
let header_secret = headers
|
||||
.get("X-Webhook-Secret")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
.unwrap_or("")
|
||||
.trim();
|
||||
if header_secret == expected_secret {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -224,7 +255,29 @@ fn webhook_secret_matches(headers: &HeaderMap, expected_secret: &str) -> bool {
|
|||
None => return false,
|
||||
};
|
||||
|
||||
password == expected_secret
|
||||
password.trim() == expected_secret
|
||||
}
|
||||
|
||||
fn webhook_secret_debug(headers: &HeaderMap) -> String {
|
||||
let header_secret = headers
|
||||
.get("X-Webhook-Secret")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let auth = headers
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("");
|
||||
|
||||
let decoded = auth
|
||||
.strip_prefix("Basic ")
|
||||
.and_then(|encoded| base64::engine::general_purpose::STANDARD.decode(encoded).ok())
|
||||
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
"x-webhook-secret={header_secret:?}; authorization={auth:?}; basic-decoded={decoded:?}"
|
||||
)
|
||||
}
|
||||
|
||||
fn extract_qa_reply(payload: &MtaHookPayload, expected_domain: &str) -> Option<(i64, String)> {
|
||||
|
|
@ -304,6 +357,21 @@ fn string_at_paths(value: &Value, paths: &[&[&str]]) -> Option<String> {
|
|||
})
|
||||
}
|
||||
|
||||
fn subject_from_headers_value(value: Option<&Value>) -> Option<String> {
|
||||
let headers = value?.as_array()?;
|
||||
headers.iter().find_map(|header| {
|
||||
let parts = header.as_array()?;
|
||||
let name = parts.first()?.as_str()?.trim();
|
||||
if !name.eq_ignore_ascii_case("Subject") {
|
||||
return None;
|
||||
}
|
||||
parts
|
||||
.get(1)?
|
||||
.as_str()
|
||||
.map(|s| s.trim().to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_qa_reply_from_value(payload: &Value, expected_domain: &str) -> Option<(i64, String)> {
|
||||
if let Some(messages) = payload.get("messages").and_then(Value::as_array) {
|
||||
for message in messages {
|
||||
|
|
@ -318,6 +386,7 @@ fn extract_qa_reply_from_value(payload: &Value, expected_domain: &str) -> Option
|
|||
&["headers", "subject"],
|
||||
],
|
||||
)
|
||||
.or_else(|| subject_from_headers_value(value_at_path(message, &["message", "headers"])))
|
||||
.as_deref(),
|
||||
&string_at_paths(message, &[&["message", "contents"], &["contents"], &["raw_message"]])
|
||||
.unwrap_or_default(),
|
||||
|
|
@ -338,6 +407,7 @@ fn extract_qa_reply_from_value(payload: &Value, expected_domain: &str) -> Option
|
|||
&["headers", "subject"],
|
||||
],
|
||||
)
|
||||
.or_else(|| subject_from_headers_value(value_at_path(payload, &["message", "headers"])))
|
||||
.as_deref(),
|
||||
&string_at_paths(payload, &[&["message", "contents"], &["contents"], &["raw_message"]])
|
||||
.unwrap_or_default(),
|
||||
|
|
@ -380,7 +450,11 @@ pub async fn webhook(
|
|||
body: String,
|
||||
) -> Result<Json<MtaHookResponse>, (StatusCode, String)> {
|
||||
if !webhook_secret_matches(&headers, &state.webhook_secret) {
|
||||
eprintln!("Rejected webhook: invalid secret");
|
||||
eprintln!(
|
||||
"Rejected webhook: invalid secret; expected_len={}; {}",
|
||||
state.webhook_secret.len(),
|
||||
webhook_secret_debug(&headers)
|
||||
);
|
||||
return Err((StatusCode::UNAUTHORIZED, "invalid secret".to_string()));
|
||||
}
|
||||
|
||||
|
|
@ -515,4 +589,28 @@ mod tests {
|
|||
Some((9, "Answer body".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_reply_from_value_with_header_pairs() {
|
||||
let payload: Value = serde_json::from_str(
|
||||
r#"{
|
||||
"envelope": {
|
||||
"to": [{"address":"qa@extremist.software"}]
|
||||
},
|
||||
"message": {
|
||||
"headers": [
|
||||
["From", " jet@extremist.software\r\n"],
|
||||
["Subject", " Re: 11 - hi\r\n"]
|
||||
],
|
||||
"contents": "Answer from header pairs"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
extract_qa_reply_from_value(&payload, "extremist.software"),
|
||||
Some((11, "Answer from header pairs".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||
"/api/questions",
|
||||
get(handlers::get_questions).post(handlers::post_question),
|
||||
)
|
||||
.route("/api/questions/stats", get(handlers::get_question_stats))
|
||||
.route("/api/webhook", post(handlers::webhook))
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue