gatewayd: a turn's speaker, the username of the one person who wrote its burst
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -151,6 +151,7 @@ fn one_turn(
|
||||
socket: &Path,
|
||||
batch: &Batch,
|
||||
resume: bool,
|
||||
speaker: Option<&str>,
|
||||
on_event: &mut dyn FnMut(&TurnEvent),
|
||||
) -> Outcome {
|
||||
// 1. Connect to the socket (else LoopDown("cannot connect to <path>: <e>")).
|
||||
@@ -167,8 +168,8 @@ fn one_turn(
|
||||
session: batch.session.clone(),
|
||||
content: batch.text.clone(),
|
||||
resume,
|
||||
// Set in M5c-1 task 03. Until then no turn has a speaker, and none is synced.
|
||||
speaker: None,
|
||||
// On the turn only, never in its text: `memoryd` reads it from the log, the model never.
|
||||
speaker: speaker.map(str::to_string),
|
||||
};
|
||||
if let Err(e) = write_frame(
|
||||
&mut stream,
|
||||
@@ -207,14 +208,20 @@ fn one_turn(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// A turn for a batch, spoken by `speaker` (a Mattermost username) when one person wrote all of
|
||||
/// it. 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,
|
||||
speaker: Option<&str>,
|
||||
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.
|
||||
match one_turn(socket, batch, batch.resume, on_event) {
|
||||
match one_turn(socket, batch, batch.resume, speaker, on_event) {
|
||||
Outcome::Refused(err) if err.code == ErrorCode::NoSuchSession && batch.resume => {
|
||||
one_turn(socket, batch, false, on_event)
|
||||
one_turn(socket, batch, false, speaker, on_event)
|
||||
}
|
||||
outcome => outcome,
|
||||
}
|
||||
@@ -230,11 +237,13 @@ fn post(poster: &dyn Poster, thread: &Thread, text: String, log: &dyn Fn(&str))
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a batch's turn and post what comes of it in its thread. A post that fails is logged.
|
||||
/// Run a batch's turn, spoken by `speaker`, 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,
|
||||
speaker: Option<&str>,
|
||||
log: &dyn Fn(&str),
|
||||
notice: &Notice,
|
||||
) {
|
||||
@@ -244,7 +253,7 @@ pub fn deliver(
|
||||
// LoopDown(why) -> log "gatewayd: <session>: <why>" and post LOOP_DOWN. A post that fails is
|
||||
// logged, exactly "gatewayd: cannot post in <channel> (thread <root>): <error>", and the rest
|
||||
// goes on.
|
||||
let outcome = run_turn(socket, batch, &mut |event| {
|
||||
let outcome = run_turn(socket, batch, speaker, &mut |event| {
|
||||
if let TurnEvent::ApprovalPending { approval, .. } = event
|
||||
&& !notice.arrives(*approval)
|
||||
{
|
||||
|
||||
@@ -158,13 +158,21 @@ impl Gateway {
|
||||
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, &|line| log(line), ¬ice);
|
||||
deliver(
|
||||
client.as_ref(),
|
||||
&socket,
|
||||
&batch,
|
||||
speaker.as_deref(),
|
||||
&|line| log(line),
|
||||
¬ice,
|
||||
);
|
||||
let _ = done.send(batch.session);
|
||||
});
|
||||
if let Err(e) = spawned {
|
||||
@@ -178,6 +186,29 @@ impl Gateway {
|
||||
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> {
|
||||
|
||||
@@ -121,6 +121,8 @@ pub(crate) struct Gateway {
|
||||
askers: HashMap<String, approvals::Asker>,
|
||||
/// How long a turn waits for its approval to come through `gateway.sock` before the notice.
|
||||
notice: Duration,
|
||||
/// Mattermost usernames by user id, each looked up once (M5c spec, section 11).
|
||||
usernames: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// The wait before attempt `n` (from 0) after a loss.
|
||||
@@ -271,6 +273,7 @@ impl Gateway {
|
||||
authors: HashMap::new(),
|
||||
askers: HashMap::new(),
|
||||
notice,
|
||||
usernames: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct Message {
|
||||
pub text: String,
|
||||
/// A thread in a channel or group message that this Boxmaker now takes part in.
|
||||
pub joins_thread: bool,
|
||||
/// Who wrote it: the post's `user_id`, a Mattermost user id.
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -245,6 +247,7 @@ impl Router {
|
||||
resume: !post.root_id.is_empty(),
|
||||
text: format!("!{rest}"),
|
||||
joins_thread: channel_type != "D",
|
||||
user: post.user_id.clone(),
|
||||
});
|
||||
}
|
||||
if let Some(command) = approval_command(message) {
|
||||
@@ -269,6 +272,7 @@ impl Router {
|
||||
resume: !post.root_id.is_empty(),
|
||||
text: message.to_string(),
|
||||
joins_thread: channel_type != "D",
|
||||
user: post.user_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -280,6 +284,22 @@ pub struct Batch {
|
||||
pub thread: Thread,
|
||||
pub resume: bool,
|
||||
pub text: String,
|
||||
/// Who wrote each message of `text`, in order: Mattermost user ids.
|
||||
pub senders: Vec<String>,
|
||||
}
|
||||
|
||||
impl Batch {
|
||||
/// The one user who wrote every message of the batch. `None` when two or more people did, or
|
||||
/// when it names nobody: a burst joined from several people has no speaker (M5c spec,
|
||||
/// section 4).
|
||||
pub fn sender(&self) -> Option<&str> {
|
||||
let first = self.senders.first()?;
|
||||
if self.senders.iter().all(|user| user == first) {
|
||||
Some(first.as_str())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -295,6 +315,8 @@ pub enum Pushed {
|
||||
struct Pending {
|
||||
thread: Thread,
|
||||
waiting: Vec<String>,
|
||||
/// The author of each message in `waiting`, in the same order.
|
||||
senders: Vec<String>,
|
||||
}
|
||||
|
||||
/// The sessions with a turn running, and the messages waiting for each.
|
||||
@@ -323,6 +345,7 @@ impl Queues {
|
||||
return Pushed::Full(pending.thread.clone());
|
||||
}
|
||||
pending.waiting.push(message.text.clone());
|
||||
pending.senders.push(message.user.clone());
|
||||
return Pushed::Waiting;
|
||||
}
|
||||
let batch = Batch {
|
||||
@@ -330,12 +353,14 @@ impl Queues {
|
||||
thread: message.thread.clone(),
|
||||
resume: message.resume,
|
||||
text: message.text.clone(),
|
||||
senders: vec![message.user.clone()],
|
||||
};
|
||||
self.running.insert(
|
||||
message.session,
|
||||
Pending {
|
||||
thread: message.thread.clone(),
|
||||
waiting: Vec::new(),
|
||||
senders: Vec::new(),
|
||||
},
|
||||
);
|
||||
Pushed::Start(batch)
|
||||
@@ -354,6 +379,7 @@ impl Queues {
|
||||
thread: pending.thread.clone(),
|
||||
resume: true,
|
||||
text: pending.waiting.join("\n\n"),
|
||||
senders: std::mem::take(&mut pending.senders),
|
||||
};
|
||||
pending.waiting.clear();
|
||||
self.running.insert(session.clone(), pending);
|
||||
|
||||
Reference in New Issue
Block a user