habits.go

  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"`
 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}