//! Which posts `gatewayd` acts on, which session each belongs to, commands, and the queue of //! messages per session (M4a spec, section 7). Pure: the state file and the network are elsewhere. use std::collections::{BTreeSet, HashMap}; use proto::SessionId; use crate::mm::Post; pub const M4B_COMMAND: &str = "approvals over Mattermost arrive in M4b; use `bxctl approvals`"; pub const UNKNOWN_COMMAND: &str = "unknown command; the commands are !approve and !deny"; pub const BUSY: &str = "busy: too many messages are waiting in this conversation"; /// Names that name nobody: every agent in the channel would answer them. const EVERYONE: [&str; 3] = ["channel", "here", "all"]; /// Why a post was not acted on. Only `NotAllowed` is logged, by user id and post id. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Ignored { Own, System, NotAllowed, NotForUs, } /// Where an answer goes: a channel, and the root of the thread in it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Thread { pub channel: String, pub root: String, } /// A message for a session. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Message { pub session: SessionId, pub thread: Thread, /// A reply in a thread: the session should exist already. pub resume: bool, pub text: String, /// A thread in a channel or group message that this Boxmaker now takes part in. pub joins_thread: bool, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Route { Ignore(Ignored), /// Answer in the thread without a turn (a command). Reply { thread: Thread, text: String, }, Queue(Message), } pub struct Router { me_id: String, me_name: String, users: BTreeSet, channels: BTreeSet, } /// Every `@name` in a message, lower-cased: `a-z`, `0-9`, `.`, `-` and `_` after an `@`, without /// trailing dots. pub fn named(message: &str) -> Vec { // Find each "@". The name after it is the longest run of ASCII letters, digits, ".", "-" and // "_", with trailing "." removed and lower-cased. Skip empty names. Continue after the name. todo!() } impl Router { pub fn new(me_id: &str, me_name: &str, users: &[String], channels: &[String]) -> Router { // Store the ids, the username lower-cased, and the two lists as sets. todo!() } /// Is a post in a channel or group message for this Boxmaker? `known` says whether it has a /// session for a thread root. fn for_us(&self, post: &Post, known: &dyn Fn(&str) -> bool) -> bool { // Named this bot -> true. Otherwise true only for a reply (root_id not empty) in a known // thread (`known(root_id)`) that names nobody but channel, here or all. todo!() } /// What to do with a new post (a post seen before is dropped by the caller first). pub fn route(&self, post: &Post, channel_type: &str, known: &dyn Fn(&str) -> bool) -> Route { // Section 7 of the spec, in this order: own post -> Ignore(Own); kind not empty -> // Ignore(System); user not allowed -> Ignore(NotAllowed); then the channel: "D" is always // ours; "O", "P" or "G" is ours when its id is allowed and `for_us`; anything else -> // Ignore(NotForUs). The thread root is root_id, or the post id when root_id is empty. Then // commands: "!!..." drops one "!" and goes on as a message; "!" then a first word approve // or deny -> Reply M4B_COMMAND; any other "!" -> Reply UNKNOWN_COMMAND. Then Queue: session // "mm-" (if SessionId::new fails, Ignore(NotForUs)), resume = root_id not empty, // joins_thread = not "D". todo!() } } /// A turn to send: every message that was waiting, joined with a blank line. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Batch { pub session: SessionId, pub thread: Thread, pub resume: bool, pub text: String, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum Pushed { /// No turn was running: send this one now. Start(Batch), /// A turn is running; the message waits for the next. Waiting, /// Too many are waiting: the message is dropped, answer `BUSY`. Full(Thread), } struct Pending { thread: Thread, resume: bool, waiting: Vec, } /// The sessions with a turn running, and the messages waiting for each. pub struct Queues { limit: usize, running: HashMap, } impl Queues { /// `limit` is the most messages that may wait per session. pub fn new(limit: usize) -> Queues { // An empty map. todo!() } pub fn push(&mut self, message: Message) -> Pushed { // If the session is running: with `limit` messages already waiting -> Full(thread); else // add the text to waiting -> Waiting. Otherwise insert it as running (its later batches // resume: true) and return Start with this message alone. todo!() } /// A session's turn ended: the next batch, or `None`, and the session is no longer running. pub fn finish(&mut self, session: &SessionId) -> Option { // Not running -> None. Nothing waiting -> remove it, None. Otherwise take every waiting // text, joined with "\n\n", as the next Batch (it stays running). todo!() } /// How many sessions have a turn running. pub fn running(&self) -> usize { // How many sessions are running. todo!() } /// The threads with a turn running, for showing this bot as typing in them. pub fn threads(&self) -> Vec { // The thread of every running session. todo!() } }