Files

102 lines
3.5 KiB
Go

package caldav
import (
"fmt"
"net/http"
"strings"
"time"
ical "github.com/emersion/go-ical"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"git.arnef.de/arnef/nidus/internal/db"
"git.arnef.de/arnef/nidus/internal/icssub"
)
// defaultICSColor is the display color for an ICS/webcal subscription
// calendar when the user hasn't chosen one, matching internal/web's.
const defaultICSColor = "#0ea5e9"
// icsSubscriptionCalendarMeta returns the caldav.Calendar metadata for
// owner's ICS subscription sub, exposed under its own chosen name.
func (b *Backend) icsSubscriptionCalendarMeta(owner string, sub db.ICSSubscription) caldav.Calendar {
return caldav.Calendar{
Path: calHomePath() + sub.Name + "/",
Name: sub.Name,
Description: "Read-only subscription: " + sub.URL,
SupportedComponentSet: []string{"VEVENT"},
MaxResourceSize: 256 * 1024,
}
}
// icsSubscriptionCalendarObjects fetches sub's remote calendar (via
// 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 {
return nil, fmt.Errorf("fetching ics subscription %q: %w", sub.Name, err)
}
var objs []caldav.CalendarObject
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
}
objs = append(objs, *obj)
}
return objs, nil
}
// icsSubscriptionCalendarObject fetches sub's remote calendar and returns
// 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 _, series := range icssub.GroupSeries(cal) {
if series.Base == nil && len(series.Instances) == 0 {
continue
}
if series.ID() != objID {
continue
}
return b.encodeICSSeries(localName, series)
}
return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("ics subscription event not found"))
}
// 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),
ModTime: time.Now(),
ContentLength: int64(len(data)),
ETag: fmt.Sprintf(`"ics-%s"`, objID),
Data: out,
}, nil
}