From b3aead23da6cf8ebeedbd9fa3b97c7ac1a3f54ec Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Tue, 14 Jul 2026 03:46:37 -0400 Subject: [PATCH] refac --- backend/open_webui/utils/automations.py | 31 ++++++++++++++++++------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/backend/open_webui/utils/automations.py b/backend/open_webui/utils/automations.py index 3fb8f0bcad..af96e2ac6e 100644 --- a/backend/open_webui/utils/automations.py +++ b/backend/open_webui/utils/automations.py @@ -66,7 +66,7 @@ def _resolve_tz(tz: str = None) -> Optional[ZoneInfo]: return None -def _parse_rule(s: str): +def _parse_rule(s: str, now: Optional[datetime] = None): """Parse RRULE with clock-aligned DTSTART for sub-daily frequencies. MINUTELY/HOURLY rules use a fixed epoch DTSTART (2000-01-01 00:00) @@ -78,6 +78,20 @@ def _parse_rule(s: str): if freq in ('MINUTELY', 'HOURLY'): epoch = datetime(2000, 1, 1, 0, 0, 0) + if ( + now is not None + and s.startswith('RRULE:') + and '\n' not in s + and '\r' not in s + and set(parts) <= {'FREQ', 'INTERVAL', 'BYMINUTE', 'BYSECOND'} + ): + try: + interval = int(parts.get('INTERVAL', '1')) + if interval > 0: + step = timedelta(minutes=interval) if freq == 'MINUTELY' else timedelta(hours=interval) + return rrulestr(s, dtstart=epoch + ((now - epoch) // step) * step, ignoretz=True) + except (TypeError, ValueError): + pass return rrulestr(s, dtstart=epoch, ignoretz=True) return rrulestr(s, ignoretz=True) @@ -89,12 +103,12 @@ def validate_rrule(s: str, tz: str = None) -> None: clock so that near-future schedules are not incorrectly rejected on servers whose system clock is ahead (e.g. UTC vs US timezones). """ - try: - rule = _parse_rule(s) - except Exception as e: - raise ValueError(ERROR_MESSAGES.AUTOMATION_INVALID_RRULE(e)) zi = _resolve_tz(tz) now = datetime.now(zi).replace(tzinfo=None) if zi else datetime.now() + try: + rule = _parse_rule(s, now) + except Exception as e: + raise ValueError(ERROR_MESSAGES.AUTOMATION_INVALID_RRULE(e)) if rule.after(now) is None: raise ValueError(ERROR_MESSAGES.AUTOMATION_NO_FUTURE_RUNS) @@ -103,7 +117,8 @@ def next_run_ns(s: str, tz: str = None) -> Optional[int]: """Next occurrence as epoch nanoseconds, respecting user timezone.""" zi = _resolve_tz(tz) now = datetime.now(zi) if zi else datetime.now() - dt = _parse_rule(s).after(now.replace(tzinfo=None)) + now_naive = now.replace(tzinfo=None) + dt = _parse_rule(s, now_naive).after(now_naive) if dt is None: return None if zi: @@ -118,9 +133,9 @@ def next_n_runs_ns(s: str, n: int = 5, tz: str = None) -> list[int]: preview matches the user's local clock (same as next_run_ns). """ zi = _resolve_tz(tz) - rule = _parse_rule(s) result = [] now = datetime.now(zi).replace(tzinfo=None) if zi else datetime.now() + rule = _parse_rule(s, now) dt = now for _ in range(n): dt = rule.after(dt) @@ -142,8 +157,8 @@ def rrule_interval_seconds(s: str) -> Optional[int]: """ if 'COUNT=1' in s: return None - rule = _parse_rule(s) now = datetime.now() + rule = _parse_rule(s, now) first = rule.after(now) if first is None: return None