node-red-node-web-nodes
Version:
A collection of Node-RED nodes for popular web services.
402 lines (387 loc) • 18.2 kB
JavaScript
/**
* Copyright 2014 IBM Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
module.exports = function(RED) {
"use strict";
var OAuth= require('oauth').OAuth;
var util = require("util");
function getOAuth(client_key, client_secret) {
return new OAuth(
"https://api.fitbit.com/oauth/request_token",
"https://api.fitbit.com/oauth/access_token",
client_key,
client_secret,
1.0,
null,
"HMAC-SHA1"
);
}
function FitbitNode(n) {
RED.nodes.createNode(this,n);
this.username = n.username;
}
RED.nodes.registerType("fitbit-credentials",FitbitNode,{
credentials: {
username: {type:"text"},
client_key: { type: "password"},
client_secret: { type: "password"},
access_token: {type: "password"},
access_token_secret: {type:"password"}
}
});
function today() {
var d = new Date();
var month = d.getMonth() + 1;
var day = d.getDate();
return d.getFullYear() + "-" +
(month < 10 ? "0" : "") + month + "-" +
(day < 10 ? "0" : "") + day;
}
function mainSleep(sleepList) {
for (var i = 0; i < sleepList.length; i++) {
if (sleepList[i].isMainSleep) {
return sleepList[i];
}
}
}
function formatError(err) {
var errors = [];
if (err.statusCode === 503) {
errors = JSON.parse(err.data).restrictions.map(function(e) {
return e.type + " \"" + e.message + "\"";
});
} else {
errors = JSON.parse(err.data).errors.map(function(e) {
return e.errorType +
(e.fieldName ? " (" + e.fieldName + ")" : "") +
" \"" + e.message + "\"";
});
}
return "Error "+ err.statusCode + ": " + errors.join(", ");
}
function FitbitInNode(n) {
RED.nodes.createNode(this,n);
this.fitbitConfig = RED.nodes.getNode(n.fitbit);
this.dataType = n.dataType || 'badges';
if (!this.fitbitConfig) {
this.warn("Missing fitbit credentials");
return;
}
var credentials = this.fitbitConfig.credentials;
if (credentials &&
credentials.access_token && credentials.access_token_secret) {
var oa = getOAuth(credentials.client_key,credentials.client_secret);
var node = this;
node.status({fill:"blue",shape:"dot",text:"initializing"});
var day = today();
if (node.dataType === 'badges') {
oa.get('https://api.fitbit.com/1/user/-/badges.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err));
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
node.state = JSON.parse(body).badges;
node.status({});
node.on('input', function(msg) {
node.status({fill:"blue",shape:"dot",text:"querying"});
oa.get('https://api.fitbit.com/1/user/-/badges.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err),msg);
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var badges = JSON.parse(body).badges;
outerLoop:
for (var i = 0; i < badges.length; i++) {
var badge = badges[i];
for (var j = 0; j < node.state.length; j++) {
if (badge.name === node.state[j].name) {
// already have this badge
if (badge.timesAchieved > node.state[j].timesAchieved) {
// achieved it again
node.sendBadge(badge, "repeat");
}
continue outerLoop;
}
}
// achieved new badge
node.sendBadge(badge, "new");
}
node.state = badges;
node.status({});
});
});
node.setInterval();
});
} else if (node.dataType === 'sleep') {
oa.get('https://api.fitbit.com/1/user/-/sleep/date/'+day+'.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err));
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var data = JSON.parse(body);
node.state = {
day: day,
sleep: mainSleep(data.sleep),
};
node.status({});
node.on('input', function(msg) {
node.status({fill:"blue",shape:"dot",text:"querying"});
var day = today();
oa.get('https://api.fitbit.com/1/user/-/sleep/date/'+day+'.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err),msg);
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var data = JSON.parse(body);
var sleep = mainSleep(data.sleep);
if (sleep &&
( day !== node.state.day ||
!node.state.sleep ) ) {
// new sleep record
node.send({ payload: sleep, data: sleep });
node.state = { day: day, sleep: sleep };
}
node.status({});
});
});
node.setInterval();
});
} else if (node.dataType === 'goals') {
oa.get('https://api.fitbit.com/1/user/-/activities/date/'+day+'.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err));
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var data = JSON.parse(body);
node.state = {
day: day,
summary: data.summary,
};
node.status({});
node.on('input', function(msg) {
node.status({fill:"blue",shape:"dot",text:"querying"});
var day = today();
var fetchDay = day;
if (day !== node.state.day) {
// it is a new day so pull the final data for
// previous day. handle next day at next interval
fetchDay = node.state.day;
}
oa.get('https://api.fitbit.com/1/user/-/activities/date/'+fetchDay+'.json',
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err),msg);
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var data = JSON.parse(body);
if (data.summary.steps >= data.goals.steps &&
(!node.state.summary ||
node.state.summary.steps < data.goals.steps)) {
node.send({ payload: data.goals.steps + ' steps goal achieved', data: data });
}
if (data.summary.floors >= data.goals.floors &&
(!node.state.summary ||
node.state.summary.floors < data.goals.floors)) {
node.send({ payload: data.goals.floors + ' floors goal achieved', data: data });
}
if (data.summary.veryActiveMinutes >= data.goals.activeMinutes &&
(!node.state.summary ||
node.state.summary.veryActiveMinutes < data.goals.activeMinutes)) {
node.send({ payload: data.goals.activeMinutes + ' active minutes goal achieved', data: data });
}
if (fetchDay !== day) {
node.state = { day: day };
} else {
node.state.summary = data.summary;
}
node.status({});
});
});
node.setInterval();
});
}
}
}
RED.nodes.registerType("fitbit in",FitbitInNode);
FitbitInNode.prototype.sendBadge = function(badge, type) {
this.send({
type: type,
payload: badge.earnedMessage,
title: badge.name,
description: badge.marketingDescription,
data: badge,
});
};
FitbitInNode.prototype.setInterval = function(repeat) {
repeat = repeat || 900000; // 15 minutes
var node = this;
var interval = setInterval(function() {
node.emit("input", {});
}, repeat);
this.on("close", function() {
if (interval !== null) {
clearInterval(interval);
}
});
};
function FitbitQueryNode(n) {
RED.nodes.createNode(this,n);
this.fitbitConfig = RED.nodes.getNode(n.fitbit);
this.dataType = n.dataType || 'activities';
var supportedTypes = ["activities","sleep","badges"];
if (supportedTypes.indexOf(this.dataType) == -1) {
this.error("Unsupported data type");
return;
}
if (!this.fitbitConfig) {
this.error("Missing fitbit credentials");
return;
}
var credentials = this.fitbitConfig.credentials;
if (credentials &&
credentials.access_token && credentials.access_token_secret) {
var oa = getOAuth(credentials.client_key,credentials.client_secret);
var node = this;
this.on('input', function(msg) {
node.status({fill:"blue",shape:"dot",text:"querying"});
var url;
if (node.dataType === 'activities' ||
node.dataType === 'sleep') {
var day = msg.date || today();
url = 'https://api.fitbit.com/1/user/-/' +
node.dataType + '/date/' + day + '.json';
} else if (node.dataType === 'badges') {
url = 'https://api.fitbit.com/1/user/-/badges.json';
}
oa.get(url,
credentials.access_token,
credentials.access_token_secret,
function(err, body, response) {
if (err) {
node.error(formatError(err),msg);
node.status({fill:"red",shape:"ring",text:"failed"});
return;
}
var data = JSON.parse(body);
msg.data = data;
if (node.dataType === 'sleep') {
var sleep = mainSleep(data.sleep);
if (!sleep) {
node.warn("no main sleep record found");
return;
} else {
msg.payload = sleep;
delete msg.error;
}
} else {
msg.payload = data;
}
node.status({});
node.send(msg);
});
});
}
}
RED.nodes.registerType("fitbit",FitbitQueryNode);
RED.httpAdmin.get('/fitbit-credentials/:id/auth', function(req, res){
if (!req.query.client_key || !req.query.client_secret ||
!req.query.callback) {
res.send(400);
return;
}
var credentials = {
client_key:req.query.client_key,
client_secret: req.query.client_secret
};
RED.nodes.addCredentials(req.params.id, credentials);
var oa = getOAuth(credentials.client_key, credentials.client_secret);
oa.getOAuthRequestToken({
oauth_callback: req.query.callback
}, function(error, oauth_token, oauth_token_secret, results) {
if (error) {
res.send('<h2>Oh no!</h2>'+
'<p>Something went wrong with the authentication process. The following error was returned:<p>'+
'<p><b>'+error.statusCode+'</b>: '+error.data+'</p>'+
'<p>One known cause of this type of failure is if the clock is wrong on system running Node-RED.</p>');
} else {
credentials.oauth_token = oauth_token;
credentials.oauth_token_secret = oauth_token_secret;
res.redirect('https://www.fitbit.com/oauth/authorize?oauth_token='+oauth_token);
RED.nodes.addCredentials(req.params.id,credentials);
}
});
});
RED.httpAdmin.get('/fitbit-credentials/:id/auth/callback', function(req, res, next){
var credentials = RED.nodes.getCredentials(req.params.id);
credentials.oauth_verifier = req.query.oauth_verifier;
var client_key = credentials.client_key;
var client_secret = credentials.client_secret;
var oa = getOAuth(client_key,client_secret);
oa.getOAuthAccessToken(
credentials.oauth_token,
credentials.oauth_token_secret,
credentials.oauth_verifier,
function(error, oauth_access_token, oauth_access_token_secret, results){
if (error){
res.send('<h2>Oh no!</h2>'+
'<p>Something went wrong with the authentication process. The following error was returned:<p>'+
'<p><b>'+error.statusCode+'</b>: '+error.data+'</p>'+
'<p>One known cause of this type of failure is if the clock is wrong on system running Node-RED.</p>');
} else {
credentials = {};
credentials.client_key = client_key;
credentials.client_secret = client_secret;
credentials.access_token = oauth_access_token;
credentials.access_token_secret = oauth_access_token_secret;
oa.get('https://api.fitbit.com/1/user/-/profile.json', credentials.access_token, credentials.access_token_secret, function(err, body, response) {
if (err) {
return res.send('<h2>Oh no!</h2>'+
'<p>Something went wrong fetching the user profile. The following error was returned:<p>'+
'<p><b>'+err.statusCode+'</b>: '+err.data+'</p>'+
'<p>One known cause of this type of failure is if the clock is wrong on system running Node-RED.</p>');
}
var data = JSON.parse(body);
credentials.username = data.user.fullName;
RED.nodes.addCredentials(req.params.id,credentials);
res.send("<html><head></head><body>Authorised - you can close this window and return to Node-RED</body></html>");
});
}
}
);
});
};