UNPKG

fitbit-oauth2-client

Version:
517 lines (512 loc) 17.1 kB
// 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