use super::{
assistant_state::Pair,
http::Response,
signin::{Door, Issuer},
};
use crate::serve::storage::Failure;
use serde::Deserialize;
use serde_json::json;
use std::time::{Duration, Instant};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Decision {
draft: String,
decision: String,
}
pub(super) fn decide<I: Issuer>(
door: &Door<I>,
pair: &Pair,
pass: &str,
bytes: &[u8],
) -> Result<Response, Failure> {
let given: Decision = serde_json::from_slice(bytes).map_err(|_| invalid())?;
if !super::assistant_wire::identifier(&given.draft)
|| !matches!(given.decision.as_str(), "approve" | "refuse")
{
return Err(invalid());
}
let mut url = url::Url::parse(door.issuer().assistant_url()?).map_err(|_| invalid())?;
let encoded: String = url::form_urlencoded::byte_serialize(given.draft.as_bytes()).collect();
url.set_fragment(Some(&format!("/access/drafts?draft={encoded}")));
let url = url.to_string();
let hub = door.assistant()?;
let (agent, generation) = match hub.seat_identity(pair) {
Ok(seat) => seat,
Err(error) => return Ok(denied(&error, &given.draft, &url)),
};
let within = Duration::from_millis(door.issuer().bounds().total_ms.get());
let started = Instant::now();
let held = match door.issuer().assistant_draft(pass, &given.draft, within) {
Ok(held) => held,
Err(error) => return Ok(denied(&error, &given.draft, &url)),
};
if !belongs(&held, &given.draft, &agent, pair) {
return Ok(denied(
&Failure::new(
"draft_other_seat",
"this draft belongs to another assistant seat or store; open its Lys link to decide it",
),
&given.draft,
&url,
));
}
match hub.seat_identity(pair) {
Ok(current) if current.1 == generation => {}
Ok(_) => {
return Ok(denied(
&Failure::new(
"draft_other_seat",
"the assistant seat changed while the draft was being checked",
),
&given.draft,
&url,
));
}
Err(error) => return Ok(denied(&error, &given.draft, &url)),
}
let remaining = within.saturating_sub(started.elapsed());
if remaining.is_zero() {
return Ok(denied(
&Failure::new(
"lys_draft_deadline",
"Lys draft lookup reached the configured sign-in request deadline",
),
&given.draft,
&url,
));
}
let answer = match door
.issuer()
.assistant_decide(pass, &held, &given.decision, remaining)
{
Ok(answer) => answer,
Err(error) => return Ok(denied(&error, &given.draft, &url)),
};
let mut receipt = json!({"draft":given.draft,"lys":url,"decision":answer,"seat_told":false});
let expected = if given.decision == "approve" {
"approved"
} else {
"refused"
};
if answer["state"] == expected {
match hub.operation_for(
pair,
Some(&generation),
json!({"decided":{"draft":given.draft,"decision":given.decision}}),
) {
Ok(id) => {
receipt["seat_told"] = json!(true);
receipt["id"] = json!(id);
}
Err(error) => {
receipt["code"] = json!(error.code);
receipt["message"] = json!(format!(
"Lys recorded the decision. Seat notification was not confirmed: {}. The seat also follows Lys draft state through /changes.",
error.message
));
}
}
} else {
receipt["code"] = json!(if answer["state"] == "waiting" {
"draft_waiting"
} else {
"draft_state_changed"
});
receipt["message"] = json!(
"Lys recorded the decision receipt. The seat also follows Lys draft state through /changes."
);
}
Ok(Response::json(200, &receipt))
}
fn denied(error: &Failure, draft: &str, url: &str) -> Response {
let status = match error.code.as_str() {
"draft_other_seat" | "assistant_not_started" => 409,
"lys_unavailable"
| "lys_draft_deadline"
| "lys_draft_pages_exceeded"
| "lys_response_too_large"
| "ProviderUnavailable"
| "SessionsUnavailable" => 502,
_ => 403,
};
Response::json(
status,
&json!({"code":error.code,"message":error.message,"draft":draft,"lys":url}),
)
}
fn belongs(held: &serde_json::Value, draft: &str, agent: &str, pair: &Pair) -> bool {
held["id"] == draft
&& held["app"] == "haematite"
&& held["holder"] == agent
&& held["responsible"] == pair.0
&& held["target"]["kind"] == "haematite.database"
&& held["target"]["id"] == pair.1
&& held["target"]["action"] == "agent_permission"
}
fn invalid() -> Failure {
Failure::new(
"assistant_decision_invalid",
"name a draft and approve or refuse",
)
}