Make birthdays calendar color configurable and expose it via CalDAV
- Add a per-user birthday_calendars table (color, cascade-deletes with
the user) and GetBirthdayCalendarColor/SetBirthdayCalendarColor in
internal/db, plus a reservedCalendarNames guard ("birthdays") in
CreateCalendarWithColor so no real calendar can collide with the
synthetic one, whether created via the web UI, nidusctl, or CalDAV
MKCALENDAR.
- Add a "Birthdays" virtual resource card to the dashboard (color
picker only, no delete/share controls) backed by a new
ResourceCard.Virtual flag and POST /web/resources/birthdays/color
handler.
- Extract the birthday-parsing/generation logic shared by the web
calendar view and CalDAV into internal/birthdays (ParseBirthday,
Collect, OccurrenceDate, Summary) instead of duplicating it.
- Expose the Birthdays calendar over real CalDAV in
internal/caldav/backend.go + birthdays.go: it's always listed for
every user, generates one VEVENT per (contact, year) for a rolling
window (current year -2..+8) with "🎂 Name (Age)" titles, is
read-only (Put/Delete/DeleteCalendar all return 403), and its
Apple/DAVx5 calendar-color is injected from the same per-user
setting used by the dashboard/web view.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+45
-1
@@ -24,6 +24,19 @@ var ErrResourceExists = errors.New("resource already exists")
|
||||
// that doesn't exist.
|
||||
var ErrResourceNotFound = errors.New("resource not found")
|
||||
|
||||
// ErrReservedName is returned when trying to create a calendar whose name
|
||||
// is reserved for a computed/virtual calendar (see reservedCalendarNames).
|
||||
var ErrReservedName = errors.New("calendar name is reserved")
|
||||
|
||||
// reservedCalendarNames are calendar names that can't be used for a real,
|
||||
// user-created calendar because they're reserved for a synthetic,
|
||||
// computed calendar shown alongside real ones (e.g. "birthdays", see
|
||||
// internal/web/calendar.go and internal/caldav/backend.go). Matched
|
||||
// case-insensitively.
|
||||
var reservedCalendarNames = map[string]bool{
|
||||
"birthdays": true,
|
||||
}
|
||||
|
||||
// User is an account stored in the database.
|
||||
type User struct {
|
||||
Username string
|
||||
@@ -175,8 +188,11 @@ func (d *DB) CreateCalendar(owner, name string) error {
|
||||
// CreateCalendarWithColor registers a new calendar owned by owner with the
|
||||
// given color (an empty string means no color is set, in which case the
|
||||
// client picks its own default). Returns ErrResourceExists if it already
|
||||
// exists.
|
||||
// exists, or ErrReservedName if name is reserved for a computed calendar.
|
||||
func (d *DB) CreateCalendarWithColor(owner, name, color string) error {
|
||||
if reservedCalendarNames[strings.ToLower(name)] {
|
||||
return ErrReservedName
|
||||
}
|
||||
_, err := d.conn.Exec(`INSERT INTO calendars (owner, name, color) VALUES (?, ?, ?)`, owner, name, color)
|
||||
if err != nil {
|
||||
if isUniqueConstraintErr(err) {
|
||||
@@ -254,6 +270,34 @@ func (d *DB) ListCalendars(owner string) ([]Calendar, error) {
|
||||
return cals, rows.Err()
|
||||
}
|
||||
|
||||
// GetBirthdayCalendarColor returns owner's display color for the
|
||||
// synthetic "Birthdays" calendar, or "" if they haven't set one (callers
|
||||
// should fall back to a built-in default in that case).
|
||||
func (d *DB) GetBirthdayCalendarColor(owner string) (string, error) {
|
||||
var color string
|
||||
err := d.conn.QueryRow(`SELECT color FROM birthday_calendars WHERE owner = ?`, owner).Scan(&color)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("getting birthday calendar color: %w", err)
|
||||
}
|
||||
return color, nil
|
||||
}
|
||||
|
||||
// SetBirthdayCalendarColor sets (or updates) owner's display color for
|
||||
// the synthetic "Birthdays" calendar.
|
||||
func (d *DB) SetBirthdayCalendarColor(owner, color string) error {
|
||||
_, err := d.conn.Exec(`
|
||||
INSERT INTO birthday_calendars (owner, color) VALUES (?, ?)
|
||||
ON CONFLICT (owner) DO UPDATE SET color = excluded.color`,
|
||||
owner, color)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting birthday calendar color: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------- address books --------
|
||||
|
||||
// CreateAddressBook registers a new address book owned by owner. Returns
|
||||
|
||||
Reference in New Issue
Block a user