diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..2669de2 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "airbnb/base" +} diff --git a/app.js b/app.js index 72cf089..4fa48cb 100644 --- a/app.js +++ b/app.js @@ -1,148 +1,161 @@ -const jsdom = require('jsdom'); -const argv = require('yargs').argv; -const fs = require('fs'); -const _ = require('lodash'); -const request = require('request'); -const jsonfile = require('jsonfile'); -const logger = require("./utils/logger"); - -let IFTTTParams = {}; -let IFTTTTimers = {}; -const { endpoints, interval, ifttt } = argv; +import jsdom from 'jsdom'; +import { argv } from 'yargs'; +import _ from 'lodash'; +import logger from './utils/logger'; +import Promise from 'bluebird'; + +const IFTTTTimers = {}; +const IFTTTParams = {}; +let endpoints = {}; +const endpointsPath = argv.endpoints; +const interval = argv.interval; +const iftttPath = argv.ifttt; const options = { selector: 'body :not(script)', jQuerySrc: 'http://code.jquery.com/jquery.js', - defaultTimeout: 10 + defaultTimeout: 10, }; -// Ensure we have required arguments -if(!_.isString(endpoints) || !_.isInteger(interval) || interval === 0) { - console.error('--endpoints and --interval are required'); +// Promisify imports +import { + stat as _stat, + readFile as _readFile, +} from 'fs'; +const stat = Promise.promisify(_stat); +const readFile = Promise.promisify(_readFile); + +import { post as _post } from 'request'; +const post = Promise.promisify(_post); + +// Ensure we have valid interval arg +if (!_.isInteger(interval) || interval < 0) { + logger.error('--interval is required'); process.exit(1); } -// Ensure list of endpoints is a file -if(!fs.statSync(endpoints).isFile()) { - console.error('--endpoints should refer to a file (list of endpoints)'); - process.exit(2); -} +// Ensure endpoints list is a file +stat(endpointsPath) +.then((endpointsFile) => { + if (!endpointsFile.isFile()) { + logger.error('--endpoints must be a file'); + process.exit(1); + } +}) +.catch((err) => { + logger.error('--endpoints must be a valid file and is required', err); + process.exit(1); +}); -// Ensure IFTTT configuration is valid -if(ifttt) { - if(!fs.statSync(ifttt).isFile()) { - console.error('--ifttt should refer to a JSON file configuration'); - process.exit(5); +// Read endpoints file and create list of endpoints +const getEndpoints = (path) => readFile(path, 'utf-8') +.then((endpointsList) => { + if (!endpointsList.length) { + logger.error('--endpoints file does not contain any endpoints'); + process.exit(4); } - const { key, eventName, bodyKey } = IFTTTParams = jsonfile.readFileSync(ifttt); + endpoints = endpointsList.split('\n').filter((endpoint) => endpoint && typeof endpoint === 'string'); //eslint-disable-line + return endpoints; +}) +.catch((err) => { + logger.error('--endpoints file could not be read', err); + process.exit(3); +}); - if(!key || !eventName || !bodyKey || !_.isString(key) || !_.isString(eventName) || !_.isString(bodyKey)) { - console.error('--ifttt file is missing required data'); - process.exit(6); - } -} +// Create IFTTT Parameters +readFile(iftttPath) + .then((file) => JSON.parse(file)) + .then((params) => { + // Validate IFTTT Parameters + if ( + !_.isString(params.key) || + !_.isString(params.eventName) || + !_.isString(params.bodyKey) + ) { + logger.error('--ifttt file is missing required data'); + process.exit(6); + } else { + IFTTTParams.key = params.key; + IFTTTParams.eventName = params.eventName; + IFTTTParams.bodyKey = params.bodyKey; + IFTTTParams.optionalTimeout = params.timeout; + } + }) + .catch((err) => logger.error('--ifttt should refer to a JSON file configuration', err)); // Make requests to endpoints -const makeRequests = (urls, callback) => { - let responses = {}; - let complete = 0; - - urls.forEach((url) => { - jsdom.env({ - url: url, - scripts: [options.jQuerySrc], - done: (err, window) => { - if(!window || !window.$ || err) { - console.error(`Resource data located at ${url} failed to load`); - } else { - const $ = window.$; - - $(options.selector).each(function() { - const responseText = $(this).text().replace(/\W+/g, ''); - - responses[url] = responseText; - }); - } - - complete++; - - if(complete === urls.length) { - callback(responses); - } +const makeRequests = (urls) => { + const responses = {}; + + Promise.map(urls, (url) => jsdom.env({ + url, + scripts: [options.jQuerySrc], + done: (err, window) => { + if (!window || !window.$ || err) { + logger.error(`Resource data located at ${url} failed to load`); + } else { + const $ = window.$; + $(options.selector).each(() => { + responses[url] = $(this).text().replace(/\W+/g, ''); + }); } - }); - }); + }, + })).then(() => Promise.resolve(responses)) + .catch((err) => logger.err('There was a problem making requests', err)); }; // Send event to IFTTT const postIFTTT = (data) => { const now = Math.round(new Date().getTime() / 1000); - const timeout = (_.isInteger(IFTTTParams.timeout) ? IFTTTParams.timeout : options.defaultTimeout); + const timeout = (IFTTTParams.optionalTimeout && _.isInteger(IFTTTParams.optionalTimeout)) ? + IFTTTParams.optionalTimeout : + options.defaultTimeout; // Ensure enough time has passed since last time an event was dispatched - if(!IFTTTTimers[data] || now - IFTTTTimers[data] > timeout) { - let postData = {}; - - postData[IFTTTParams.bodyKey] = data; + if (!IFTTTTimers[data] || now - IFTTTTimers[data] > timeout) { IFTTTTimers[data] = now; - - request.post({ + const request = { url: `https://maker.ifttt.com/trigger/${IFTTTParams.eventName}/with/key/${IFTTTParams.key}`, - form: postData - }, (err, response) => { - if(err) { - console.log('- IFTTT event dispatch failed'); - } else { - console.log('- IFTTT event dispatched'); - } - }); + form: { bodyKey: data }, + }; + + post(request) + .then((response) => logger.info('- IFTTT event dispatched', response)) + .catch((err) => logger.error('- IFTT event dispatch failed', err)); } else { - console.log('- IFTTT event ignored due to timeout'); + logger.info('- IFTTT event ignored due to timeout'); } }; -// Read endpoints file and create list of endpoints -fs.readFile(endpoints, 'utf-8', (err, data) => { - if(err) { - console.error('--endpoints file could not be read'); - process.exit(3); - } - - const endpointsList = _.remove(data.split('\n'), (item) => { - return _.isString(item) && !_.isEmpty(item); - }); - - if(!endpointsList.length) { - console.error('--endpoints file does not contain any endpoints'); - process.exit(4); - } - - // Cache initial endpoint responses - makeRequests(endpointsList, (responses) => { - const cache = responses; - - console.log(`${_.keys(responses).length} of ${endpointsList.length} responses cached`); - - setInterval(() => { - makeRequests(endpointsList, (responses) => { - const differences = _.difference(_.values(responses), _.values(cache)); - - if(differences.length) { - differences.forEach((difference) => { - const endpoint = _.invert(responses)[difference]; +// Cache passed responses and then setup diffing intervals +const diffCacheInterval = (responses) => { + const cache = responses; + logger.info(`${_.keys(responses).length} of ${endpoints.length} responses cached.`); - cache[endpoint] = difference; + const poll = () => makeRequests(endpoints).then((pollResponses) => { + const diff = _.difference(_.values(pollResponses), _.values(cache)); - console.log(`Difference identified within ${endpoint}`); + if (diff.length) { + diff.forEach((change) => { + const endpoint = _.invert(pollResponses)[change]; + cache[endpoint] = change; + logger.info(`Difference identified within ${endpoint}`); - if(ifttt) { - postIFTTT(endpoint); - } - }); - } else { - console.log(`No differences identified for ${_.keys(responses).length} responses`) - } + postIFTTT(endpoint); }); - }, interval * 1000); + } else { + logger.info(`No differences identified for ${_.keys(pollResponses).length} responses`); + } }); + + return setInterval(poll, interval * 1000); +}; + +// Start App +getEndpoints(endpointsPath) +.then((validEndpoints) => makeRequests(validEndpoints)) +.then((responses) => diffCacheInterval(responses)) +.catch((err) => { + logger.error('Error connecting to endpoints', err); + process.exit(1); }); diff --git a/example/endpoints b/example/endpoints index 21366ca..e4616fb 100644 --- a/example/endpoints +++ b/example/endpoints @@ -1,3 +1,3 @@ -http://first.web.site -http://second.web.site -http://third.web.site +http://www.google.com +http://www.facebook.com +http://www.myspace.com diff --git a/package.json b/package.json index e6e4615..d8d6339 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,16 @@ "dependencies": { "babel-preset-es2015": "^6.6.0", "babel-register": "^6.7.2", - "fs": "0.0.2", + "bluebird": "^3.3.4", "jsdom": "^8.1.0", "jsonfile": "^2.2.3", "lodash": "^4.6.1", "request": "^2.69.0", "winston": "^2.2.0", "yargs": "^4.3.2" + }, + "devDependencies": { + "eslint": "^2.6.0", + "eslint-config-airbnb": "^6.2.0" } } diff --git a/utils/logger.js b/utils/logger.js index cd49380..6b13be2 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -3,20 +3,16 @@ const winston = require('winston'); // Define logger configuration -let logger = new winston.Logger({ +const logger = new winston.Logger({ transports: [ new winston.transports.Console({ handleExceptions: true, json: false, colorize: true, - timestamp: true - }) + timestamp: true, + }), ], - exitOnError: false + exitOnError: false, }); -// Use logger in favor of native -console.log = logger.info; -console.error = logger.error; - module.exports = logger;