Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "airbnb/base"
}
241 changes: 127 additions & 114 deletions app.js
Original file line number Diff line number Diff line change
@@ -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);
});
6 changes: 3 additions & 3 deletions example/endpoints
Original file line number Diff line number Diff line change
@@ -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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
12 changes: 4 additions & 8 deletions utils/logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;