people.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// Person is a contact in Lunatask's relationship tracker.
 16// FirstName and LastName are encrypted client-side and will be null when read.
 17type Person struct {
 18	ID                   string    `json:"id"`
 19	RelationshipStrength *string   `json:"relationship_strength"`
 20	Sources              []Source  `json:"sources"`
 21	CreatedAt            time.Time `json:"created_at"`
 22	UpdatedAt            time.Time `json:"updated_at"`
 23}
 24
 25// CreatePersonRequest defines a new person.
 26// Use [PersonBuilder] for a fluent construction API.
 27type CreatePersonRequest struct {
 28	FirstName            *string `json:"first_name,omitempty"`
 29	LastName             *string `json:"last_name,omitempty"`
 30	RelationshipStrength *string `json:"relationship_strength,omitempty"`
 31	Source               *string `json:"source,omitempty"`
 32	SourceID             *string `json:"source_id,omitempty"`
 33}
 34
 35// personResponse wraps a single person from the API.
 36type personResponse struct {
 37	Person Person `json:"person"`
 38}
 39
 40// peopleResponse wraps a list of people from the API.
 41type peopleResponse struct {
 42	People []Person `json:"people"`
 43}
 44
 45// ListPeopleOptions filters people by source integration.
 46type ListPeopleOptions struct {
 47	Source   *string
 48	SourceID *string
 49}
 50
 51// ListPeople returns all people, optionally filtered. Pass nil for all.
 52func (c *Client) ListPeople(ctx context.Context, opts *ListPeopleOptions) ([]Person, error) {
 53	path := "/people"
 54
 55	if opts != nil {
 56		params := url.Values{}
 57		if opts.Source != nil && *opts.Source != "" {
 58			params.Set("source", *opts.Source)
 59		}
 60
 61		if opts.SourceID != nil && *opts.SourceID != "" {
 62			params.Set("source_id", *opts.SourceID)
 63		}
 64
 65		if len(params) > 0 {
 66			path = fmt.Sprintf("%s?%s", path, params.Encode())
 67		}
 68	}
 69
 70	resp, _, err := doJSON[peopleResponse](ctx, c, http.MethodGet, path, nil)
 71	if err != nil {
 72		return nil, err
 73	}
 74
 75	return resp.People, nil
 76}
 77
 78// GetPerson fetches a person by ID. Name fields will be null (E2EE).
 79func (c *Client) GetPerson(ctx context.Context, personID string) (*Person, error) {
 80	if personID == "" {
 81		return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
 82	}
 83
 84	resp, _, err := doJSON[personResponse](ctx, c, http.MethodGet, "/people/"+personID, nil)
 85	if err != nil {
 86		return nil, err
 87	}
 88
 89	return &resp.Person, nil
 90}
 91
 92// CreatePerson adds a person. Returns (nil, nil) if a duplicate exists
 93// with matching source/source_id.
 94func (c *Client) CreatePerson(ctx context.Context, person *CreatePersonRequest) (*Person, error) {
 95	resp, noContent, err := doJSON[personResponse](ctx, c, http.MethodPost, "/people", person)
 96	if err != nil {
 97		return nil, err
 98	}
 99
100	if noContent {
101		// Intentional: duplicate exists (HTTP 204), not an error
102		return nil, nil //nolint:nilnil
103	}
104
105	return &resp.Person, nil
106}
107
108// DeletePerson removes a person and returns their final state.
109func (c *Client) DeletePerson(ctx context.Context, personID string) (*Person, error) {
110	if personID == "" {
111		return nil, fmt.Errorf("%w: person ID cannot be empty", ErrBadRequest)
112	}
113
114	resp, _, err := doJSON[personResponse](ctx, c, http.MethodDelete, "/people/"+personID, nil)
115	if err != nil {
116		return nil, err
117	}
118
119	return &resp.Person, nil
120}