diff options
Diffstat (limited to 'internal/api/todoist.go')
| -rw-r--r-- | internal/api/todoist.go | 256 |
1 files changed, 0 insertions, 256 deletions
diff --git a/internal/api/todoist.go b/internal/api/todoist.go deleted file mode 100644 index 2454233..0000000 --- a/internal/api/todoist.go +++ /dev/null @@ -1,256 +0,0 @@ -package api - -import ( - "context" - "fmt" - "strings" - "time" - - "task-dashboard/internal/config" - "task-dashboard/internal/models" -) - -const ( - todoistBaseURL = "https://api.todoist.com/api/v1" -) - -// TodoistClient handles interactions with the Todoist API -type TodoistClient struct { - BaseClient - apiKey string -} - -// NewTodoistClient creates a new Todoist API client -func NewTodoistClient(apiKey string) *TodoistClient { - return &TodoistClient{ - BaseClient: NewBaseClient(todoistBaseURL), - apiKey: apiKey, - } -} - -func (c *TodoistClient) authHeaders() map[string]string { - return map[string]string{"Authorization": "Bearer " + c.apiKey} -} - -// todoistTaskResponse represents the API response structure -type todoistTaskResponse 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"` - URL string `json:"url"` - CreatedAt string `json:"created_at"` -} - -// todoistProjectResponse represents the project API response -type todoistProjectResponse struct { - ID string `json:"id"` - Name string `json:"name"` -} - - -// 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 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) - projectMap := make(map[string]string) - if err == nil { - for _, proj := range projects { - projectMap[proj.ID] = proj.Name - } - } - - // Convert to our model - tasks := make([]models.Task, 0, len(apiTasks)) - for _, apiTask := range apiTasks { - task := models.Task{ - ID: apiTask.ID, - Content: apiTask.Content, - Description: apiTask.Description, - ProjectID: apiTask.ProjectID, - ProjectName: projectMap[apiTask.ProjectID], - Priority: apiTask.Priority, - Completed: false, - Labels: apiTask.Labels, - URL: apiTask.URL, - } - - if createdAt, err := time.Parse(time.RFC3339, apiTask.CreatedAt); err == nil { - task.CreatedAt = createdAt - } - - task.DueDate = parseDueDate(apiTask.Due) - if apiTask.Due != nil { - task.IsRecurring = apiTask.Due.IsRecurring - } - tasks = append(tasks, task) - } - - 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 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 { - projects = append(projects, models.Project{ - ID: apiProj.ID, - Name: apiProj.Name, - }) - } - - return projects, nil -} - -// 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} - if projectID != "" { - payload["project_id"] = projectID - } - if dueDate != nil { - payload["due_date"] = dueDate.Format("2006-01-02") - } - if priority > 0 { - payload["priority"] = priority - } - - var apiTask todoistTaskResponse - if err := c.Post(ctx, "/tasks", c.authHeaders(), payload, &apiTask); err != nil { - return nil, fmt.Errorf("failed to create task: %w", err) - } - - task := &models.Task{ - ID: apiTask.ID, - Content: apiTask.Content, - Description: apiTask.Description, - ProjectID: apiTask.ProjectID, - Priority: apiTask.Priority, - Completed: false, - Labels: apiTask.Labels, - URL: apiTask.URL, - } - - if createdAt, err := time.Parse(time.RFC3339, apiTask.CreatedAt); err == nil { - task.CreatedAt = createdAt - } - - task.DueDate = parseDueDate(apiTask.Due) - return task, nil -} - -// UpdateTask updates a task with the specified changes -func (c *TodoistClient) UpdateTask(ctx context.Context, taskID string, updates map[string]interface{}) error { - path := fmt.Sprintf("/tasks/%s", taskID) - if err := c.Post(ctx, path, c.authHeaders(), updates, nil); err != nil { - return fmt.Errorf("failed to update task: %w", err) - } - return nil -} - -// CompleteTask marks a task as complete in Todoist -func (c *TodoistClient) CompleteTask(ctx context.Context, taskID string) error { - path := fmt.Sprintf("/tasks/%s/close", taskID) - if err := c.PostEmpty(ctx, path, c.authHeaders()); err != nil { - return fmt.Errorf("failed to complete task: %w", err) - } - return nil -} - -// ReopenTask marks a completed task as active in Todoist -func (c *TodoistClient) ReopenTask(ctx context.Context, taskID string) error { - path := fmt.Sprintf("/tasks/%s/reopen", taskID) - if err := c.PostEmpty(ctx, path, c.authHeaders()); err != nil { - return fmt.Errorf("failed to reopen task: %w", err) - } - return nil -} - -// parseDueDate parses due date from API response -// dueInfo represents the due date structure from Todoist API -type dueInfo struct { - Date string `json:"date"` - Datetime string `json:"datetime"` - IsRecurring bool `json:"is_recurring"` -} - -func parseDueDate(due *dueInfo) *time.Time { - if due == nil { - return nil - } - var dueDate time.Time - var err error - if due.Datetime != "" { - // RFC3339 includes timezone, then convert to display timezone - dueDate, err = time.Parse(time.RFC3339, due.Datetime) - if err == nil { - dueDate = config.ToDisplayTZ(dueDate) - } - } else if due.Date != "" { - // Todoist may put a local datetime (no tz offset) in the date field - // e.g. "2026-03-22T19:00:00" for recurring tasks with a set time. - // Fall back to date-only "2006-01-02" if no T is present. - if strings.Contains(due.Date, "T") { - dueDate, err = config.ParseDateTimeInDisplayTZ("2006-01-02T15:04:05", due.Date) - } else { - dueDate, err = config.ParseDateInDisplayTZ(due.Date) - } - } - if err != nil { - return nil - } - return &dueDate -} |
