fix(caldav): expand RRULE for recurring events
This commit is contained in:
+26
-45
@@ -30,22 +30,14 @@ func (b *Backend) icsSubscriptionCalendarMeta(owner string, sub db.ICSSubscripti
|
||||
}
|
||||
}
|
||||
|
||||
// icsObjectUID returns the UID a fetched VEVENT should be addressed by:
|
||||
// its own UID property if it has one, otherwise a placeholder derived from
|
||||
// the object ID (so the event still has a *unique* UID in the returned
|
||||
// VCALENDAR).
|
||||
func icsObjectUID(ev ical.Event, fallback string) string {
|
||||
if p := ev.Props.Get(ical.PropUID); p != nil && p.Value != "" {
|
||||
return p.Value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// icsSubscriptionCalendarObjects fetches sub's remote calendar (via
|
||||
// b.icsCache) and returns one caldav.CalendarObject per VEVENT. The
|
||||
// object path is derived from icssub.EventID(ev) so the same event keeps
|
||||
// the same path across fetches, even if its position in the document
|
||||
// changes.
|
||||
// b.icsCache) and returns one caldav.CalendarObject per Series — the
|
||||
// source feed's own group of VEVENTs sharing a UID (a recurring base plus
|
||||
// its explicit per-occurrence instances). Exposing the whole group as a
|
||||
// single object is the correct, non-lossy shape: a CalDAV client keeps the
|
||||
// entire series (RRULE + overrides) instead of the base and its instances
|
||||
// arriving as competing objects. The object path is derived from
|
||||
// series.ID(), stable across fetches of the same feed.
|
||||
func (b *Backend) listICSSubscriptionCalendarObjects(localName string, sub db.ICSSubscription) ([]caldav.CalendarObject, error) {
|
||||
cal, err := b.icsCache.Get(sub.URL)
|
||||
if err != nil {
|
||||
@@ -53,8 +45,11 @@ func (b *Backend) listICSSubscriptionCalendarObjects(localName string, sub db.IC
|
||||
}
|
||||
|
||||
var objs []caldav.CalendarObject
|
||||
for _, ev := range cal.Events() {
|
||||
obj, err := b.encodeICSObject(localName, ev)
|
||||
for _, series := range icssub.GroupSeries(cal) {
|
||||
if series.Base == nil && len(series.Instances) == 0 {
|
||||
continue
|
||||
}
|
||||
obj, err := b.encodeICSSeries(localName, series)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -64,51 +59,37 @@ func (b *Backend) listICSSubscriptionCalendarObjects(localName string, sub db.IC
|
||||
}
|
||||
|
||||
// icsSubscriptionCalendarObject fetches sub's remote calendar and returns
|
||||
// the single VEVENT whose derived object ID matches objID.
|
||||
// the Series whose stable ID matches objID.
|
||||
func (b *Backend) icsSubscriptionCalendarObject(localName, objID string, sub db.ICSSubscription) (*caldav.CalendarObject, error) {
|
||||
cal, err := b.icsCache.Get(sub.URL)
|
||||
if err != nil {
|
||||
return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("fetching ics subscription: %w", err))
|
||||
}
|
||||
for _, ev := range cal.Events() {
|
||||
if icssub.EventID(ev) != objID {
|
||||
for _, series := range icssub.GroupSeries(cal) {
|
||||
if series.Base == nil && len(series.Instances) == 0 {
|
||||
continue
|
||||
}
|
||||
return b.encodeICSObject(localName, ev)
|
||||
if series.ID() != objID {
|
||||
continue
|
||||
}
|
||||
return b.encodeICSSeries(localName, series)
|
||||
}
|
||||
return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("ics subscription event not found"))
|
||||
}
|
||||
|
||||
// encodeICSObject wraps a single fetched VEVENT ev into its own
|
||||
// caldav.CalendarObject, encoding it as a standalone one-event calendar
|
||||
// the same way every other calendar object in this backend is represented.
|
||||
func (b *Backend) encodeICSObject(localName string, ev ical.Event) (*caldav.CalendarObject, error) {
|
||||
objID := icssub.EventID(ev)
|
||||
if objID == "" {
|
||||
// Not addressable (no DTSTART) — skip.
|
||||
return nil, fmt.Errorf("event has no DTSTART; not addressable")
|
||||
}
|
||||
uid := icsObjectUID(ev, objID)
|
||||
|
||||
event := ical.NewEvent()
|
||||
event.Props = ev.Props
|
||||
if event.Props.Get(ical.PropUID) == nil {
|
||||
event.Props.SetText(ical.PropUID, uid)
|
||||
}
|
||||
if event.Props.Get(ical.PropDateTimeStamp) == nil {
|
||||
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
|
||||
}
|
||||
|
||||
out := ical.NewCalendar()
|
||||
out.Props.SetText(ical.PropVersion, "2.0")
|
||||
out.Props.SetText(ical.PropProductID, "-//nidus//ics-subscription//EN")
|
||||
out.Children = append(out.Children, event.Component)
|
||||
// encodeICSSeries wraps a Series (base VEVENT + explicit instances, all the
|
||||
// feed's events that share a UID) into its own caldav.CalendarObject,
|
||||
// encoded as a multi-VEVENT VCALENDAR the same way the source subscription
|
||||
// is published.
|
||||
func (b *Backend) encodeICSSeries(localName string, series *icssub.Series) (*caldav.CalendarObject, error) {
|
||||
out := series.Calendar()
|
||||
|
||||
var buf strings.Builder
|
||||
if err := ical.NewEncoder(&buf).Encode(out); err != nil {
|
||||
return nil, fmt.Errorf("encoding ics subscription event: %w", err)
|
||||
}
|
||||
data := []byte(buf.String())
|
||||
objID := series.ID()
|
||||
|
||||
return &caldav.CalendarObject{
|
||||
Path: calObjectPath(localName, objID),
|
||||
|
||||
Reference in New Issue
Block a user