Files
huskies/server/src/service/timer/parse.rs
T

180 lines
5.6 KiB
Rust
Raw Normal View History

//! Pure parsing logic for timer bot-commands and time display formatting.
//!
//! No side effects: no filesystem access, no clock reads.
use chrono::{DateTime, Local, Utc};
use chrono_tz::Tz;
use crate::chat::util::strip_bot_mention;
// ── Command types ──────────────────────────────────────────────────────────
/// A parsed `timer` command.
#[derive(Debug, PartialEq)]
pub enum TimerCommand {
/// `timer <story_id_or_number> <HH:MM>` — schedule a deferred start.
Schedule {
story_number_or_id: String,
hhmm: String,
},
/// `timer list` — list all pending timers.
List,
/// `timer cancel <story_id_or_number>` — remove a pending timer.
Cancel { story_number_or_id: String },
/// Malformed arguments.
BadArgs,
}
// ── Command extraction ─────────────────────────────────────────────────────
/// Parse a `timer` command from a raw message body.
///
/// Strips the bot mention prefix and matches the `timer` keyword.
/// Returns `None` when the message is not a timer command at all.
pub fn extract_timer_command(
message: &str,
bot_name: &str,
bot_user_id: &str,
) -> Option<TimerCommand> {
let stripped = strip_bot_mention(message, bot_name, bot_user_id);
let trimmed = stripped
.trim()
.trim_start_matches(|c: char| !c.is_alphanumeric());
let (cmd, args) = match trimmed.split_once(char::is_whitespace) {
Some((c, a)) => (c, a.trim()),
None => (trimmed, ""),
};
if !cmd.eq_ignore_ascii_case("timer") {
return None;
}
// `timer` with no args or `timer list`
if args.is_empty() || args.eq_ignore_ascii_case("list") {
return Some(TimerCommand::List);
}
let (sub, rest) = match args.split_once(char::is_whitespace) {
Some((s, r)) => (s, r.trim()),
None => (args, ""),
};
// `timer cancel <id>`
if sub.eq_ignore_ascii_case("cancel") {
if rest.is_empty() {
return Some(TimerCommand::BadArgs);
}
return Some(TimerCommand::Cancel {
story_number_or_id: rest.to_string(),
});
}
// `timer <id> <HH:MM>`
if rest.is_empty() {
return Some(TimerCommand::BadArgs);
}
Some(TimerCommand::Schedule {
story_number_or_id: sub.to_string(),
hhmm: rest.to_string(),
})
}
// ── Display helpers ────────────────────────────────────────────────────────
/// Format a UTC instant for display in the given timezone (or local time when
/// `timezone` is `None`). Returns `(formatted_string, label)` where `label`
/// is either the IANA timezone name or `"server local time"`.
pub(super) fn format_in_timezone(dt: DateTime<Utc>, timezone: Option<&str>) -> (String, String) {
match timezone.and_then(|s| s.parse::<Tz>().ok()) {
Some(tz) => {
let tz_time = dt.with_timezone(&tz);
(tz_time.format("%Y-%m-%d %H:%M").to_string(), tz.to_string())
}
None => {
let local_time = dt.with_timezone(&Local);
(
local_time.format("%Y-%m-%d %H:%M").to_string(),
"server local time".to_string(),
)
}
}
}
// ── Tests ──────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn non_timer_command_returns_none() {
assert!(extract_timer_command("Timmy help", "Timmy", "@bot:home").is_none());
}
#[test]
fn timer_list_no_args() {
assert_eq!(
extract_timer_command("Timmy timer", "Timmy", "@bot:home"),
Some(TimerCommand::List)
);
}
#[test]
fn timer_list_explicit() {
assert_eq!(
extract_timer_command("Timmy timer list", "Timmy", "@bot:home"),
Some(TimerCommand::List)
);
}
#[test]
fn timer_cancel_story_id() {
assert_eq!(
extract_timer_command("Timmy timer cancel 421_story_foo", "Timmy", "@bot:home"),
Some(TimerCommand::Cancel {
story_number_or_id: "421_story_foo".to_string()
})
);
}
#[test]
fn timer_cancel_no_arg_is_bad_args() {
assert_eq!(
extract_timer_command("Timmy timer cancel", "Timmy", "@bot:home"),
Some(TimerCommand::BadArgs)
);
}
#[test]
fn timer_schedule_with_story_id() {
assert_eq!(
extract_timer_command("Timmy timer 421_story_foo 14:30", "Timmy", "@bot:home"),
Some(TimerCommand::Schedule {
story_number_or_id: "421_story_foo".to_string(),
hhmm: "14:30".to_string(),
})
);
}
#[test]
fn timer_schedule_with_number() {
assert_eq!(
extract_timer_command("Timmy timer 421 14:30", "Timmy", "@bot:home"),
Some(TimerCommand::Schedule {
story_number_or_id: "421".to_string(),
hhmm: "14:30".to_string(),
})
);
}
#[test]
fn timer_schedule_missing_time_is_bad_args() {
assert_eq!(
extract_timer_command("Timmy timer 421_story_foo", "Timmy", "@bot:home"),
Some(TimerCommand::BadArgs)
);
}
}