gatewayd: strip a leading mention of this bot; recognise approval commands
This commit is contained in:
@@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use crate::deliver::{LOOP_DOWN, deliver};
|
||||
use crate::mm::{Post, typing};
|
||||
use crate::serve::{Gateway, Stop};
|
||||
use crate::sessions::{BUSY, Batch, Ignored, Pushed, Route};
|
||||
use crate::sessions::{BUSY, Batch, Ignored, M4B_COMMAND, Pushed, Route};
|
||||
use crate::state::InFlight;
|
||||
use crate::ws::WsError;
|
||||
use crate::ws::conn::Ws;
|
||||
@@ -81,6 +81,8 @@ impl Gateway {
|
||||
}
|
||||
Route::Ignore(_) => {}
|
||||
Route::Reply { thread, text } => self.post(&thread.channel, &thread.root, &text),
|
||||
// Until the approvals module is wired in (task 15), M4a's notice.
|
||||
Route::Approval { thread, .. } => self.post(&thread.channel, &thread.root, M4B_COMMAND),
|
||||
Route::Queue(message) => {
|
||||
if message.joins_thread {
|
||||
self.state.join_thread(&message.thread.root)?;
|
||||
|
||||
@@ -51,6 +51,81 @@ pub enum Route {
|
||||
text: String,
|
||||
},
|
||||
Queue(Message),
|
||||
/// `!approve` or `!deny`, for the approvals module to judge.
|
||||
Approval {
|
||||
thread: Thread,
|
||||
command: ApprovalCommand,
|
||||
/// Who wrote it, which post it is, and when (Mattermost's `create_at`).
|
||||
user: String,
|
||||
post: String,
|
||||
at: i64,
|
||||
},
|
||||
}
|
||||
|
||||
/// An approval command as typed: exactly `!approve <id>`, `!deny <id>` or `!deny <id> <reason>`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ApprovalCommand {
|
||||
Approve(u64),
|
||||
Deny(u64, Option<String>),
|
||||
/// `!approve` or `!deny` in any other form.
|
||||
Malformed,
|
||||
}
|
||||
|
||||
/// The longest reason a `!deny` may give, in characters.
|
||||
pub const MAX_REASON: usize = proto::MAX_REASON;
|
||||
|
||||
/// `!approve …` or `!deny …` as an approval command; `None` for any other text.
|
||||
pub fn approval_command(text: &str) -> Option<ApprovalCommand> {
|
||||
let after = text.strip_prefix('!')?;
|
||||
let (word, rest) = match after.split_once(' ') {
|
||||
Some((word, rest)) => (word, Some(rest)),
|
||||
None => (after, None),
|
||||
};
|
||||
if !matches!(word, "approve" | "deny") {
|
||||
return None;
|
||||
}
|
||||
let id = |s: &str| -> Option<u64> {
|
||||
if s.is_empty() || s.len() > 20 || !s.bytes().all(|b| b.is_ascii_digit()) {
|
||||
return None;
|
||||
}
|
||||
s.parse().ok()
|
||||
};
|
||||
let parsed = match (word, rest) {
|
||||
("approve", Some(rest)) => id(rest).map(ApprovalCommand::Approve),
|
||||
("deny", Some(rest)) => match rest.split_once(' ') {
|
||||
None => id(rest).map(|n| ApprovalCommand::Deny(n, None)),
|
||||
Some((n, reason)) => {
|
||||
let fits = !reason.is_empty() && reason.chars().count() <= MAX_REASON;
|
||||
id(n)
|
||||
.filter(|_| fits)
|
||||
.map(|n| ApprovalCommand::Deny(n, Some(reason.to_string())))
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
Some(parsed.unwrap_or(ApprovalCommand::Malformed))
|
||||
}
|
||||
|
||||
/// The text after a leading mention of this bot (`me`, lower-case), and the spaces after it. The
|
||||
/// mention is matched as `named` matches names; nothing else is removed. A message that would be
|
||||
/// left empty is kept as it was.
|
||||
pub fn strip_mention<'a>(message: &'a str, me: &str) -> &'a str {
|
||||
let Some(after) = message.strip_prefix('@') else {
|
||||
return message;
|
||||
};
|
||||
let len = after
|
||||
.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')))
|
||||
.unwrap_or(after.len());
|
||||
let raw = after.get(..len).unwrap_or_default();
|
||||
let name = raw.trim_end_matches('.');
|
||||
if name.is_empty() || !name.eq_ignore_ascii_case(me) {
|
||||
return message;
|
||||
}
|
||||
let rest = after
|
||||
.get(name.len()..)
|
||||
.unwrap_or_default()
|
||||
.trim_start_matches(' ');
|
||||
if rest.is_empty() { message } else { rest }
|
||||
}
|
||||
|
||||
pub struct Router {
|
||||
@@ -126,8 +201,9 @@ impl Router {
|
||||
// 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
|
||||
// commands: Then a leading mention of this bot is stripped (M4b spec, section 7), and:
|
||||
// "!!..." drops one "!" and goes on as a message; `approval_command` -> Approval; 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".
|
||||
if post.user_id == self.me_id {
|
||||
@@ -162,7 +238,7 @@ impl Router {
|
||||
root: root.clone(),
|
||||
};
|
||||
|
||||
let message = &post.message;
|
||||
let message = strip_mention(&post.message, &self.me_name);
|
||||
if let Some(rest) = message.strip_prefix("!!") {
|
||||
return Route::Queue(Message {
|
||||
session,
|
||||
@@ -172,14 +248,19 @@ impl Router {
|
||||
joins_thread: channel_type != "D",
|
||||
});
|
||||
}
|
||||
if let Some(after) = message.strip_prefix('!') {
|
||||
let answer = match after.split_whitespace().next() {
|
||||
Some("approve") | Some("deny") => M4B_COMMAND,
|
||||
_ => UNKNOWN_COMMAND,
|
||||
if let Some(command) = approval_command(message) {
|
||||
return Route::Approval {
|
||||
thread,
|
||||
command,
|
||||
user: post.user_id.clone(),
|
||||
post: post.id.clone(),
|
||||
at: post.create_at,
|
||||
};
|
||||
}
|
||||
if message.starts_with('!') {
|
||||
return Route::Reply {
|
||||
thread,
|
||||
text: answer.to_string(),
|
||||
text: UNKNOWN_COMMAND.to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -187,7 +268,7 @@ impl Router {
|
||||
session,
|
||||
thread,
|
||||
resume: !post.root_id.is_empty(),
|
||||
text: message.clone(),
|
||||
text: message.to_string(),
|
||||
joins_thread: channel_type != "D",
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user