@@ -3,3 +3,114 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package lunatask
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+)
+
+// Person represents a person/relationship returned from the Lunatask API.
+// Note: first_name and last_name are E2EE and not returned by the API.
+type Person struct {
+ ID string `json:"id"`
+ RelationshipStrength *string `json:"relationship_strength"`
+ Sources []Source `json:"sources"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// CreatePersonRequest represents the request to create a person in Lunatask.
+type CreatePersonRequest struct {
+ FirstName *string `json:"first_name,omitempty"`
+ LastName *string `json:"last_name,omitempty"`
+ RelationshipStrength *string `json:"relationship_strength,omitempty"`
+ Source *string `json:"source,omitempty"`
+ SourceID *string `json:"source_id,omitempty"`
+}
+
+// personResponse represents a single person response from the API.
+type personResponse struct {
+ Person Person `json:"person"`
+}
+
+// peopleResponse represents a list of people response from the API.
+type peopleResponse struct {
+ People []Person `json:"people"`
+}
+
+// ListPeopleOptions contains optional filters for listing people.
+type ListPeopleOptions struct {
+ Source *string
+ SourceID *string
+}
+
+// ListPeople retrieves all people, optionally filtered by source and/or source_id.
+func (c *Client) ListPeople(ctx context.Context, opts *ListPeopleOptions) ([]Person, error) {
+ path := "/people"
+
+ 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[peopleResponse](c, ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return resp.People, nil
+}
+
+// GetPerson retrieves a specific person by ID.
+func (c *Client) GetPerson(ctx context.Context, personID string) (*Person, error) {
+ if personID == "" {
+ return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
+ }
+
+ resp, _, err := doJSON[personResponse](c, ctx, http.MethodGet, "/people/"+personID, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.Person, nil
+}
+
+// CreatePerson creates a new person/relationship in Lunatask.
+// Returns nil, nil if a matching person already exists with the same
+// source/source_id (HTTP 204).
+func (c *Client) CreatePerson(ctx context.Context, person *CreatePersonRequest) (*Person, error) {
+ resp, noContent, err := doJSON[personResponse](c, ctx, http.MethodPost, "/people", person)
+ if err != nil {
+ return nil, err
+ }
+ if noContent {
+ return nil, nil
+ }
+
+ return &resp.Person, nil
+}
+
+// DeletePerson deletes a person in Lunatask.
+func (c *Client) DeletePerson(ctx context.Context, personID string) (*Person, error) {
+ if personID == "" {
+ return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
+ }
+
+ resp, _, err := doJSON[personResponse](c, ctx, http.MethodDelete, "/people/"+personID, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.Person, nil
+}
@@ -3,3 +3,48 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
package lunatask
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+// PersonTimelineNote represents a note on a person's memory timeline.
+// Note: content is E2EE and not returned by the API.
+type PersonTimelineNote struct {
+ ID string `json:"id"`
+ DateOn *Date `json:"date_on"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+// CreatePersonTimelineNoteRequest represents the request to create a timeline note.
+type CreatePersonTimelineNoteRequest struct {
+ // PersonID is the ID of the person to add the note to (required).
+ PersonID string `json:"person_id"`
+ // DateOn is the ISO-8601 date for the note (optional, defaults to today).
+ DateOn *Date `json:"date_on,omitempty"`
+ // Content is the Markdown content of the note (optional but impractical if empty).
+ Content *string `json:"content,omitempty"`
+}
+
+// personTimelineNoteResponse represents a single timeline note response from the API.
+type personTimelineNoteResponse struct {
+ PersonTimelineNote PersonTimelineNote `json:"person_timeline_note"`
+}
+
+// CreatePersonTimelineNote creates a new note on a person's memory timeline.
+func (c *Client) CreatePersonTimelineNote(ctx context.Context, note *CreatePersonTimelineNoteRequest) (*PersonTimelineNote, error) {
+ if note.PersonID == "" {
+ return nil, fmt.Errorf("%w: person_id is required", ErrBadRequest)
+ }
+
+ resp, _, err := doJSON[personTimelineNoteResponse](c, ctx, http.MethodPost, "/person_timeline_notes", note)
+ if err != nil {
+ return nil, err
+ }
+
+ return &resp.PersonTimelineNote, nil
+}