-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathbasic_api_functions.R
284 lines (248 loc) · 10 KB
/
basic_api_functions.R
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
## https://cran.r-project.org/web/packages/httr/vignettes/api-packages.html
## required functions
## library(httr)
## library(jsonlite)
## library(RCurl)
##' Make a GET request on \url{https://api.fitbit.com}.
##'
##' This is the main building block of this package. It is a wrapper for the \code{\link[httr]{GET}} function.
##'
##' @param path Character scalar specifying the requested address from \url{https://api.fitbit.com/}.
##' @param token Fitbit API token generated with \code{\link{get_fitbit_token}}.
##' @param ... Additional arguments passed to \code{\link{get_fitbit_token}}.
##' @return Response object of class "response", as returned by the \code{\link[httr]{GET}} function. Use \code{\link{fitbit_parse}} to get the body of the output.
##'
##' @details This function has one side effect: if updates the "rate_limit" attribute in the token according to the returned request header.
##' @examples
##' \dontrun{
##' resp <- fitbit_GET("1/user/-/sleep/date/2015-12-12.json", token = fitbit_token )
##' ret <- fitbit_parse(resp)
##' }
##'
##' @export
##' @import httr
##' @import data.table
fitbit_GET <- function(path, token, ...){
req <- GET("https://api.fitbit.com", path = path, config = config(token = token))
## add_headers("Accept-Locale" = "Germany"))
fitbit_check(req)
## append token information
attr(token, "rate_limit") <- extract_rate_limit(req)
req
}
##" Check if the request was successful
##" HIDE
fitbit_check <- function(req) {
if (req$status_code < 400) return(invisible())
message <- fitbit_parse(req)$message
stop("HTTP failure: ", req$status_code, "\n", message, call. = FALSE)
}
##' Parse the output of \code{\link{fitbit_GET}} using \code{\link[jsonlite]{fromJSON}}.
##'
##' @param req Output object of \code{\link{fitbit_GET}}.
##' @return List representing the JSON content of the GET response.
##'
##' @export
fitbit_parse <- function(req) {
text <- content(req, as = "text")
if (identical(text, "")) stop("No output to parse", call. = FALSE)
jsonlite::fromJSON(text, simplifyVector = FALSE)
}
##' Get the fitbit authentication token.
##'
##' This function generates the fitbit token and performs the required API authorization.
##'
##' @param file_path Character scalar; Path to the credentials file described in details. Leave the arguments key, secret and scope set to NULL if you want to use the credentials file for the authentication.
##' @param appname Character scalar; Fitbit application name.
##' @param key Character scalar; Fitbit OAuth 2.0 Client ID.
##' @param secret Character scalar; Fitbit Client (Consumer) Secret.
##' @param scope Character vector; API permission scope. Leave the defaults in order to access
##' all the features listed in \code{\link{activity_features}}.
##' @return Fitbit token providing the access to your Fitbit account data.
##'
##' @details Request rate limit information is stored in the fitbit token as the "rate_limit" attribute. It gets updated every time fitbit_GET is called.
##'
##' @details Example credentials file (3 rows: appname, key, secret):
##' \preformatted{
##' GetMyData
##' 14C889
##' apaojsdpoj213412p4j115j1p5oj21p5o
##' }
##'
##' @examples
##' \dontrun{
##' ## If the credentials file is located in "~/.fitbitr" just run:
##' fitbit_token <- get_fitbit_token()
##'
##' ## Else, specify the new credentials file location:
##'
##' fitbit_token <- get_fitbit_token(file_path = "~/.custom_fitbitr")
##'
##' ## or enter the credentials as arguments:
##' fitbit_token <- get_fitbit_token(appname = "GetMyData",
##' key = "14C889",
##' secret = "apaojsdpoj213412p4j115j1p5oj21p5o"
##' )
##' }
##' @export
get_fitbit_token <- function(file_path = "~/.fitbitr",
appname = NULL,
key = NULL,
secret = NULL,
scope = c("activity", "heartrate", "sleep", "weight")) {
## read the file from the file
if (all(is.null(appname), is.null(key), is.null(secret))) {
myapp <- read_fitbit_auth(file_path = file_path)
} else {
## all app args have to be specified
if (any(is.null(appname), is.null(key), is.null(secret))) {
warning("Please provide all the authorization arguments (appname, key, secret) \nor none at all. file_path will be used to get the credentials.")
myapp <- read_fitbit_auth(file_path = file_path)
} else {
## generate the app
myapp <- oauth_app(
appname = appname,
key = key,
secret = secret
)
}
}
fitbit_endpoint <- oauth_endpoint(
request = "https://api.fitbit.com/oauth2/token",
authorize = "https://www.fitbit.com/oauth2/authorize",
access = "https://api.fitbit.com/oauth2/token")
fitbit_token <- oauth2.0_token(endpoint = fitbit_endpoint,
app = myapp,
scope = scope,
use_basic_auth = TRUE)
attr(fitbit_token, "rate_limit") <- list(total = 150,
remains = 150,
next_reset = NA,
current_time = Sys.time())
return(fitbit_token)
}
##" Read the authorization information from file
##"
##" This function reads lines from \code{file_path} and then uses \code{\link[httr]{oauth_app}}
##" to generate the app object.
##"
##" @details Example authorization file (3 rows: appname, key, secret):
##" \preformatted{
##" GetMyData
##" 14C889
##" apaojsdpoj213412p4j115j1p5oj21p5o
##" }
##"
##" #@param file_path Character scalar; File path to the authorization file.
##" #@return oauth_app used by \code{\link{get_fitbit_token}} as input.
read_fitbit_auth <- function(file_path = "~/.fitbitr"){
if(!file.exists(file_path)) stop("Authorization file (", file_path, ") doesn't exits")
fitbit_info <- readLines(file_path)
stopifnot(length(fitbit_info)==3)
myapp <- oauth_app(
appname = fitbit_info[1],
key = fitbit_info[2],
secret = fitbit_info[3]
)
return(myapp)
}
##' Refresh the expired fitbit token.
##'
##' This function refreshes the fitbit token as described in \url{https://dev.fitbit.com/docs/oauth2/#refreshing-tokens}.
##' The internal refresh function (\code{refresh_oauth2.0})
##' of the \code{\link{httr}} package somehow fails to refresh it.
##'
##' @param token Fitbit API token generated with \code{\link{get_fitbit_token}}.
##' @examples
##' \dontrun{
##' token <- refresh_fitbit_token(token)
##' }
##' @export
refresh_fitbit_token <- function(token){
## See the implementation of oauth-refresh.
## https://github.com/hadley/httr/blob/master/R/oauth-refresh.R
endpoint <- token$endpoint
app <- token$app
credentials <- token$credentials
if (is.null(credentials$refresh_token)) {
stop("Refresh token not available", call. = FALSE)
}
response <- POST("https://api.fitbit.com/oauth2/token",
encode = "form",
body = list(grant_type = "refresh_token",
refresh_token = credentials$refresh_token
),
add_headers(Authorization =
paste("Basic",
RCurl::base64(paste0(app$key,":", app$secret)))
),
content_type("application/x-www-form-urlencoded")
)
refresh_data <- content(response)
## not
if(!("access_token" %in% names(refresh_data))) {
warning("Unable to refresh token")
warning(refresh_data$errors[[1]]$message)
return(NULL)
}
new_credentials <- utils::modifyList(credentials, refresh_data)
token$credentials <- new_credentials
return(token)
}
##" Parse response to get the request limit info.
##" Number of requests left.
##" TODO - display error only if we are close to the end of requests
##" @param req Request response of \code{\link{fitbit_GET}}.
extract_rate_limit <- function(req){
req_headers <- headers(req)
total <- req_headers[["fitbit-rate-limit-limit"]]
remains <- req_headers[["fitbit-rate-limit-remaining"]]
next_reset <- req_headers[["fitbit-rate-limit-reset"]]
current_time <- Sys.time()
## current_time <- req_headers[["date"]]
## current_time <- strsplit(current_time, split = " ")[[1]][5:6]
## current_time <- strptime(current_time[1], format = "%H:%M:%OS", tz = current_time[2])
## base::format(as.POSIXct(current_time), tz = base::format(Sys.time(), format="%Z"), usetz=TRUE)
## Sys.Date()
return(list(total = as.integer(total),
remains = as.integer(remains),
next_reset = as.integer(next_reset),
current_time = current_time
)
)
## cat(remains, " / ", total, " (Reset in ", round(as.integer(next_reset)/60), " min)\n", sep = "");
}
## BUG - Reset can be negative.
print_fitbit_rate_limit <- function(rate_limit) {
next_reset <- rate_limit$current_time + rate_limit$next_reset
due_reset <- round(as.numeric(as.character(next_reset - Sys.time())))
message("Requests left:\n",rate_limit$remains, " / ",
rate_limit$total,
" (reset in ", due_reset,
" min, at ", strftime(next_reset, "%H:%M:%S"), ")", sep = "")
}
get_rate_limit <- function(token) {
return(attr(token, "rate_limit"))
}
##' Get the number of requests left
##'
##' This function displays the information about how many API requests are left and
##' when the rate limit will be reset.
##'
##' @param token Fitbit API token generated with \code{\link{get_fitbit_token}}.
##' @param show_message Boolean scalar; Should the function print the message?
##' @return List containing the information about the number of requests left returned invisibly.
##'
##' @examples
##' \dontrun{
##' resp <- fitbit_GET("1/user/-/sleep/date/2015-12-12.json", token = fitbit_token )
##' rate_limit(fitbit_token)
##' ## Requests left:
##' ## 120 / 150 (reset in 22 min, at 18:00:01)
##' }
##' @export
rate_limit <- function(token, show_message = TRUE) {
rl <- get_rate_limit(token)
if(show_message == TRUE) print_fitbit_rate_limit(rl)
return(invisible(rl))
}