1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
}
|