use super::{format_recovery, parse_iso_recovery_time};
use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, TimeZone, Utc};
pub(crate) fn parse_recovery_time(message: &str) -> Option<String> {
if let Some((_, seconds)) = message.rsplit_once('|')
&& (message.starts_with("Claude AI usage limit reached")
|| message.starts_with("You've hit your limit"))
&& seconds.len() == 10
&& seconds.bytes().all(|byte| byte.is_ascii_digit())
&& let Ok(seconds) = seconds.parse::<i64>()
&& let Some(at) = DateTime::from_timestamp(seconds, 0)
&& at <= Utc::now() + chrono::Duration::days(8)
{
return Some(format_recovery(at.with_timezone(&Local).naive_local()));
}
let prefix = "try again at ";
if let Some(start) = message.find(prefix) {
let start = start + prefix.len();
let remainder = &message[start..];
let end = remainder.find('.').unwrap_or(remainder.len());
Some(remainder[..end].trim().to_string())
} else {
parse_iso_recovery_time(message)
}
}
pub(crate) fn parse_claude_reset_clock(lower: &str) -> Option<NaiveDateTime> {
let rest = lower.split_once("resets ")?.1;
let (clock, zone) = rest.split_once(" (")?;
let offset = match zone.split_once(')')?.0 {
"asia/shanghai" => FixedOffset::east_opt(8 * 3600)?,
"utc" => FixedOffset::east_opt(0)?,
_ => return None,
};
let clock = clock.trim();
let (digits, meridiem) = clock.split_at(clock.len().checked_sub(2)?);
let (hour, minute) = match digits.split_once(':') {
Some((hour, minute)) => (hour.parse::<u32>().ok()?, minute.parse::<u32>().ok()?),
None => (digits.parse::<u32>().ok()?, 0),
};
if !(1..=12).contains(&hour) || minute > 59 {
return None;
}
let hour = match meridiem {
"am" => hour % 12,
"pm" => hour % 12 + 12,
_ => return None,
};
let now = Utc::now().with_timezone(&offset);
let mut date = now.date_naive();
if date.and_hms_opt(hour, minute, 0)? <= now.naive_local() {
date = date.succ_opt()?;
}
let at = offset.from_local_datetime(&date.and_hms_opt(hour, minute, 0)?).single()?;
Some(at.with_timezone(&Local).naive_local())
}
pub(crate) fn claude_rejected_limit(object: &serde_json::Map<String, serde_json::Value>) -> Option<String> {
let info = object.get("rate_limit_info")?;
(info.get("status")?.as_str()? == "rejected").then(|| {
match info.get("resetsAt").and_then(serde_json::Value::as_i64) {
Some(seconds) => format!("You've hit your limit|{seconds}"),
None => "You've hit your limit".to_string(),
}
})
}
#[cfg(test)]
#[path = "claude_tests.rs"]
mod tests;