package db import ( "database/sql" "errors" "fmt" "strings" ) // ICSSubscription is a user-added read-only calendar backed by a remote // ICS/webcal URL, exposed alongside real calendars under // "/cal/home//" (see internal/caldav/ics.go). type ICSSubscription struct { Name string URL string Color string } // CreateICSSubscription registers a new ICS subscription owned by owner. // Returns ErrReservedName if name is reserved for a computed calendar (see // reservedCalendarNames), or ErrResourceExists if name is already used by // one of owner's calendars or ICS subscriptions. func (d *DB) CreateICSSubscription(owner, name, url, color string) error { if reservedCalendarNames[strings.ToLower(name)] { return ErrReservedName } // Shares the "/cal/home//" namespace with real calendars, so // reject a name already taken by one of those. var calExists int if err := d.conn.QueryRow(`SELECT COUNT(*) FROM calendars WHERE owner = ? AND name = ?`, owner, name).Scan(&calExists); err != nil { return fmt.Errorf("checking calendar name: %w", err) } if calExists > 0 { return ErrResourceExists } _, err := d.conn.Exec(`INSERT INTO ics_subscriptions (owner, name, url, color) VALUES (?, ?, ?, ?)`, owner, name, url, color) if err != nil { if isUniqueConstraintErr(err) { return ErrResourceExists } return fmt.Errorf("creating ics subscription: %w", err) } return nil } // DeleteICSSubscription removes an ICS subscription. Returns // ErrResourceNotFound if it doesn't exist. func (d *DB) DeleteICSSubscription(owner, name string) error { res, err := d.conn.Exec(`DELETE FROM ics_subscriptions WHERE owner = ? AND name = ?`, owner, name) if err != nil { return fmt.Errorf("deleting ics subscription: %w", err) } return requireRowsAffected(res, ErrResourceNotFound) } // SetICSSubscriptionColor updates an ICS subscription's display color. // Returns ErrResourceNotFound if it doesn't exist. func (d *DB) SetICSSubscriptionColor(owner, name, color string) error { res, err := d.conn.Exec(`UPDATE ics_subscriptions SET color = ? WHERE owner = ? AND name = ?`, color, owner, name) if err != nil { return fmt.Errorf("setting ics subscription color: %w", err) } return requireRowsAffected(res, ErrResourceNotFound) } // GetICSSubscription returns owner's ICS subscription registered under // name. Returns ErrResourceNotFound if it doesn't exist. func (d *DB) GetICSSubscription(owner, name string) (ICSSubscription, error) { sub := ICSSubscription{Name: name} err := d.conn.QueryRow(`SELECT url, color FROM ics_subscriptions WHERE owner = ? AND name = ?`, owner, name).Scan(&sub.URL, &sub.Color) if errors.Is(err, sql.ErrNoRows) { return ICSSubscription{}, ErrResourceNotFound } if err != nil { return ICSSubscription{}, fmt.Errorf("getting ics subscription: %w", err) } return sub, nil } // ListICSSubscriptions returns all of owner's ICS subscriptions, sorted by // name. func (d *DB) ListICSSubscriptions(owner string) ([]ICSSubscription, error) { rows, err := d.conn.Query(`SELECT name, url, color FROM ics_subscriptions WHERE owner = ? ORDER BY name`, owner) if err != nil { return nil, fmt.Errorf("listing ics subscriptions: %w", err) } defer rows.Close() var subs []ICSSubscription for rows.Next() { var s ICSSubscription if err := rows.Scan(&s.Name, &s.URL, &s.Color); err != nil { return nil, fmt.Errorf("scanning ics subscription: %w", err) } subs = append(subs, s) } return subs, rows.Err() }