package storage import ( "context" "fmt" "net/http" "time" "luxick/chronological/internal/models" "github.com/emersion/go-ical" "github.com/emersion/go-webdav/caldav" ) // CalDAVStore handles fetching events from a CalDAV server. type CalDAVStore struct { url string username string password string client *caldav.Client } // NewCalDAVStore creates a new CalDAV store with the given credentials. func NewCalDAVStore(url, username, password string) *CalDAVStore { return &CalDAVStore{ url: url, username: username, password: password, } } // Connect establishes a connection to the CalDAV server. func (s *CalDAVStore) Connect(ctx context.Context) error { httpClient := &http.Client{ Transport: &basicAuthTransport{ username: s.username, password: s.password, }, } client, err := caldav.NewClient(httpClient, s.url) if err != nil { return fmt.Errorf("failed to create caldav client: %w", err) } s.client = client return nil } // LoadEvents fetches events from the CalDAV server for a date range. func (s *CalDAVStore) LoadEvents(ctx context.Context, start, end time.Time) ([]*models.Event, error) { if s.client == nil { return nil, fmt.Errorf("caldav client not connected") } principal, err := s.client.FindCurrentUserPrincipal(ctx) if err != nil { return nil, fmt.Errorf("failed to find user principal: %w", err) } homeSet, err := s.client.FindCalendarHomeSet(ctx, principal) if err != nil { return nil, fmt.Errorf("failed to find calendar home set: %w", err) } calendars, err := s.client.FindCalendars(ctx, homeSet) if err != nil { return nil, fmt.Errorf("failed to find calendars: %w", err) } var allEvents []*models.Event for _, cal := range calendars { query := &caldav.CalendarQuery{ CompRequest: caldav.CalendarCompRequest{ Name: "VCALENDAR", Comps: []caldav.CalendarCompRequest{{ Name: "VEVENT", Props: []string{ "SUMMARY", "DESCRIPTION", "DTSTART", "DTEND", "LOCATION", "UID", }, }}, }, CompFilter: caldav.CompFilter{ Name: "VCALENDAR", Comps: []caldav.CompFilter{{ Name: "VEVENT", Start: start, End: end, }}, }, } objects, err := s.client.QueryCalendar(ctx, cal.Path, query) if err != nil { continue // Skip calendars that fail } events, err := parseCalDAVObjects(objects) if err != nil { continue } allEvents = append(allEvents, events...) } return allEvents, nil } // LoadEventsForMonth fetches events for a specific month. func (s *CalDAVStore) LoadEventsForMonth(ctx context.Context, year int, month time.Month) ([]*models.Event, error) { start := time.Date(year, month, 1, 0, 0, 0, 0, time.Local) end := start.AddDate(0, 1, 0) return s.LoadEvents(ctx, start, end) } func parseCalDAVObjects(objects []caldav.CalendarObject) ([]*models.Event, error) { var events []*models.Event for _, obj := range objects { cal := obj.Data if cal == nil { continue } for _, child := range cal.Children { if child.Name != ical.CompEvent { continue } event := ical.Event{Component: child} uid, _ := event.Props.Text(ical.PropUID) summary, _ := event.Props.Text(ical.PropSummary) description, _ := event.Props.Text(ical.PropDescription) location, _ := event.Props.Text(ical.PropLocation) dtstart, err := event.Props.DateTime(ical.PropDateTimeStart, time.Local) if err != nil { continue } dtend, err := event.Props.DateTime(ical.PropDateTimeEnd, time.Local) if err != nil { dtend = dtstart.Add(time.Hour) } // Check if all-day event prop := event.Props.Get(ical.PropDateTimeStart) allDay := false if prop != nil { valueParam := prop.Params.Get(ical.ParamValue) allDay = valueParam == "DATE" } events = append(events, &models.Event{ ID: uid, Title: summary, Description: description, Start: dtstart, End: dtend, AllDay: allDay, Location: location, Source: models.SourceCalDAV, }) } } return events, nil } // basicAuthTransport adds basic auth to HTTP requests. type basicAuthTransport struct { username string password string } func (t *basicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { req.SetBasicAuth(t.username, t.password) return http.DefaultTransport.RoundTrip(req) }