146 lines
4.4 KiB
Rust
146 lines
4.4 KiB
Rust
//! Rebuild command: trigger a server rebuild and restart.
|
|
//!
|
|
//! `{bot_name} rebuild` stops all running agents, rebuilds the server binary
|
|
//! with `cargo build`, and re-execs the process with the new binary. If the
|
|
//! build fails the error is reported back to the room and the server keeps
|
|
//! running.
|
|
|
|
use crate::agents::AgentPool;
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
/// A parsed rebuild command.
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct RebuildCommand;
|
|
|
|
/// Parse a rebuild command from a raw message body.
|
|
///
|
|
/// Strips the bot mention prefix and checks whether the command word is
|
|
/// `rebuild`. Returns `None` when the message is not a rebuild command.
|
|
pub fn extract_rebuild_command(
|
|
message: &str,
|
|
bot_name: &str,
|
|
bot_user_id: &str,
|
|
) -> Option<RebuildCommand> {
|
|
let stripped = strip_mention(message, bot_name, bot_user_id);
|
|
let trimmed = stripped
|
|
.trim()
|
|
.trim_start_matches(|c: char| !c.is_alphanumeric());
|
|
|
|
let cmd = match trimmed.split_once(char::is_whitespace) {
|
|
Some((c, _)) => c,
|
|
None => trimmed,
|
|
};
|
|
|
|
if cmd.eq_ignore_ascii_case("rebuild") {
|
|
Some(RebuildCommand)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Handle a rebuild command: trigger server rebuild and restart.
|
|
///
|
|
/// Returns a string describing the outcome. On build failure the error
|
|
/// message is returned so it can be posted to the room; the server keeps
|
|
/// running. On success this function never returns (the process re-execs).
|
|
pub async fn handle_rebuild(
|
|
bot_name: &str,
|
|
project_root: &Path,
|
|
agents: &Arc<AgentPool>,
|
|
) -> String {
|
|
crate::slog!("[matrix-bot] rebuild command received (bot={bot_name})");
|
|
match crate::rebuild::rebuild_and_restart(agents, project_root, None).await {
|
|
Ok(msg) => msg,
|
|
Err(e) => format!("Rebuild failed: {e}"),
|
|
}
|
|
}
|
|
|
|
/// Strip the bot mention prefix from a raw Matrix message body.
|
|
fn strip_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
|
|
let trimmed = message.trim();
|
|
if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) {
|
|
return rest;
|
|
}
|
|
if let Some(localpart) = bot_user_id.split(':').next()
|
|
&& let Some(rest) = strip_prefix_ci(trimmed, localpart)
|
|
{
|
|
return rest;
|
|
}
|
|
if let Some(rest) = strip_prefix_ci(trimmed, bot_name) {
|
|
return rest;
|
|
}
|
|
trimmed
|
|
}
|
|
|
|
fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
|
if text.len() < prefix.len() {
|
|
return None;
|
|
}
|
|
if !text[..prefix.len()].eq_ignore_ascii_case(prefix) {
|
|
return None;
|
|
}
|
|
let rest = &text[prefix.len()..];
|
|
match rest.chars().next() {
|
|
None => Some(rest),
|
|
Some(c) if c.is_alphanumeric() || c == '-' || c == '_' => None,
|
|
_ => Some(rest),
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn extract_with_display_name() {
|
|
let cmd = extract_rebuild_command("Timmy rebuild", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_with_full_user_id() {
|
|
let cmd = extract_rebuild_command(
|
|
"@timmy:home.local rebuild",
|
|
"Timmy",
|
|
"@timmy:home.local",
|
|
);
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_with_localpart() {
|
|
let cmd = extract_rebuild_command("@timmy rebuild", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_case_insensitive() {
|
|
let cmd = extract_rebuild_command("Timmy REBUILD", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_non_rebuild_returns_none() {
|
|
let cmd = extract_rebuild_command("Timmy help", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, None);
|
|
}
|
|
|
|
#[test]
|
|
fn extract_ignores_extra_args() {
|
|
// "rebuild" with trailing text is still a rebuild command
|
|
let cmd = extract_rebuild_command("Timmy rebuild now", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, Some(RebuildCommand));
|
|
}
|
|
|
|
#[test]
|
|
fn extract_no_match_returns_none() {
|
|
let cmd = extract_rebuild_command("Timmy status", "Timmy", "@timmy:home.local");
|
|
assert_eq!(cmd, None);
|
|
}
|
|
}
|