125 lines
4.3 KiB
Go
125 lines
4.3 KiB
Go
package caldav
|
|
|
|
import (
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"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"
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// icsObjectUID returns the UID a fetched VEVENT should be addressed by:
|
|
// its own UID property if it has one, otherwise a stable hash of its
|
|
// position so it still round-trips consistently between requests.
|
|
func icsObjectUID(ev ical.Event, fallback string) string {
|
|
if p := ev.Props.Get(ical.PropUID); p != nil && p.Value != "" {
|
|
return p.Value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// icsObjID builds the object ID (file-name-like, ".ics" suffixed) used to
|
|
// address a fetched VEVENT within its subscription calendar, derived from
|
|
// its UID so it stays stable across fetches of the same feed.
|
|
func icsObjID(uid string) string {
|
|
sum := sha1.Sum([]byte(uid))
|
|
return hex.EncodeToString(sum[:]) + ".ics"
|
|
}
|
|
|
|
// listICSSubscriptionCalendarObjects fetches sub's remote calendar (via
|
|
// b.icsCache) and returns one caldav.CalendarObject per VEVENT.
|
|
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 i, ev := range cal.Events() {
|
|
obj, err := b.encodeICSObject(localName, ev, fmt.Sprintf("event-%d", i))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
objs = append(objs, *obj)
|
|
}
|
|
return objs, nil
|
|
}
|
|
|
|
// icsSubscriptionCalendarObject fetches sub's remote calendar and returns
|
|
// the single VEVENT whose derived object 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 i, ev := range cal.Events() {
|
|
uid := icsObjectUID(ev, fmt.Sprintf("event-%d", i))
|
|
if icsObjID(uid) != objID {
|
|
continue
|
|
}
|
|
return b.encodeICSObject(localName, ev, fmt.Sprintf("event-%d", i))
|
|
}
|
|
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. fallbackUID is used to derive the object ID/UID if ev has
|
|
// no UID property of its own.
|
|
func (b *Backend) encodeICSObject(localName string, ev ical.Event, fallbackUID string) (*caldav.CalendarObject, error) {
|
|
uid := icsObjectUID(ev, fallbackUID)
|
|
objID := icsObjID(uid)
|
|
|
|
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)
|
|
|
|
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())
|
|
|
|
return &caldav.CalendarObject{
|
|
Path: calObjectPath(localName, objID),
|
|
ModTime: time.Now(),
|
|
ContentLength: int64(len(data)),
|
|
ETag: fmt.Sprintf(`"ics-%s"`, objID),
|
|
Data: out,
|
|
}, nil
|
|
}
|