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// Note is a note in Lunatask. Name and Content are encrypted client-side
16// and will be null when read back from the API.
17type Note struct {
18 ID string `json:"id"`
19 NotebookID *string `json:"notebook_id"`
20 DateOn *Date `json:"date_on"`
21 Sources []Source `json:"sources"`
22 CreatedAt time.Time `json:"created_at"`
23 UpdatedAt time.Time `json:"updated_at"`
24}
25
26// CreateNoteRequest defines a new note.
27// Use [NoteBuilder] for a fluent construction API.
28type CreateNoteRequest struct {
29 Name *string `json:"name,omitempty"`
30 Content *string `json:"content,omitempty"`
31 NotebookID *string `json:"notebook_id,omitempty"`
32 Source *string `json:"source,omitempty"`
33 SourceID *string `json:"source_id,omitempty"`
34}
35
36// UpdateNoteRequest specifies which fields to change on a note.
37// Only non-nil fields are updated. Content replaces entirely (E2EE prevents appending).
38type UpdateNoteRequest struct {
39 Name *string `json:"name,omitempty"`
40 Content *string `json:"content,omitempty"`
41 NotebookID *string `json:"notebook_id,omitempty"`
42 DateOn *Date `json:"date_on,omitempty"`
43}
44
45// noteResponse wraps a single note from the API.
46type noteResponse struct {
47 Note Note `json:"note"`
48}
49
50// notesResponse wraps a list of notes from the API.
51type notesResponse struct {
52 Notes []Note `json:"notes"`
53}
54
55// ListNotesOptions filters notes by source integration.
56type ListNotesOptions struct {
57 Source *string
58 SourceID *string
59}
60
61// ListNotes returns all notes, optionally filtered. Pass nil for all notes.
62func (c *Client) ListNotes(ctx context.Context, opts *ListNotesOptions) ([]Note, error) {
63 path := "/notes"
64
65 if opts != nil {
66 params := url.Values{}
67 if opts.Source != nil && *opts.Source != "" {
68 params.Set("source", *opts.Source)
69 }
70
71 if opts.SourceID != nil && *opts.SourceID != "" {
72 params.Set("source_id", *opts.SourceID)
73 }
74
75 if len(params) > 0 {
76 path = fmt.Sprintf("%s?%s", path, params.Encode())
77 }
78 }
79
80 resp, _, err := doJSON[notesResponse](ctx, c, http.MethodGet, path, nil)
81 if err != nil {
82 return nil, err
83 }
84
85 return resp.Notes, nil
86}
87
88// GetNote fetches a note by ID. Name and Content will be null (E2EE).
89func (c *Client) GetNote(ctx context.Context, noteID string) (*Note, error) {
90 if noteID == "" {
91 return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
92 }
93
94 resp, _, err := doJSON[noteResponse](ctx, c, http.MethodGet, "/notes/"+noteID, nil)
95 if err != nil {
96 return nil, err
97 }
98
99 return &resp.Note, nil
100}
101
102// CreateNote adds a note. Returns (nil, nil) if a duplicate exists
103// in the same notebook with matching source/source_id.
104func (c *Client) CreateNote(ctx context.Context, note *CreateNoteRequest) (*Note, error) {
105 resp, noContent, err := doJSON[noteResponse](ctx, c, http.MethodPost, "/notes", note)
106 if err != nil {
107 return nil, err
108 }
109
110 if noContent {
111 // Intentional: duplicate exists (HTTP 204), not an error
112 return nil, nil //nolint:nilnil
113 }
114
115 return &resp.Note, nil
116}
117
118// UpdateNote modifies a note. Only non-nil fields in the request are changed.
119func (c *Client) UpdateNote(ctx context.Context, noteID string, note *UpdateNoteRequest) (*Note, error) {
120 if noteID == "" {
121 return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
122 }
123
124 resp, _, err := doJSON[noteResponse](ctx, c, http.MethodPut, "/notes/"+noteID, note)
125 if err != nil {
126 return nil, err
127 }
128
129 return &resp.Note, nil
130}
131
132// DeleteNote removes a note and returns its final state.
133func (c *Client) DeleteNote(ctx context.Context, noteID string) (*Note, error) {
134 if noteID == "" {
135 return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
136 }
137
138 resp, _, err := doJSON[noteResponse](ctx, c, http.MethodDelete, "/notes/"+noteID, nil)
139 if err != nil {
140 return nil, err
141 }
142
143 return &resp.Note, nil
144}