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	"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}