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	"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 = &note
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 matrix quadrant (0–4).
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 duplicate exists
207// with matching source/source_id.
208func (b *TaskBuilder) Create(ctx context.Context, c *Client) (*Task, error) {
209	if b.req.Name == "" {
210		return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
211	}
212
213	return create(ctx, c, "/tasks", b.req, func(r taskResponse) Task { return r.Task })
214}
215
216// TaskUpdateBuilder constructs and updates a task via method chaining.
217// Only fields you set will be modified; others remain unchanged.
218//
219//	task, err := lunatask.NewTaskUpdate(taskID).
220//		WithStatus(lunatask.StatusCompleted).
221//		CompletedAt(time.Now()).
222//		Update(ctx, client)
223type TaskUpdateBuilder struct {
224	taskID string
225	req    updateTaskRequest
226}
227
228// NewTaskUpdate starts building a task update for the given task ID.
229func NewTaskUpdate(taskID string) *TaskUpdateBuilder {
230	return &TaskUpdateBuilder{taskID: taskID} //nolint:exhaustruct
231}
232
233// Name changes the task's name.
234func (b *TaskUpdateBuilder) Name(name string) *TaskUpdateBuilder {
235	b.req.Name = &name
236
237	return b
238}
239
240// InArea moves the task to an area. IDs are in the area's settings in the app.
241func (b *TaskUpdateBuilder) InArea(areaID string) *TaskUpdateBuilder {
242	b.req.AreaID = &areaID
243
244	return b
245}
246
247// InGoal moves the task to a goal. IDs are in the goal's settings in the app.
248func (b *TaskUpdateBuilder) InGoal(goalID string) *TaskUpdateBuilder {
249	b.req.GoalID = &goalID
250
251	return b
252}
253
254// WithNote replaces the task's Markdown note.
255func (b *TaskUpdateBuilder) WithNote(note string) *TaskUpdateBuilder {
256	b.req.Note = &note
257
258	return b
259}
260
261// WithStatus sets the workflow status.
262// Use one of the Status* constants (e.g., [StatusNext]).
263func (b *TaskUpdateBuilder) WithStatus(status TaskStatus) *TaskUpdateBuilder {
264	b.req.Status = &status
265
266	return b
267}
268
269// WithMotivation sets why this task matters.
270// Use one of the Motivation* constants (e.g., [MotivationMust]).
271func (b *TaskUpdateBuilder) WithMotivation(motivation Motivation) *TaskUpdateBuilder {
272	b.req.Motivation = &motivation
273
274	return b
275}
276
277// WithEstimate sets the expected duration in minutes (0–720).
278func (b *TaskUpdateBuilder) WithEstimate(minutes int) *TaskUpdateBuilder {
279	b.req.Estimate = &minutes
280
281	return b
282}
283
284// WithPriority sets importance from -2 (lowest) to 2 (highest).
285func (b *TaskUpdateBuilder) WithPriority(priority int) *TaskUpdateBuilder {
286	b.req.Priority = &priority
287
288	return b
289}
290
291// WithEisenhower sets the matrix quadrant (0–4).
292func (b *TaskUpdateBuilder) WithEisenhower(eisenhower int) *TaskUpdateBuilder {
293	b.req.Eisenhower = &eisenhower
294
295	return b
296}
297
298// ScheduledOn sets when the task should appear on your schedule.
299func (b *TaskUpdateBuilder) ScheduledOn(date Date) *TaskUpdateBuilder {
300	b.req.ScheduledOn = &date
301
302	return b
303}
304
305// CompletedAt marks the task completed at a specific time.
306func (b *TaskUpdateBuilder) CompletedAt(t time.Time) *TaskUpdateBuilder {
307	b.req.CompletedAt = &t
308
309	return b
310}
311
312// Update sends the changes to Lunatask.
313func (b *TaskUpdateBuilder) Update(ctx context.Context, c *Client) (*Task, error) {
314	return update(ctx, c, "/tasks", b.taskID, "task", b.req, func(r taskResponse) Task { return r.Task })
315}