292 lines
12 KiB
Rust
292 lines
12 KiB
Rust
//! What the event loop does with a post, a finished turn, and the time between: routing, starting
|
|
//! turns, typing, and catching up after a gap.
|
|
|
|
use std::sync::Arc;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use crate::approvals::Judged;
|
|
use crate::deliver::{LOOP_DOWN, Notice, deliver};
|
|
use crate::mm::{Post, typing};
|
|
use crate::serve::approvals::Asker;
|
|
use crate::serve::{Gateway, Stop};
|
|
use crate::sessions::{BUSY, Batch, Ignored, Pushed, Route};
|
|
use crate::state::InFlight;
|
|
use crate::ws::WsError;
|
|
use crate::ws::conn::Ws;
|
|
|
|
/// Now, in Mattermost's milliseconds.
|
|
fn now_ms() -> i64 {
|
|
// `let ms = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis()).unwrap_or(0);`
|
|
// then `i64::try_from(ms).unwrap_or(i64::MAX)`.
|
|
let ms = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.map(|d| d.as_millis())
|
|
.unwrap_or(0);
|
|
i64::try_from(ms).unwrap_or(i64::MAX)
|
|
}
|
|
|
|
impl Gateway {
|
|
/// Post in a thread; a failure is logged, not fatal.
|
|
pub(super) fn post(&self, channel: &str, root: &str, text: &str) {
|
|
// `if let Err(e) = self.client.create_post(channel, root, text)`: log
|
|
// "gatewayd: cannot post in <channel> (thread <root>): <e>", with `(self.log)(&line)`.
|
|
// Nothing else: posting records nothing in the state.
|
|
if let Err(e) = self.client.create_post(channel, root, text) {
|
|
(self.log)(&format!(
|
|
"gatewayd: cannot post in {channel} (thread {root}): {e}"
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Is this channel one whose posts `gatewayd` keeps track of?
|
|
fn tracked(&self, channel: &str, channel_type: &str) -> bool {
|
|
// `channel_type == "D" || self.config.allow.channels.iter().any(|c| c == channel)`.
|
|
channel_type == "D" || self.config.allow.channels.iter().any(|c| c == channel)
|
|
}
|
|
|
|
/// One new post, live or caught up.
|
|
pub(super) fn handle_post(&mut self, post: &Post, channel_type: &str) -> Result<(), Stop> {
|
|
// 1. `if !self.tracked(&post.channel_id, channel_type) || self.state.seen(&post.id)`:
|
|
// return Ok(()).
|
|
// 2. `self.state.handled(&post.id, &post.channel_id, post.create_at)?;` before anything
|
|
// else: after a crash, a post is never answered twice.
|
|
// 3. Route it, with the state answering "is this thread ours?":
|
|
// `let state = &self.state;`
|
|
// `let route = self.router.route(post, channel_type, &|root| state.knows_thread(root));`
|
|
// then `match route`:
|
|
// - Route::Ignore(Ignored::NotAllowed): log
|
|
// "gatewayd: ignored post <post.id> from <post.user_id>: not allowed"
|
|
// (never the text).
|
|
// - Route::Ignore(_): nothing.
|
|
// - Route::Reply { thread, text }: `self.post(&thread.channel, &thread.root, &text)`.
|
|
// - Route::Queue(message): if `message.joins_thread`,
|
|
// `self.state.join_thread(&message.thread.root)?`; then
|
|
// `match self.queues.push(message)`:
|
|
// Pushed::Start(batch) -> `self.start(batch)?`; Pushed::Waiting -> nothing;
|
|
// Pushed::Full(thread) -> `self.post(&thread.channel, &thread.root, BUSY)`.
|
|
// 4. Ok(()).
|
|
if !self.tracked(&post.channel_id, channel_type) || self.state.seen(&post.id) {
|
|
return Ok(());
|
|
}
|
|
self.state
|
|
.handled(&post.id, &post.channel_id, post.create_at)?;
|
|
let state = &self.state;
|
|
let route = self
|
|
.router
|
|
.route(post, channel_type, &|root| state.knows_thread(root));
|
|
match route {
|
|
Route::Ignore(Ignored::NotAllowed) => {
|
|
(self.log)(&format!(
|
|
"gatewayd: ignored post {} from {}: not allowed",
|
|
post.id, post.user_id
|
|
));
|
|
}
|
|
Route::Ignore(_) => {}
|
|
Route::Reply { thread, text } => self.post(&thread.channel, &thread.root, &text),
|
|
Route::Approval {
|
|
thread,
|
|
command,
|
|
user,
|
|
post: command_post,
|
|
at,
|
|
} => {
|
|
let approvers = self.config.approvers();
|
|
let judged = self.registry.judge(
|
|
&command,
|
|
&user,
|
|
&thread.root,
|
|
&command_post,
|
|
at,
|
|
approvers,
|
|
);
|
|
if let Judged::Reply(text) = judged {
|
|
self.post(&thread.channel, &thread.root, &text);
|
|
}
|
|
}
|
|
Route::Queue(message) => {
|
|
self.authors.insert(
|
|
message.session.clone(),
|
|
(post.user_id.clone(), channel_type.to_string()),
|
|
);
|
|
if message.joins_thread {
|
|
self.state.join_thread(&message.thread.root)?;
|
|
}
|
|
match self.queues.push(message) {
|
|
Pushed::Start(batch) => self.start(batch)?,
|
|
Pushed::Waiting => {}
|
|
Pushed::Full(thread) => self.post(&thread.channel, &thread.root, BUSY),
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Record the turn as in flight and run it on its own thread.
|
|
fn start(&mut self, batch: Batch) -> Result<(), Stop> {
|
|
// 1. `self.state.start_turn(InFlight { session: batch.session.as_str().to_string(),
|
|
// channel: batch.thread.channel.clone(), root: batch.thread.root.clone() })?;`
|
|
// 2. Clone what the thread takes, before the `move`:
|
|
// `let (client, socket, log, done) = (Arc::clone(&self.client),
|
|
// self.loop_socket.clone(), Arc::clone(&self.log), self.done_tx.clone());`
|
|
// `let (session, thread) = (batch.session.clone(), batch.thread.clone());`
|
|
// 3. `let spawned = std::thread::Builder::new().spawn(move || {
|
|
// deliver(client.as_ref(), &socket, &batch, &|line| log(line));
|
|
// let _ = done.send(batch.session); });`
|
|
// 4. `if let Err(e) = spawned`: log "gatewayd: cannot start a thread for <session>: <e>"
|
|
// (`session.as_str()`), `self.post(&thread.channel, &thread.root, LOOP_DOWN)`, and
|
|
// `let _ = self.done_tx.send(session);` so the session does not stay busy.
|
|
// 5. Ok(()).
|
|
self.state.start_turn(InFlight {
|
|
session: batch.session.as_str().to_string(),
|
|
channel: batch.thread.channel.clone(),
|
|
root: batch.thread.root.clone(),
|
|
})?;
|
|
if let Some((user, channel_type)) = self.authors.get(&batch.session) {
|
|
let asker = Asker {
|
|
user: user.clone(),
|
|
channel_type: channel_type.clone(),
|
|
prompt: batch.text.clone(),
|
|
};
|
|
self.askers
|
|
.insert(batch.session.as_str().to_string(), asker);
|
|
}
|
|
let (registry, wait) = (Arc::clone(&self.registry), self.notice);
|
|
let (client, socket, log, done) = (
|
|
Arc::clone(&self.client),
|
|
self.loop_socket.clone(),
|
|
Arc::clone(&self.log),
|
|
self.done_tx.clone(),
|
|
);
|
|
let (session, thread) = (batch.session.clone(), batch.thread.clone());
|
|
let speaker = self.speaker(&batch);
|
|
let spawned = std::thread::Builder::new().spawn(move || {
|
|
let posted = |id: u64| registry.was_posted(id);
|
|
let notice = Notice {
|
|
posted: &posted,
|
|
wait,
|
|
};
|
|
deliver(
|
|
client.as_ref(),
|
|
&socket,
|
|
&batch,
|
|
speaker.as_deref(),
|
|
&|line| log(line),
|
|
¬ice,
|
|
);
|
|
let _ = done.send(batch.session);
|
|
});
|
|
if let Err(e) = spawned {
|
|
(self.log)(&format!(
|
|
"gatewayd: cannot start a thread for {}: {e}",
|
|
session.as_str()
|
|
));
|
|
self.post(&thread.channel, &thread.root, LOOP_DOWN);
|
|
let _ = self.done_tx.send(session);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The turn's speaker: the username of the one person who wrote all of the batch, as
|
|
/// Mattermost gives it (M5c spec, section 11). `None` when two or more people did, or when
|
|
/// the name cannot be found: never the user id, never a guess. A name found is kept; a
|
|
/// failure is logged and not kept, so the next turn looks again.
|
|
fn speaker(&mut self, batch: &Batch) -> Option<String> {
|
|
let user = batch.sender()?;
|
|
if let Some(name) = self.usernames.get(user) {
|
|
return Some(name.clone());
|
|
}
|
|
match self.client.username(user) {
|
|
Ok(name) => {
|
|
self.usernames.insert(user.to_string(), name.clone());
|
|
Some(name)
|
|
}
|
|
Err(e) => {
|
|
(self.log)(&format!(
|
|
"gatewayd: no username for {user}: {e}; the turn has no speaker"
|
|
));
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Turns that ended: out of flight, who asked forgotten, and the next batch of each session
|
|
/// started. Never blocks.
|
|
pub(super) fn finished(&mut self) -> Result<(), Stop> {
|
|
while let Ok(session) = self.done_rx.try_recv() {
|
|
self.state.end_turn(session.as_str())?;
|
|
self.askers.remove(session.as_str());
|
|
match self.queues.finish(&session) {
|
|
Some(batch) => self.start(batch)?,
|
|
None => {
|
|
self.authors.remove(&session);
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Show this bot as typing in every thread with a turn running.
|
|
pub(super) fn typing(&mut self, ws: &mut Ws) -> Result<(), WsError> {
|
|
// `let waiting = self.registry.waiting_sessions();` then for each thread, none that waits
|
|
// for its approval: A turn waiting for its approval is not working: no typing in its thread.
|
|
// Otherwise `self.seq += 1; ws.send_text(...)`; then Ok(()).
|
|
let waiting = self.registry.waiting_sessions();
|
|
for thread in self.queues.threads() {
|
|
if waiting.contains(&format!("mm-{}", thread.root)) {
|
|
continue;
|
|
}
|
|
self.seq += 1;
|
|
ws.send_text(&typing(self.seq, &thread.channel, &thread.root))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Answer what arrived while `gatewayd` was away: the direct channel with each allowed user,
|
|
/// and each allowed channel. Written for you: it is the glue.
|
|
pub(super) fn catch_up(&mut self) -> Result<(), Stop> {
|
|
let mut channels = Vec::new();
|
|
for user in self.config.allow.users.clone() {
|
|
match self.client.direct_channel(&self.me.id, &user) {
|
|
Ok(id) => channels.push((id, "D")),
|
|
Err(e) => (self.log)(&format!("gatewayd: no direct channel with {user}: {e}")),
|
|
}
|
|
}
|
|
channels.extend(self.config.allow.channels.iter().map(|c| (c.clone(), "O")));
|
|
for (channel, channel_type) in channels {
|
|
self.catch_up_channel(&channel, channel_type)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// One channel's posts since its mark. A channel seen for the first time starts from now.
|
|
fn catch_up_channel(&mut self, channel: &str, channel_type: &str) -> Result<(), Stop> {
|
|
// 1. `let Some(since) = self.state.since(channel) else { ... }`: with no mark,
|
|
// `self.state.mark(channel, now_ms())?` and return Ok(()).
|
|
// 2. `self.client.posts_since(channel, since)`: an Err(e) is logged
|
|
// "gatewayd: cannot catch up <channel>: <e>" and returns Ok(()).
|
|
// 3. When `found.full`, log "gatewayd: <channel>: too many posts to catch up; some may be
|
|
// missed".
|
|
// 4. `for post in &found.posts { self.handle_post(post, channel_type)?; }` and Ok(()).
|
|
let Some(since) = self.state.since(channel) else {
|
|
self.state.mark(channel, now_ms())?;
|
|
return Ok(());
|
|
};
|
|
let found = match self.client.posts_since(channel, since) {
|
|
Ok(found) => found,
|
|
Err(e) => {
|
|
(self.log)(&format!("gatewayd: cannot catch up {channel}: {e}"));
|
|
return Ok(());
|
|
}
|
|
};
|
|
if found.full {
|
|
(self.log)(&format!(
|
|
"gatewayd: {channel}: too many posts to catch up; some may be missed"
|
|
));
|
|
}
|
|
for post in &found.posts {
|
|
self.handle_post(post, channel_type)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|