1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5package lunatask
6
7import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "errors"
12 "fmt"
13 "io"
14 "net/http"
15 "strings"
16
17 "github.com/go-playground/validator/v10"
18)
19
20// TrackHabitActivityRequest represents the request to track a habit activity
21type TrackHabitActivityRequest struct {
22 PerformedOn string `json:"performed_on" validate:"required,datetime=2006-01-02T15:04:05Z07:00"`
23}
24
25// TrackHabitActivityResponse represents the response from Lunatask API when tracking a habit activity
26type TrackHabitActivityResponse struct {
27 Status string `json:"status"`
28 Message string `json:"message,omitempty"`
29}
30
31// ValidateTrackHabitActivity validates the track habit activity request
32func ValidateTrackHabitActivity(request *TrackHabitActivityRequest) error {
33 validate := validator.New()
34 if err := validate.Struct(request); err != nil {
35 var invalidValidationError *validator.InvalidValidationError
36 if errors.As(err, &invalidValidationError) {
37 return fmt.Errorf("invalid validation error: %w", err)
38 }
39
40 var validationErrs validator.ValidationErrors
41 if errors.As(err, &validationErrs) {
42 var msgBuilder strings.Builder
43 msgBuilder.WriteString("habit activity validation failed:")
44 for _, e := range validationErrs {
45 fmt.Fprintf(&msgBuilder, " field '%s' failed '%s' validation (was: %v);", e.Field(), e.Tag(), e.Value())
46 }
47 return errors.New(msgBuilder.String())
48 }
49 return fmt.Errorf("validation error: %w", err)
50 }
51 return nil
52}
53
54// TrackHabitActivity tracks an activity for a habit in Lunatask
55func (c *Client) TrackHabitActivity(ctx context.Context, habitID string, request *TrackHabitActivityRequest) (*TrackHabitActivityResponse, error) {
56 if habitID == "" {
57 return nil, errors.New("habit ID cannot be empty")
58 }
59
60 // Validate the request
61 if err := ValidateTrackHabitActivity(request); err != nil {
62 return nil, err
63 }
64
65 // Marshal the request to JSON
66 payloadBytes, err := json.Marshal(request)
67 if err != nil {
68 return nil, fmt.Errorf("failed to marshal payload: %w", err)
69 }
70
71 // Create the request
72 req, err := http.NewRequestWithContext(
73 ctx,
74 "POST",
75 fmt.Sprintf("%s/habits/%s/track", c.BaseURL, habitID),
76 bytes.NewBuffer(payloadBytes),
77 )
78 if err != nil {
79 return nil, fmt.Errorf("failed to create HTTP request: %w", err)
80 }
81
82 // Set headers
83 req.Header.Set("Content-Type", "application/json")
84 req.Header.Set("Authorization", "bearer "+c.AccessToken)
85
86 // Send the request
87 resp, err := c.HTTPClient.Do(req)
88 if err != nil {
89 return nil, fmt.Errorf("failed to send HTTP request: %w", err)
90 }
91 defer func() {
92 if resp.Body != nil {
93 if err := resp.Body.Close(); err != nil {
94 // We're in a defer, so we can only log the error
95 fmt.Printf("Error closing response body: %v\n", err)
96 }
97 }
98 }()
99
100 // Handle error status codes
101 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
102 respBody, _ := io.ReadAll(resp.Body)
103 return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(respBody))
104 }
105
106 // Read and parse the response
107 respBody, err := io.ReadAll(resp.Body)
108 if err != nil {
109 return nil, fmt.Errorf("failed to read response body: %w", err)
110 }
111
112 var response TrackHabitActivityResponse
113 err = json.Unmarshal(respBody, &response)
114 if err != nil {
115 return nil, fmt.Errorf("failed to parse response: %w", err)
116 }
117
118 return &response, nil
119}