//! 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 ` — schedule a deferred start. Schedule { story_number_or_id: String, hhmm: String, }, /// `timer list` — list all pending timers. List, /// `timer cancel ` — 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 { 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 ` 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 ` 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, timezone: Option<&str>) -> (String, String) { match timezone.and_then(|s| s.parse::().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) ); } }