tasks.go

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