summaryrefslogtreecommitdiff
path: root/internal/api/todoist.go
blob: be59e73af321fa7c93bb0f71aff524469c00d768 (plain)
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
package api

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"task-dashboard/internal/models"
)

const (
	todoistBaseURL = "https://api.todoist.com/rest/v2"
)

// TodoistClient handles interactions with the Todoist API
type TodoistClient struct {
	apiKey     string
	httpClient *http.Client
}

// NewTodoistClient creates a new Todoist API client
func NewTodoistClient(apiKey string) *TodoistClient {
	return &TodoistClient{
		apiKey: apiKey,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

// 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         *struct {
		Date     string `json:"date"`
		Datetime string `json:"datetime"`
	} `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"`
}

// GetTasks fetches all active tasks from Todoist
func (c *TodoistClient) GetTasks(ctx context.Context) ([]models.Task, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/tasks", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.apiKey)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch tasks: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body))
	}

	var apiTasks []todoistTaskResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiTasks); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Fetch projects to get project names
	projects, err := c.GetProjects(ctx)
	if err != nil {
		// If we can't get projects, continue with empty project names
		projects = make(map[string]string)
	}

	// 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: projects[apiTask.ProjectID],
			Priority:    apiTask.Priority,
			Completed:   false,
			Labels:      apiTask.Labels,
			URL:         apiTask.URL,
		}

		// Parse created_at
		if createdAt, err := time.Parse(time.RFC3339, apiTask.CreatedAt); err == nil {
			task.CreatedAt = createdAt
		}

		// Parse due date
		if apiTask.Due != nil {
			var dueDate time.Time
			if apiTask.Due.Datetime != "" {
				dueDate, err = time.Parse(time.RFC3339, apiTask.Due.Datetime)
			} else if apiTask.Due.Date != "" {
				dueDate, err = time.Parse("2006-01-02", apiTask.Due.Date)
			}
			if err == nil {
				task.DueDate = &dueDate
			}
		}

		tasks = append(tasks, task)
	}

	return tasks, nil
}

// GetProjects fetches all projects and returns a map of project ID to name
func (c *TodoistClient) GetProjects(ctx context.Context) (map[string]string, error) {
	req, err := http.NewRequestWithContext(ctx, "GET", todoistBaseURL+"/projects", nil)
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Authorization", "Bearer "+c.apiKey)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch projects: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("todoist API error (status %d): %s", resp.StatusCode, string(body))
	}

	var apiProjects []todoistProjectResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiProjects); err != nil {
		return nil, fmt.Errorf("failed to decode response: %w", err)
	}

	// Convert to map
	projects := make(map[string]string, len(apiProjects))
	for _, project := range apiProjects {
		projects[project.ID] = project.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) {
	// This will be implemented in Phase 2
	return nil, fmt.Errorf("not implemented yet")
}

// CompleteTask marks a task as complete in Todoist
func (c *TodoistClient) CompleteTask(ctx context.Context, taskID string) error {
	// This will be implemented in Phase 2
	return fmt.Errorf("not implemented yet")
}