Add ICS/webcal HTTP subscription calendars
Users can now subscribe to a remote ICS/webcal feed from the dashboard (name + color), the same way they set up a real calendar or the virtual Birthdays calendar. Subscriptions are read-only, per-user, and share the "/cal/home/<name>/" namespace with real calendars and "birthdays" (name collisions are rejected in both directions). - internal/db: new ics_subscriptions table + CRUD (internal/db/ics.go); CreateCalendarWithColor checks for a colliding subscription name. - internal/icssub: shared HTTP-fetch + TTL cache (15 min) for remote ICS calendars, with webcal:// -> https:// rewriting and stale-on-error fallback, used by both the web UI and the CalDAV backend. - internal/web: dashboard "Subscribe to an ICS/webcal calendar" form, color picker, delete button (internal/web/ics.go, templates/dashboard.templ); month view renders subscription events in their chosen color, read-only (internal/web/calendar.go). - internal/caldav: subscriptions are exposed as read-only calendars (internal/caldav/ics.go) - listed in PROPFIND, events served via GET/REPORT, PUT/DELETE on individual events rejected with 403, but DELETE on the calendar itself unsubscribes; calendar-color is injected the same way as for real calendars and Birthdays. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -193,6 +193,16 @@ func (d *DB) CreateCalendarWithColor(owner, name, color string) error {
|
||||
if reservedCalendarNames[strings.ToLower(name)] {
|
||||
return ErrReservedName
|
||||
}
|
||||
// The "/cal/home/<name>/" namespace is shared with ICS subscriptions
|
||||
// (see internal/db/ics.go), so a real calendar can't be created under
|
||||
// a name already taken by one of those either.
|
||||
var subExists int
|
||||
if err := d.conn.QueryRow(`SELECT COUNT(*) FROM ics_subscriptions WHERE owner = ? AND name = ?`, owner, name).Scan(&subExists); err != nil {
|
||||
return fmt.Errorf("checking ics subscription name: %w", err)
|
||||
}
|
||||
if subExists > 0 {
|
||||
return ErrResourceExists
|
||||
}
|
||||
_, err := d.conn.Exec(`INSERT INTO calendars (owner, name, color) VALUES (?, ?, ?)`, owner, name, color)
|
||||
if err != nil {
|
||||
if isUniqueConstraintErr(err) {
|
||||
|
||||
Reference in New Issue
Block a user