Each task's tests were run against a reference at its end state; the end states were replayed from master in order with the gate at each step (650 to 762 tests); each skeleton compiles against its tests and fails them. The reference is kept off this machine. Lessons T27 (every wait in a test has a limit) and T28 (mutate the reference before hand-over) come from this work. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
163 lines
5.5 KiB
Rust
163 lines
5.5 KiB
Rust
//! 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<String>,
|
|
channels: BTreeSet<String>,
|
|
}
|
|
|
|
/// Every `@name` in a message, lower-cased: `a-z`, `0-9`, `.`, `-` and `_` after an `@`, without
|
|
/// trailing dots.
|
|
pub fn named(message: &str) -> Vec<String> {
|
|
// 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-<root>" (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<String>,
|
|
}
|
|
|
|
/// The sessions with a turn running, and the messages waiting for each.
|
|
pub struct Queues {
|
|
limit: usize,
|
|
running: HashMap<SessionId, Pending>,
|
|
}
|
|
|
|
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<Batch> {
|
|
// 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<Thread> {
|
|
// The thread of every running session.
|
|
todo!()
|
|
}
|
|
}
|