summaryrefslogtreecommitdiff
path: root/internal/api/google_calendar.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/google_calendar.go')
-rw-r--r--internal/api/google_calendar.go99
1 files changed, 99 insertions, 0 deletions
diff --git a/internal/api/google_calendar.go b/internal/api/google_calendar.go
index 7381527..bb17812 100644
--- a/internal/api/google_calendar.go
+++ b/internal/api/google_calendar.go
@@ -5,6 +5,7 @@ import (
"fmt"
"log"
"sort"
+ "strconv"
"strings"
"time"
@@ -83,6 +84,86 @@ func deduplicateEvents(events []models.CalendarEvent) []models.CalendarEvent {
return unique
}
+var recurrenceFreqNames = map[string]string{
+ "DAILY": "daily",
+ "WEEKLY": "weekly",
+ "MONTHLY": "monthly",
+ "YEARLY": "yearly",
+}
+
+var recurrenceFreqUnits = map[string]string{
+ "DAILY": "day",
+ "WEEKLY": "week",
+ "MONTHLY": "month",
+ "YEARLY": "year",
+}
+
+var recurrenceWeekdayNames = map[string]string{
+ "SU": "Sunday",
+ "MO": "Monday",
+ "TU": "Tuesday",
+ "WE": "Wednesday",
+ "TH": "Thursday",
+ "FR": "Friday",
+ "SA": "Saturday",
+}
+
+// formatRecurrence turns Google Calendar RRULE strings into short English.
+// This intentionally covers only the common cases (FREQ, INTERVAL, BYDAY) --
+// not a full RFC 5545 parser. Anything it can't confidently describe falls
+// back to "Recurring event" rather than showing nothing or an error.
+func formatRecurrence(rrules []string) string {
+ for _, rule := range rrules {
+ rule = strings.TrimPrefix(rule, "RRULE:")
+ parts := make(map[string]string)
+ for _, kv := range strings.Split(rule, ";") {
+ pieces := strings.SplitN(kv, "=", 2)
+ if len(pieces) == 2 {
+ parts[pieces[0]] = pieces[1]
+ }
+ }
+
+ freqKey := parts["FREQ"]
+ unit, ok := recurrenceFreqUnits[freqKey]
+ if !ok {
+ continue
+ }
+
+ interval := 1
+ if iv := parts["INTERVAL"]; iv != "" {
+ if n, err := strconv.Atoi(iv); err == nil && n > 0 {
+ interval = n
+ }
+ }
+
+ var phrase string
+ if interval == 1 {
+ phrase = "Repeats " + recurrenceFreqNames[freqKey]
+ } else {
+ phrase = fmt.Sprintf("Repeats every %d %ss", interval, unit)
+ }
+
+ if byday := parts["BYDAY"]; byday != "" {
+ var days []string
+ for _, code := range strings.Split(byday, ",") {
+ code = strings.TrimSpace(code)
+ if len(code) >= 2 {
+ code = code[len(code)-2:]
+ }
+ if name, ok := recurrenceWeekdayNames[code]; ok {
+ days = append(days, name)
+ }
+ }
+ if len(days) > 0 {
+ phrase += " on " + strings.Join(days, ", ")
+ }
+ }
+
+ return phrase
+ }
+ return "Recurring event"
+}
+
// NewGoogleCalendarClient creates a client that fetches from multiple calendars.
// calendarIDs can be comma-separated (e.g., "cal1@group.calendar.google.com,cal2@group.calendar.google.com")
// timezone is the IANA timezone name for display (e.g., "Pacific/Honolulu")
@@ -203,3 +284,21 @@ func (c *GoogleCalendarClient) GetCalendarList(ctx context.Context) ([]models.Ca
}
return calendars, nil
}
+
+// GetRecurrenceRule looks up a recurring event's master record and returns
+// its formatted recurrence schedule. Google's API only puts the RRULE on
+// the master event, not on expanded instances (see parseEventTime's
+// SingleEvents(true) callers), so this does a live lookup by the instance's
+// RecurringEventId. There's no per-event calendar attribution stored today
+// (events from all configured calendars are merged without recording which
+// one they came from), so this tries each configured calendar in turn.
+func (c *GoogleCalendarClient) GetRecurrenceRule(ctx context.Context, recurringEventID string) (string, error) {
+ for _, calendarID := range c.calendarIDs {
+ event, err := c.srv.Events.Get(calendarID, recurringEventID).Do()
+ if err != nil {
+ continue
+ }
+ return formatRecurrence(event.Recurrence), nil
+ }
+ return "", fmt.Errorf("recurring event %s not found on any configured calendar", recurringEventID)
+}