notes.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// 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}