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 is a task in Lunatask. Name and Note are encrypted client-side
16// and will be null when read back from the API.
17type Task struct {
18 ID string `json:"id"`
19 AreaID *string `json:"area_id"`
20 GoalID *string `json:"goal_id"`
21 Name *string `json:"name"`
22 Note *string `json:"note"`
23 Status *string `json:"status"`
24 PreviousStatus *string `json:"previous_status"`
25 Estimate *int `json:"estimate"`
26 Priority *int `json:"priority"`
27 Progress *int `json:"progress"`
28 Motivation *string `json:"motivation"`
29 Eisenhower *int `json:"eisenhower"`
30 Sources []Source `json:"sources"`
31 ScheduledOn *Date `json:"scheduled_on"`
32 CompletedAt *time.Time `json:"completed_at"`
33 CreatedAt time.Time `json:"created_at"`
34 UpdatedAt time.Time `json:"updated_at"`
35}
36
37// CreateTaskRequest defines a new task.
38// Use [TaskBuilder] for a fluent construction API.
39type CreateTaskRequest struct {
40 Name string `json:"name"`
41 AreaID *string `json:"area_id,omitempty"`
42 GoalID *string `json:"goal_id,omitempty"`
43 Note *string `json:"note,omitempty"`
44 Status *string `json:"status,omitempty"`
45 Motivation *string `json:"motivation,omitempty"`
46 Estimate *int `json:"estimate,omitempty"`
47 Priority *int `json:"priority,omitempty"`
48 Eisenhower *int `json:"eisenhower,omitempty"`
49 ScheduledOn *Date `json:"scheduled_on,omitempty"`
50 CompletedAt *time.Time `json:"completed_at,omitempty"`
51 Source *string `json:"source,omitempty"`
52 SourceID *string `json:"source_id,omitempty"`
53}
54
55// UpdateTaskRequest specifies which fields to change on a task.
56// Only non-nil fields are updated. Use [TaskUpdateBuilder] for fluent construction.
57type UpdateTaskRequest struct {
58 Name *string `json:"name,omitempty"`
59 AreaID *string `json:"area_id,omitempty"`
60 GoalID *string `json:"goal_id,omitempty"`
61 Note *string `json:"note,omitempty"`
62 Status *string `json:"status,omitempty"`
63 Motivation *string `json:"motivation,omitempty"`
64 Estimate *int `json:"estimate,omitempty"`
65 Priority *int `json:"priority,omitempty"`
66 Eisenhower *int `json:"eisenhower,omitempty"`
67 ScheduledOn *Date `json:"scheduled_on,omitempty"`
68 CompletedAt *time.Time `json:"completed_at,omitempty"`
69}
70
71// taskResponse wraps a single task from the API.
72type taskResponse struct {
73 Task Task `json:"task"`
74}
75
76// tasksResponse wraps a list of tasks from the API.
77type tasksResponse struct {
78 Tasks []Task `json:"tasks"`
79}
80
81// ListTasksOptions filters tasks by source integration.
82type ListTasksOptions struct {
83 Source *string
84 SourceID *string
85}
86
87// ListTasks returns all tasks, optionally filtered. Pass nil for all.
88func (c *Client) ListTasks(ctx context.Context, opts *ListTasksOptions) ([]Task, error) {
89 path := "/tasks"
90
91 if opts != nil {
92 params := url.Values{}
93 if opts.Source != nil && *opts.Source != "" {
94 params.Set("source", *opts.Source)
95 }
96
97 if opts.SourceID != nil && *opts.SourceID != "" {
98 params.Set("source_id", *opts.SourceID)
99 }
100
101 if len(params) > 0 {
102 path = fmt.Sprintf("%s?%s", path, params.Encode())
103 }
104 }
105
106 resp, _, err := doJSON[tasksResponse](ctx, c, http.MethodGet, path, nil)
107 if err != nil {
108 return nil, err
109 }
110
111 return resp.Tasks, nil
112}
113
114// GetTask fetches a task by ID. Name and Note will be null (E2EE).
115func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error) {
116 if taskID == "" {
117 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
118 }
119
120 resp, _, err := doJSON[taskResponse](ctx, c, http.MethodGet, "/tasks/"+taskID, nil)
121 if err != nil {
122 return nil, err
123 }
124
125 return &resp.Task, nil
126}
127
128// CreateTask adds a task. Returns (nil, nil) if a duplicate exists
129// with matching source/source_id.
130func (c *Client) CreateTask(ctx context.Context, task *CreateTaskRequest) (*Task, error) {
131 if task.Name == "" {
132 return nil, fmt.Errorf("%w: name is required", ErrBadRequest)
133 }
134
135 resp, noContent, err := doJSON[taskResponse](ctx, c, http.MethodPost, "/tasks", task)
136 if err != nil {
137 return nil, err
138 }
139
140 if noContent {
141 // Intentional: duplicate exists (HTTP 204), not an error
142 return nil, nil //nolint:nilnil
143 }
144
145 return &resp.Task, nil
146}
147
148// UpdateTask modifies a task. Only non-nil fields in the request are changed.
149func (c *Client) UpdateTask(ctx context.Context, taskID string, task *UpdateTaskRequest) (*Task, error) {
150 if taskID == "" {
151 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
152 }
153
154 resp, _, err := doJSON[taskResponse](ctx, c, http.MethodPut, "/tasks/"+taskID, task)
155 if err != nil {
156 return nil, err
157 }
158
159 return &resp.Task, nil
160}
161
162// DeleteTask removes a task and returns its final state.
163func (c *Client) DeleteTask(ctx context.Context, taskID string) (*Task, error) {
164 if taskID == "" {
165 return nil, fmt.Errorf("%w: task ID cannot be empty", ErrBadRequest)
166 }
167
168 resp, _, err := doJSON[taskResponse](ctx, c, http.MethodDelete, "/tasks/"+taskID, nil)
169 if err != nil {
170 return nil, err
171 }
172
173 return &resp.Task, nil
174}