馃尫 the cutsie hackatime helper
1// Package wakatime provides a Go client for interacting with the WakaTime API.
2// WakaTime is a time tracking service for programmers that automatically tracks
3// how much time is spent on coding projects.
4package wakatime
5
6import (
7 "bytes"
8 "encoding/json"
9 "fmt"
10 "net/http"
11 "time"
12)
13
14// Default API URL for the WakaTime API v1
15const (
16 DefaultAPIURL = "https://api.wakatime.com/api/v1"
17)
18
19// Error types returned by the client
20var (
21 // ErrMarshalingHeartbeat occurs when a heartbeat can't be marshaled to JSON
22 ErrMarshalingHeartbeat = fmt.Errorf("failed to marshal heartbeat to JSON")
23 // ErrCreatingRequest occurs when the HTTP request cannot be created
24 ErrCreatingRequest = fmt.Errorf("failed to create HTTP request")
25 // ErrSendingRequest occurs when the HTTP request fails to send
26 ErrSendingRequest = fmt.Errorf("failed to send HTTP request")
27 // ErrInvalidStatusCode occurs when the API returns a non-success status code
28 ErrInvalidStatusCode = fmt.Errorf("received invalid status code from API")
29 // ErrDecodingResponse occurs when the API response can't be decoded
30 ErrDecodingResponse = fmt.Errorf("failed to decode API response")
31 // ErrUnauthorized occurs when the API rejects the provided credentials
32 ErrUnauthorized = fmt.Errorf("unauthorized: invalid API key or insufficient permissions")
33)
34
35// Client represents a WakaTime API client with authentication and connection settings.
36type Client struct {
37 // APIKey is the user's WakaTime API key used for authentication
38 APIKey string
39 // APIURL is the base URL for the WakaTime API
40 APIURL string
41 // HTTPClient is the HTTP client used to make requests to the WakaTime API
42 HTTPClient *http.Client
43}
44
45// NewClient creates a new WakaTime API client with the provided API key
46// and a default HTTP client with a 10-second timeout.
47func NewClient(apiKey string) *Client {
48 return &Client{
49 APIKey: apiKey,
50 APIURL: DefaultAPIURL,
51 HTTPClient: &http.Client{Timeout: 10 * time.Second},
52 }
53}
54
55// NewClientWithOptions creates a new WakaTime API client with the provided API key,
56// custom API URL and a default HTTP client with a 10-second timeout.
57func NewClientWithOptions(apiKey string, apiURL string) *Client {
58 return &Client{
59 APIKey: apiKey,
60 APIURL: apiURL,
61 HTTPClient: &http.Client{Timeout: 10 * time.Second},
62 }
63}
64
65// Heartbeat represents a coding activity heartbeat sent to the WakaTime API.
66// Heartbeats are the core data structure for tracking time spent coding.
67type Heartbeat struct {
68 // Entity is the file path or resource being worked on
69 Entity string `json:"entity"`
70 // Type specifies the entity type (usually "file")
71 Type string `json:"type"`
72 // Time is the timestamp of the heartbeat in UNIX epoch format
73 Time float64 `json:"time"`
74 // Project is the optional project name associated with the entity
75 Project string `json:"project,omitempty"`
76 // Language is the optional programming language of the entity
77 Language string `json:"language,omitempty"`
78 // IsWrite indicates if the file was being written to (vs. just viewed)
79 IsWrite bool `json:"is_write,omitempty"`
80 // EditorName is the optional name of the editor or IDE being used
81 EditorName string `json:"editor_name,omitempty"`
82}
83
84// StatusBarResponse represents the response from the WakaTime Status Bar API endpoint.
85// This contains summary information about a user's coding activity for a specific time period.
86type StatusBarResponse struct {
87 // Data contains coding duration information
88 Data struct {
89 // GrandTotal contains the aggregated coding time information
90 GrandTotal struct {
91 // Text is the human-readable representation of the total coding time
92 // Example: "3 hrs 42 mins"
93 Text string `json:"text"`
94 // TotalSeconds is the total time spent coding in seconds
95 // This can be used for precise calculations or custom formatting
96 TotalSeconds int `json:"total_seconds"`
97 } `json:"grand_total"`
98 } `json:"data"`
99}
100
101// SendHeartbeat sends a coding activity heartbeat to the WakaTime API.
102// It returns an error if the request fails or returns a non-success status code.
103func (c *Client) SendHeartbeat(heartbeat Heartbeat) error {
104 data, err := json.Marshal(heartbeat)
105 if err != nil {
106 return fmt.Errorf("%w: %v", ErrMarshalingHeartbeat, err)
107 }
108
109 req, err := http.NewRequest("POST", c.APIURL+"/users/current/heartbeats", bytes.NewBuffer(data))
110 if err != nil {
111 return fmt.Errorf("%w: %v", ErrCreatingRequest, err)
112 }
113
114 req.Header.Set("Content-Type", "application/json")
115 req.Header.Set("Authorization", "Basic "+c.APIKey)
116
117 resp, err := c.HTTPClient.Do(req)
118 if err != nil {
119 return fmt.Errorf("%w: %v", ErrSendingRequest, err)
120 }
121 defer resp.Body.Close()
122
123 if resp.StatusCode == http.StatusUnauthorized {
124 return ErrUnauthorized
125 } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
126 return fmt.Errorf("%w: status code %d", ErrInvalidStatusCode, resp.StatusCode)
127 }
128
129 return nil
130}
131
132// GetStatusBar retrieves a user's current day coding activity summary from the WakaTime API.
133// It returns an error if the request fails or returns a non-success status code.
134func (c *Client) GetStatusBar() (StatusBarResponse, error) {
135 req, err := http.NewRequest("GET", fmt.Sprintf("%s/users/current/statusbar/today", c.APIURL), nil)
136 if err != nil {
137 return StatusBarResponse{}, fmt.Errorf("%w: %v", ErrCreatingRequest, err)
138 }
139
140 req.Header.Set("Authorization", "Basic "+c.APIKey)
141
142 resp, err := c.HTTPClient.Do(req)
143 if err != nil {
144 return StatusBarResponse{}, fmt.Errorf("%w: %v", ErrSendingRequest, err)
145 }
146 defer resp.Body.Close()
147
148 if resp.StatusCode == http.StatusUnauthorized {
149 return StatusBarResponse{}, ErrUnauthorized
150 } else if resp.StatusCode < 200 || resp.StatusCode >= 300 {
151 return StatusBarResponse{}, fmt.Errorf("%w: status code %d", ErrInvalidStatusCode, resp.StatusCode)
152 }
153
154 var durationResp StatusBarResponse
155 if err := json.NewDecoder(resp.Body).Decode(&durationResp); err != nil {
156 return StatusBarResponse{}, fmt.Errorf("%w: %v", ErrDecodingResponse, err)
157 }
158
159 return durationResp, nil
160}