Files
chronological/internal/storage/caldav.go
2025-12-10 11:41:18 +01:00

187 lines
4.3 KiB
Go

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)
}