use super::{
assistant_state::Pair,
http::{Request, Response},
route::{Site, child_given_up, refusal},
signin::{Door, Issuer},
};
use crate::serve::storage::Failure;
use serde_json::{Value, json};
pub(super) fn answer<I: Issuer>(
door: &Door<I>,
site: &Site,
request: &Request,
now: u64,
) -> Response {
result(door, site, request, now).unwrap_or_else(refusal)
}
fn result<I: Issuer>(
door: &Door<I>,
site: &Site,
request: &Request,
now: u64,
) -> Result<Response, Failure> {
let write = request.method == "POST";
if (write && request.header("origin") != Some(site.origin.as_str()))
|| request
.header("origin")
.is_some_and(|origin| origin != site.origin)
{
return Err(Failure::new(
"origin_refused",
"assistant requests are taken only from Studio's own origin",
));
}
let rest = request
.path
.strip_prefix("/api/assistant/")
.ok_or_else(unknown)?;
let (store, route) = rest.split_once('/').unwrap_or((rest, ""));
let socket = site
.stores
.get(store)
.ok_or_else(|| Failure::new("store_unknown", "the door serves no such store"))?;
let session = request
.cookie("haem_session")
.ok_or_else(|| Failure::new("session_unknown", "sign in to Studio first"))?;
let token = match door.pass(session, now) {
Ok(token) => token,
Err(error)
if matches!(
error.code.as_str(),
"lys_could_not_be_asked" | "lys_unavailable"
) =>
{
return Ok(Response::json(
502,
&json!({"code":"lys_unavailable","message":"Lys is temporarily unavailable; retry with the same session"}),
));
}
Err(error) => return Err(error),
};
if let Some(response) = child_given_up(site, store) {
return Ok(response);
}
let person = door.session_at(session, now)?["holder"]
.as_str()
.ok_or_else(|| Failure::new("session_unknown", "session has no person"))?
.to_owned();
if !super::assistant_wire::person(&person) {
return Err(Failure::new(
"assistant_person_invalid",
"the session person is not a valid assistant identifier",
));
}
let pair = (person, store.to_owned());
match (request.method.as_str(), route) {
("POST", "start") => {
match super::assistant_record::place(socket, site.timeout, &token) {
Ok(()) => {}
Err(refused) if refused.code == "permission_refused" => {
return Ok(Response::json(
403,
&json!({"code":"assistant_record_unplaced","message":format!("Starting the assistant creates this store's _assistant collection, where it keeps your conversations, and you may not create it: {}", refused.message),"lys":door.issuer().assistant_url()?}),
));
}
Err(failure) => return Err(failure),
}
Ok(Response::json(
501,
&json!({"code":"assistant_start_in_lys","message":"start the assistant profile for this store in Lys","lys":door.issuer().assistant_url()?,"profile":"assistant","store":store}),
))
}
("POST", "decide") => super::assistant_decide::decide(door, &pair, &token, &request.body),
("POST", "") => {
let op = super::assistant_ops::parse(&request.body, store)?;
deliver(door, &pair, op)
}
("GET", "events") => super::assistant_events::events(
door,
request,
pair,
socket.clone(),
site.timeout,
token,
),
("GET", "conversations") => {
let hub = door.assistant()?;
let conversations = super::assistant_record::conversations(
socket,
site.timeout,
&token,
&pair.0,
hub.config.events_after_bytes.get(),
)?;
Ok(Response::json(200, &json!({"conversations":conversations})))
}
_ => Err(unknown()),
}
}
pub(super) fn deliver<I: Issuer>(
door: &Door<I>,
pair: &Pair,
op: Value,
) -> Result<Response, Failure> {
match door.assistant()?.operation(pair, op) {
Ok(id) => Ok(Response::json(202, &json!({"id":id}))),
Err(error) if error.code == "assistant_not_started" => Ok(Response::json(
409,
&json!({"code":error.code,"message":error.message,"start":format!("/api/assistant/{}/start",pair.1),"lys":door.issuer().assistant_url()?}),
)),
Err(error) => {
let status = if error.code == "assistant_bound" {
429
} else if error.code == "assistant_ack_unknown" {
502
} else {
409
};
Ok(Response::json(
status,
&json!({"code":error.code,"message":error.message}),
))
}
}
}
fn unknown() -> Failure {
Failure::new("route_unknown", "the door serves no such assistant route")
}