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