diff options
Diffstat (limited to 'internal/api/todoist.go')
| -rw-r--r-- | internal/api/todoist.go | 141 |
1 files changed, 140 insertions, 1 deletions
diff --git a/internal/api/todoist.go b/internal/api/todoist.go index 511753d..b51fffd 100644 --- a/internal/api/todoist.go +++ b/internal/api/todoist.go @@ -13,7 +13,8 @@ import ( ) const ( - todoistBaseURL = "https://api.todoist.com/rest/v2" + todoistBaseURL = "https://api.todoist.com/rest/v2" + todoistSyncBaseURL = "https://api.todoist.com/sync/v9" ) // TodoistClient handles interactions with the Todoist API @@ -56,6 +57,40 @@ 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 *struct { + Date string `json:"date"` + Datetime string `json:"datetime"` + } `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"` +} + // GetTasks fetches all active tasks from Todoist func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/tasks", nil) @@ -167,6 +202,110 @@ func (c *TodoistClient) GetProjects(ctx context.Context) ([]models.Project, erro return projects, nil } +// Sync performs an incremental sync using the Sync API v9 +// If syncToken is empty or "*", a full sync is performed +// Returns the new sync token and the sync response +func (c *TodoistClient) Sync(ctx context.Context, syncToken string) (*TodoistSyncResponse, error) { + if syncToken == "" { + syncToken = "*" // Full sync + } + + // Prepare sync request + payload := map[string]interface{}{ + "sync_token": syncToken, + "resource_types": []string{"items", "projects"}, + } + + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("failed to marshal sync request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, "POST", todoistSyncBaseURL+"/sync", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("failed to create sync request: %w", err) + } + + req.Header.Set("Authorization", "Bearer "+c.apiKey) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform sync: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("todoist sync API error (status %d): %s", resp.StatusCode, string(body)) + } + + var syncResp TodoistSyncResponse + if err := json.NewDecoder(resp.Body).Decode(&syncResp); err != nil { + return nil, fmt.Errorf("failed to decode sync response: %w", err) + } + + return &syncResp, nil +} + +// 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 { + // Skip completed or deleted items + if item.IsCompleted || item.IsDeleted { + continue + } + + 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/showTask?id=%s", item.ID), + } + + // Parse added_at + if item.AddedAt != "" { + if createdAt, err := time.Parse(time.RFC3339, item.AddedAt); err == nil { + task.CreatedAt = createdAt + } + } + + // Parse due date + if item.Due != nil { + var dueDate time.Time + var err error + if item.Due.Datetime != "" { + dueDate, err = time.Parse(time.RFC3339, item.Due.Datetime) + } else if item.Due.Date != "" { + dueDate, err = time.Parse("2006-01-02", item.Due.Date) + } + if err == nil { + task.DueDate = &dueDate + } + } + + 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) { // Prepare request payload |
