fitbit-oauth2-client
Version:
Fitbit client API for OAuth2
517 lines (512 loc) • 17.1 kB
JavaScript
// src/Fitbit.ts
import axios from "axios";
import { stringify } from "qs";
// src/FileTokenManager.ts
import { readFile, writeFile } from "fs";
// src/LogManager.ts
var DEFAULT_JSON_IDENT = 3;
var Logger = class {
constructor(logger = null, jsonIdent = DEFAULT_JSON_IDENT) {
this.logger = logger;
this.jsonIdent = jsonIdent;
}
log(level, msg, data) {
if (!this.logger) {
return;
}
if (typeof this.logger === "function") {
data ? this.logger(msg, JSON.stringify(data, void 0, this.jsonIdent)) : this.logger(msg);
return;
}
if (this.logger[level]) {
data ? this.logger[level](msg, JSON.stringify(data, void 0, this.jsonIdent)) : this.logger[level](msg);
return;
}
if (level === "trace" && this.logger["silly"]) {
data ? this.logger["silly"](msg, JSON.stringify(data, void 0, this.jsonIdent)) : this.logger["silly"](msg);
return;
}
}
trace(msg, data) {
this.log("trace", msg, data);
}
debug(msg, data) {
this.log("debug", msg, data);
}
info(msg, data) {
this.log("info", msg, data);
}
warn(msg, data) {
this.log("warn", msg, data);
}
error(msg, data) {
this.log("error", msg, data);
}
};
var LogManager = class {
static getLogger(logger = null) {
return new Logger(logger);
}
};
// src/FileTokenManager.ts
var _logger = LogManager.getLogger();
var FileTokenManager = class {
// eslint-disable-next-line no-unused-vars
constructor(tokenFilePath) {
this.tokenFilePath = tokenFilePath;
}
static setLogger(logger) {
_logger = LogManager.getLogger(logger);
}
read() {
return new Promise((resolve, reject) => {
try {
_logger.debug("Reading token file [" + this.tokenFilePath + "]");
readFile(this.tokenFilePath, { encoding: "utf8", flag: "r" }, function(err, data) {
if (err) {
_logger.error("Read failed:", err);
return reject(err);
}
try {
const token = JSON.parse(data);
_logger.trace("Read token:", token);
resolve(token);
} catch (error) {
_logger.error("Read failed<2>:", error);
reject(error);
}
});
} catch (error) {
_logger.error("Read failed<2>:", error);
reject(error);
}
});
}
write(token) {
return new Promise((resolve, reject) => {
try {
_logger.debug("Writing token file [" + this.tokenFilePath + "]:", token);
writeFile(this.tokenFilePath, JSON.stringify(token), { encoding: "utf8", flag: "w" }, (err) => {
if (err) {
_logger.error("Write failed:", err);
reject(err);
} else {
_logger.trace("Wrote token:", token);
resolve(token);
}
});
} catch (error) {
_logger.error("Write failed<2>:", error);
reject(error);
}
});
}
};
var FileTokenManager_default = FileTokenManager;
// src/Fitbit.ts
import { AuthorizationCode } from "simple-oauth2";
var _request = axios.request;
var post = axios.post;
var PROCENTAGE_TO_DECIMAL_DIVIDER = 100;
var SUBSTRACT_MILLIS = 6e4;
var _logger2 = LogManager.getLogger();
var FITBIT_CONFIG_DEFAULTS = {
timeout: 6e4,
tokenExpiresFactorProcentage: 80,
uris: {
authorizationUri: "https://www.fitbit.com",
authorizationPath: "/oauth2/authorize",
tokenUri: "https://api.fitbit.com",
tokenPath: "/oauth2/token"
},
authorizationUri: {
scope: "location activity nutrition social settings profile sleep heartrate weight",
state: "3(#0/!~"
}
};
var FitbitBase = class {
static setLogger(logger) {
_logger2 = LogManager.getLogger(logger);
}
static addExpiresAt(token, tokenExpiresFactor, requestDateTime) {
const now = requestDateTime || /* @__PURE__ */ new Date();
now.setSeconds(now.getSeconds() + Math.round(token.expires_in * tokenExpiresFactor));
if (!requestDateTime) {
now.setSeconds(now.getSeconds() - SUBSTRACT_MILLIS);
}
const expires_at = now.toISOString();
const expires_at_timestamp = now.getTime();
return { ...token, expires_at, expires_at_timestamp };
}
static createQueuedRequest(options, fitbit) {
const promiseInspector = {
// eslint-disable-next-line no-unused-vars
resolve: (data) => {
console.log(data);
},
// eslint-disable-next-line no-unused-vars
reject: (error) => {
console.log(error);
}
};
const queuedRequest = {
promise: new Promise((resolve, reject) => {
promiseInspector.reject = reject;
promiseInspector.resolve = resolve;
}),
handler: () => {
_logger2.trace(`request[dequeued] ${options.url}:`, options);
fitbit.request(options).then((response) => promiseInspector.resolve(response)).catch((error) => promiseInspector.reject(error));
}
};
return queuedRequest;
}
static hasTokenExpired(token) {
if (!token.expires_at) {
return true;
}
const now = /* @__PURE__ */ new Date();
if (token.expires_at_timestamp) {
return now.getTime() >= token.expires_at_timestamp;
}
const then = new Date(token.expires_at);
console.log("then:", then);
return now.getTime() >= then.getTime();
}
static createData(data) {
return stringify(data);
}
static normalizeOptions(options, config) {
const newOptions = { ...options, url: options.url || options.uri };
if (!newOptions.url) {
throw new Error(`Parameter url is missing in: ${JSON.stringify(newOptions)}`);
}
if (newOptions.uri) {
delete options.uri;
}
if (!newOptions.timeout) {
newOptions.timeout = config.timeout;
}
if (!options.headers) {
options.headers = {};
}
return newOptions;
}
};
var Fitbit = class _Fitbit extends FitbitBase {
_config;
_tokenManager;
_tokenData = null;
_isTokenRefreshingOrInitiating = false;
_tokenExpiresFactor;
_requestQueue = [];
_limits;
constructor(config, tokenManager) {
super();
if (!config) {
throw new Error("Config expected");
}
this._config = {
...FITBIT_CONFIG_DEFAULTS,
...config,
authorizationUri: { ...FITBIT_CONFIG_DEFAULTS.authorizationUri, ...config.authorizationUri },
uris: { ...FITBIT_CONFIG_DEFAULTS.uris, ...config.uris }
};
this._tokenManager = tokenManager ? tokenManager : (() => {
if (!config.tokenFilePath) {
throw new Error("Make sure config has property 'tokenFilePath' defined or provide a token persist manager");
}
return new FileTokenManager_default(config.tokenFilePath);
})();
this._tokenExpiresFactor = this._config.tokenExpiresFactorProcentage > 1 ? this._config.tokenExpiresFactorProcentage / PROCENTAGE_TO_DECIMAL_DIVIDER : this._config.tokenExpiresFactorProcentage;
if (this._tokenExpiresFactor > 1) {
this._tokenExpiresFactor = this._tokenExpiresFactor / PROCENTAGE_TO_DECIMAL_DIVIDER;
}
}
authorizeURL() {
const config = {
client: {
id: this._config.creds.clientId,
secret: this._config.creds.clientSecret
},
auth: {
tokenHost: this._config.uris.authorizationUri,
tokenPath: this._config.uris.tokenPath,
authorizePath: this._config.uris.authorizationPath
}
};
const client = new AuthorizationCode(config);
return client.authorizeURL(this._config.authorizationUri);
}
fetchToken(code) {
return this.internalFetchToken("", code);
}
internalFetchToken(refreshToken, code) {
_logger2.debug(`fetchToken: refreshToken=${refreshToken}, code=${code}`);
const self = this;
const url = self._config.uris.tokenUri + self._config.uris.tokenPath;
const data = stringify(
code ? {
code,
redirect_uri: self._config.authorizationUri.redirectUri,
grant_type: "authorization_code",
client_id: self._config.creds.clientId,
client_secret: self._config.creds.clientSecret
} : {
grant_type: "refresh_token",
refresh_token: refreshToken
}
);
const config = {
headers: {
Authorization: "Basic " + Buffer.from(self._config.creds.clientId + ":" + self._config.creds.clientSecret).toString("base64"),
Accept: "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded"
},
timeout: self._config.timeout
};
const requestDateTime = /* @__PURE__ */ new Date();
_logger2.trace(`fetchToken[axios.post]: ${url}`, { data, config });
return post(url, data, config).then((response) => {
const token = _Fitbit.addExpiresAt(response.data, self._tokenExpiresFactor, requestDateTime);
self._tokenData = token;
_logger2.trace("fetchToken[axios.post][response]:", token);
return token;
}).then((token) => {
return self._tokenManager.write(token);
});
}
refresh(refreshToken) {
_logger2.debug("refresh");
return this.internalFetchToken(refreshToken);
}
request(options) {
return this.internalRequest(_Fitbit.normalizeOptions(options, this._config));
}
internalRequest(options) {
const self = this;
if (self._isTokenRefreshingOrInitiating) {
_logger2.debug("request[enqueued]:", options);
const queuedRequest = _Fitbit.createQueuedRequest(options, self);
self._requestQueue.push(queuedRequest.handler);
return queuedRequest.promise;
}
_logger2.debug("request:", options);
const performRequest = () => {
var _a;
if (!self._tokenData || !self._tokenData.access_token) {
return new Promise((resolve, reject) => {
const error = new Error("token appears corrupt:" + JSON.stringify(self._tokenData));
reject(error);
});
}
if (!options.headers) {
options.headers = {};
}
if (!((_a = options == null ? void 0 : options.headers) == null ? void 0 : _a.Authorization)) {
options.headers.Authorization = "Bearer " + self._tokenData.access_token;
}
_logger2.trace("request.performRequest[axios.request]:", options);
return _request(options).then((response) => {
_logger2.trace(
`Request ${options.url}:`,
response.toString ? response.toString() : response.data ? response.data : "..."
);
if (response.headers) {
self._limits = {
limit: response.headers["fitbit-rate-limit-limit"],
remaining: response.headers["fitbit-rate-limit-remaining"],
reset: response.headers["fitbit-rate-limit-reset"]
};
}
return response;
});
};
const processQueuedRequests = () => {
setTimeout(() => {
const noqr = self._requestQueue.length;
_logger2.debug(`request.processQueuedRequests: Processing ${noqr} queued requests...`);
while (self._requestQueue.length > 0) {
const qr = self._requestQueue.shift();
if (!qr) {
break;
}
qr();
}
}, 0);
};
if (!self._tokenData) {
self._isTokenRefreshingOrInitiating = true;
return self._tokenManager.read().then((token) => {
_logger2.trace("request[tokenManager.read]:", token);
if (!token.expires_at_timestamp) {
token = _Fitbit.addExpiresAt(token, self._tokenExpiresFactor);
self._tokenData = token;
_logger2.trace("request[tokenManager.read] Expire info added:", token);
return self._tokenManager.write(token);
}
self._tokenData = token;
return token;
}).then((token) => {
if (_Fitbit.hasTokenExpired(token)) {
_logger2.trace("request[tokenManager.read] Token expired:", token);
return self.refresh(token.refresh_token);
}
return token;
}).then(() => {
if (self._isTokenRefreshingOrInitiating) {
self._isTokenRefreshingOrInitiating = false;
processQueuedRequests();
}
return performRequest();
});
} else if (_Fitbit.hasTokenExpired(self._tokenData)) {
this._isTokenRefreshingOrInitiating = true;
_logger2.trace("request[Token Expired]:", self._tokenData);
return self.refresh(self._tokenData.refresh_token).then(() => {
if (self._isTokenRefreshingOrInitiating) {
self._isTokenRefreshingOrInitiating = false;
processQueuedRequests();
}
return performRequest();
});
}
return performRequest();
}
getLimits() {
return this._limits;
}
};
var Fitbit_default = Fitbit;
// src/FitbitApi.ts
import dayjs, { extend } from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";
extend(utc);
extend(timezone);
var THOUSAND_FACTOR = 1e3;
var handleResponse = (response) => response.data;
var getDayjsInstance = (dayjsDateOrTimestamp) => {
const at = dayjsDateOrTimestamp ? dayjsDateOrTimestamp.isValid ? dayjsDateOrTimestamp : dayjs(dayjsDateOrTimestamp) : dayjs();
if (!at.isValid()) {
throw new Error("Invalid instance: " + at);
}
return at;
};
var handleRequest = (options, fitbitClient, apiCallInterceptor) => {
if (apiCallInterceptor) {
if (apiCallInterceptor(options)) {
return new Promise((resolve, reject) => {
reject();
});
}
}
return fitbitClient.request(options).then(handleResponse);
};
var FitbitApi = class _FitbitApi {
constructor(fitbitClient, apiCallInterceptor) {
this.fitbitClient = fitbitClient;
this.apiCallInterceptor = apiCallInterceptor;
}
static fromUTC = (timestamp, tz) => {
return tz ? dayjs.utc(timestamp).tz(tz) : dayjs.utc(timestamp);
};
static getDateTime = (dayjsDateOrTimestamp) => {
const at = getDayjsInstance(dayjsDateOrTimestamp);
return {
date: at.format("YYYY-MM-DD"),
time: at.format("HH:mm:ss")
};
};
static getDate = (dayjsDateOrTimestamp) => {
const at = getDayjsInstance(dayjsDateOrTimestamp);
return at.format("YYYY-MM-DD");
};
static getLifetimeStatsUrl = (userId = "-") => {
return `https://api.fitbit.com/1/user/${userId}/activities.json`;
};
static getProfileUrl = (userId = "-") => {
return `https://api.fitbit.com/1/user/${userId}/profile.json`;
};
static getBodyFatUrl = (dayjsDateOrTimestamp, userId) => {
const dateStr = _FitbitApi.getDate(dayjsDateOrTimestamp);
return `https://api.fitbit.com/1/user/${userId || "-"}/body/log/fat/date/${dateStr}.json`;
};
static getLogBodyFatUrl = (userId = "-") => {
return `https://api.fitbit.com/1/user/${userId}/body/log/fat.json`;
};
static getLogWeightUrl = (userId) => {
return `https://api.fitbit.com/1/user/${userId || "-"}/body/log/weight.json`;
};
static getWeightUrl = (dayjsDateOrTimestamp, userId) => {
const dateStr = _FitbitApi.getDate(dayjsDateOrTimestamp);
return `https://api.fitbit.com/1/user/${userId || "-"}/body/log/weight/date/${dateStr}.json`;
};
getLifetimeStats = (userId) => {
const url = _FitbitApi.getLifetimeStatsUrl(userId);
const options = {
url,
method: "GET"
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
getProfile = (userId) => {
const url = _FitbitApi.getProfileUrl(userId);
const options = {
url,
method: "GET"
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
getBodyFat = (dayjsDateOrTimestamp, userId) => {
const url = _FitbitApi.getBodyFatUrl(dayjsDateOrTimestamp, userId);
const options = {
url,
method: "GET"
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
logBodyFat = (fat, dayjsDateOrTimestamp, userId) => {
const dateTime = _FitbitApi.getDateTime(dayjsDateOrTimestamp);
const url = _FitbitApi.getLogBodyFatUrl(userId);
const options = {
url,
method: "POST",
data: Fitbit_default.createData({
fat,
date: dateTime.date,
time: dateTime.time
})
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
logWeight = (weight, dayjsDateOrTimestamp, userId) => {
const dateTime = _FitbitApi.getDateTime(dayjsDateOrTimestamp);
const url = _FitbitApi.getLogWeightUrl(userId);
const verifiedWeight = weight > THOUSAND_FACTOR ? weight / THOUSAND_FACTOR : weight;
const options = {
url,
method: "POST",
data: Fitbit_default.createData({
weight: verifiedWeight,
date: dateTime.date,
time: dateTime.time
})
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
getWeight = (dayjsDateOrTimestamp, userId) => {
const url = _FitbitApi.getWeightUrl(dayjsDateOrTimestamp, userId);
const options = {
url,
method: "GET"
};
return handleRequest(options, this.fitbitClient, this.apiCallInterceptor);
};
};
export {
FileTokenManager,
Fitbit,
FitbitApi
};
//# sourceMappingURL=index.mjs.map