commit c6cffe029d6043628ef8dccdb0e4db454f9d65c0 Author: Christian Kellner Date: Sat Jan 20 20:23:27 2018 +0100 Release v1.0.0 :tada: diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..dc7b278 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +npm-debug.log +config.json +store.json +.DS_Store diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9fe1720 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +sudo: false +language: node_js +node_js: + - "8" \ No newline at end of file diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md new file mode 100644 index 0000000..9cc8201 --- /dev/null +++ b/CONTRIBUTION.md @@ -0,0 +1,28 @@ +# Contributing + +If you want to contribute, please make sure you've executed the tests. + + +### How to write new provider? +- create the provider filer under `/lib/provider` +- create the corresponding config both in `config/config.example` and `config/config.test.json` +- make sure the selector matches and that the needed fields are available +- create a test under /test and make sure it is running successfully + +### How to write new notification adapter? +- create the provider filer under `/lib/notification/adapter` +- make sure it exports a function `enabled` and a function `send` +- create the corresponding config both in `config/config.example` and `config/config.test.json` + +#### Running Tests +If you've written a new provider you are an awesome person. You know it and I do. If you now write tests for it, you are even more awesome. And who doesn't want to be more awesome right? + +To write tests for provider, you need to use Node 8 as the tests are using `async / await` + +##### To do before merging: + +- executed tests? (`npm run test`) +- executed reformat? (`npm run format`) +- sure the changes are useful for everybody? Or is it maybe a custom modification just for your case? + +_Thanks!_ :heart: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..049a92e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Christian Kellner + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..45eb0d8 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Fredy + +[F]ind [R]eal [E]states [D]amn Eas[y] :heart: + +My wife and I wanted to buy an apartment in germany. As the prices are quite high and good deals are very rare, we searched the "big 4" every morning. + +This however can get pretty frustrating. _Fredy_ will take this work away from you. It crawls the `provider`, mentioned below (Immonet, Immoscout...) every _x_ minutes. (The provider list can be extended easily...) + +If _Fredy_ found matching results, it will send them to via Slack. (More adapter possible.) _Fredy_ is remembering what it already has found to not send stuff twice. + +## Usage + +- Make sure to use Node 8 and above +- Install the dependencies using `npm install` +- create your configuration file. Use the example config for this. `cp conf/config.example conf/config.json` +- configure the desired values +- start _Fredy_ using `npm start` + + +## Configuration + +Before running _Fredy_ for the first time, you need to create a configuration file: + +Copy the example config as a start. +``` +cp conf/config.example conf/config.json +``` + +### 1. Notification + +You want to get notified when _Fredy_ found a new listing. Currently _Fredy_ support Slack to send notification. _Fredy_ also includes a notification adapter to print to the console instead of sending to a services. + +Adding new notification adapter is easy however. See [Contribution](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTION.md) +```json +"slack": { + "channel": "someChannel", + "token": "someToken", + "enabled": true +} +``` + +### 2. Configure the providers + +Configure the providers like described below. To disable a provider just remove its entry from the configuration or set it to `false`. + +#### Ebay Kleinanzeigen, Immoscout, Immowelt, Immonet and Kalaydo + +```json +"kleinanzeigen": { + "url": "https://www.ebay-kleinanzeigen.de/...", + "enabled": true +} +"immoscout": { + "url": "https://www.immobilienscout24.de/...", + "enabled": true +}, +"immowelt": { + "url": "https://www.immowelt.de/...", + "enabled": true +}, +"immonet": { + "url": "http://www.immonet.de/...", + "enabled": true +}, +"kalaydo": { + "url": "http://www.kalaydo.de/...", + "enabled": true +}, +``` + +Go to the respective provider page and create your custom search queries by +using the provided filter options. Then just copy and paste the whole URL of +the resulting listings page. + +**IMPORTANT:** Make sure to always sort by newest listings! This way _Fredy_ makes sure to not accidentally report stuff twice. + +#### Custom provider + +See [Contribution](https://github.com/orangecoding/fredy/blob/master/CONTRIBUTION.md) + +### 3. Add Filters (optional) + + +#### Blacklist + +```json +"blacklist": [ + "vermietet" +] +``` + +Listings which contain at least on of the given terms (ignoring case, only +whole words) are removed. + +#### District blacklist +```json +"blacklistedDistrics": [ + "Altstadt" +] +``` +Districts that are unwanted can be blacklisted here. + +This makes sense for provider that only offer limited filter functions like Kalaydo/Ebay. diff --git a/conf/config.example b/conf/config.example new file mode 100755 index 0000000..1910792 --- /dev/null +++ b/conf/config.example @@ -0,0 +1,50 @@ +{ + "interval": 10, + "enableStats": false, + "statsPort": 9875, + "notification": { + "slack": { + "token": "someToken", + "channel": "someChannel", + "enabled": false + }, + "console": { + "enabled": true + } + }, + "sources": { + "immoscout": { + "url": "https://www.immobilienscout24.de/...", + "enabled": true + }, + "immonet": { + "url": "https://www.immonet.de/...", + "enabled": true + }, + "immowelt": { + "url": "https://www.immowelt.de/...", + "enabled": true + }, + "kleinanzeigen": { + "url": "https://www.ebay-kleinanzeigen.de/...", + "enabled": true + }, + "kalaydo": { + "url": "https://www.kalaydo.de/...", + "enabled": true + } + }, + "blacklist": [ + "swap", + "tausch", + "wg", + "Vermietete", + "Vermietet", + "vermietete", + "vermietet" + ], + "blacklistedDistrics": [ + "some District you don't want" + ], + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36" +} diff --git a/conf/config.test.json b/conf/config.test.json new file mode 100755 index 0000000..37e1a77 --- /dev/null +++ b/conf/config.test.json @@ -0,0 +1,76 @@ +{ + "interval": 10, + "enableStats": false, + "statsPort": 9875, + "notification": { + "slack": { + "token": "", + "channel": "test", + "enabled": false + }, + "console": { + "enabled": true + } + }, + "sources": { + "immoscout": { + "url": "https://www.immobilienscout24.de/Suche/S-2/Wohnung-Kauf/Nordrhein-Westfalen/Duesseldorf/-/-/-/-/false/-/3,6,7,8,40,113,117,118,127?enteredFrom=result_list", + "enabled": true + }, + "immonet": { + "url": "https://www.immonet.de/immobiliensuche/sel.do?pageoffset=1&listsize=25&objecttype=1&locationname=D%C3%BCsseldorf&acid=&actype=&city=100207&ajaxIsRadiusActive=false&sortby=19&suchart=2&radius=0&pcatmtypes=1_1&pCatMTypeStoragefield=1_1&parentcat=1&marketingtype=1&fromprice=&toprice=&fromarea=&toarea=&fromplotarea=&toplotarea=&fromrooms=&torooms=&objectcat=225&objectcat=18&objectcat=17&objectcat=12&objectcat=16&objectcat=181&objectcat=14&objectcat=15&objectcat=226&objectcat=13&wbs=-1&fromyear=&toyear=&fulltext=&absenden=Ergebnisse+anzeigen", + "enabled": true + }, + "immowelt": { + "url": "https://www.immowelt.de/liste/duesseldorf/wohnungen/kaufen?lat=51.22061&lon=6.80575&sort=createdate%2Bdesc", + "enabled": true + }, + "kleinanzeigen": { + "url": "https://www.ebay-kleinanzeigen.de/s-wohnung-kaufen/duesseldorf/anzeige:angebote/wohnung/k0c196l2068r5", + "enabled": true + }, + "kalaydo": { + "url": "https://www.kalaydo.de/immobilien/eigentumswohnung-kaufen/o/duesseldorf/4/", + "enabled": true + } + }, + "blacklist": [ + "swap", + "tausch", + "wg", + "Vermietete", + "Vermietet", + "vermietete", + "vermietet" + ], + "blacklistedDistrics": [ + "Altstadt", + "Angermund", + "Carlstadt", + "Flehe", + "Friedrichstadt", + "Garath", + "Hafen", + "Hamm", + "Hassels", + "Heerdt", + "Hellerhof", + "Himmelgeist", + "Hubbelrath", + "Itter", + "Kaiserswerth", + "Kalkum", + "Lichtenbroich", + "Lohausen", + "Ludenberg", + "Niederkassel", + "Oberkassel", + "Stadtmittel", + "Stockum", + "Urdenbach", + "Vennhausen", + "Volmerswerth", + "Wittlaer" + ], + "userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36" +} diff --git a/index.js b/index.js new file mode 100755 index 0000000..b9f3d8f --- /dev/null +++ b/index.js @@ -0,0 +1,14 @@ +require('rootpath')(); +const fs = require('fs'); +const path = './lib/provider'; +const sources = fs.readdirSync(path); +const config = require('conf/config.json'); +const stats = require('./lib/services/stats'); + +setInterval( + (function exec() { + sources.forEach(source => require(`${path}/${source}`).run(stats)); + return exec; + })(), + config.interval * 60 * 1000 +); diff --git a/lib/errors.js b/lib/errors.js new file mode 100755 index 0000000..a35e0c2 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,16 @@ +class ExtendableError extends Error { + constructor(message) { + super(message); + this.name = this.constructor.name; + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor) + } else { + this.stack = (new Error(message)).stack + } + } +} + +class NoNewListingsError extends ExtendableError { +} + +module.exports = {NoNewListingsError}; diff --git a/lib/fredy.js b/lib/fredy.js new file mode 100755 index 0000000..38f33da --- /dev/null +++ b/lib/fredy.js @@ -0,0 +1,108 @@ +const {NoNewListingsError} = require('./errors'); +const Store = require('./services/store'); + +const notify = require('./notification/notify'); +const xray = require('./services/scraper'); + +class Fredy { + constructor(source) { + this._store = new Store(source.name); + this._fullCrawl = true; + this._source = source; + this._stats = null; + } + + run(stats) { + + if(!this._stats){ + this._stats = stats; + } + + if (!this._source.enabled) return Promise.resolve(); + + return Promise.resolve(this._source.url) + .then(this._store.warmup) + .then(this._getListings.bind(this)) + .then(this._normalize.bind(this)) + .then(this._filter.bind(this)) + .then(this._findNew.bind(this)) + .then(this._save.bind(this)) + .then(this._notify.bind(this)) + .then(this._updateStates.bind(this)) + .catch(this._handleError.bind(this)) + } + + _getListings(url) { + return new Promise((resolve, reject) => { + let x = xray(url, this._source.crawlContainer, [this._source.crawlFields]); + + if (this._source.paginage && this._fullCrawl) { + this._fullCrawl = false; + x = x.paginate(this._source.paginage) + } + + x((err, listings) => { + if (err) reject(err); + else resolve(listings) + }) + }) + } + + _normalize(listings) { + return listings.map(this._source.normalize) + } + + _filter(listings) { + return listings.filter(this._source.filter) + } + + _findNew(listings) { + const newListings = listings.filter( + o => this._store.knownListings.indexOf(o.id) === -1 + ); + + if (newListings.length === 0) { + this._updateStates([]); + throw new NoNewListingsError(); + } + + return newListings + } + + _notify(newListings) { + const sendNotifications = newListings.map(payload => { + return notify.send(this._source.name, payload); + } + ); + + return Promise.all(sendNotifications).then(() => newListings) + } + + _updateStates(newListings){ + this._stats.setLastScrape(this._source.name, newListings.length); + return newListings; + } + + _save(newListings) { + this._store.knownListings = [ + ...this._store.knownListings, + ...newListings.map(l => l.id) + ]; + return newListings; + } + + _handleError(err) { + if (err.name !== 'NoNewListingsError') console.error(err) + } + + /** + * for testing purposes only + * @returns {Store} + * @private + */ + _getStore(){ + return this._store; + } +} + +module.exports = Fredy; diff --git a/lib/notification/adapter/console.js b/lib/notification/adapter/console.js new file mode 100755 index 0000000..ded9475 --- /dev/null +++ b/lib/notification/adapter/console.js @@ -0,0 +1,18 @@ +const config = require('../../../conf/config.json'); + +/** + * simply prints out the found data to the console + * @param serviceName e.g immoscout + * @param payload the actual payload that is used to construct the message + * @returns {Promise | void} + */ +exports.send = (serviceName, payload) => { + return Promise.resolve(console.info(`Found entry from service ${serviceName}:`, payload)) +}; + +/** + * each integration needs to implement this method + */ +exports.enabled = () => { + return config.notification.console.enabled; +}; diff --git a/lib/notification/adapter/slack.js b/lib/notification/adapter/slack.js new file mode 100755 index 0000000..95b2a13 --- /dev/null +++ b/lib/notification/adapter/slack.js @@ -0,0 +1,54 @@ +const Slack = require('slack'); +const config = require('../../conf/config.json'); +const msg = Slack.chat.postMessage; + +const {token, channel} = config.notification.slack; + +/** + * sends a new listing to slack + * @param serviceName e.g immoscout + * @param payload the actual payload that is used to construct the message + * @returns {Promise | void} + */ +exports.send = (serviceName, payload) => { + return msg({ + token, + channel, + text: `*(${serviceName})* - ${payload.title}`, + "attachments": [ + { + "fallback": payload.title, + "color": "#36a64f", + "title": "Link to Exposé", + "title_link": payload.link, + "fields": [ + { + "title": "Price", + "value": payload.price, + "short": false + }, + { + "title": "Size", + "value": payload.size, + "short": false + }, + { + "title": "Address", + "value": payload.address, + "short": false + } + ], + "footer": "Powered by Fredy", + ts: new Date().getTime() / 1000 + } + ] + } + ); +}; + +/** + * each integration needs to implement this method + */ +exports.enabled = () => { + return config.notification.slack.enabled; +}; diff --git a/lib/notification/notify.js b/lib/notification/notify.js new file mode 100755 index 0000000..2d79e20 --- /dev/null +++ b/lib/notification/notify.js @@ -0,0 +1,17 @@ +const fs = require('fs'); +const path = './adapter'; + +/** Read every integration existing in ./adapter **/ +const adapter = fs + .readdirSync('./lib/notification/adapter') + .map(integPath => require(`${path}/${integPath}`)) + .filter(integration => integration.enabled()); + +if (adapter.length === 0) { + throw new Error('Please specify at least one notification provider'); +} + +exports.send = (serviceName, payload) => { + //this is not being used in tests, therefor adapter are always set + return adapter.map(a => a.send(serviceName, payload)); +}; diff --git a/lib/provider/immonet.js b/lib/provider/immonet.js new file mode 100755 index 0000000..e4d0be2 --- /dev/null +++ b/lib/provider/immonet.js @@ -0,0 +1,38 @@ +const config = require('../../conf/config.json'); +const Fredy = require('../fredy'); +const utils = require('../utils'); + +function normalize(o) { + const id = parseInt(o.id.split('_')[1], 10); + const title = o.title.replace('NEU ', ''); + const address = o.address.split(' - ')[1]; + + return Object.assign(o, { id, title, address }); +} + +function applyBlacklist(o) { + const titleNotBlacklisted = !utils.isOneOf(o.title, config.blacklist); + const descNotBlacklisted = !utils.isOneOf(o.description, config.blacklist); + + return titleNotBlacklisted && descNotBlacklisted; +} + +const immonet = { + name: 'immonet', + enabled: config.sources.immonet.enabled, + url: config.sources.immonet.url, + crawlContainer: '#idResultList .search-object', + crawlFields: { + id: '.search-info a@id', + price: '#keyfacts-bar div:first-child span', + size: '#keyfacts-bar div:nth-child(2) .text-primary-highlight', + title: '.search-info a | removeNewline | trim', + link: '.search-info a@href', + address: '.search-info p | removeNewline | trim' + }, + paginate: '#idResultList .margin-bottom-6.margin-bottom-sm-12 .panel a.pull-right@href', + normalize: normalize, + filter: applyBlacklist +}; + +module.exports = new Fredy(immonet); diff --git a/lib/provider/immoscout.js b/lib/provider/immoscout.js new file mode 100755 index 0000000..9f2f4a4 --- /dev/null +++ b/lib/provider/immoscout.js @@ -0,0 +1,34 @@ +const config = require('../../conf/config.json'); +const Fredy = require('../fredy'); +const utils = require('../utils'); + +function normalize(o) { + const title = o.title.replace('NEU', ''); + const address = (o.address || '').replace(/\(.*\),.*$/, '').trim(); + + return Object.assign(o, { title, address }); +} + +function applyBlacklist(o) { + return !utils.isOneOf(o.title, config.blacklist); +} + +const immoscout = { + name: 'immoscout', + enabled: config.sources.immoscout.enabled, + url: config.sources.immoscout.url, + crawlContainer: '#resultListItems li.result-list__listing', + crawlFields: { + id: '.result-list-entry@data-obid | int', + price: '.result-list-entry .result-list-entry__criteria .grid-item:first-child dd | removeNewline | trim', + size: '.result-list-entry .result-list-entry__criteria .grid-item:nth-child(2) dd | removeNewline | trim', + title: '.result-list-entry .result-list-entry__brand-title-container h5 | removeNewline | trim', + link: '.result-list-entry .result-list-entry__brand-title-container@href', + address: '.result-list-entry .result-list-entry__address a' + }, + paginate: '#pager .align-right a@href', + normalize: normalize, + filter: applyBlacklist +}; + +module.exports = new Fredy(immoscout); diff --git a/lib/provider/immowelt.js b/lib/provider/immowelt.js new file mode 100755 index 0000000..5c352e2 --- /dev/null +++ b/lib/provider/immowelt.js @@ -0,0 +1,38 @@ +const Fredy = require('../fredy'); +const config = require('../../conf/config.json'); +const utils = require('../utils'); + +function normalize(o) { + const size = o.size.split('Wohnfläche')[0]; + const address = o.address; + + return Object.assign(o, { size, address }); +} + +function applyBlacklist(o) { + const titleNotBlacklisted = !utils.isOneOf(o.title, config.blacklist); + const descNotBlacklisted = !utils.isOneOf(o.description, config.blacklist); + + return titleNotBlacklisted && descNotBlacklisted; +} + +const immowelt = { + name: 'immowelt', + enabled: config.sources.immowelt.enabled, + url: config.sources.immowelt.url, + crawlContainer: '.immoliste .js-object.listitem_wrap ', + crawlFields: { + id: '@data-estateid | int', + price: '.hardfacts_3 strong | removeNewline | trim', + size: '.hardfacts_3 div:nth-child(2):not(.hardfactlabel)| removeNewline | trim', + title: '.listcontent.clear h2', + link: 'a@href', + description: '.listconten_offset .listmerkmale| removeNewline | trim', + address: '.listconten_offset .listlocation| removeNewline | trim' + }, + paginate: '#pnlPaging #nlbPlus@href', + normalize: normalize, + filter: applyBlacklist +}; + +module.exports = new Fredy(immowelt); diff --git a/lib/provider/kalaydo.js b/lib/provider/kalaydo.js new file mode 100755 index 0000000..2cb1d7e --- /dev/null +++ b/lib/provider/kalaydo.js @@ -0,0 +1,46 @@ +const config = require('../../conf/config.json'); +const Fredy = require('../fredy'); +const utils = require('../utils'); + +function normalize(o) { + const id = o.id + .split('/') + .filter(Boolean) + .reverse()[0]; + const price = o.price.replace('Preis: ', ''); + let size = o.size.replace('Wohnfläche: ', '').replace('ca. ', ''); + size += ' / ' + o.rooms; + const address = '---'; + + return Object.assign(o, { id, price, size, address }); +} + +function applyBlacklist(o) { + const titleNotBlacklisted = !utils.isOneOf(o.title, config.blacklist); + const descNotBlacklisted = !utils.isOneOf(o.description, config.blacklist); + + const isBlacklistedDistrict = + config.blacklistedDistrics.length === 0 ? false : utils.isOneOf(o.title, config.blacklistedDistrics); + + return !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted; +} + +const kalaydo = { + name: 'kalaydo', + enabled: config.sources.kalaydo.enabled, + url: config.sources.kalaydo.url, + crawlContainer: '#resultList .resultitem-content-container', + crawlFields: { + id: '.resultitem-content-container a@href', + price: '.clear-row .rent | removeNewline | trim', + title: '.resultitem-content-container a@title', + link: '.resultitem-content-container a@href', + rooms: '.resultitem-content-container .no-of-rooms | removeNewline | trim', + size: '.resultitem-content-container .living-area | removeNewline | trim' + }, + paginate: '.markt_pagination_pageLinkNext .markt_pagination_link@href', + normalize: normalize, + filter: applyBlacklist +}; + +module.exports = new Fredy(kalaydo); diff --git a/lib/provider/kleinanzeigen.js b/lib/provider/kleinanzeigen.js new file mode 100755 index 0000000..b2429df --- /dev/null +++ b/lib/provider/kleinanzeigen.js @@ -0,0 +1,39 @@ +const Fredy = require('../fredy'); +const config = require('../../conf/config.json'); +const utils = require('../utils'); + +function normalize(o) { + const address = o.address.split('\n')[4].trim(); + + return Object.assign(o, { address }); +} + +function applyBlacklist(o) { + const titleNotBlacklisted = !utils.isOneOf(o.title, config.blacklist); + const descNotBlacklisted = !utils.isOneOf(o.description, config.blacklist); + const isBlacklistedDistrict = + config.blacklistedDistrics.length === 0 ? false : utils.isOneOf(o.description, config.blacklistedDistrics); + + return !isBlacklistedDistrict && titleNotBlacklisted && descNotBlacklisted; +} + +const kleinanzeigen = { + name: 'kleinanzeigen', + enabled: config.sources.kleinanzeigen.enabled, + url: config.sources.kleinanzeigen.url, + crawlContainer: '#srchrslt-adtable .ad-listitem', + crawlFields: { + id: '.aditem@data-adid | int', + price: '.aditem-details strong | removeNewline | trim', + size: '.aditem-main .text-module-end span:nth-child(2) | removeNewline | trim', + title: '.aditem-main .text-module-begin a | removeNewline | trim', + link: '.aditem-main .text-module-begin a@href | removeNewline | trim', + description: '.aditem-main p:not(.text-module-end) | removeNewline | trim', + address: '.aditem-details' + }, + paginate: '#srchrslt-pagination .pagination-next@href', + normalize: normalize, + filter: applyBlacklist +}; + +module.exports = new Fredy(kleinanzeigen); diff --git a/lib/services/scraper.js b/lib/services/scraper.js new file mode 100755 index 0000000..b26d696 --- /dev/null +++ b/lib/services/scraper.js @@ -0,0 +1,42 @@ +const makeDriver = require('request-x-ray'); +const config = require('../../conf/config.json'); +const Xray = require('x-ray'); + +class Scraper { + constructor() { + const filters = { + removeNewline: this._removeNewline, + trim: this._trim, + int: this._int + }; + + const driver = makeDriver({ + headers: { + 'User-Agent': config.userAgent + } + }); + + const xray = Xray({ filters }); + xray.driver(driver); + + this.xray = xray; + } + + get x() { + return this.xray; + } + + _removeNewline(value) { + return typeof value === 'string' ? value.replace(/\\n/g, '') : value; + } + + _trim(value) { + return typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : value; + } + + _int(value) { + return typeof value === 'string' ? parseInt(value, 10) : value; + } +} + +module.exports = new Scraper().x; diff --git a/lib/services/stats.js b/lib/services/stats.js new file mode 100644 index 0000000..053f0d3 --- /dev/null +++ b/lib/services/stats.js @@ -0,0 +1,25 @@ +const config = require('../../conf/config.json'); +let lastScrape = {}; + +if (config.enableStats) { + const http = require('http'); + http + .createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + config, + lastScrape + }) + ); + }) + .listen(config.statsPort, '127.0.0.1'); +} + +exports.setLastScrape = (serviceName, numberFound) => { + lastScrape[serviceName] = lastScrape[serviceName] || []; + lastScrape[serviceName].push({ + scapeTime: new Date().toString(), + numberFound: numberFound + }); +}; diff --git a/lib/services/store.js b/lib/services/store.js new file mode 100755 index 0000000..a2f2597 --- /dev/null +++ b/lib/services/store.js @@ -0,0 +1,36 @@ +const path = require('path'); +const DB_PATH = path.dirname(require.main.filename) + '/conf/store.json'; + +const FileAsync = require('lowdb/adapters/FileAsync'); +const adapter = new FileAsync(DB_PATH); +const low = require('lowdb'); + +const lowdb = low(adapter); + +class Store { + constructor(name) { + this._name = name; + this._db = null; + } + + get warmup() { + return new Promise(resolve => { + lowdb.then(db => { + this._db = db; + resolve(); + }); + }); + } + + set knownListings(value) { + if (!Array.isArray(value)) throw Error('Not a valid array'); + + return this._db.set(this._name, value).write(); + } + + get knownListings() { + return this._db.get(this._name).value() || []; + } +} + +module.exports = Store; diff --git a/lib/utils.js b/lib/utils.js new file mode 100755 index 0000000..28a8b81 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,8 @@ +function isOneOf (word, arr) { + const expression = String.raw`\b(${arr.join('|')})\b`; + const blacklist = new RegExp(expression, 'ig'); + + return blacklist.test(word) +} + +module.exports = { isOneOf }; diff --git a/package.json b/package.json new file mode 100755 index 0000000..a126201 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "Fredy", + "version": "1.0.0", + "description": "[F]ind [R]eal [E]states [d]amn eas[y].", + "scripts": { + "start": "node index.js", + "format": "prettier --write lib/**/*.js test/**/*.js *.js --single-quote --print-width 120", + "test": "mocha --timeout 8000" + }, + "main": "index.js", + "author": "Christian Kellner", + "keywords": [ + "flat", + "flatfinder", + "fredy", + "real estates", + "germany", + "apartment", + "house", + "rent", + "immoscout", + "immonet", + "immowelt", + "immobilienscout24" + ], + "bugs": { + "url": "https://github.com/orangecoding/fredy/issues" + }, + "license": "MIT", + "dependencies": { + "chai": "^4.1.2", + "lowdb": "^1.0.0", + "mocha": "^4.1.0", + "request-x-ray": "^0.1.4", + "rootpath": "^0.1.2", + "slack": "^10.0.0", + "x-ray": "^2.3.2" + }, + "devDependencies": { + "prettier": "^1.0.0", + "proxyquire": "^1.8.0" + } +} diff --git a/test/immonet.test.js b/test/immonet.test.js new file mode 100644 index 0000000..dbc02a0 --- /dev/null +++ b/test/immonet.test.js @@ -0,0 +1,49 @@ +const mockNotification = require('./mocks/mockNotification'); +const mockConfig = require('../conf/config.test'); +const mockStore = require('./mocks/mockStore'); +const mockStats = require('./mocks/mockStats'); +const proxyquire = require('proxyquire').noCallThru(); +const expect = require('chai').expect; + +describe('#immonet testsuite()', () => { + + const immonet = proxyquire('../lib/provider/immonet', { + '../../conf/config.json': mockConfig, + '../lib/fredy': proxyquire('../lib/fredy', { + './services/store': mockStore, + './notification/notify': mockNotification + }) + }); + + it('should test immonet provider', async () => { + return await new Promise(resolve => { + immonet.run(mockStats).then(() => { + const immonetDbContent = immonet._getStore()._db; + + expect(immonetDbContent.immonet).to.be.a('array'); + + const notificationObj = mockNotification.get(); + expect(notificationObj).to.be.a('object'); + expect(notificationObj.serviceName).to.equal('immonet'); + + /** check the actual structure **/ + expect(notificationObj.payload.id).to.be.a('number'); + expect(notificationObj.payload.price).to.be.a('string'); + expect(notificationObj.payload.size).to.be.a('string'); + expect(notificationObj.payload.title).to.be.a('string'); + expect(notificationObj.payload.link).to.be.a('string'); + expect(notificationObj.payload.address).to.be.a('string'); + + /** check the values if possible **/ + expect(notificationObj.payload.id).to.equal(immonetDbContent.immonet[immonetDbContent.immonet.length - 1]); + expect(notificationObj.payload.price).that.does.include('€'); + expect(notificationObj.payload.size).that.does.include('m²'); + expect(notificationObj.payload.title).to.be.not.empty; + expect(notificationObj.payload.link).that.does.include('https://www.immonet.de'); + expect(notificationObj.payload.address).to.be.not.empty; + + resolve(); + }); + }); + }); +}); diff --git a/test/immoscout.test.js b/test/immoscout.test.js new file mode 100644 index 0000000..26b3ea2 --- /dev/null +++ b/test/immoscout.test.js @@ -0,0 +1,50 @@ +const mockNotification = require('./mocks/mockNotification'); +const mockConfig = require('../conf/config.test'); +const mockStats = require('./mocks/mockStats'); +const mockStore = require('./mocks/mockStore'); +const proxyquire = require('proxyquire').noCallThru(); +const expect = require('chai').expect; + +describe('#immoscout testsuite()', () => { + + const immoscout = proxyquire('../lib/provider/immoscout', { + '../../conf/config.json': mockConfig, + '../lib/fredy': proxyquire('../lib/fredy', { + './services/store': mockStore, + './notification/notify': mockNotification + }) + }); + + it('should test immoscout provider', async () => { + return await new Promise(resolve => { + immoscout.run(mockStats).then(() => { + const immoscoutDbContent = immoscout._getStore()._db; + expect(immoscoutDbContent.immoscout).to.be.a('array'); + + const notificationObj = mockNotification.get(); + expect(notificationObj).to.be.a('object'); + expect(notificationObj.serviceName).to.equal('immoscout'); + + /** check the actual structure **/ + expect(notificationObj.payload.id).to.be.a('number'); + expect(notificationObj.payload.price).to.be.a('string'); + expect(notificationObj.payload.size).to.be.a('string'); + expect(notificationObj.payload.title).to.be.a('string'); + expect(notificationObj.payload.link).to.be.a('string'); + expect(notificationObj.payload.address).to.be.a('string'); + + /** check the values if possible **/ + expect(notificationObj.payload.id).to.equal( + immoscoutDbContent.immoscout[immoscoutDbContent.immoscout.length - 1] + ); + expect(notificationObj.payload.price).that.does.include('€'); + expect(notificationObj.payload.size).that.does.include('m²'); + expect(notificationObj.payload.title).to.be.not.empty; + expect(notificationObj.payload.link).that.does.include('https://www.immobilienscout24.de'); + expect(notificationObj.payload.address).to.be.not.empty; + + resolve(); + }).catch(resolve); + }); + }); +}); diff --git a/test/immowelt.test.js b/test/immowelt.test.js new file mode 100644 index 0000000..1ff7258 --- /dev/null +++ b/test/immowelt.test.js @@ -0,0 +1,51 @@ +const mockNotification = require('./mocks/mockNotification'); +const mockConfig = require('../conf/config.test'); +const mockStats = require('./mocks/mockStats'); +const mockStore = require('./mocks/mockStore'); +const proxyquire = require('proxyquire').noCallThru(); +const expect = require('chai').expect; + +describe('#immowelt testsuite()', () => { + + it('should test immowelt provider', async () => { + + const immowelt = proxyquire('../lib/provider/immowelt', { + '../../conf/config.json': mockConfig, + '../lib/fredy': proxyquire('../lib/fredy', { + './services/store': mockStore, + './notification/notify': mockNotification + }) + }); + + return await new Promise(resolve => { + immowelt.run(mockStats).then(() => { + const immoweltDbContent = immowelt._getStore()._db; + expect(immoweltDbContent.immowelt).to.be.a('array'); + + const notificationObj = mockNotification.get(); + expect(notificationObj).to.be.a('object'); + expect(notificationObj.serviceName).to.equal('immowelt'); + + /** check the actual structure **/ + expect(notificationObj.payload.id).to.be.a('number'); + expect(notificationObj.payload.price).to.be.a('string'); + expect(notificationObj.payload.size).to.be.a('string'); + expect(notificationObj.payload.title).to.be.a('string'); + expect(notificationObj.payload.link).to.be.a('string'); + expect(notificationObj.payload.address).to.be.a('string'); + + /** check the values if possible **/ + expect(notificationObj.payload.id).to.equal( + immoweltDbContent.immowelt[immoweltDbContent.immowelt.length - 1] + ); + expect(notificationObj.payload.price).that.does.include('€'); + expect(notificationObj.payload.size).that.does.include('m²'); + expect(notificationObj.payload.title).to.be.not.empty; + expect(notificationObj.payload.link).that.does.include('https://www.immowelt.de'); + expect(notificationObj.payload.address).to.be.not.empty; + + resolve(); + }); + }); + }); +}); diff --git a/test/kalaydo.test.js b/test/kalaydo.test.js new file mode 100644 index 0000000..4abd10c --- /dev/null +++ b/test/kalaydo.test.js @@ -0,0 +1,48 @@ +const mockNotification = require('./mocks/mockNotification'); +const mockConfig = require('../conf/config.test'); +const mockStore = require('./mocks/mockStore'); +const mockStats = require('./mocks/mockStats'); +const proxyquire = require('proxyquire').noCallThru(); +const expect = require('chai').expect; + +describe('#kalaydo testsuite()', () => { + + const kalaydo = proxyquire('../lib/provider/kalaydo', { + '../../conf/config.json': mockConfig, + '../lib/fredy': proxyquire('../lib/fredy', { + './services/store': mockStore, + './notification/notify': mockNotification + }) + }); + + it('should test kalaydo provider', async () => { + return await new Promise(resolve => { + kalaydo.run(mockStats).then(() => { + const kalaydoDbContent = kalaydo._getStore()._db; + + expect(kalaydoDbContent.kalaydo).to.be.a('array'); + + + const notificationObj = mockNotification.get(); + expect(notificationObj).to.be.a('object'); + expect(notificationObj.serviceName).to.equal('kalaydo'); + + /** check the actual structure **/ + expect(notificationObj.payload.id).to.be.a('string'); + expect(notificationObj.payload.price).to.be.a('string'); + expect(notificationObj.payload.size).to.be.a('string'); + expect(notificationObj.payload.title).to.be.a('string'); + expect(notificationObj.payload.link).to.be.a('string'); + + /** check the values if possible **/ + expect(notificationObj.payload.id).to.equal(kalaydoDbContent.kalaydo[kalaydoDbContent.kalaydo.length - 1]); + expect(notificationObj.payload.price).that.does.include('€'); + expect(notificationObj.payload.size).that.does.include('m²'); + expect(notificationObj.payload.title).to.be.not.empty; + expect(notificationObj.payload.link).that.does.include('https://www.kalaydo.de'); + + resolve(); + }); + }); + }); +}); diff --git a/test/kleinanzeigen.test.js b/test/kleinanzeigen.test.js new file mode 100644 index 0000000..ef35f3a --- /dev/null +++ b/test/kleinanzeigen.test.js @@ -0,0 +1,51 @@ +const mockNotification = require('./mocks/mockNotification'); +const mockConfig = require('../conf/config.test'); +const mockStats = require('./mocks/mockStats'); +const mockStore = require('./mocks/mockStore'); +const proxyquire = require('proxyquire').noCallThru(); +const expect = require('chai').expect; + +describe('#kleinanzeigen testsuite()', () => { + + it('should test kleinanzeigen provider', async () => { + + const kleinanzeigen = proxyquire('../lib/provider/kleinanzeigen', { + '../../conf/config.json': mockConfig, + '../lib/fredy': proxyquire('../lib/fredy', { + './services/store': mockStore, + './notification/notify': mockNotification + }) + }); + + return await new Promise(resolve => { + kleinanzeigen.run(mockStats).then(() => { + const kleinanzeigenDbContent = kleinanzeigen._getStore()._db; + expect(kleinanzeigenDbContent.kleinanzeigen).to.be.a('array'); + + const notificationObj = mockNotification.get(); + expect(notificationObj).to.be.a('object'); + expect(notificationObj.serviceName).to.equal('kleinanzeigen'); + + /** check the actual structure **/ + expect(notificationObj.payload.id).to.be.a('number'); + expect(notificationObj.payload.price).to.be.a('string'); + expect(notificationObj.payload.size).to.be.a('string'); + expect(notificationObj.payload.title).to.be.a('string'); + expect(notificationObj.payload.link).to.be.a('string'); + expect(notificationObj.payload.address).to.be.a('string'); + + /** check the values if possible **/ + expect(notificationObj.payload.id).to.equal( + kleinanzeigenDbContent.kleinanzeigen[kleinanzeigenDbContent.kleinanzeigen.length - 1] + ); + expect(notificationObj.payload.price).that.does.include('€'); + expect(notificationObj.payload.size).that.does.include('m²'); + expect(notificationObj.payload.title).to.be.not.empty; + expect(notificationObj.payload.link).that.does.include('https://www.ebay-kleinanzeigen.de'); + expect(notificationObj.payload.address).to.be.not.empty; + + resolve(); + }); + }); + }); +}); diff --git a/test/mocks/mockNotification.js b/test/mocks/mockNotification.js new file mode 100644 index 0000000..2cd6f42 --- /dev/null +++ b/test/mocks/mockNotification.js @@ -0,0 +1,11 @@ +module.exports = { + _tmpStore: {}, + + send: (serviceName, payload) => { + this._tmpStore = { serviceName, payload }; + }, + + get: () => { + return this._tmpStore; + } +}; diff --git a/test/mocks/mockStats.js b/test/mocks/mockStats.js new file mode 100644 index 0000000..e2a73e7 --- /dev/null +++ b/test/mocks/mockStats.js @@ -0,0 +1,5 @@ +module.exports = { + setLastScrape: () => { + /*noop*/ + } +}; diff --git a/test/mocks/mockStore.js b/test/mocks/mockStore.js new file mode 100644 index 0000000..b617063 --- /dev/null +++ b/test/mocks/mockStore.js @@ -0,0 +1,27 @@ +class Store { + constructor(name) { + this._name = name; + this._db = {}; + } + + get warmup() { + this._db = {}; + return new Promise(resolve => resolve()); + } + + set knownListings(value) { + if (!Array.isArray(value)) throw Error('Not a valid array'); + return new Promise(resolve => { + this._db[this._name] = value; + resolve(value); + }); + } + + get bla() {} + + get knownListings() { + return this._db[this._name] || []; + } +} + +module.exports = Store; diff --git a/test/utils.test.js b/test/utils.test.js new file mode 100644 index 0000000..3e7082f --- /dev/null +++ b/test/utils.test.js @@ -0,0 +1,13 @@ +const utils = require('../lib/utils'); +const assert = require('assert'); + +describe('utils', () => { + describe('#isOneOf()', () => { + it('should be false', () => { + assert.equal(utils.isOneOf('bla', ['blub']), false); + }); + it('should be true', () => { + assert.equal(utils.isOneOf('bla blub blubber', ['bla']), true); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock new file mode 100755 index 0000000..fd48f54 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1000 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +CSSselect@~0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/CSSselect/-/CSSselect-0.4.1.tgz#f8ab7e1f8418ce63cda6eb7bd778a85d7ec492b2" + dependencies: + CSSwhat "0.4" + domutils "1.4" + +CSSwhat@0.4: + version "0.4.7" + resolved "https://registry.yarnpkg.com/CSSwhat/-/CSSwhat-0.4.7.tgz#867da0ff39f778613242c44cfea83f0aa4ebdf9b" + +abab@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" + +accepts@^1.2.5: + version "1.3.3" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" + dependencies: + mime-types "~2.1.11" + negotiator "0.6.1" + +acorn-globals@^1.0.4: + version "1.0.9" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-1.0.9.tgz#55bb5e98691507b74579d0513413217c380c54cf" + dependencies: + acorn "^2.1.0" + +acorn@^2.1.0, acorn@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-2.7.0.tgz#ab6e7d9d886aaca8b085bc3312b79a198433f0e7" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + +ansi-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.0.0.tgz#c5061b6e0ef8a81775e50f5d66151bf6bf371107" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +asn1@~0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" + +assert-plus@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" + +assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + +async@^1.4.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f" + +aws4@^1.2.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.5.0.tgz#0a29ffb79c31c9e712eeb087e8e7a64b4a56d755" + +batch@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.5.3.tgz#3f3414f380321743bfc1042f9a83ff1d5824d464" + +bcrypt-pbkdf@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz#3ca76b85241c7170bf7d9703e7b9aa74630040d4" + dependencies: + tweetnacl "^0.14.3" + +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + +boom@2.x.x: + version "2.10.1" + resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" + dependencies: + hoek "2.x.x" + +caseless@~0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7" + +chalk@^1.1.1, chalk@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +cheerio@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.18.0.tgz#4e1c06377e725b740e996e0dfec353863de677fa" + dependencies: + CSSselect "~0.4.0" + dom-serializer "~0.0.0" + entities "~1.1.1" + htmlparser2 "~3.8.1" + lodash "~2.4.1" + +cheerio@~0.20.0: + version "0.20.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.20.0.tgz#5c710f2bab95653272842ba01c6ea61b3545ec35" + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.0" + entities "~1.1.1" + htmlparser2 "~3.8.1" + lodash "^4.1.0" + optionalDependencies: + jsdom "^7.0.2" + +co@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/co/-/co-3.1.0.tgz#4ea54ea5a08938153185e15210c68d9092bc1b78" + +combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" + dependencies: + graceful-readlink ">= 1.0.0" + +component-emitter@~1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + +content-disposition@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.1.tgz#87476c6a67c8daa87e32e87616df883ba7fb071b" + +content-type@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" + +cookiejar@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.0.6.tgz#0abf356ad00d1c5a219d88d44518046dd026acfe" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cryptiles@2.x.x: + version "2.0.5" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" + dependencies: + boom "2.x.x" + +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" + +cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": + version "0.3.1" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.1.tgz#c9e37ef2490e64f6d1baa10fda852257082c25d3" + +"cssstyle@>= 0.2.29 < 0.3.0": + version "0.2.37" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" + dependencies: + cssom "0.3.x" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +debug@2, debug@^2.1.3, debug@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-0.1.0.tgz#b4b57be11a1653517a04b27f0949bdc327dfe390" + +destroy@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +dom-serializer@0, dom-serializer@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +dom-serializer@~0.0.0: + version "0.0.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.0.1.tgz#9589827f1e32d22c37c829adabd59b3247af8eaf" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1, domelementtype@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + dependencies: + domelementtype "1" + +domutils@1.4: + version "1.4.3" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.4.3.tgz#0865513796c6b306031850e175516baf80b72a6f" + dependencies: + domelementtype "1" + +domutils@1.5, domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" + dependencies: + jsbn "~0.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +emitter-component@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/emitter-component/-/emitter-component-1.1.1.tgz#065e2dbed6959bf470679edabeaf7981d1003ab6" + +enqueue@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/enqueue/-/enqueue-1.0.2.tgz#9014e9bce570ee93ca96e6c8e63ad54c192b6bc8" + dependencies: + sliced "0.0.5" + +enstore@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/enstore/-/enstore-1.0.1.tgz#a20fe71eaebde8a3813a0a1240475f55854a81ab" + dependencies: + monotonic-timestamp "0.0.8" + +entities@1.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" + +entities@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" + +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + +escape-html@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +escodegen@^1.6.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.8.1.tgz#5a5b53af4693110bebb0867aa3430dd3b70a1018" + dependencies: + esprima "^2.7.1" + estraverse "^1.9.1" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.2.0" + +esprima@^2.7.1: + version "2.7.3" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" + +estraverse@^1.9.1: + version "1.9.3" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +extend@3.0.0, extend@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.0.tgz#5a474353b9f3353ddd8176dfd37b91c83a46f1d4" + +extsprintf@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550" + +fast-levenshtein@~2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.5.tgz#bd33145744519ab1c36c3ee9f31f08e9079b67f2" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@1.0.0-rc3: + version "1.0.0-rc3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-1.0.0-rc3.tgz#d35bc62e7fbc2937ae78f948aaa0d38d90607577" + dependencies: + async "^1.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.3" + +form-data@~2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.2.tgz#89c3534008b97eada4cbb157d58f6f5df025eae4" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.5" + mime-types "^2.1.12" + +format-parser@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/format-parser/-/format-parser-0.0.2.tgz#4318822a8a9f1a29a0137253b908719c4f9222a2" + +formidable@~1.0.14: + version "1.0.17" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559" + +generate-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74" + +generate-object-property@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0" + dependencies: + is-property "^1.0.0" + +getpass@^0.1.1: + version "0.1.6" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.6.tgz#283ffd9fc1256840875311c1b60e8c40187110e6" + dependencies: + assert-plus "^1.0.0" + +graceful-fs@^4.1.3: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + +har-validator@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d" + dependencies: + chalk "^1.1.1" + commander "^2.9.0" + is-my-json-valid "^2.12.4" + pinkie-promise "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +hawk@~3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" + dependencies: + boom "2.x.x" + cryptiles "2.x.x" + hoek "2.x.x" + sntp "1.x.x" + +hoek@2.x.x: + version "2.16.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" + +htmlparser2@~3.8.1: + version "3.8.3" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" + dependencies: + domelementtype "1" + domhandler "2.3" + domutils "1.5" + entities "1.0" + readable-stream "1.1" + +http-context@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-context/-/http-context-1.1.1.tgz#a40171b389dba63042a9c4872229c7ad0efca510" + dependencies: + accepts "^1.2.5" + assert "^1.3.0" + content-disposition "^0.5.0" + content-type "^1.0.1" + delegates "^0.1.0" + destroy "^1.0.3" + error-inject "^1.0.0" + escape-html "^1.0.1" + http-incoming "^0.12.0" + http-outgoing "^0.12.0" + koa-is-json "^1.0.0" + mime-types "^2.0.10" + on-finished "^2.2.0" + parseurl "^1.3.0" + querystring "^0.2.0" + statuses "^1.2.1" + type-is "^1.6.1" + vary "^1.0.0" + +http-incoming@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/http-incoming/-/http-incoming-0.12.0.tgz#10783cd2b5deb8ca92ab3ff2ad171315d61b4ff6" + +http-outgoing@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/http-outgoing/-/http-outgoing-0.12.0.tgz#662f3a27c7a4d14c924b5f5314909efabde1830d" + +http-signature@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.1.1.tgz#df72e267066cd0ac67fb76adf8e134a8fbcf91bf" + dependencies: + assert-plus "^0.2.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + +inherits@~2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +is-browser@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-browser/-/is-browser-2.0.1.tgz#8bf0baf799a9c62fd9de5bcee4cf3397c3e7529a" + +is-my-json-valid@^2.12.4: + version "2.15.0" + resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz#936edda3ca3c211fd98f3b2d3e08da43f7b2915b" + dependencies: + generate-function "^2.0.0" + generate-object-property "^1.1.0" + jsonpointer "^4.0.0" + xtend "^4.0.0" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-property@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +is-url@~1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.2.tgz#498905a593bf47cc2d9e7f738372bbf7696c7f26" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isobject@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.0.0.tgz#208de872bd7378c2a92af9428a3f56eb91a122c4" + dependencies: + isarray "0.0.1" + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jodid25519@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/jodid25519/-/jodid25519-1.0.2.tgz#06d4912255093419477d425633606e0e90782967" + dependencies: + jsbn "~0.1.0" + +jsbn@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.0.tgz#650987da0dd74f4ebf5a11377a2aa2d273e97dfd" + +jsdom@^7.0.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-7.2.2.tgz#40b402770c2bda23469096bee91ab675e3b1fc6e" + dependencies: + abab "^1.0.0" + acorn "^2.4.0" + acorn-globals "^1.0.4" + cssom ">= 0.3.0 < 0.4.0" + cssstyle ">= 0.2.29 < 0.3.0" + escodegen "^1.6.1" + nwmatcher ">= 1.3.7 < 2.0.0" + parse5 "^1.5.1" + request "^2.55.0" + sax "^1.1.4" + symbol-tree ">= 3.1.0 < 4.0.0" + tough-cookie "^2.2.0" + webidl-conversions "^2.0.0" + whatwg-url-compat "~0.6.5" + xml-name-validator ">= 2.0.1 < 3.0.0" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonpointer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.0.tgz#6661e161d2fc445f19f98430231343722e1fcbd5" + +jsprim@^1.2.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.3.1.tgz#2a7256f70412a29ee3670aaca625994c4dcff252" + dependencies: + extsprintf "1.0.2" + json-schema "0.2.3" + verror "1.3.6" + +koa-is-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +lodash@4, lodash@^4.1.0: + version "4.17.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42" + +lodash@~2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-2.4.2.tgz#fadd834b9683073da179b3eae6d9c0d15053f73e" + +lowdb@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lowdb/-/lowdb-1.0.0.tgz#5243be6b22786ccce30e50c9a33eac36b20c8064" + dependencies: + graceful-fs "^4.1.3" + is-promise "^2.1.0" + lodash "4" + pify "^3.0.0" + steno "^0.4.1" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +methods@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.25.0: + version "1.25.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.25.0.tgz#c18dbd7c73a5dbf6f44a024dc0d165a1e7b1c392" + +mime-types@^2.0.10, mime-types@^2.1.12, mime-types@^2.1.3, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: + version "2.1.13" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.13.tgz#e07aaa9c6c6b9a7ca3012c69003ad25a39e92a88" + dependencies: + mime-db "~1.25.0" + +mime@1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" + +monotonic-timestamp@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/monotonic-timestamp/-/monotonic-timestamp-0.0.8.tgz#67987d02a41c15f568b6c0a05885989dd2402ba0" + +ms@0.7.1, ms@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +nth-check@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" + dependencies: + boolbase "~1.0.0" + +"nwmatcher@>= 1.3.7 < 2.0.0": + version "1.3.9" + resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.3.9.tgz#8bab486ff7fa3dfd086656bbe8b17116d3692d2a" + +oauth-sign@~0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" + +object-assign@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.0.1.tgz#99504456c3598b5cad4fc59c26e8a9bb107fe0bd" + +on-finished@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +optionator@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +parse5@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" + +parseurl@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + +prettier@^1.0.0: + version "1.10.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-2.3.3.tgz#e9e85adbe75da0bbe4c8e0476a086290f863b404" + +qs@~6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442" + +querystring@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + +readable-stream@1.0.27-1: + version "1.0.27-1" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.27-1.tgz#6b67983c20357cefd07f0165001a16d710d91078" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@1.1: + version "1.1.13" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +reduce-component@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/reduce-component/-/reduce-component-1.0.1.tgz#e0c93542c574521bea13df0f9488ed82ab77c5da" + +request-x-ray@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/request-x-ray/-/request-x-ray-0.1.4.tgz#58f5401b4f5bd0d44275c36a4659961921bdd8c1" + dependencies: + request "^2.74.0" + +request@^2.55.0, request@^2.74.0: + version "2.79.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de" + dependencies: + aws-sign2 "~0.6.0" + aws4 "^1.2.1" + caseless "~0.11.0" + combined-stream "~1.0.5" + extend "~3.0.0" + forever-agent "~0.6.1" + form-data "~2.1.1" + har-validator "~2.0.6" + hawk "~3.1.3" + http-signature "~1.1.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.7" + oauth-sign "~0.8.1" + qs "~6.3.0" + stringstream "~0.0.4" + tough-cookie "~2.3.0" + tunnel-agent "~0.4.1" + uuid "^3.0.0" + +rootpath@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/rootpath/-/rootpath-0.1.2.tgz#5b379a87dca906e9b91d690a599439bef267ea6b" + +sax@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + +selectn@^0.9.6: + version "0.9.6" + resolved "https://registry.yarnpkg.com/selectn/-/selectn-0.9.6.tgz#bd873a556d18f96d8515fc91503ec6ff398ff9a2" + +slack@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/slack/-/slack-10.0.0.tgz#4b46c174f89c9fa4a12e1da62a86beb0fb87f401" + dependencies: + tiny-json-http "^5.3.2" + +sliced@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-0.0.5.tgz#5edc044ca4eb6f7816d50ba2fc63e25d8fe4707f" + +sntp@1.x.x: + version "1.0.9" + resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" + dependencies: + hoek "2.x.x" + +source-map@~0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" + dependencies: + amdefine ">=0.0.4" + +sshpk@^1.7.0: + version "1.10.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.10.1.tgz#30e1a5d329244974a1af61511339d595af6638b0" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + dashdash "^1.12.0" + getpass "^0.1.1" + optionalDependencies: + bcrypt-pbkdf "^1.0.0" + ecc-jsbn "~0.1.1" + jodid25519 "^1.0.0" + jsbn "~0.1.0" + tweetnacl "~0.14.0" + +statuses@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +steno@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/steno/-/steno-0.4.4.tgz#071105bdfc286e6615c0403c27e9d7b5dcb855cb" + dependencies: + graceful-fs "^4.1.3" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +stringstream@~0.0.4: + version "0.0.5" + resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +superagent@^1.1.0: + version "1.8.4" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-1.8.4.tgz#68b2c8a400f1753ddb39410906899e42f420fd3a" + dependencies: + component-emitter "~1.2.0" + cookiejar "2.0.6" + debug "2" + extend "3.0.0" + form-data "1.0.0-rc3" + formidable "~1.0.14" + methods "~1.1.1" + mime "1.3.4" + qs "2.3.3" + readable-stream "1.0.27-1" + reduce-component "1.0.1" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +"symbol-tree@>= 3.1.0 < 4.0.0": + version "3.1.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.1.4.tgz#02b279348d337debc39694c5c95f882d448a312a" + +tiny-json-http@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/tiny-json-http/-/tiny-json-http-5.3.2.tgz#4756a58511a63eccc6ddde2a32f80c9c651f346b" + +tough-cookie@^2.2.0, tough-cookie@~2.3.0: + version "2.3.2" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a" + dependencies: + punycode "^1.4.1" + +tr46@~0.0.1: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + +tunnel-agent@~0.4.1: + version "0.4.3" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.3.tgz#3da382f670f25ded78d7b3d1792119bca0b7132d" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + dependencies: + prelude-ls "~1.1.2" + +type-is@^1.6.1: + version "1.6.14" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.14.tgz#e219639c17ded1ca0789092dd54a03826b817cb2" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.13" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + +uuid@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.0.tgz#6728fc0459c450d796a99c31837569bdf672d728" + +vary@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.0.tgz#e1e5affbbd16ae768dd2674394b9ad3022653140" + +verror@1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.3.6.tgz#cff5df12946d297d2baaefaa2689e25be01c005c" + dependencies: + extsprintf "1.0.2" + +webidl-conversions@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-2.0.1.tgz#3bf8258f7d318c7443c36f2e169402a1a6703506" + +whatwg-url-compat@~0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz#00898111af689bb097541cd5a45ca6c8798445bf" + dependencies: + tr46 "~0.0.1" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + +wrap-fn@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/wrap-fn/-/wrap-fn-0.1.5.tgz#f21b6e41016ff4a7e31720dbc63a09016bdf9845" + dependencies: + co "3.1.0" + +x-ray-crawler@~2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/x-ray-crawler/-/x-ray-crawler-2.0.2.tgz#0773d213e9e7be41e2b5bacd35693cae6736b138" + dependencies: + cheerio "^0.18.0" + debug "^2.1.3" + delegates "^0.1.0" + emitter-component "^1.1.1" + enqueue "^1.0.2" + http-context "^1.1.0" + ms "^0.7.0" + selectn "^0.9.6" + sliced "0.0.5" + superagent "^1.1.0" + wrap-fn "^0.1.4" + x-ray-parse "^1.0.0" + yieldly "0.0.1" + +x-ray-parse@^1.0.0, x-ray-parse@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/x-ray-parse/-/x-ray-parse-1.0.1.tgz#08d0ffc5e3ad139c11050af9aea4760949f29694" + dependencies: + format-parser "0.0.2" + +x-ray@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/x-ray/-/x-ray-2.3.2.tgz#5d1ecc84b48a01ebea73f70a6fa0d1171bbc39e4" + dependencies: + batch "~0.5.2" + chalk "~1.1.1" + cheerio "~0.20.0" + debug "~2.2.0" + enstore "~1.0.1" + is-url "~1.2.0" + isobject "~2.0.0" + object-assign "~4.0.1" + x-ray-crawler "~2.0.1" + x-ray-parse "~1.0.1" + +"xml-name-validator@>= 2.0.1 < 3.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yieldly@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/yieldly/-/yieldly-0.0.1.tgz#7d64c856e4f1cd3c35a78f86e0a2960e6a7d0474" + dependencies: + is-browser "2.0.1"