馃尫 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}