//! The serve loop's part in approvals (M4b spec, sections 6 and 8): posting what a `gateway.sock` //! connection brings, marking it when it ends, and marking what a restart left behind. The loop //! alone touches Mattermost and the state file; connection threads only ask. use std::sync::mpsc::Sender; use proto::{ApprovalReply, GatewayOutcome, PendingApproval}; use crate::approvals::Posted; use crate::approvals::text::{self, Ended}; use crate::gateway::{Ending, ToLoop}; use crate::serve::{Gateway, Stop}; use crate::state::ApprovalPosts; /// An approval's last post, the one that is edited when it ends. #[derive(Debug)] pub(super) struct Open { pub(super) last_post: String, pub(super) last_text: String, } impl Gateway { /// Everything the connection threads asked since the last look, never blocking. pub(super) fn gateway_requests(&mut self) -> Result<(), Stop> { while let Ok(asked) = self.gateway_rx.try_recv() { match asked { ToLoop::Show { info, answer, shown, } => { let result = self.show(&info, answer)?; let _ = shown.send(result); } ToLoop::Ended { approval, ending } => self.ended(approval, ending)?, } } Ok(()) } /// Where an approval goes: its session's turn in flight, or the first approver's direct /// messages as a new thread (an empty root). fn place(&self, info: &PendingApproval) -> Result<(String, String), String> { // 1. `self.state.turn_of(info.session.as_str())`: if Some(turn), `Ok((turn.channel, // turn.root))`. // 2. Otherwise the first approver's direct messages, as a new thread: // `self.config.first_approver()` (None is Err("no approver is configured")), then // `self.client.direct_channel(&self.me.id, first)` (an Err(e) is Err(format!("no direct // channel with the first approver: {e}"))). `Ok((channel, String::new()))`. todo!() } /// Post an approval's parts in order, record them, and register it for answers. An `Err` /// inside is why it could not be posted; the outer one stops `gatewayd` (the state file). fn show( &mut self, info: &PendingApproval, answer: Sender, ) -> Result, String>, Stop> { let id = info.approval; let (channel, mut root) = match self.place(info) { Ok(place) => place, Err(why) => return Ok(Err(why)), }; let shown = text::render(info); let mut posts = Vec::new(); let mut shown_at = 0; for body in &shown.posts { match self.client.create_post(&channel, &root, body) { Ok(post) => { if root.is_empty() { root = post.id.clone(); } shown_at = post.create_at; posts.push(post.id); } Err(e) => { // Parts already posted must not look answerable. if let (Some(last), Some(text)) = (posts.last(), shown.posts.get(posts.len().wrapping_sub(1))) { self.mark(id, last, &text::with_ending(text, &Ended::NoLonger)); } return Ok(Err(format!("cannot post approval {id}: {e}"))); } } } self.state.add_approval(ApprovalPosts { approval: id, channel: channel.clone(), posts: posts.clone(), })?; self.registry.add(Posted { approval: id, session: info.session.as_str().to_string(), channel, root, shown_at, answerable: shown.answerable, answer, }); if let (Some(last_post), Some(last_text)) = (posts.last(), shown.posts.last()) { self.open.insert( id, Open { last_post: last_post.clone(), last_text: last_text.clone(), }, ); } Ok(Ok(posts)) } /// An approval ended: forget it, and add how it ended to its last post. fn ended(&mut self, id: u64, ending: Ending) -> Result<(), Stop> { // 1. `self.registry.remove(id);` and `self.state.remove_approval(id)?;`. // 2. `let Some(open) = self.open.remove(&id) else { return Ok(()); };` // 3. The `Ended`: Ending::NoLonger -> Ended::NoLonger. Ending::Answered { by, reason, // outcome } -> `by` becomes "@" from `self.client.username(&by)`, or stays the // id if that fails; then outcome Runs -> Ended::Runs { by }, Denied { reason } -> // Ended::Denied { by, reason }, Refused -> Ended::Refused { by, reason }, Gone -> // Ended::NoLonger. // 4. `self.mark(id, &open.last_post, &text::with_ending(&open.last_text, &ended));` and // Ok(()). todo!() } /// At the first connection after a start: the approval posts the last run left are no longer /// answerable. Their last post is replaced by its title and the restart line. pub(super) fn mark_left_behind(&mut self) -> Result<(), Stop> { // For each `left` in `self.state.take_approvals()?`: if it has a last post, // `self.mark(left.approval, last, &body)` with `body = format!("**Approval {}**\n\n{}", // left.approval, text::ended_line(&Ended::Restarted))`. Then Ok(()). todo!() } /// Edit a post; a failure is logged, not retried. fn mark(&self, id: u64, post: &str, body: &str) { // `if let Err(e) = self.client.patch_post(post, body)`: log "gatewayd: cannot mark approval // {id} as ended: {e}" with `(self.log)(&line)`. todo!() } }