//! One turn on `loop.sock` and its answer in the thread (M4a spec, section 8). Typing is shown by //! `serve`, which owns the WebSocket; this module only sends the turn and posts what comes back. use std::os::unix::net::UnixStream; use std::path::Path; use proto::{ Envelope, ErrorCode, Message, PROTOCOL_VERSION, Turn, TurnEvent, WireError, read_frame, write_frame, }; use crate::mm::MmError; use crate::mm::rest::Client; use crate::sessions::Batch; /// The longest reply we post, in characters (a post holds at most 16,383). pub const MAX_POST: usize = 16_000; pub const LOOP_DOWN: &str = "Boxmaker's loop is not running (see docs/runbook.md#loop-unavailable)"; pub const EMPTY_ANSWER: &str = "(the answer was empty)"; /// Somewhere to post: Mattermost, or a test's record. pub trait Poster: Send + Sync { fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError>; } impl Poster for Client { fn post(&self, channel: &str, root: &str, text: &str) -> Result<(), MmError> { self.create_post(channel, root, text).map(|_| ()) } } /// How a turn ended. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Outcome { Answer(String), Refused(WireError), /// `loop.sock` could not be reached or closed early; why, for the log. LoopDown(String), } pub fn approval_text(approval: u64) -> String { format!( "waiting for approval {approval}: approve or deny it with `bxctl` (Mattermost approvals arrive in M4b)" ) } /// "Error: : "; the detail carries `loopd`'s runbook pointer when there is one. pub fn error_text(error: &WireError) -> String { // "Error: : ", where is the snake_case name serde gives the ErrorCode // (`serde_json::to_value(code)` is a JSON string, e.g. "no_such_session"). todo!() } /// An answer in posts of at most `MAX_POST` characters: each cut at the last newline before the /// limit (the newline is dropped), or at the limit when there is none. pub fn split_answer(text: &str) -> Vec { // Blank (only whitespace) -> [EMPTY_ANSWER]. Otherwise, while the rest is longer than MAX_POST // *characters*: take the first MAX_POST characters; if they hold a newline past position 0, cut // at the last one and drop that newline; else cut at MAX_POST characters. The last part is the // rest. todo!() } /// Send one turn and read it to its end; `on_event` sees every event. fn one_turn( socket: &Path, batch: &Batch, resume: bool, on_event: &mut dyn FnMut(&TurnEvent), ) -> Outcome { // 1. Connect to the socket (else LoopDown("cannot connect to : ")). // 2. `write_frame` one Envelope: v PROTOCOL_VERSION, id 1, final true, msg Turn (else // LoopDown). // 3. `read_frame` until the end: (id 1, not final, TurnEvent) -> on_event; (1, final, TurnDone) // -> Answer with its content; (1, final, Error) -> Refused; a read error -> LoopDown("the // turn ended early: "); anything else -> LoopDown("an unexpected frame"). todo!() } /// A turn for a batch. A reply in a thread `loopd` does not know creates the session, as /// `bxctl chat --session` does. pub fn run_turn(socket: &Path, batch: &Batch, on_event: &mut dyn FnMut(&TurnEvent)) -> Outcome { // `one_turn` with batch.resume. When that is Refused with NoSuchSession and batch.resume was // true, one more `one_turn` with resume false. Otherwise the first outcome. todo!() } /// Run a batch's turn and post what comes of it in its thread. A post that fails is logged. pub fn deliver(poster: &dyn Poster, socket: &Path, batch: &Batch, log: &dyn Fn(&str)) { // Post in the batch thread. An ApprovalPending event posts `approval_text(approval)` at once. // Then: Answer -> every part of `split_answer`, in order; Refused -> `error_text`; // LoopDown(why) -> log "gatewayd: : " and post LOOP_DOWN. A post that fails is // logged, exactly "gatewayd: cannot post in (thread ): ", and the rest // goes on. todo!() }