1use async_trait::async_trait;
2use std::fmt::Debug;
3use time::{Date, OffsetDateTime, Time, UtcOffset, macros::format_description};
4use tokio_util::sync::CancellationToken;
5
6#[async_trait]
8pub trait Notifiable: Sync + Send + Debug {
9 fn get_task(&self) -> Task;
11
12 async fn on_time(&self, cancel: CancellationToken) {
16 cancel.cancel();
17 }
18
19 async fn on_skip(&self, _cancel: CancellationToken) {
21 }
23}
24
25#[derive(Debug, Clone, PartialEq)]
26pub enum Skip {
27 Date(Date),
29 DateRange(Date, Date),
31 Day(Vec<u8>),
35 DayRange(usize, usize),
39 Time(Time),
41 TimeRange(Time, Time),
45 None,
47}
48
49impl Default for Skip {
50 fn default() -> Self {
51 Self::None
52 }
53}
54
55impl std::fmt::Display for Skip {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Skip::Date(date) => write!(f, "date: {date}"),
59 Skip::DateRange(start, end) => write!(f, "date range: {start} - {end}"),
60 Skip::Day(day) => write!(f, "day: {day:?}"),
61 Skip::DayRange(start, end) => write!(f, "day range: {start} - {end}"),
62 Skip::Time(time) => write!(f, "time: {time}"),
63 Skip::TimeRange(start, end) => write!(f, "time range: {start} - {end}"),
64 Skip::None => write!(f, "none"),
65 }
66 }
67}
68
69impl Skip {
70 pub fn is_skip(&self, time: OffsetDateTime) -> bool {
72 match self {
73 Skip::Date(date) => time.date() == *date,
74 Skip::DateRange(start, end) => time.date() >= *start && time.date() <= *end,
75 Skip::Day(day) => day.contains(&(time.weekday().number_from_monday())),
76 Skip::DayRange(start, end) => {
77 let weekday = time.weekday().number_from_monday() as usize;
78 weekday >= *start && weekday <= *end
79 }
80 Skip::Time(skip_time) => time.time() == *skip_time,
81 Skip::TimeRange(start, end) => {
82 let current_time = time.time();
83 if start <= end {
84 current_time >= *start && current_time <= *end
86 } else {
87 current_time >= *start || current_time <= *end
89 }
90 }
91 Skip::None => false,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
97pub enum Task {
98 Wait(u64, Option<Vec<Skip>>),
100 Interval(u64, Option<Vec<Skip>>),
102 At(Time, Option<Vec<Skip>>),
104 Once(OffsetDateTime, Option<Vec<Skip>>),
106}
107
108impl PartialEq for Task {
109 fn eq(&self, other: &Self) -> bool {
110 match (self, other) {
111 (Task::Wait(a, skip_a), Task::Wait(b, skip_b)) => a == b && skip_a == skip_b,
112 (Task::Interval(a, skip_a), Task::Interval(b, skip_b)) => a == b && skip_a == skip_b,
113 (Task::At(a, skip_a), Task::At(b, skip_b)) => a == b && skip_a == skip_b,
114 (Task::Once(a, skip_a), Task::Once(b, skip_b)) => a == b && skip_a == skip_b,
115 _ => false,
116 }
117 }
118}
119
120impl Task {
121 pub fn get_next_run_time<T: Notifiable + 'static>(
123 &self,
124 timezone_minutes: i16,
125 ) -> Option<OffsetDateTime> {
126 let now = get_now(timezone_minutes).unwrap_or_else(|_| OffsetDateTime::now_utc());
127
128 match self.clone() {
129 Task::Wait(wait, skip) => {
130 let mut next_time = now + time::Duration::seconds(wait as i64);
131
132 if let Some(skip_rules) = skip {
133 let mut attempts = 0;
134 const MAX_ATTEMPTS: u32 = 1000;
135
136 while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
137 {
138 next_time += time::Duration::seconds(wait as i64);
139 attempts += 1;
140 }
141
142 if attempts >= MAX_ATTEMPTS {
143 return None;
144 }
145 }
146
147 Some(next_time)
148 }
149 Task::Interval(interval, skip) => {
150 let mut next_time = now + time::Duration::seconds(interval as i64);
151
152 if let Some(skip_rules) = skip {
153 let mut attempts = 0;
154 const MAX_ATTEMPTS: u32 = 1000;
155
156 while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
157 {
158 next_time += time::Duration::seconds(interval as i64);
159 attempts += 1;
160 }
161
162 if attempts >= MAX_ATTEMPTS {
163 return None;
164 }
165 }
166
167 Some(next_time)
168 }
169 Task::At(time, skip) => {
170 let mut next_time = get_next_time(now, time);
171
172 if let Some(skip_rules) = skip {
173 let mut attempts = 0;
174 const MAX_ATTEMPTS: u32 = 365;
175
176 while skip_rules.iter().any(|s| s.is_skip(next_time)) && attempts < MAX_ATTEMPTS
177 {
178 next_time += time::Duration::days(1);
179 attempts += 1;
180 }
181
182 if attempts >= MAX_ATTEMPTS {
183 return None;
184 }
185 }
186
187 Some(next_time)
188 }
189 Task::Once(once_time, skip) => {
190 if once_time <= now {
191 return None;
192 }
193
194 if let Some(skip_rules) = skip {
195 if skip_rules.iter().any(|s| s.is_skip(once_time)) {
196 return None;
197 }
198 }
199
200 Some(once_time)
201 }
202 }
203 }
204}
205
206impl Task {
207 pub fn parse(s: &str) -> Result<Self, String> {
222 let s = s.trim();
223
224 let open_paren = s.find('(').ok_or_else(|| {
226 format!("Invalid task format: '{s}'. Expected format like 'wait(10)'")
227 })?;
228
229 let close_paren = s
230 .rfind(')')
231 .ok_or_else(|| format!("Missing closing parenthesis in: '{s}'"))?;
232
233 if close_paren <= open_paren {
234 return Err(format!("Invalid parentheses in: '{s}'"));
235 }
236
237 let function_name = s[..open_paren].trim();
238 let args = s[open_paren + 1..close_paren].trim();
239
240 let (primary_arg, skip_conditions) = Self::parse_arguments(args)?;
242
243 match function_name {
244 "wait" => {
245 let seconds = primary_arg.parse::<u64>().map_err(|_| {
246 format!("Invalid seconds value '{primary_arg}' in wait({primary_arg})")
247 })?;
248 Ok(Task::Wait(seconds, skip_conditions))
249 }
250 "interval" => {
251 let seconds = primary_arg.parse::<u64>().map_err(|_| {
252 format!("Invalid seconds value '{primary_arg}' in interval({primary_arg})")
253 })?;
254 Ok(Task::Interval(seconds, skip_conditions))
255 }
256 "at" => {
257 let format = format_description!("[hour]:[minute]");
258 let time = Time::parse(&primary_arg, &format).map_err(|_| {
259 format!("Invalid time format '{primary_arg}' in at({primary_arg}). Expected format: HH:MM")
260 })?;
261 Ok(Task::At(time, skip_conditions))
262 }
263 "once" => {
264 let format = format_description!(
265 "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory]"
266 );
267 let datetime = OffsetDateTime::parse(&primary_arg, &format)
268 .map_err(|_| format!("Invalid datetime format '{primary_arg}' in once({primary_arg}). Expected format: YYYY-MM-DD HH:MM:SS +HH"))?;
269 Ok(Task::Once(datetime, skip_conditions))
270 }
271 _ => Err(format!(
272 "Unknown task type '{function_name}'. Supported types: wait, interval, at, once"
273 )),
274 }
275 }
276
277 fn parse_arguments(args: &str) -> Result<(String, Option<Vec<Skip>>), String> {
278 let args = args.trim();
279
280 if let Some(comma_pos) = args.find(',') {
282 let primary_arg = args[..comma_pos].trim().to_string();
283 let skip_part = args[comma_pos + 1..].trim();
284
285 let skip_conditions = Self::parse_skip_conditions(skip_part)?;
286 Ok((primary_arg, Some(skip_conditions)))
287 } else {
288 Ok((args.to_string(), None))
289 }
290 }
291
292 fn parse_skip_conditions(skip_str: &str) -> Result<Vec<Skip>, String> {
293 let skip_str = skip_str.trim();
294
295 if skip_str.starts_with('[') && skip_str.ends_with(']') {
297 let list_content = &skip_str[1..skip_str.len() - 1];
298 Self::parse_skip_list(list_content)
299 } else {
300 let skip = Self::parse_single_skip(skip_str)?;
302 Ok(vec![skip])
303 }
304 }
305
306 fn parse_skip_list(list_str: &str) -> Result<Vec<Skip>, String> {
307 let mut skips = Vec::new();
308 let list_str = list_str.trim();
309
310 if list_str.is_empty() {
311 return Ok(skips);
312 }
313
314 for part in list_str.split(',') {
316 let part = part.trim();
317 if !part.is_empty() {
318 let skip = Self::parse_single_skip(part)?;
319 skips.push(skip);
320 }
321 }
322
323 Ok(skips)
324 }
325
326 fn parse_single_skip(skip_str: &str) -> Result<Skip, String> {
327 let skip_str = skip_str.trim();
328 let parts: Vec<&str> = skip_str.split_whitespace().collect();
329
330 if parts.is_empty() {
331 return Err("Empty skip condition".to_string());
332 }
333
334 match parts[0] {
335 "weekday" => {
336 if parts.len() != 2 {
337 return Err(format!(
338 "Invalid weekday format: '{skip_str}'. Expected 'weekday N'"
339 ));
340 }
341 let day = parts[1]
342 .parse::<u8>()
343 .map_err(|_| format!("Invalid weekday number: '{}'", parts[1]))?;
344 if !(1..=7).contains(&day) {
345 return Err(format!("Weekday must be between 1-7, got: {day}"));
346 }
347 Ok(Skip::Day(vec![day]))
348 }
349 "date" => {
350 if parts.len() != 2 {
351 return Err(format!(
352 "Invalid date format: '{skip_str}'. Expected 'date YYYY-MM-DD'"
353 ));
354 }
355 let date_str = parts[1];
356 let date_parts: Vec<&str> = date_str.split('-').collect();
357 if date_parts.len() != 3 {
358 return Err(format!(
359 "Invalid date format: '{date_str}'. Expected 'YYYY-MM-DD'"
360 ));
361 }
362
363 let year = date_parts[0]
364 .parse::<i32>()
365 .map_err(|_| format!("Invalid year: '{}'", date_parts[0]))?;
366 let month = date_parts[1]
367 .parse::<u8>()
368 .map_err(|_| format!("Invalid month: '{}'", date_parts[1]))?;
369 let day = date_parts[2]
370 .parse::<u8>()
371 .map_err(|_| format!("Invalid day: '{}'", date_parts[2]))?;
372
373 let month_enum =
374 time::Month::try_from(month).map_err(|_| format!("Invalid month: {month}"))?;
375 let date = time::Date::from_calendar_date(year, month_enum, day)
376 .map_err(|_| format!("Invalid date: {year}-{month}-{day}"))?;
377
378 Ok(Skip::Date(date))
379 }
380 "time" => {
381 if parts.len() != 2 {
382 return Err(format!(
383 "Invalid time format: '{skip_str}'. Expected 'time HH:MM..HH:MM'"
384 ));
385 }
386 let time_range = parts[1];
387 if let Some(range_pos) = time_range.find("..") {
388 let start_str = &time_range[..range_pos];
389 let end_str = &time_range[range_pos + 2..];
390
391 let format = format_description!("[hour]:[minute]");
392 let start_time = Time::parse(start_str, &format)
393 .map_err(|_| format!("Invalid start time: '{start_str}'"))?;
394 let end_time = Time::parse(end_str, &format)
395 .map_err(|_| format!("Invalid end time: '{end_str}'"))?;
396
397 Ok(Skip::TimeRange(start_time, end_time))
398 } else {
399 let format = format_description!("[hour]:[minute]");
401 let time = Time::parse(time_range, &format)
402 .map_err(|_| format!("Invalid time: '{time_range}'"))?;
403 Ok(Skip::Time(time))
404 }
405 }
406 _ => Err(format!(
407 "Unknown skip type: '{}'. Supported types: weekday, date, time",
408 parts[0]
409 )),
410 }
411 }
412}
413
414impl From<&str> for Task {
415 fn from(s: &str) -> Self {
423 Task::parse(s).unwrap_or_else(|err| {
424 panic!("Failed to parse task from string '{s}': {err}");
425 })
426 }
427}
428
429impl From<String> for Task {
430 fn from(s: String) -> Self {
431 Self::from(s.as_str())
432 }
433}
434
435impl From<&String> for Task {
436 fn from(s: &String) -> Self {
437 Self::from(s.as_str())
438 }
439}
440
441#[macro_export]
442macro_rules! task {
443 (wait $seconds:tt) => {
445 $crate::Task::Wait($seconds, None)
446 };
447 (interval $seconds:tt) => {
448 $crate::Task::Interval($seconds, None)
449 };
450 (at $hour:tt : $minute:tt) => {
451 $crate::Task::At(
452 time::Time::from_hms($hour, $minute, 0).unwrap(),
453 None
454 )
455 };
456
457 (wait $seconds:tt, weekday $day:tt) => {
459 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
460 };
461 (wait $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
462 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::Date(
463 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
464 )]))
465 };
466 (wait $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
467 $crate::Task::Wait($seconds, Some(vec![$crate::Skip::TimeRange(
468 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
469 time::Time::from_hms($end_h, $end_m, 0).unwrap()
470 )]))
471 };
472
473 (interval $seconds:tt, weekday $day:tt) => {
474 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Day(vec![$day])]))
475 };
476 (interval $seconds:tt, date $year:tt - $month:tt - $day:tt) => {
477 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::Date(
478 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
479 )]))
480 };
481 (interval $seconds:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
482 $crate::Task::Interval($seconds, Some(vec![$crate::Skip::TimeRange(
483 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
484 time::Time::from_hms($end_h, $end_m, 0).unwrap()
485 )]))
486 };
487
488 (at $hour:tt : $minute:tt, weekday $day:tt) => {
489 $crate::Task::At(
490 time::Time::from_hms($hour, $minute, 0).unwrap(),
491 Some(vec![$crate::Skip::Day(vec![$day])])
492 )
493 };
494 (at $hour:tt : $minute:tt, date $year:tt - $month:tt - $day:tt) => {
495 $crate::Task::At(
496 time::Time::from_hms($hour, $minute, 0).unwrap(),
497 Some(vec![$crate::Skip::Date(
498 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
499 )])
500 )
501 };
502 (at $hour:tt : $minute:tt, time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt) => {
503 $crate::Task::At(
504 time::Time::from_hms($hour, $minute, 0).unwrap(),
505 Some(vec![$crate::Skip::TimeRange(
506 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
507 time::Time::from_hms($end_h, $end_m, 0).unwrap()
508 )])
509 )
510 };
511
512 (wait $seconds:tt, [$($skip:tt)*]) => {
514 $crate::Task::Wait($seconds, Some($crate::task!(@build_skips $($skip)*)))
515 };
516 (interval $seconds:tt, [$($skip:tt)*]) => {
517 $crate::Task::Interval($seconds, Some($crate::task!(@build_skips $($skip)*)))
518 };
519 (at $hour:tt : $minute:tt, [$($skip:tt)*]) => {
520 $crate::Task::At(
521 time::Time::from_hms($hour, $minute, 0).unwrap(),
522 Some($crate::task!(@build_skips $($skip)*))
523 )
524 };
525
526 (@build_skips) => { vec![] };
528 (@build_skips weekday $day:tt $(, $($rest:tt)*)?) => {
529 {
530 let mut skips = vec![$crate::Skip::Day(vec![$day])];
531 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
532 skips
533 }
534 };
535 (@build_skips date $year:tt - $month:tt - $day:tt $(, $($rest:tt)*)?) => {
536 {
537 let mut skips = vec![$crate::Skip::Date(
538 time::Date::from_calendar_date($year, time::Month::try_from($month).unwrap(), $day).unwrap()
539 )];
540 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
541 skips
542 }
543 };
544 (@build_skips time $start_h:tt : $start_m:tt .. $end_h:tt : $end_m:tt $(, $($rest:tt)*)?) => {
545 {
546 let mut skips = vec![$crate::Skip::TimeRange(
547 time::Time::from_hms($start_h, $start_m, 0).unwrap(),
548 time::Time::from_hms($end_h, $end_m, 0).unwrap()
549 )];
550 $(skips.extend($crate::task!(@build_skips $($rest)*));)?
551 skips
552 }
553 };
554}
555
556impl std::fmt::Display for Task {
557 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558 match self {
559 Task::Wait(wait, skip) => {
560 let skip = skip
561 .clone()
562 .unwrap_or_default()
563 .into_iter()
564 .map(|s| s.to_string())
565 .collect::<Vec<String>>()
566 .join(", ");
567 write!(f, "wait: {wait} {skip}")
568 }
569 Task::Interval(interval, skip) => {
570 let skip = skip
571 .clone()
572 .unwrap_or_default()
573 .into_iter()
574 .map(|s| s.to_string())
575 .collect::<Vec<String>>()
576 .join(", ");
577 write!(f, "interval: {interval} {skip}")
578 }
579 Task::At(time, skip) => {
580 let skip = skip
581 .clone()
582 .unwrap_or_default()
583 .into_iter()
584 .map(|s| s.to_string())
585 .collect::<Vec<String>>()
586 .join(", ");
587 write!(f, "at: {time} {skip}")
588 }
589 Task::Once(time, skip) => {
590 let skip = skip
591 .clone()
592 .unwrap_or_default()
593 .into_iter()
594 .map(|s| s.to_string())
595 .collect::<Vec<String>>()
596 .join(", ");
597 write!(f, "once: {time} {skip}")
598 }
599 }
600 }
601}
602
603pub fn get_next_time(now: OffsetDateTime, time: Time) -> OffsetDateTime {
604 let mut next = now.replace_time(time);
605 if next < now {
606 next += time::Duration::days(1);
607 }
608 next
609}
610
611pub fn get_now(timezone_minutes: i16) -> Result<OffsetDateTime, time::error::ComponentRange> {
612 let hours = timezone_minutes / 60;
613 let minutes = timezone_minutes % 60;
614 let offset = UtcOffset::from_hms(hours as i8, minutes as i8, 0)?;
615 Ok(OffsetDateTime::now_utc().to_offset(offset))
616}