diff options
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/google_tasks.go | 173 | ||||
| -rw-r--r-- | internal/api/interfaces.go | 9 |
2 files changed, 182 insertions, 0 deletions
diff --git a/internal/api/google_tasks.go b/internal/api/google_tasks.go new file mode 100644 index 0000000..0b4d7c2 --- /dev/null +++ b/internal/api/google_tasks.go @@ -0,0 +1,173 @@ +package api + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "task-dashboard/internal/models" + + "google.golang.org/api/option" + "google.golang.org/api/tasks/v1" +) + +// GoogleTasksClient handles interactions with Google Tasks API +type GoogleTasksClient struct { + srv *tasks.Service + tasklistID string + displayTZ *time.Location +} + +// NewGoogleTasksClient creates a client for Google Tasks. +// tasklistID can be "@default" for the primary list, or a specific list ID. +// Multiple lists can be comma-separated. +func NewGoogleTasksClient(ctx context.Context, credentialsFile, tasklistID, timezone string) (*GoogleTasksClient, error) { + srv, err := tasks.NewService(ctx, option.WithCredentialsFile(credentialsFile)) + if err != nil { + return nil, fmt.Errorf("unable to create Tasks client: %v", err) + } + + if tasklistID == "" { + tasklistID = "@default" + } + + // Load display timezone + displayTZ, err := time.LoadLocation(timezone) + if err != nil { + log.Printf("Warning: invalid timezone %q, using UTC: %v", timezone, err) + displayTZ = time.UTC + } + + return &GoogleTasksClient{ + srv: srv, + tasklistID: tasklistID, + displayTZ: displayTZ, + }, nil +} + +// GetTasks fetches all incomplete tasks from the configured task list(s) +func (c *GoogleTasksClient) GetTasks(ctx context.Context) ([]models.GoogleTask, error) { + var allTasks []models.GoogleTask + + // Support comma-separated list IDs + listIDs := strings.Split(c.tasklistID, ",") + for _, listID := range listIDs { + listID = strings.TrimSpace(listID) + if listID == "" { + continue + } + + tasks, err := c.getTasksFromList(ctx, listID) + if err != nil { + log.Printf("Warning: failed to fetch tasks from list %s: %v", listID, err) + continue + } + allTasks = append(allTasks, tasks...) + } + + return allTasks, nil +} + +// getTasksFromList fetches tasks from a specific list +func (c *GoogleTasksClient) getTasksFromList(ctx context.Context, listID string) ([]models.GoogleTask, error) { + call := c.srv.Tasks.List(listID). + ShowCompleted(false). + ShowHidden(false). + MaxResults(100) + + taskList, err := call.Context(ctx).Do() + if err != nil { + return nil, fmt.Errorf("failed to list tasks: %v", err) + } + + var result []models.GoogleTask + for _, item := range taskList.Items { + task := models.GoogleTask{ + ID: item.Id, + Title: item.Title, + Notes: item.Notes, + Status: item.Status, + Completed: item.Status == "completed", + ListID: listID, + } + + // Parse due date if present (RFC3339 format, but date-only) + if item.Due != "" { + // Google Tasks due dates are in RFC3339 format but typically just the date part + dueDate, err := time.Parse(time.RFC3339, item.Due) + if err != nil { + // Try date-only format + dueDate, err = time.ParseInLocation("2006-01-02", item.Due[:10], c.displayTZ) + } + if err == nil { + dueInTZ := dueDate.In(c.displayTZ) + task.DueDate = &dueInTZ + } + } + + // Parse updated time + if item.Updated != "" { + if updated, err := time.Parse(time.RFC3339, item.Updated); err == nil { + task.UpdatedAt = updated.In(c.displayTZ) + } + } + + // Build URL to Google Tasks + task.URL = fmt.Sprintf("https://tasks.google.com/embed/?origin=https://mail.google.com&fullWidth=1") + + result = append(result, task) + } + + return result, nil +} + +// GetTasksByDateRange fetches tasks with due dates in the specified range +func (c *GoogleTasksClient) GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error) { + allTasks, err := c.GetTasks(ctx) + if err != nil { + return nil, err + } + + // Filter by date range + var filtered []models.GoogleTask + for _, task := range allTasks { + if task.DueDate == nil { + continue + } + dueDay := time.Date(task.DueDate.Year(), task.DueDate.Month(), task.DueDate.Day(), 0, 0, 0, 0, c.displayTZ) + startDay := time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, c.displayTZ) + endDay := time.Date(end.Year(), end.Month(), end.Day(), 0, 0, 0, 0, c.displayTZ) + + if !dueDay.Before(startDay) && dueDay.Before(endDay) { + filtered = append(filtered, task) + } + } + + return filtered, nil +} + +// CompleteTask marks a task as completed +func (c *GoogleTasksClient) CompleteTask(ctx context.Context, listID, taskID string) error { + task := &tasks.Task{ + Status: "completed", + } + _, err := c.srv.Tasks.Patch(listID, taskID, task).Context(ctx).Do() + if err != nil { + return fmt.Errorf("failed to complete task: %v", err) + } + return nil +} + +// UncompleteTask marks a task as not completed +func (c *GoogleTasksClient) UncompleteTask(ctx context.Context, listID, taskID string) error { + task := &tasks.Task{ + Status: "needsAction", + } + _, err := c.srv.Tasks.Patch(listID, taskID, task).Context(ctx).Do() + if err != nil { + return fmt.Errorf("failed to uncomplete task: %v", err) + } + return nil +} diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go index 70bba1f..aa351ab 100644 --- a/internal/api/interfaces.go +++ b/internal/api/interfaces.go @@ -42,10 +42,19 @@ type GoogleCalendarAPI interface { GetEventsByDateRange(ctx context.Context, start, end time.Time) ([]models.CalendarEvent, error) } +// GoogleTasksAPI defines the interface for Google Tasks operations +type GoogleTasksAPI interface { + GetTasks(ctx context.Context) ([]models.GoogleTask, error) + GetTasksByDateRange(ctx context.Context, start, end time.Time) ([]models.GoogleTask, error) + CompleteTask(ctx context.Context, listID, taskID string) error + UncompleteTask(ctx context.Context, listID, taskID string) error +} + // Ensure concrete types implement interfaces var ( _ TodoistAPI = (*TodoistClient)(nil) _ TrelloAPI = (*TrelloClient)(nil) _ PlanToEatAPI = (*PlanToEatClient)(nil) _ GoogleCalendarAPI = (*GoogleCalendarClient)(nil) + _ GoogleTasksAPI = (*GoogleTasksClient)(nil) ) |
