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}