@@ -3,3 +3,129 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package lunatask
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// Note represents a note returned from the Lunatask API.
+// Note: name and content are E2EE and not returned by the API.
+type Note struct {
+ ID string `json:"id"`
+ NotebookID string `json:"notebook_id"`
+ DateOn *Date `json:"date_on"`
+ Sources []Source `json:"sources"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// CreateNoteRequest represents the request to create a note in Lunatask.
+type CreateNoteRequest struct {
+ Name *string `json:"name,omitempty"`
+ Content *string `json:"content,omitempty"`
+ NotebookID string `json:"notebook_id"`
+ Source *string `json:"source,omitempty"`
+ SourceID *string `json:"source_id,omitempty"`
+}
+
+// UpdateNoteRequest represents the request to update a note in Lunatask.
+// All fields are optional; only provided fields will be updated.
+// Note: updating content replaces the entire content (E2EE prevents appending).
+type UpdateNoteRequest struct {
+ Name *string `json:"name,omitempty"`
+ Content *string `json:"content,omitempty"`
+ NotebookID *string `json:"notebook_id,omitempty"`
+ DateOn *Date `json:"date_on,omitempty"`
+}
+
+// noteResponse represents a single note response from the API.
+type noteResponse struct {
+ Note Note `json:"note"`
+}
+
+// notesResponse represents a list of notes response from the API.
+type notesResponse struct {
+ Notes []Note `json:"notes"`
+}
+
+// ListNotesOptions contains optional filters for listing notes.
+type ListNotesOptions struct {
+ Source *string
+ SourceID *string
+}
+
+// ListNotes retrieves all notes, optionally filtered by source and/or source_id.
+func (c *Client) ListNotes(ctx context.Context, opts *ListNotesOptions) ([]Note, error) {
+ path := "/notes"
+
+ if opts != nil {
+ params := url.Values{}
+ if opts.Source != nil && *opts.Source != "" {
+ params.Set("source", *opts.Source)
+ }
+ if opts.SourceID != nil && *opts.SourceID != "" {
+ params.Set("source_id", *opts.SourceID)
+ }
+ if len(params) > 0 {
+ path = fmt.Sprintf("%s?%s", path, params.Encode())
+ }
+ }
+
+ resp, _, err := doJSON[notesResponse](c, ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.Notes, nil
+}
+
+// CreateNote creates a new note in Lunatask.
+// Returns nil, nil if a matching note already exists in the same notebook
+// with the same source/source_id (HTTP 204).
+func (c *Client) CreateNote(ctx context.Context, note *CreateNoteRequest) (*Note, error) {
+ if note.NotebookID == "" {
+ return nil, fmt.Errorf("%w: notebook_id is required", ErrBadRequest)
+ }
+
+ resp, noContent, err := doJSON[noteResponse](c, ctx, http.MethodPost, "/notes", note)
+ if err != nil {
+ return nil, err
+ }
+ if noContent {
+ return nil, nil
+ }
+
+ return &resp.Note, nil
+}
+
+// UpdateNote updates an existing note in Lunatask.
+func (c *Client) UpdateNote(ctx context.Context, noteID string, note *UpdateNoteRequest) (*Note, error) {
+ if noteID == "" {
+ return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
+ }
+
+ resp, _, err := doJSON[noteResponse](c, ctx, http.MethodPut, "/notes/"+noteID, note)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.Note, nil
+}
+
+// DeleteNote deletes a note in Lunatask.
+func (c *Client) DeleteNote(ctx context.Context, noteID string) (*Note, error) {
+ if noteID == "" {
+ return nil, fmt.Errorf("%w: note ID cannot be empty", ErrBadRequest)
+ }
+
+ resp, _, err := doJSON[noteResponse](c, ctx, http.MethodDelete, "/notes/"+noteID, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.Note, nil
+}