1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package lunatask
6
7import (
8 "context"
9 "fmt"
10 "net/http"
11 "net/url"
12 "time"
13)
14
15// Task represents a task returned from the Lunatask API
16type Task struct {
17 ID string `json:"id"`
18 AreaID *string `json:"area_id"`
19 GoalID *string `json:"goal_id"`
20 Name *string `json:"name"`
21 Note *string `json:"note"`
22 Status *string `json:"status"`
23 PreviousStatus *string `json:"previous_status"`
24 Estimate *int `json:"estimate"`
25 Priority *int `json:"priority"`
26 Progress *int `json:"progress"`
27 Motivation *string `json:"motivation"`
28 Eisenhower *int `json:"eisenhower"`
29 Sources []Source `json:"sources"`
30 ScheduledOn *Date `json:"scheduled_on"`
31 CompletedAt *time.Time `json:"completed_at"`
32 CreatedAt time.Time `json:"created_at"`
33 UpdatedAt time.Time `json:"updated_at"`
34}
35
36// CreateTaskRequest represents the request to create a task in Lunatask
37type CreateTaskRequest struct {
38 Name string `json:"name"`
39 AreaID *string `json:"area_id,omitempty"`
40 GoalID *string `json:"goal_id,omitempty"`
41 Note *string `json:"note,omitempty"`
42 Status *string `json:"status,omitempty"`
43 Motivation *string `json:"motivation,omitempty"`
44 Estimate *int `json:"estimate,omitempty"`
45 Priority *int `json:"priority,omitempty"`
46 Eisenhower *int `json:"eisenhower,omitempty"`
47 ScheduledOn *Date `json:"scheduled_on,omitempty"`
48 CompletedAt *time.Time `json:"completed_at,omitempty"`
49 Source *string `json:"source,omitempty"`
50 SourceID *string `json:"source_id,omitempty"`
51}
52
53// UpdateTaskRequest represents the request to update a task in Lunatask.
54// All fields are optional; only provided fields will be updated.
55type UpdateTaskRequest struct {
56 Name *string `json:"name,omitempty"`
57 AreaID *string `json:"area_id,omitempty"`
58 GoalID *string `json:"goal_id,omitempty"`
59 Note *string `json:"note,omitempty"`
60 Status *string `json:"status,omitempty"`
61 Motivation *string `json:"motivation,omitempty"`
62 Estimate *int `json:"estimate,omitempty"`
63 Priority *int `json:"priority,omitempty"`
64 Eisenhower *int `json:"eisenhower,omitempty"`
65 ScheduledOn *Date `json:"scheduled_on,omitempty"`
66 CompletedAt *time.Time `json:"completed_at,omitempty"`
67}
68
69// taskResponse represents a single task response from the API
70type taskResponse struct {
71 Task Task `json:"task"`
72}
73
74// tasksResponse represents a list of tasks response from the API
75type tasksResponse struct {
76 Tasks []Task `json:"tasks"`
77}
78
79// ListTasksOptions contains optional filters for listing tasks
80type ListTasksOptions struct {
81 Source *string
82 SourceID *string
83}
84
85// ListTasks retrieves all tasks, optionally filtered by source and/or source_id
86func (c *Client) ListTasks(ctx context.Context, opts *ListTasksOptions) ([]Task, error) {
87 path := "/tasks"
88
89 if opts != nil {
90 params := url.Values{}
91 if opts.Source != nil && *opts.Source != "" {
92 params.Set("source", *opts.Source)
93 }
94 if opts.SourceID != nil && *opts.SourceID != "" {
95 params.Set("source_id", *opts.SourceID)
96 }
97 if len(params) > 0 {
98 path = fmt.Sprintf("%s?%s", path, params.Encode())
99 }
100 }
101
102 resp, _, err := doJSON[tasksResponse](c, ctx, http.MethodGet, path, nil)
103 if err != nil {
104 return nil, err
105 }
106
107 return resp.Tasks, nil
108}
109
110// GetTask retrieves a specific task by ID
111func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error) {
112 if taskID == "" {
113 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
114 }
115
116 resp, _, err := doJSON[taskResponse](c, ctx, http.MethodGet, "/tasks/"+taskID, nil)
117 if err != nil {
118 return nil, err
119 }
120
121 return &resp.Task, nil
122}
123
124// CreateTask creates a new task in Lunatask.
125// Returns nil, nil if a matching task already exists (HTTP 204).
126func (c *Client) CreateTask(ctx context.Context, task *CreateTaskRequest) (*Task, error) {
127 if task.Name == "" {
128 return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
129 }
130
131 resp, noContent, err := doJSON[taskResponse](c, ctx, http.MethodPost, "/tasks", task)
132 if err != nil {
133 return nil, err
134 }
135 if noContent {
136 return nil, nil
137 }
138
139 return &resp.Task, nil
140}
141
142// UpdateTask updates an existing task in Lunatask
143func (c *Client) UpdateTask(ctx context.Context, taskID string, task *UpdateTaskRequest) (*Task, error) {
144 if taskID == "" {
145 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
146 }
147
148 resp, _, err := doJSON[taskResponse](c, ctx, http.MethodPut, "/tasks/"+taskID, task)
149 if err != nil {
150 return nil, err
151 }
152
153 return &resp.Task, nil
154}
155
156// DeleteTask deletes a task in Lunatask
157func (c *Client) DeleteTask(ctx context.Context, taskID string) (*Task, error) {
158 if taskID == "" {
159 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
160 }
161
162 resp, _, err := doJSON[taskResponse](c, ctx, http.MethodDelete, "/tasks/"+taskID, nil)
163 if err != nil {
164 return nil, err
165 }
166
167 return &resp.Task, nil
168}