mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
fix(calendar): cap RRULE expansion (#2902)
This commit is contained in:
@@ -460,6 +460,9 @@ def _event_to_dict(ev: CalendarEvent) -> dict:
|
||||
|
||||
# ── Recurrence expansion ──
|
||||
|
||||
_RRULE_EXPANSION_LIMIT = 1000
|
||||
|
||||
|
||||
def _expand_rrule(
|
||||
ev: CalendarEvent, start: datetime, end: datetime
|
||||
) -> List[dict]:
|
||||
@@ -482,6 +485,7 @@ def _expand_rrule(
|
||||
d = _event_to_dict(ev)
|
||||
d["is_recurrence"] = False
|
||||
d["series_uid"] = ev.uid
|
||||
d["truncated"] = False
|
||||
return [d]
|
||||
|
||||
# Parse the rrule, applying it to the base dtstart.
|
||||
@@ -507,6 +511,7 @@ def _expand_rrule(
|
||||
d = _event_to_dict(ev)
|
||||
d["is_recurrence"] = False
|
||||
d["series_uid"] = ev.uid
|
||||
d["truncated"] = False
|
||||
# Malformed RRULE rows are fetched by the recurring SQL branch
|
||||
# with only dtstart < end_dt — the base event may not actually
|
||||
# overlap the window. Only return if it does.
|
||||
@@ -519,22 +524,26 @@ def _expand_rrule(
|
||||
# (matching non-recurring overlap semantics: dtstart < end AND
|
||||
# dtend > start).
|
||||
expand_start = start - duration
|
||||
occurrences = rule.between(expand_start, end, inc=True)
|
||||
if not occurrences:
|
||||
return []
|
||||
|
||||
results = []
|
||||
truncated = False
|
||||
base = _event_to_dict(ev)
|
||||
|
||||
for occ_start in occurrences:
|
||||
for occ_start in rule.xafter(expand_start, inc=True):
|
||||
if occ_start >= end:
|
||||
break
|
||||
|
||||
occ_end = occ_start + duration
|
||||
|
||||
# Overlap filter: occurrence must intersect [start, end).
|
||||
# This enforces exclusive-end semantics (occ_start >= end is
|
||||
# excluded) and includes multi-day crossings (occ_end > start).
|
||||
if occ_start >= end or occ_end <= start:
|
||||
if occ_end <= start:
|
||||
continue
|
||||
|
||||
if len(results) >= _RRULE_EXPANSION_LIMIT:
|
||||
truncated = True
|
||||
break
|
||||
|
||||
# Build the compound uid: {base_uid}::{date} or ::{datetime}
|
||||
if ev.all_day:
|
||||
occ_uid = f"{ev.uid}::{occ_start.strftime('%Y-%m-%d')}"
|
||||
@@ -545,6 +554,7 @@ def _expand_rrule(
|
||||
d["uid"] = occ_uid
|
||||
d["series_uid"] = ev.uid
|
||||
d["is_recurrence"] = True
|
||||
d["truncated"] = False
|
||||
|
||||
if ev.all_day:
|
||||
d["dtstart"] = occ_start.strftime("%Y-%m-%d")
|
||||
@@ -557,6 +567,10 @@ def _expand_rrule(
|
||||
|
||||
results.append(d)
|
||||
|
||||
if truncated:
|
||||
for d in results:
|
||||
d["truncated"] = True
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@@ -786,8 +800,12 @@ def setup_calendar_routes() -> APIRouter:
|
||||
expanded.extend(_expand_rrule(e, start_dt, end_dt))
|
||||
|
||||
# Sort by occurrence start time for consistent frontend ordering.
|
||||
truncated = any(e.get("truncated") for e in expanded)
|
||||
expanded.sort(key=lambda d: d["dtstart"])
|
||||
return {"events": expanded}
|
||||
response: dict = {"events": expanded}
|
||||
if truncated:
|
||||
response["truncated"] = True
|
||||
return response
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user