summaryrefslogtreecommitdiff
path: root/internal/api/todoist.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-22 23:45:19 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-22 23:45:19 +0000
commit8abc63efdbc0bb96cd6c9aa99d6e9166e0bcabae (patch)
treef4d6a082eed9b10bc67436a3ca5188e0182961eb /internal/api/todoist.go
parent11b905fd437d651b2e39745aa82a5dd36f70331e (diff)
chore: unify and centralize agent configuration in .agent/
Diffstat (limited to 'internal/api/todoist.go')
-rw-r--r--internal/api/todoist.go155
1 files changed, 46 insertions, 109 deletions
diff --git a/internal/api/todoist.go b/internal/api/todoist.go
index be699ce..d6058d3 100644
--- a/internal/api/todoist.go
+++ b/internal/api/todoist.go
@@ -10,22 +10,19 @@ import (
)
const (
- todoistBaseURL = "https://api.todoist.com/api/v1"
- todoistSyncBaseURL = "https://api.todoist.com/sync/v9"
+ todoistBaseURL = "https://api.todoist.com/api/v1"
)
// TodoistClient handles interactions with the Todoist API
type TodoistClient struct {
BaseClient
- syncClient BaseClient
- apiKey string
+ apiKey string
}
// NewTodoistClient creates a new Todoist API client
func NewTodoistClient(apiKey string) *TodoistClient {
return &TodoistClient{
BaseClient: NewBaseClient(todoistBaseURL),
- syncClient: NewBaseClient(todoistSyncBaseURL),
apiKey: apiKey,
}
}
@@ -53,43 +50,33 @@ type todoistProjectResponse struct {
Name string `json:"name"`
}
-// Sync API v9 response types
-// TodoistSyncResponse represents the Sync API response
-type TodoistSyncResponse struct {
- SyncToken string `json:"sync_token"`
- FullSync bool `json:"full_sync"`
- Items []SyncItemResponse `json:"items"`
- Projects []SyncProjectResponse `json:"projects"`
-}
-
-// SyncItemResponse represents a task item from Sync API
-type SyncItemResponse struct {
- ID string `json:"id"`
- Content string `json:"content"`
- Description string `json:"description"`
- ProjectID string `json:"project_id"`
- Priority int `json:"priority"`
- Labels []string `json:"labels"`
- Due *dueInfo `json:"due"`
- IsCompleted bool `json:"is_completed"`
- IsDeleted bool `json:"is_deleted"`
- AddedAt string `json:"added_at"`
-}
-
-// SyncProjectResponse represents a project from Sync API
-type SyncProjectResponse struct {
- ID string `json:"id"`
- Name string `json:"name"`
- IsDeleted bool `json:"is_deleted"`
+// todoistTasksPage represents the paginated response from the Todoist REST API v1
+type todoistTasksPage struct {
+ Results []todoistTaskResponse `json:"results"`
+ NextCursor *string `json:"next_cursor"`
}
// GetTasks fetches all active tasks from Todoist
func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
- var apiTasks []todoistTaskResponse
- if err := c.Get(ctx, "/tasks", c.authHeaders(), &apiTasks); err != nil {
- return nil, fmt.Errorf("failed to fetch tasks: %w", err)
+ var allTasks []todoistTaskResponse
+ cursor := ""
+ for {
+ path := "/tasks"
+ if cursor != "" {
+ path = "/tasks?cursor=" + cursor
+ }
+ var page todoistTasksPage
+ if err := c.Get(ctx, path, c.authHeaders(), &page); err != nil {
+ return nil, fmt.Errorf("failed to fetch tasks: %w", err)
+ }
+ allTasks = append(allTasks, page.Results...)
+ if page.NextCursor == nil || *page.NextCursor == "" {
+ break
+ }
+ cursor = *page.NextCursor
}
+ apiTasks := allTasks
// Fetch projects to get project names
projects, err := c.GetProjects(ctx)
@@ -129,12 +116,32 @@ func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
return tasks, nil
}
+// todoistProjectsPage represents the paginated response for projects
+type todoistProjectsPage struct {
+ Results []todoistProjectResponse `json:"results"`
+ NextCursor *string `json:"next_cursor"`
+}
+
// GetProjects fetches all projects
func (c *TodoistClient) GetProjects(ctx context.Context) ([]models.Project, error) {
- var apiProjects []todoistProjectResponse
- if err := c.Get(ctx, "/projects", c.authHeaders(), &apiProjects); err != nil {
- return nil, fmt.Errorf("failed to fetch projects: %w", err)
+ var allProjects []todoistProjectResponse
+ cursor := ""
+ for {
+ path := "/projects"
+ if cursor != "" {
+ path = "/projects?cursor=" + cursor
+ }
+ var page todoistProjectsPage
+ if err := c.Get(ctx, path, c.authHeaders(), &page); err != nil {
+ return nil, fmt.Errorf("failed to fetch projects: %w", err)
+ }
+ allProjects = append(allProjects, page.Results...)
+ if page.NextCursor == nil || *page.NextCursor == "" {
+ break
+ }
+ cursor = *page.NextCursor
}
+ apiProjects := allProjects
projects := make([]models.Project, 0, len(apiProjects))
for _, apiProj := range apiProjects {
@@ -147,76 +154,6 @@ func (c *TodoistClient) GetProjects(ctx context.Context) ([]models.Project, erro
return projects, nil
}
-// Sync performs an incremental sync using the Sync API v9
-func (c *TodoistClient) Sync(ctx context.Context, syncToken string) (*TodoistSyncResponse, error) {
- if syncToken == "" {
- syncToken = "*" // Full sync
- }
-
- payload := map[string]interface{}{
- "sync_token": syncToken,
- "resource_types": []string{"items", "projects"},
- }
-
- var syncResp TodoistSyncResponse
- if err := c.syncClient.Post(ctx, "/sync", c.authHeaders(), payload, &syncResp); err != nil {
- return nil, fmt.Errorf("failed to perform sync: %w", err)
- }
-
- return &syncResp, nil
-}
-
-// ConvertSyncItemToTask converts a single sync item to a Task model.
-// Returns the task and true if the item is active, or a zero Task and false if it should be skipped.
-func ConvertSyncItemToTask(item SyncItemResponse, projectMap map[string]string) (models.Task, bool) {
- if item.IsCompleted || item.IsDeleted {
- return models.Task{}, false
- }
-
- task := models.Task{
- ID: item.ID,
- Content: item.Content,
- Description: item.Description,
- ProjectID: item.ProjectID,
- ProjectName: projectMap[item.ProjectID],
- Priority: item.Priority,
- Completed: false,
- Labels: item.Labels,
- URL: fmt.Sprintf("https://todoist.com/app/task/%s", item.ID),
- }
-
- if item.AddedAt != "" {
- if createdAt, err := time.Parse(time.RFC3339, item.AddedAt); err == nil {
- task.CreatedAt = createdAt
- }
- }
-
- task.DueDate = parseDueDate(item.Due)
- return task, true
-}
-
-// ConvertSyncItemsToTasks converts sync API items to Task models
-func ConvertSyncItemsToTasks(items []SyncItemResponse, projectMap map[string]string) []models.Task {
- tasks := make([]models.Task, 0, len(items))
- for _, item := range items {
- if task, ok := ConvertSyncItemToTask(item, projectMap); ok {
- tasks = append(tasks, task)
- }
- }
- return tasks
-}
-
-// BuildProjectMapFromSync builds a project ID to name map from sync response
-func BuildProjectMapFromSync(projects []SyncProjectResponse) map[string]string {
- projectMap := make(map[string]string)
- for _, proj := range projects {
- if !proj.IsDeleted {
- projectMap[proj.ID] = proj.Name
- }
- }
- return projectMap
-}
-
// CreateTask creates a new task in Todoist
func (c *TodoistClient) CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error) {
payload := map[string]interface{}{"content": content}