Skip to content

Commit 02bda05

Browse files
committed
fix: Resolve Apple CalDAV event creation errors and add caching
- Fix CalDAV PUT request paths using resolved principal/calendar IDs - Add required If-None-Match header for new event creation - Implement thread-safe calendar resolution caching with sync.Once - Combine CreateEvent/UpdateEvent methods to reduce duplication - Resolve Google->Apple sync 400 Bad Request errors
1 parent 138f0c5 commit 02bda05

2 files changed

Lines changed: 54 additions & 12 deletions

File tree

internal/adapter/apple/adapter.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ func (c *CalendarAPI) Initialize(ctx context.Context, openBrowser bool, config m
112112
} else {
113113
log.Infof("Discovered calendars: %v", calendars)
114114

115-
principalID, resolvedID, err := c.appleClient.ResolveCalendarID(ctx, c.calendarID)
115+
principalID, resolvedID, err := c.appleClient.(*ACalClient).getResolvedCalendarInfo(ctx)
116116
if err != nil {
117117
return fmt.Errorf("failed to resolve calendar '%s': %w", c.calendarID, err)
118118
}

internal/adapter/apple/client.go

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"io"
1111
"net/http"
1212
"strings"
13+
"sync"
1314
"time"
1415

1516
"github.com/charmbracelet/log"
@@ -24,6 +25,11 @@ type ACalClient struct {
2425
AppPassword string
2526
CalendarID string
2627
httpClient *http.Client
28+
29+
resolveOnce sync.Once
30+
cachedPrincipalID string
31+
cachedCalendarID string
32+
resolutionError error
2733
}
2834

2935
type CalDAVPropfind struct {
@@ -418,30 +424,51 @@ func (c *ACalClient) convertVEventToEvent(vevent *ical.Component, href string) *
418424
return event
419425
}
420426

421-
func (c *ACalClient) CreateEvent(ctx context.Context, event models.Event) error {
427+
func (c *ACalClient) createOrUpdateEvent(ctx context.Context, event models.Event, isUpdate bool) error {
428+
principalID, resolvedCalendarID, err := c.getResolvedCalendarInfo(ctx)
429+
if err != nil {
430+
return fmt.Errorf("failed to resolve calendar '%s': %w", c.CalendarID, err)
431+
}
432+
422433
icalData := c.eventToICalendar(event)
423434

424-
// Generate unique filename
425-
filename := fmt.Sprintf("%s.ics", event.Metadata.SyncID)
426-
eventPath := fmt.Sprintf("/%s/calendars/%s/%s/%s", c.Username, c.Username, c.CalendarID, filename)
435+
uid := strings.ReplaceAll(event.ICalUID, "@", "-")
436+
filename := fmt.Sprintf("%s.ics", uid)
437+
eventPath := fmt.Sprintf("/%s/calendars/%s/%s", principalID, resolvedCalendarID, filename)
427438

428-
_, err := c.makeRequest(ctx, "PUT", eventPath, []byte(icalData), map[string]string{
439+
// Set headers based on operation type
440+
headers := map[string]string{
429441
"Content-Type": "text/calendar; charset=utf-8",
430-
})
442+
}
443+
444+
// Only add If-None-Match for new resource creation
445+
if !isUpdate {
446+
headers["If-None-Match"] = "*"
447+
}
431448

449+
_, err = c.makeRequest(ctx, "PUT", eventPath, []byte(icalData), headers)
432450
return err
433451
}
434452

453+
func (c *ACalClient) CreateEvent(ctx context.Context, event models.Event) error {
454+
return c.createOrUpdateEvent(ctx, event, false)
455+
}
456+
435457
func (c *ACalClient) UpdateEvent(ctx context.Context, event models.Event) error {
436-
// For updates, we use the same PUT method with the existing resource path
437-
return c.CreateEvent(ctx, event)
458+
return c.createOrUpdateEvent(ctx, event, true)
438459
}
439460

440461
func (c *ACalClient) DeleteEvent(ctx context.Context, event models.Event) error {
441-
filename := fmt.Sprintf("%s.ics", event.Metadata.SyncID)
442-
eventPath := fmt.Sprintf("/%s/calendars/%s/%s/%s", c.Username, c.Username, c.CalendarID, filename)
462+
principalID, resolvedCalendarID, err := c.getResolvedCalendarInfo(ctx)
463+
if err != nil {
464+
return fmt.Errorf("failed to resolve calendar '%s': %w", c.CalendarID, err)
465+
}
443466

444-
_, err := c.makeRequest(ctx, "DELETE", eventPath, nil, nil)
467+
uid := strings.ReplaceAll(event.ICalUID, "@", "-")
468+
filename := fmt.Sprintf("%s.ics", uid)
469+
eventPath := fmt.Sprintf("/%s/calendars/%s/%s", principalID, resolvedCalendarID, filename)
470+
471+
_, err = c.makeRequest(ctx, "DELETE", eventPath, nil, nil)
445472
return err
446473
}
447474

@@ -585,3 +612,18 @@ func (c *ACalClient) GetCalendarHash() string {
585612
id = append(id, sum[:]...)
586613
return base64.URLEncoding.EncodeToString(id)
587614
}
615+
616+
func (c *ACalClient) getResolvedCalendarInfo(ctx context.Context) (string, string, error) {
617+
c.resolveOnce.Do(func() {
618+
c.cachedPrincipalID, c.cachedCalendarID, c.resolutionError = c.ResolveCalendarID(ctx, c.CalendarID)
619+
})
620+
621+
return c.cachedPrincipalID, c.cachedCalendarID, c.resolutionError
622+
}
623+
624+
func (c *ACalClient) resetCache() {
625+
c.resolveOnce = sync.Once{}
626+
c.cachedPrincipalID = ""
627+
c.cachedCalendarID = ""
628+
c.resolutionError = nil
629+
}

0 commit comments

Comments
 (0)