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 "time"
11)
12
13// Task is a task in Lunatask. Name and Note are encrypted client-side
14// and will be null when read back from the API.
15type Task struct {
16 ID string `json:"id"`
17 AreaID *string `json:"area_id"`
18 GoalID *string `json:"goal_id"`
19 Name *string `json:"name"`
20 Note *string `json:"note"`
21 Status *TaskStatus `json:"status"`
22 PreviousStatus *TaskStatus `json:"previous_status"`
23 Estimate *int `json:"estimate"`
24 Priority *int `json:"priority"`
25 Progress *int `json:"progress"`
26 Motivation *Motivation `json:"motivation"`
27 Eisenhower *int `json:"eisenhower"`
28 Sources []Source `json:"sources"`
29 ScheduledOn *Date `json:"scheduled_on"`
30 CompletedAt *time.Time `json:"completed_at"`
31 CreatedAt time.Time `json:"created_at"`
32 UpdatedAt time.Time `json:"updated_at"`
33}
34
35// createTaskRequest defines a new task for JSON serialization.
36type createTaskRequest struct {
37 Name string `json:"name"`
38 AreaID *string `json:"area_id,omitempty"`
39 GoalID *string `json:"goal_id,omitempty"`
40 Note *string `json:"note,omitempty"`
41 Status *TaskStatus `json:"status,omitempty"`
42 Motivation *Motivation `json:"motivation,omitempty"`
43 Estimate *int `json:"estimate,omitempty"`
44 Priority *int `json:"priority,omitempty"`
45 Eisenhower *int `json:"eisenhower,omitempty"`
46 ScheduledOn *Date `json:"scheduled_on,omitempty"`
47 CompletedAt *time.Time `json:"completed_at,omitempty"`
48 Source *string `json:"source,omitempty"`
49 SourceID *string `json:"source_id,omitempty"`
50}
51
52// updateTaskRequest specifies which fields to change on a task.
53type updateTaskRequest struct {
54 Name *string `json:"name,omitempty"`
55 AreaID *string `json:"area_id,omitempty"`
56 GoalID *string `json:"goal_id,omitempty"`
57 Note *string `json:"note,omitempty"`
58 Status *TaskStatus `json:"status,omitempty"`
59 Motivation *Motivation `json:"motivation,omitempty"`
60 Estimate *int `json:"estimate,omitempty"`
61 Priority *int `json:"priority,omitempty"`
62 Eisenhower *int `json:"eisenhower,omitempty"`
63 ScheduledOn *Date `json:"scheduled_on,omitempty"`
64 CompletedAt *time.Time `json:"completed_at,omitempty"`
65}
66
67// taskResponse wraps a single task from the API.
68type taskResponse struct {
69 Task Task `json:"task"`
70}
71
72// tasksResponse wraps a list of tasks from the API.
73type tasksResponse struct {
74 Tasks []Task `json:"tasks"`
75}
76
77// ListTasksOptions filters tasks by source integration.
78type ListTasksOptions struct {
79 Source *string
80 SourceID *string
81}
82
83// GetSource implements [SourceFilter].
84func (o *ListTasksOptions) GetSource() *string { return o.Source }
85
86// GetSourceID implements [SourceFilter].
87func (o *ListTasksOptions) GetSourceID() *string { return o.SourceID }
88
89// ListTasks returns all tasks, optionally filtered. Pass nil for all.
90func (c *Client) ListTasks(ctx context.Context, opts *ListTasksOptions) ([]Task, error) {
91 var filter SourceFilter
92 if opts != nil {
93 filter = opts
94 }
95
96 return list(ctx, c, "/tasks", filter, func(r tasksResponse) []Task { return r.Tasks })
97}
98
99// GetTask fetches a task by ID. Name and Note will be null (E2EE).
100func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error) {
101 return get(ctx, c, "/tasks", taskID, "task", func(r taskResponse) Task { return r.Task })
102}
103
104// DeleteTask removes a task and returns its final state.
105func (c *Client) DeleteTask(ctx context.Context, taskID string) (*Task, error) {
106 return del(ctx, c, "/tasks", taskID, "task", func(r taskResponse) Task { return r.Task })
107}
108
109// TaskBuilder constructs and creates a task via method chaining.
110//
111// task, err := lunatask.NewTask("Review PR").
112// InArea(areaID).
113// WithStatus(lunatask.StatusNext).
114// WithEstimate(30).
115// Create(ctx, client)
116type TaskBuilder struct {
117 req createTaskRequest
118}
119
120// NewTask starts building a task with the given name.
121func NewTask(name string) *TaskBuilder {
122 return &TaskBuilder{req: createTaskRequest{Name: name}} //nolint:exhaustruct
123}
124
125// InArea assigns the task to an area. IDs are in the area's settings in the app.
126func (b *TaskBuilder) InArea(areaID string) *TaskBuilder {
127 b.req.AreaID = &areaID
128
129 return b
130}
131
132// InGoal assigns the task to a goal. IDs are in the goal's settings in the app.
133func (b *TaskBuilder) InGoal(goalID string) *TaskBuilder {
134 b.req.GoalID = &goalID
135
136 return b
137}
138
139// WithNote attaches a Markdown note to the task.
140func (b *TaskBuilder) WithNote(note string) *TaskBuilder {
141 b.req.Note = ¬e
142
143 return b
144}
145
146// WithStatus sets the workflow status.
147// Use one of the Status* constants (e.g., [StatusNext]).
148func (b *TaskBuilder) WithStatus(status TaskStatus) *TaskBuilder {
149 b.req.Status = &status
150
151 return b
152}
153
154// WithMotivation sets why this task matters.
155// Use one of the Motivation* constants (e.g., [MotivationMust]).
156func (b *TaskBuilder) WithMotivation(motivation Motivation) *TaskBuilder {
157 b.req.Motivation = &motivation
158
159 return b
160}
161
162// WithEstimate sets the expected duration in minutes (0–720).
163func (b *TaskBuilder) WithEstimate(minutes int) *TaskBuilder {
164 b.req.Estimate = &minutes
165
166 return b
167}
168
169// WithPriority sets importance from -2 (lowest) to 2 (highest).
170func (b *TaskBuilder) WithPriority(priority int) *TaskBuilder {
171 b.req.Priority = &priority
172
173 return b
174}
175
176// WithEisenhower sets the Eisenhower matrix quadrant:
177// 0=uncategorized, 1=urgent+important, 2=urgent, 3=important, 4=neither.
178func (b *TaskBuilder) WithEisenhower(eisenhower int) *TaskBuilder {
179 b.req.Eisenhower = &eisenhower
180
181 return b
182}
183
184// ScheduledOn sets when the task should appear on your schedule.
185func (b *TaskBuilder) ScheduledOn(date Date) *TaskBuilder {
186 b.req.ScheduledOn = &date
187
188 return b
189}
190
191// CompletedAt marks the task completed at a specific time.
192func (b *TaskBuilder) CompletedAt(t time.Time) *TaskBuilder {
193 b.req.CompletedAt = &t
194
195 return b
196}
197
198// FromSource tags the task with a free-form origin identifier, useful for
199// tracking tasks created by scripts or external integrations.
200func (b *TaskBuilder) FromSource(source, sourceID string) *TaskBuilder {
201 b.req.Source = &source
202 b.req.SourceID = &sourceID
203
204 return b
205}
206
207// Create sends the task to Lunatask. Returns (nil, nil) if a not-completed
208// task already exists in the same area with matching source/source_id.
209func (b *TaskBuilder) Create(ctx context.Context, c *Client) (*Task, error) {
210 if b.req.Name == "" {
211 return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
212 }
213
214 return create(ctx, c, "/tasks", b.req, func(r taskResponse) Task { return r.Task })
215}
216
217// TaskUpdateBuilder constructs and updates a task via method chaining.
218// Only fields you set will be modified; others remain unchanged.
219//
220// task, err := lunatask.NewTaskUpdate(taskID).
221// WithStatus(lunatask.StatusCompleted).
222// CompletedAt(time.Now()).
223// Update(ctx, client)
224type TaskUpdateBuilder struct {
225 taskID string
226 req updateTaskRequest
227}
228
229// NewTaskUpdate starts building a task update for the given task ID.
230func NewTaskUpdate(taskID string) *TaskUpdateBuilder {
231 return &TaskUpdateBuilder{taskID: taskID} //nolint:exhaustruct
232}
233
234// Name changes the task's name.
235func (b *TaskUpdateBuilder) Name(name string) *TaskUpdateBuilder {
236 b.req.Name = &name
237
238 return b
239}
240
241// InArea moves the task to an area. IDs are in the area's settings in the app.
242func (b *TaskUpdateBuilder) InArea(areaID string) *TaskUpdateBuilder {
243 b.req.AreaID = &areaID
244
245 return b
246}
247
248// InGoal moves the task to a goal. IDs are in the goal's settings in the app.
249func (b *TaskUpdateBuilder) InGoal(goalID string) *TaskUpdateBuilder {
250 b.req.GoalID = &goalID
251
252 return b
253}
254
255// WithNote replaces the task's Markdown note.
256func (b *TaskUpdateBuilder) WithNote(note string) *TaskUpdateBuilder {
257 b.req.Note = ¬e
258
259 return b
260}
261
262// WithStatus sets the workflow status.
263// Use one of the Status* constants (e.g., [StatusNext]).
264func (b *TaskUpdateBuilder) WithStatus(status TaskStatus) *TaskUpdateBuilder {
265 b.req.Status = &status
266
267 return b
268}
269
270// WithMotivation sets why this task matters.
271// Use one of the Motivation* constants (e.g., [MotivationMust]).
272func (b *TaskUpdateBuilder) WithMotivation(motivation Motivation) *TaskUpdateBuilder {
273 b.req.Motivation = &motivation
274
275 return b
276}
277
278// WithEstimate sets the expected duration in minutes (0–720).
279func (b *TaskUpdateBuilder) WithEstimate(minutes int) *TaskUpdateBuilder {
280 b.req.Estimate = &minutes
281
282 return b
283}
284
285// WithPriority sets importance from -2 (lowest) to 2 (highest).
286func (b *TaskUpdateBuilder) WithPriority(priority int) *TaskUpdateBuilder {
287 b.req.Priority = &priority
288
289 return b
290}
291
292// WithEisenhower sets the Eisenhower matrix quadrant:
293// 0=uncategorized, 1=urgent+important, 2=urgent, 3=important, 4=neither.
294func (b *TaskUpdateBuilder) WithEisenhower(eisenhower int) *TaskUpdateBuilder {
295 b.req.Eisenhower = &eisenhower
296
297 return b
298}
299
300// ScheduledOn sets when the task should appear on your schedule.
301func (b *TaskUpdateBuilder) ScheduledOn(date Date) *TaskUpdateBuilder {
302 b.req.ScheduledOn = &date
303
304 return b
305}
306
307// CompletedAt marks the task completed at a specific time.
308func (b *TaskUpdateBuilder) CompletedAt(t time.Time) *TaskUpdateBuilder {
309 b.req.CompletedAt = &t
310
311 return b
312}
313
314// Update sends the changes to Lunatask.
315func (b *TaskUpdateBuilder) Update(ctx context.Context, c *Client) (*Task, error) {
316 return update(ctx, c, "/tasks", b.taskID, "task", b.req, func(r taskResponse) Task { return r.Task })
317}