summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/interfaces.go1
-rw-r--r--internal/api/todoist.go141
-rw-r--r--internal/api/trello.go3
3 files changed, 144 insertions, 1 deletions
diff --git a/internal/api/interfaces.go b/internal/api/interfaces.go
index db7e6c0..2419707 100644
--- a/internal/api/interfaces.go
+++ b/internal/api/interfaces.go
@@ -13,6 +13,7 @@ type TodoistAPI interface {
GetProjects(ctx context.Context) ([]models.Project, error)
CreateTask(ctx context.Context, content, projectID string, dueDate *time.Time, priority int) (*models.Task, error)
CompleteTask(ctx context.Context, taskID string) error
+ Sync(ctx context.Context, syncToken string) (*TodoistSyncResponse, error)
}
// TrelloAPI defines the interface for Trello operations
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
diff --git a/internal/api/trello.go b/internal/api/trello.go
index 91d6d66..037c881 100644
--- a/internal/api/trello.go
+++ b/internal/api/trello.go
@@ -68,6 +68,7 @@ func (c *TrelloClient) GetBoards(ctx context.Context) ([]models.Board, error) {
params.Set("key", c.apiKey)
params.Set("token", c.token)
params.Set("filter", "open")
+ params.Set("fields", "id,name") // Only fetch required fields
reqURL := fmt.Sprintf("%s/members/me/boards?%s", c.baseURL, params.Encode())
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
@@ -111,6 +112,7 @@ func (c *TrelloClient) GetCards(ctx context.Context, boardID string) ([]models.C
params.Set("key", c.apiKey)
params.Set("token", c.token)
params.Set("filter", "visible")
+ params.Set("fields", "id,name,idList,due,url,idBoard") // Only fetch required fields
reqURL := fmt.Sprintf("%s/boards/%s/cards?%s", c.baseURL, boardID, params.Encode())
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
@@ -174,6 +176,7 @@ func (c *TrelloClient) getLists(ctx context.Context, boardID string) ([]models.L
params := url.Values{}
params.Set("key", c.apiKey)
params.Set("token", c.token)
+ params.Set("fields", "id,name") // Only fetch required fields
reqURL := fmt.Sprintf("%s/boards/%s/lists?%s", c.baseURL, boardID, params.Encode())
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)