forked from AbhiAgarwal/go-rescuetime
/
rescuetime.go
286 lines (264 loc) · 11.3 KB
/
rescuetime.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// Package rescuetime provides a Golang library for the RescueTime API
package rescuetime
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"regexp"
"strconv"
"time"
simplejson "github.com/bitly/go-simplejson"
)
const (
analyticDataURL string = "https://www.rescuetime.com/anapi/data"
dailySummaryURL string = "https://www.rescuetime.com/anapi/daily_summary_feed"
)
// RescueTime contains the user's API key
type RescueTime struct {
APIKey string
}
// DailySummary is a users summary for a single day
type DailySummary struct {
AllDistractingDurationFormatted string `json:"all_distracting_duration_formatted"`
AllDistractingHours float64 `json:"all_distracting_hours"`
AllDistractingPercentage float64 `json:"all_distracting_percentage"`
AllProductiveDurationFormatted string `json:"all_productive_duration_formatted"`
AllProductiveHours float64 `json:"all_productive_hours"`
AllProductivePercentage float64 `json:"all_productive_percentage"`
BusinessDurationFormatted string `json:"business_duration_formatted"`
BusinessHours float64 `json:"business_hours"`
BusinessPercentage float64 `json:"business_percentage"`
CommunicationAndSchedulingDurationFormatted string `json:"communication_and_scheduling_duration_formatted"`
CommunicationAndSchedulingHours float64 `json:"communication_and_scheduling_hours"`
CommunicationAndSchedulingPercentage float64 `json:"communication_and_scheduling_percentage"`
Date string `json:"date"`
DesignAndCompositionDurationFormatted string `json:"design_and_composition_duration_formatted"`
DesignAndCompositionHours float64 `json:"design_and_composition_hours"`
DesignAndCompositionPercentage float64 `json:"design_and_composition_percentage"`
DistractingDurationFormatted string `json:"distracting_duration_formatted"`
DistractingHours float64 `json:"distracting_hours"`
DistractingPercentage float64 `json:"distracting_percentage"`
EntertainmentDurationFormatted string `json:"entertainment_duration_formatted"`
EntertainmentHours float64 `json:"entertainment_hours"`
EntertainmentPercentage float64 `json:"entertainment_percentage"`
ID float64 `json:"id"`
NeutralDurationFormatted string `json:"neutral_duration_formatted"`
NeutralHours float64 `json:"neutral_hours"`
NeutralPercentage float64 `json:"neutral_percentage"`
NewsDurationFormatted string `json:"news_duration_formatted"`
NewsHours float64 `json:"news_hours"`
NewsPercentage float64 `json:"news_percentage"`
ProductiveDurationFormatted string `json:"productive_duration_formatted"`
ProductiveHours float64 `json:"productive_hours"`
ProductivePercentage float64 `json:"productive_percentage"`
ProductivityPulse float64 `json:"productivity_pulse"`
ReferenceAndLearningDurationFormatted string `json:"reference_and_learning_duration_formatted"`
ReferenceAndLearningHours float64 `json:"reference_and_learning_hours"`
ReferenceAndLearningPercentage float64 `json:"reference_and_learning_percentage"`
ShoppingDurationFormatted string `json:"shopping_duration_formatted"`
ShoppingHours float64 `json:"shopping_hours"`
ShoppingPercentage float64 `json:"shopping_percentage"`
SocialNetworkingDurationFormatted string `json:"social_networking_duration_formatted"`
SocialNetworkingHours float64 `json:"social_networking_hours"`
SocialNetworkingPercentage float64 `json:"social_networking_percentage"`
SoftwareDevelopmentDurationFormatted string `json:"software_development_duration_formatted"`
SoftwareDevelopmentHours float64 `json:"software_development_hours"`
SoftwareDevelopmentPercentage float64 `json:"software_development_percentage"`
TotalDurationFormatted string `json:"total_duration_formatted"`
TotalHours float64 `json:"total_hours"`
UncategorizedDurationFormatted string `json:"uncategorized_duration_formatted"`
UncategorizedHours float64 `json:"uncategorized_hours"`
UncategorizedPercentage float64 `json:"uncategorized_percentage"`
UtilitiesDurationFormatted string `json:"utilities_duration_formatted"`
UtilitiesHours float64 `json:"utilities_hours"`
UtilitiesPercentage float64 `json:"utilities_percentage"`
VeryDistractingDurationFormatted string `json:"very_distracting_duration_formatted"`
VeryDistractingHours float64 `json:"very_distracting_hours"`
VeryDistractingPercentage float64 `json:"very_distracting_percentage"`
VeryProductiveDurationFormatted string `json:"very_productive_duration_formatted"`
VeryProductiveHours float64 `json:"very_productive_hours"`
VeryProductivePercentage float64 `json:"very_productive_percentage"`
}
// AnalyticDataQueryParameters is used to provide parameters to the Analytic Data API
type AnalyticDataQueryParameters struct {
Perspective string `field_name:"perspective"`
ResolutionTime string `field_name:"resolution_time"`
RestrictGroup string `field_name:"restrict_group"`
RestrictBegin string `field_name:"restrict_begin"`
RestrictEnd string `field_name:"restrict_end"`
RestrictKind string `field_name:"restrict_kind"`
RestrictThing string `field_name:"restrict_thing"`
RestrictThingy string `field_name:"restrict_thingy"`
}
// AnalyticData describes an Analytic Data API result
type AnalyticData struct {
Notes string `json:"notes"`
RowHeaders []string `json:"row_headers"`
Rows []row `json:"rows"`
Parameters *AnalyticDataQueryParameters `json:"-,omitempty"`
}
// Row is a single row in an Analytic Data API result
type row struct {
Date time.Time `json:"date,omitempty"`
Rank int `json:"rank,omitempty"`
TimeSpentSeconds int `json:"timeSpentSeconds,omitempty"`
NumberOfPeople int `json:"numberOfPeople,omitempty"`
Person string `json:"person,omitempty"`
Activity string `json:"activity,omitempty"`
Category string `json:"category,omitempty"`
Productivity int `json:"productivity,omitempty"`
}
func structToMap(i interface{}) (values url.Values) {
values = url.Values{}
iVal := reflect.ValueOf(i).Elem()
typ := iVal.Type()
for i := 0; i < iVal.NumField(); i++ {
f := iVal.Field(i)
// Convert each type into a string for the url.Values string map
var v string
switch f.Interface().(type) {
case int, int8, int16, int32, int64:
v = strconv.FormatInt(f.Int(), 10)
case uint, uint8, uint16, uint32, uint64:
v = strconv.FormatUint(f.Uint(), 10)
case float32:
v = strconv.FormatFloat(f.Float(), 'f', 4, 32)
case float64:
v = strconv.FormatFloat(f.Float(), 'f', 4, 64)
case []byte:
v = string(f.Bytes())
case string:
v = f.String()
}
if v == "" {
continue
}
values.Set(typ.Field(i).Tag.Get("field_name"), v)
}
return
}
func (r *RescueTime) buildURL(baseURL string, urlValues url.Values) (string, error) {
parsedURL, err := url.Parse(baseURL)
if err != nil {
return "", err
}
urlValues.Set("key", r.APIKey)
urlValues.Set("format", "json")
parsedURL.RawQuery = urlValues.Encode()
return parsedURL.String(), nil
}
var titlingRegex = regexp.MustCompile("[0-9A-Za-z]+")
func titleCase(src string) string {
byteSrc := []byte(src)
chunks := titlingRegex.FindAll(byteSrc, -1)
for idx, val := range chunks {
chunks[idx] = bytes.Title(val)
}
return string(bytes.Join(chunks, nil))
}
func (r *RescueTime) getResponse(getURL string) ([]byte, error) {
if r.APIKey == "" {
return nil, errors.New("Please provide API key")
}
response, err := http.Get(getURL)
if err != nil {
return nil, err
}
defer response.Body.Close()
contents, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
return contents, nil
}
// GetAnalyticData makes a request to the Analytic Data API with the provided parameters.
// If a timezone is given, all dates will be located in the given timezone, otherwise system's local timezone.
func (r *RescueTime) GetAnalyticData(timezone string, parameters *AnalyticDataQueryParameters) (AnalyticData, error) {
var rtd AnalyticData
params := structToMap(parameters)
builtURL, err := r.buildURL(analyticDataURL, params)
if err != nil {
return rtd, err
}
contents, err := r.getResponse(builtURL)
if err != nil {
return rtd, err
}
currentJSON, err := simplejson.NewJson(contents)
if err != nil {
return rtd, err
}
data := AnalyticData{
Parameters: parameters,
}
var notes string
notes = fmt.Sprintf("%s", currentJSON.Get("notes").MustString())
data.Notes = notes
var rowHeaders []string
headersMap := make(map[int]string)
headerRegex := regexp.MustCompile("[^A-Za-z0-9]+")
for i, s := range currentJSON.Get("row_headers").MustStringArray() {
rowHeaders = append(rowHeaders, s)
headersMap[i] = headerRegex.ReplaceAllString(titleCase(s), "")
}
data.RowHeaders = rowHeaders
var toAppend []row
for _, entry := range currentJSON.Get("rows").MustArray() {
var aRow row
for index, column := range entry.([]interface{}) {
thisHeader := headersMap[index]
field := reflect.ValueOf(&aRow).Elem().FieldByName(thisHeader)
switch field.Interface().(type) {
case int, int8, int16, int32, int64:
intValue, err := column.(json.Number).Int64()
if err != nil {
return data, err
}
field.SetInt(intValue)
case time.Time:
parsed, err := time.Parse("2006-01-02T15:04:05", column.(string))
if err != nil {
return rtd, err
}
if timezone != "" {
location, err := time.LoadLocation(timezone)
if err != nil {
return rtd, err
}
parsed = parsed.In(location)
}
field.Set(reflect.ValueOf(parsed))
default:
field.Set(reflect.ValueOf(column))
}
}
toAppend = append(toAppend, aRow)
}
data.Rows = toAppend
return data, nil
}
// GetDailySummary returns the last two weeks of daily summaries for the user.
// It does not include the current day, and new summaries for the previous day
// are available at 12:01 am in the user’s local time zone.
func (r *RescueTime) GetDailySummary() ([]DailySummary, error) {
var summaries []DailySummary
builtURL, err := r.buildURL(dailySummaryURL, url.Values{})
if err != nil {
return summaries, err
}
contents, err := r.getResponse(builtURL)
if err != nil {
return summaries, err
}
var keys []DailySummary
err = json.Unmarshal(contents, &keys)
if err != nil {
return summaries, err
}
return keys, nil
}