package api import ( "context" "fmt" "log" "sort" "strings" "time" "task-dashboard/internal/models" "google.golang.org/api/calendar/v3" "google.golang.org/api/option" ) type GoogleCalendarClient struct { srv *calendar.Service calendarIDs []string } // 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") func NewGoogleCalendarClient(ctx context.Context, credentialsFile, calendarIDs string) (*GoogleCalendarClient, error) { srv, err := calendar.NewService(ctx, option.WithCredentialsFile(credentialsFile)) if err != nil { return nil, fmt.Errorf("unable to retrieve Calendar client: %v", err) } // Parse comma-separated calendar IDs ids := strings.Split(calendarIDs, ",") var trimmedIDs []string for _, id := range ids { if trimmed := strings.TrimSpace(id); trimmed != "" { trimmedIDs = append(trimmedIDs, trimmed) } } return &GoogleCalendarClient{ srv: srv, calendarIDs: trimmedIDs, }, nil } func (c *GoogleCalendarClient) GetUpcomingEvents(ctx context.Context, maxResults int) ([]models.CalendarEvent, error) { t := time.Now().Format(time.RFC3339) var allEvents []models.CalendarEvent for _, calendarID := range c.calendarIDs { events, err := c.srv.Events.List(calendarID).ShowDeleted(false). SingleEvents(true).TimeMin(t).MaxResults(int64(maxResults)).OrderBy("startTime").Do() if err != nil { log.Printf("Warning: failed to fetch events from calendar %s: %v", calendarID, err) continue // Don't fail entirely, just skip this calendar } for _, item := range events.Items { var start, end time.Time if item.Start.DateTime == "" { // All-day event start, _ = time.Parse("2006-01-02", item.Start.Date) end, _ = time.Parse("2006-01-02", item.End.Date) } else { start, _ = time.Parse(time.RFC3339, item.Start.DateTime) end, _ = time.Parse(time.RFC3339, item.End.DateTime) } allEvents = append(allEvents, models.CalendarEvent{ ID: item.Id, Summary: item.Summary, Description: item.Description, Start: start, End: end, HTMLLink: item.HtmlLink, }) } } // Deduplicate events (same event may appear in multiple calendars) // Use Unix timestamp to handle timezone differences seen := make(map[string]bool) var uniqueEvents []models.CalendarEvent for _, event := range allEvents { // Use summary + unix timestamp as dedup key (handles timezone differences) key := fmt.Sprintf("%s|%d", event.Summary, event.Start.Unix()) if !seen[key] { seen[key] = true uniqueEvents = append(uniqueEvents, event) } } // Sort all events by start time sort.Slice(uniqueEvents, func(i, j int) bool { return uniqueEvents[i].Start.Before(uniqueEvents[j].Start) }) // Limit to maxResults if len(uniqueEvents) > maxResults { uniqueEvents = uniqueEvents[:maxResults] } return uniqueEvents, nil }