├── .editorconfig
├── .gitignore
├── README.md
├── UNUSED
├── CouchDB.js
├── crawler.js
├── demo-downloaded.js
├── hackernews.js
└── rig-timing.js
├── app
├── args
│ ├── getAction.js
│ ├── getDomain.js
│ ├── getMongoConnectionString.js
│ ├── getMysqlSettings.js
│ ├── getThreads.js
│ └── index.js
├── crawler
│ ├── README.md
│ └── index.js
├── hud
│ ├── README.md
│ ├── error.js
│ ├── index.js
│ ├── initOnce.js
│ ├── intro.js
│ ├── keyboard.js
│ ├── message.js
│ ├── numLines.js
│ ├── progress.js
│ ├── title.js
│ ├── urlState.js
│ └── writeLine.js
├── mongo
│ ├── README.md
│ ├── getDb.js
│ ├── index.js
│ ├── init.js
│ ├── setup
│ │ └── index.js
│ └── webgraph
│ │ ├── getProgress.js
│ │ ├── getUncrawledUrl.js
│ │ ├── updateUrl.js
│ │ └── upsertNewUrls.js
└── mysql
│ ├── README.md
│ ├── index.js
│ ├── init.js
│ ├── query.js
│ ├── setup
│ ├── scraper.mwb
│ └── scraper.sql
│ └── webgraph
│ ├── getProgress.js
│ ├── getUncrawledUrl.js
│ ├── updateUrl.js
│ └── upsertNewUrls.js
├── domains
├── checkatrade.com
│ └── index.js
├── oodavid.com
│ └── index.js
└── template
│ └── template.js
├── index.js
├── package-lock.json
└── package.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Private Credentials
2 | creds.json
3 |
4 | # Errors
5 | error.log
6 |
7 | # Backup files
8 | *.bak
9 |
10 | # Images
11 | images/*
12 |
13 |
14 |
15 | # Created by https://www.gitignore.io/api/osx,node,linux,windows,visualstudiocode
16 |
17 | ### Linux ###
18 | *~
19 |
20 | # temporary files which can be created if a process still has a handle open of a deleted file
21 | .fuse_hidden*
22 |
23 | # KDE directory preferences
24 | .directory
25 |
26 | # Linux trash folder which might appear on any partition or disk
27 | .Trash-*
28 |
29 | # .nfs files are created when an open file is removed but is still being accessed
30 | .nfs*
31 |
32 | ### Node ###
33 | # Logs
34 | logs
35 | *.log
36 | npm-debug.log*
37 | yarn-debug.log*
38 | yarn-error.log*
39 |
40 | # Runtime data
41 | pids
42 | *.pid
43 | *.seed
44 | *.pid.lock
45 |
46 | # Directory for instrumented libs generated by jscoverage/JSCover
47 | lib-cov
48 |
49 | # Coverage directory used by tools like istanbul
50 | coverage
51 |
52 | # nyc test coverage
53 | .nyc_output
54 |
55 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
56 | .grunt
57 |
58 | # Bower dependency directory (https://bower.io/)
59 | bower_components
60 |
61 | # node-waf configuration
62 | .lock-wscript
63 |
64 | # Compiled binary addons (http://nodejs.org/api/addons.html)
65 | build/Release
66 |
67 | # Dependency directories
68 | node_modules/
69 | jspm_packages/
70 |
71 | # Typescript v1 declaration files
72 | typings/
73 |
74 | # Optional npm cache directory
75 | .npm
76 |
77 | # Optional eslint cache
78 | .eslintcache
79 |
80 | # Optional REPL history
81 | .node_repl_history
82 |
83 | # Output of 'npm pack'
84 | *.tgz
85 |
86 | # Yarn Integrity file
87 | .yarn-integrity
88 |
89 | # dotenv environment variables file
90 | .env
91 |
92 |
93 | ### OSX ###
94 | *.DS_Store
95 | .AppleDouble
96 | .LSOverride
97 |
98 | # Icon must end with two \r
99 | Icon
100 |
101 | # Thumbnails
102 | ._*
103 |
104 | # Files that might appear in the root of a volume
105 | .DocumentRevisions-V100
106 | .fseventsd
107 | .Spotlight-V100
108 | .TemporaryItems
109 | .Trashes
110 | .VolumeIcon.icns
111 | .com.apple.timemachine.donotpresent
112 |
113 | # Directories potentially created on remote AFP share
114 | .AppleDB
115 | .AppleDesktop
116 | Network Trash Folder
117 | Temporary Items
118 | .apdisk
119 |
120 |
121 | ### Windows ###
122 | # Windows thumbnail cache files
123 | Thumbs.db
124 | ehthumbs.db
125 | ehthumbs_vista.db
126 |
127 | # Folder config file
128 | Desktop.ini
129 |
130 | # Recycle Bin used on file shares
131 | $RECYCLE.BIN/
132 |
133 | # Windows Installer files
134 | *.cab
135 | *.msi
136 | *.msm
137 | *.msp
138 |
139 | # Windows shortcuts
140 | *.lnk
141 |
142 |
143 | ### VisualStudioCode ###
144 | .vscode/*
145 | # !.vscode/settings.json
146 | # !.vscode/tasks.json
147 | # !.vscode/launch.json
148 | # !.vscode/extensions.json
149 | # .history
150 |
151 |
152 | # End of https://www.gitignore.io/api/osx,node,linux,windows,visualstudiocode
153 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Pup
2 |
3 |
4 |
5 |
6 |
7 | Rough Todo
8 |
9 | When doing "from memory" it really should be "existing promise" for multithreading
10 |
11 | Always use `module.exports`, not `exports`
12 |
13 |
14 | # Northstar - Scrape for Paul!
15 |
16 | What remains (15th Nov)
17 |
18 | * [x] `updateUrl.js` - 1hr
19 | * [x] `browserPool.js` review - 2hr
20 | * ...conclusion, I don't need my own browserPool, each puppeteer instance has `pages` (ala tabs)
21 | * [x] MD5 Logic
22 | * [x] DB - Rename `host` to `domain`
23 | * [x] Progress Bar
24 | * [x] Update periodically
25 | * [x] Add time estimate
26 | * [x] Ignore #hashes in URLs
27 | * [x] Switch back to MongoDB, MySQL is irking me
28 | * [x] Args + Connect
29 | * [x] Setup Script
30 | * [x] Reimplement webgraph logic
31 | * [x] Thread Management
32 | * [x] Obey `--threads` argument
33 | * [x] Pause / Unpause
34 | * [x] Increase / Decrease Threads
35 | * [x] `hud.progress()`
36 | * [x] Progress - Show start time
37 | * [x] Pretty this up a little
38 | * [x] `hud.message()`
39 | * [x] Write to file `messages.log`
40 | * [x] `hud.writeLine()`
41 | * [x] Coerce messages to a single line
42 | * [x] Truncate
43 | * [x] Per-domain parsing
44 | * [x] URL weighting
45 | * [x] During Write
46 | * [x] During Read
47 | * [ ] Add proxy support
48 | * https://proxymesh.com/
49 | * [ ] Saved to `creds.json` via args
50 | * [ ] User Agent Randmiser
51 | * [ ] Apply adblock to strip ads, analytics and social tools
52 | * https://github.com/bbondy/abp-filter-parser
53 | * https://easylist.to/
54 | * [ ] Screenshots
55 | * [ ] Sits behind a flag (domain settings)
56 | * [ ] When disabled, option to block image requests (speed)
57 | * [ ] Upload Screenshots to remote
58 | * https://www.npmjs.com/package/aws-sdk
59 | * [ ] Save to DB
60 | * [ ] Throw an error if we get a duplicate hash
61 | * [ ] Draft a MapReduce function to show how to combine data objects from multiple sources
62 | * See: https://docs.mongodb.com/manual/aggregation/
63 | * Just for demonstration (`README.md`), this doesn't really need coding
64 | * Example: `db.urls.find({ 'data.id': 'JamesPond' }, { url: 1, domain: 1, data: 1 }).pretty()`
65 | * [ ] Update the README
66 | * [ ] Punchy title and intro
67 | * [ ] Merge all these todo lists!
68 | * [ ] Up-to-date GIF
69 | * [ ] Set the github topics
70 | * [ ] Link to how-to tutorial
71 | * [ ] Youtube Video?
72 |
73 | # Parsing tasks
74 |
75 | * [ ] Parse companies house / duedil for business data
76 | * [ ] Get a list of competitors, with locations
77 | * [ ] Compute complementary businesses (by location)
78 | * [ ] Build a drip campaign
79 |
80 | --
81 |
82 | * [ ] Upon boot
83 | * [ ] Extra prompt - reset database?
84 | * [ ] Extra prompt - reset wip?
85 | * [ ] Review `domain settings` files
86 |
87 | --
88 |
89 | * [x] `app/mysql/index.js`
90 | * [x] Pool, Connect, Query
91 | * [x] Return some mysql interface (3rd party)
92 | * [ ] BONUS - Dump useful error if we can't connect
93 | * [x] `app/crawler/index.js`
94 | * [x] `init(browser)`
95 | * [ ] Send notification (SMS) when complete
96 | * [ ] Write about URL structure, use this as a reference:
97 | * [ ] https://developer.mozilla.org/en-US/docs/Web/API/Location
98 | * [x] Create a HUD
99 | * [x] Use https://github.com/cronvel/terminal-kit
100 | * [x] initialize - reserve space for the hud
101 | * [x] title - set title in terminal and HUD
102 | * [x] keyboard - handle `n` keyboard callbacks, render them on HUD
103 | * [x] progress - show the overall progress
104 | * [x] urlState - show the running threads (might rename)
105 | * [x] message - render short message on HUD
106 | * [x] error - for handling errors - write to file, render short message on HUD
107 | * [x] Hash MD5 logic
108 | * [ ] BONUS - Consider adding this to the domain settings (it may want to exclude non-content html)
109 | * [ ] Bonus - Create an executable - https://github.com/zeit/pkg
110 | * [ ] Consider using `scrapy style pipelines` - https://doc.scrapy.org/en/latest/topics/item-pipeline.html
111 |
112 |
113 |
114 | # Reading List
115 |
116 | Read these for reference and notes
117 |
118 |
119 | * https://www.quora.com/What-are-examples-of-how-real-businesses-use-web-scraping-Are-there-any-types-of-businesses-which-use-this-more-than-others
120 | * Turbocharge the data with:
121 | * https://www.integrationsjs.com/
122 | * https://open.blockspring.com/browse
123 | * https://www.blockspring.com/
124 | * https://hunter.io/
125 | * Test sites:
126 | * http://webscraper.io/test-sites
127 | * http://airbnb.com
128 | * https://www.google.com/intl/bn/insidesearch/howsearchworks/crawling-indexing.html
129 | * https://hexfox.com/p/scrapy-vs-beautifulsoup/
130 | * https://hexfox.com/p/how-to-filter-out-duplicate-urls-from-scrapys-start-urls/
131 | * https://stackoverflow.com/a/9736414/1122851 - database pools (and pools in general)
132 |
133 | # A Better Scraper, with Puppeteer
134 |
135 | Firstly, go forth and read [Getting started with Puppeteer and Chrome Headless for Web Scraping](https://medium.com/@e_mad_ehsan/getting-started-with-puppeteer-and-chrome-headless-for-web-scrapping-6bf5979dee3e).
136 |
137 | Now we will scale this idea up by:
138 |
139 | * Adding multi-threading, proxies and throttling
140 | * Allowing workers to be distributed
141 | * Catering for multiple domains
142 | * Allowing our data to be analysed after-the-fact
143 |
144 | # Requirements
145 |
146 | * [ ] Data is decentralised
147 | * [ ] Use FireBase, it's vogue and I'm keen
148 | * [ ] Workers are distributed
149 | * [ ] Prevent working at cross purposes
150 | * [ ] Track name and IP
151 | * [ ] Optional authentication
152 | * [ ] Users can select which domain to scrape
153 | * [x] Users can control how many threads they run
154 | * [x] Keyboard control
155 | * [ ] Can use proxies
156 | * [ ] Has IP throttling
157 |
158 | # Components
159 |
160 | Having a clear separation of concerns will keep the system simple and workable.
161 |
162 | The scraper should be broken down into the following parts.
163 |
164 | ## 1 - Create The Webgraph
165 |
166 | This can be thought of as a virtual surfer. She will surf the web, clicking all links, submitting all forms and documenting her findings.
167 |
168 | * Deals with authentication
169 | * Navigates between pages
170 | * Evaluates javascript
171 | * Builds lists of links
172 | * Follows links / submits forms
173 | * Takes screenshots (they're useful)
174 |
175 | The effect of this would be to create a Webgraph, stored in a database, of this structure:
176 |
177 | * The nodes
178 | * Represent URLs
179 | * Contains a key denoting the "state" of the URL (none > wip > scraped > analysed)
180 | * Contains the URL and HTML (after javascript has run)
181 | * Contains the date the URL was scraped
182 | * Unsolved
183 | * How to represent pages which have the same URL but different "context"? (session, or POST data)
184 | * The edges
185 | * Represents links between nodes
186 |
187 | You could use this data to create a "PageRank", if you so wished.
188 |
189 | ## 2 - Page Analysis
190 |
191 | Loads an URL that is `scraped` (ie: `unanlysed`)
192 | Load the analysis rules for the URL (see section #3 below)
193 | Parse the HTML into useful data (users, organizations, whatever)
194 |
195 | ## 3 - Management Interface
196 |
197 | At first this will not be necessary, config. can be filebased and analysis direct on the database.
198 |
199 | * Visualisation
200 | * Active Scrapers
201 | * Results Overview
202 | * Results Drilldown
203 | * Management
204 | * Add domains
205 | * Add domain rules
206 | * DOM selectors
207 | * URL weights (0-1)
208 | * A weight of `0` would mean **don't visit this URL**
209 | * A weight of `1` would mean **visit this URL ASAP**
210 | * Trigger Re-Analysis
211 |
212 | # Tools
213 |
214 | ## Headless Browser
215 |
216 | **Puppeteer** is now the obvious choice.
217 |
218 | ## Database
219 |
220 | **CouchDB** gives versioning out of the box
221 | **CouchDB** + PouchDB means our visualisations can be realtime
222 |
223 | **Firebase Firestore** does the same, with zero "setup"
224 |
225 | #### References
226 |
227 | * https://github.com/GoogleChrome/puppeteer
228 | * https://github.com/emadehsan/thal
229 | * https://github.com/Quramy/angular-puppeteer-demo
230 |
--------------------------------------------------------------------------------
/UNUSED/CouchDB.js:
--------------------------------------------------------------------------------
1 | const Promise = require('bluebird');
2 | const CREDS = require('../creds.js').CouchDb;
3 | const nano = require('nano')(`${CREDS.protocol}://${CREDS.username}:${CREDS.userpass}@${CREDS.host}:${CREDS.port}`);
4 |
5 |
6 | var webgraphDb = nano.db.use('webgraph');
7 | Promise.promisifyAll(webgraphDb);
8 |
9 |
10 | module.exports.getUnscrapedUrls = getUnscrapedUrls;
11 | module.exports.setScrapeData = setScrapeData;
12 |
13 |
14 | // Promise to return `num` URLs
15 | // Flag said URLs as `queued`
16 | async function getUnscrapedUrls(num){
17 | const docs = await webgraphDb.viewAsync('pages', 'unscraped', { limit: num });
18 | // Update the docs
19 | let updatedDocs = docs.rows.map((doc) => {
20 | doc.key.status = 'queued';
21 | return doc.key;
22 | });
23 | const bulkResults = await webgraphDb.bulkAsync({ docs: updatedDocs });
24 | // Review the update
25 | for(var b=bulkResults.length-1; b>=0; b--){
26 | const bulkResult = bulkResults[b];
27 | if(bulkResult.error){
28 | updatedDocs.splice(b, 1); // Remove any docs that couldn't be flagged as `queued` (likely due to conflicts)
29 | } else {
30 | updatedDocs[b]._rev = bulkResult.rev; // Update the _rev of those that could
31 | }
32 | }
33 | // Make sure the url is a property
34 | updatedDocs.forEach(function(doc){
35 | doc.url = doc._id;
36 | });
37 | // Return
38 | return updatedDocs;
39 | }
40 |
41 |
42 | // Promise to update the URL status, html and screenshot
43 | // add links add new urls to database
44 | async function setScrapeData(doc, html, status_code, links){
45 | // Update the doc
46 | delete doc.url;
47 | doc.status = 'scraped';
48 | doc.html = html;
49 | doc.status_code = status_code;
50 | doc.links = {};
51 | links.forEach(function(o){
52 | doc.links[o] = true;
53 | });
54 | // Update
55 | await webgraphDb.insertAsync(doc);
56 | // Create the link URLs
57 | let docs = links.map(function(link){
58 | return { _id: link };
59 | });
60 | await webgraphDb.bulkAsync({ docs: docs });
61 | // Make sure the url is a property
62 | doc.url = doc._id;
63 | // Return
64 | return doc;
65 | }
66 |
67 |
68 | // Test out the commands
69 | //
70 | (async () => {
71 | // Read a batch of URLs
72 | console.log(`Getting Unscraped URLs`);
73 | const docs = await getUnscrapedUrls(1);
74 | console.log(docs);
75 |
76 | // Scrape an URL
77 | const doc = docs[0];
78 | console.log(`Scraping ${doc.url} (demo)`);
79 | const html = 'Hello World
';
80 | const status_code = 200;
81 | const links = [
82 | 'https://oodavid.com/testing/',
83 | 'https://oodavid.com/testing-123/',
84 | ];
85 | const updatedDoc = await setScrapeData(doc, html, status_code, links);
86 | console.log(updatedDoc);
87 | })();
88 |
--------------------------------------------------------------------------------
/UNUSED/crawler.js:
--------------------------------------------------------------------------------
1 | const browserPool = require('./browserPool/');
2 | const sanitize = require("sanitize-filename");
3 | require('./keyboard.js');
4 |
5 | let unscraped = [
6 | 'https://oodavid.com',
7 | 'https://oodavid.com/todo/',
8 | 'https://oodavid.com/about-me/',
9 | 'https://oodavid.com/article/angularjs-meta-tags-management/',
10 | 'https://oodavid.com/article/jekyll-is-dead-long-live-hugo/',
11 | 'https://oodavid.com/article/angularjs-meta-tags-management/',
12 | 'https://oodavid.com/article/style-rendering-test/',
13 | ];
14 |
15 | browserPool.setPoolSize(2);
16 | spawnBrowsers();
17 |
18 | function spawnBrowsers(){
19 | let numSpawned = 0;
20 | for(var b=0; b<=browserPool.getIdleCount(); b++){
21 | const url = getUnscrapedUrl();
22 | if(url){
23 | spawnBrowser(url);
24 | numSpawned++;
25 | }
26 | }
27 | if(!numSpawned){
28 | console.log('nothing to do (start exponential backoff)');
29 | }
30 | };
31 |
32 | async function spawnBrowser(url){
33 | console.log('parsing', url);
34 | let browser;
35 | try {
36 | browser = await browserPool.getBrowser();
37 | const page = await browser.newPage();
38 | await page.goto(url);
39 | // Analysis
40 | const filename = sanitize(url);
41 | await page.screenshot({ path: `${__dirname}/../../images/${filename}.png` });
42 | const html = await page.content();
43 | } catch(e) {
44 | console.error(e);
45 | }
46 | // Always release the browser
47 | await browserPool.releaseBrowser(browser);
48 | spawnBrowsers();
49 | }
50 |
51 | function getUnscrapedUrl(){
52 | return unscraped.shift();
53 | }
54 |
--------------------------------------------------------------------------------
/UNUSED/demo-downloaded.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const CREDS = require('./creds');
3 | const mongoose = require('mongoose');
4 | const User = require('./models/user');
5 |
6 | async function run() {
7 | const browser = await puppeteer.launch();
8 |
9 | const page = await browser.newPage();
10 |
11 | // await page.goto('https://github.com');
12 | // await page.screenshot path: 'screenshots/github.png' });
13 |
14 | await page.goto('https://github.com/login');
15 |
16 | // dom element selectors
17 | const USERNAME_SELECTOR = '#login_field';
18 | const PASSWORD_SELECTOR = '#password';
19 | const BUTTON_SELECTOR = '#login > form > div.auth-form-body.mt-3 > input.btn.btn-primary.btn-block';
20 |
21 | await page.click(USERNAME_SELECTOR);
22 | await page.type(CREDS.username);
23 |
24 | await page.click(PASSWORD_SELECTOR);
25 | await page.type(CREDS.password);
26 |
27 | await page.click(BUTTON_SELECTOR);
28 | await page.waitForNavigation();
29 |
30 | const userToSearch = 'oodavid';
31 | const searchUrl = `https://github.com/search?q=${userToSearch}&type=Users&utf8=%E2%9C%93`;
32 | // let searchUrl = 'https://github.com/search?utf8=%E2%9C%93&q=bashua&type=Users';
33 |
34 | await page.goto(searchUrl);
35 | await page.waitFor(2 * 1000);
36 |
37 | // const LIST_USERNAME_SELECTOR = '#user_search_results > div.user-list > div:nth-child(1) > div.d-flex > div > a';
38 | const LIST_USERNAME_SELECTOR = '#user_search_results > div.user-list > div:nth-child(INDEX) > div.d-flex > div > a';
39 | // const LIST_EMAIL_SELECTOR = '#user_search_results > div.user-list > div:nth-child(1) > div.d-flex > div > ul > li:nth-child(2) > a';
40 | const LIST_EMAIL_SELECTOR = '#user_search_results > div.user-list > div:nth-child(INDEX) > div.d-flex > div > ul > li:nth-child(2) > a';
41 | const LENGTH_SELECTOR_CLASS = 'user-list-item';
42 | // const numPages = await getNumPages(page);
43 | const numPages = 2;
44 |
45 | console.log('Numpages: ', numPages);
46 |
47 | for (let h = 1; h <= numPages; h++) {
48 | let pageUrl = searchUrl + '&p=' + h;
49 | await page.goto(pageUrl);
50 |
51 | let listLength = await page.evaluate((sel) => {
52 | return document.getElementsByClassName(sel).length;
53 | }, LENGTH_SELECTOR_CLASS);
54 |
55 | for (let i = 1; i <= listLength; i++) {
56 | // change the index to the next child
57 | let usernameSelector = LIST_USERNAME_SELECTOR.replace("INDEX", i);
58 | let emailSelector = LIST_EMAIL_SELECTOR.replace("INDEX", i);
59 |
60 | let username = await page.evaluate((sel) => {
61 | return document.querySelector(sel).getAttribute('href').replace('/', '');
62 | }, usernameSelector);
63 |
64 | let email = await page.evaluate((sel) => {
65 | let element = document.querySelector(sel);
66 | return element ? element.innerHTML : null;
67 | }, emailSelector);
68 |
69 | // not all users have emails visible
70 | if (!email)
71 | continue;
72 |
73 | console.log(username, ' -> ', email);
74 |
75 | upsertUser({
76 | username: username,
77 | email: email,
78 | dateCrawled: new Date()
79 | });
80 | }
81 | }
82 |
83 | browser.close();
84 | }
85 |
86 | async function getNumPages(page) {
87 | const NUM_USER_SELECTOR = '#js-pjax-container > div.container > div > div.column.three-fourths.codesearch-results.pr-6 > div.d-flex.flex-justify-between.border-bottom.pb-3 > h3';
88 |
89 | let inner = await page.evaluate((sel) => {
90 | let html = document.querySelector(sel).innerHTML;
91 |
92 | // format is: "69,803 users"
93 | return html.replace(',', '').replace('users', '').trim();
94 | }, NUM_USER_SELECTOR);
95 |
96 | const numUsers = parseInt(inner);
97 |
98 | console.log('numUsers: ', numUsers);
99 |
100 | /**
101 | * GitHub shows 10 resuls per page, so
102 | */
103 | return Math.ceil(numUsers / 10);
104 | }
105 |
106 | function upsertUser(userObj) {
107 | const DB_URL = 'mongodb://localhost/thal';
108 | console.log('Upserting user', userObj);
109 |
110 | if (mongoose.connection.readyState == 0) {
111 | mongoose.connect(DB_URL);
112 | }
113 |
114 | // if this email exists, update the entry, don't insert
115 | const conditions = { email: userObj.email };
116 | const options = { upsert: true, new: true, setDefaultsOnInsert: true };
117 |
118 | User.findOneAndUpdate(conditions, userObj, options, (err, result) => {
119 | if (err) {
120 | throw err;
121 | }
122 | });
123 | }
124 |
125 | run();
--------------------------------------------------------------------------------
/UNUSED/hackernews.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 | const cheerio = require('cheerio');
3 |
4 | async function run() {
5 | const browser = await puppeteer.launch();
6 | const page = await browser.newPage();
7 | await page.goto('https://news.ycombinator.com');
8 |
9 | let content = await page.content();
10 | var $ = cheerio.load(content);
11 | $('span.comhead').each(function(i, element){
12 | var a = $(this).prev();
13 | var rank = a.parent().parent().text();
14 | var title = a.text();
15 | var url = a.attr('href');
16 | var subtext = a.parent().parent().next().children('.subtext').children();
17 | var points = $(subtext).eq(0).text();
18 | var username = $(subtext).eq(1).text();
19 | var comments = $(subtext).eq(2).text();
20 |
21 | var metadata = {
22 | rank: parseInt(rank),
23 | title: title,
24 | url: url,
25 | points: parseInt(points),
26 | username: username,
27 | comments: parseInt(comments)
28 | };
29 | console.log(metadata);
30 | });
31 |
32 | browser.close();
33 | }
34 |
35 | run();
--------------------------------------------------------------------------------
/UNUSED/rig-timing.js:
--------------------------------------------------------------------------------
1 | const puppeteer = require('puppeteer');
2 |
3 | (async () => {
4 |
5 | console.time('puppeteer.launch');
6 | const browser = await puppeteer.launch();
7 | console.timeEnd('puppeteer.launch');
8 |
9 | console.time('browser.newPage');
10 | const page = await browser.newPage();
11 | console.timeEnd('browser.newPage');
12 |
13 | console.time('page.goto');
14 | await page.goto('https://oodavid.com/article/angularjs-memento-factory/');
15 | console.timeEnd('page.goto');
16 |
17 | console.time('page.screenshot');
18 | await page.screenshot({ path: 'screenshots/oodavid.com.png' });
19 | console.timeEnd('page.screenshot');
20 |
21 | console.time('page.content');
22 | const html = await page.content();
23 | console.timeEnd('page.content');
24 |
25 | await browser.close();
26 |
27 | })();
28 |
--------------------------------------------------------------------------------
/app/args/getAction.js:
--------------------------------------------------------------------------------
1 | module.exports = getAction;
2 |
3 |
4 | const argv = require('yargs').argv;
5 | const inquirer = require('inquirer');
6 |
7 |
8 | const actions = {
9 | 'Crawl and Parse': 'crawl',
10 | 'Just Parse': 'parse',
11 | };
12 | let action;
13 |
14 |
15 | async function getAction(){
16 | // From memory
17 | if(action){
18 | return action;
19 | }
20 | // From the command-line
21 | if(argv.action){
22 | action = argv.action;
23 | return action;
24 | }
25 | // Prompt
26 | return inquirer.prompt([
27 | {
28 | type: 'list',
29 | name: 'action',
30 | message: 'What would you like to do?',
31 | choices: Object.keys(actions),
32 | }
33 | ])
34 | .then(function(answers){
35 | action = actions[answers.action];
36 | return action;
37 | })
38 | .catch(function(err){
39 | console.error(err);
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/app/args/getDomain.js:
--------------------------------------------------------------------------------
1 | module.exports = getDomain;
2 |
3 |
4 | const argv = require('yargs').argv;
5 | const inquirer = require('inquirer');
6 | const autocomplete = require('inquirer-autocomplete-prompt');
7 | const fuzzy = require('fuzzy');
8 | const glob = require('glob');
9 | inquirer.registerPrompt('autocomplete', autocomplete);
10 |
11 |
12 | let domain;
13 |
14 |
15 | async function getDomain(){
16 | // From memory
17 | if(domain){
18 | return domain;
19 | }
20 | // From the command-line
21 | if(argv.domain){
22 | domain = argv.domain;
23 | return domain;
24 | }
25 | // Prompt
26 | return inquirer.prompt([
27 | {
28 | type: 'autocomplete',
29 | name: 'domain',
30 | message: 'Which domain?',
31 | source: autocompleteDomains,
32 | },
33 | ])
34 | .then(function(answers){
35 | domain = answers.domain;
36 | return domain;
37 | })
38 | .catch(function(err){
39 | console.error(err);
40 | });
41 | }
42 |
43 |
44 | var domainsCache = false;
45 | function getDomains(){
46 | if(!domainsCache){
47 | domainsCache = glob
48 | .sync('./domains/*/')
49 | .map(function(path){
50 | return path.split('/').splice(-2, 1).pop();
51 | })
52 | .filter(function(path){
53 | return path !== 'template';
54 | });
55 | }
56 | return domainsCache;
57 | }
58 |
59 |
60 | function autocompleteDomains(answersSoFar, input){
61 | return new Promise(function(resolve, reject){
62 | resolve(
63 | fuzzy
64 | .filter((input || ''), getDomains())
65 | .map(el => el.original)
66 | );
67 | });
68 | }
69 |
--------------------------------------------------------------------------------
/app/args/getMongoConnectionString.js:
--------------------------------------------------------------------------------
1 | module.exports = getMongoConnectionString;
2 |
3 |
4 | const inquirer = require('inquirer');
5 | const fs = require('fs');
6 | const util = require('util');
7 | const writeFile = util.promisify(fs.writeFile);
8 | const term = require('terminal-kit').terminal;
9 |
10 |
11 | const credsFilePath = `${__dirname}/../../creds.json`;
12 |
13 |
14 | let creds;
15 |
16 |
17 | async function getMongoConnectionString(){
18 | // From memory
19 | if(creds && creds.mongoConnectionString){
20 | return creds.mongoConnectionString;
21 | }
22 | // From file...
23 | try {
24 | creds = require(credsFilePath);
25 | } catch(e){
26 | creds = {};
27 | }
28 | if(creds.mongoConnectionString){
29 | return creds.mongoConnectionString;
30 | }
31 | // Prompt
32 | var answers = await inquirer.prompt([{
33 | type: 'input',
34 | name: 'mongoConnectionString',
35 | message: 'Please Enter your MongoDB Connection String',
36 | }]);
37 | // Update
38 | creds.mongoConnectionString = answers.mongoConnectionString;
39 | // Write the file
40 | await writeFile(credsFilePath, JSON.stringify(creds, null, 4));
41 | term(`^r❯^: ^+MongoDB settings saved to^: ^ccreds.json^:\n`);
42 | // Return
43 | return creds.mongoConnectionString;
44 | }
45 |
--------------------------------------------------------------------------------
/app/args/getMysqlSettings.js:
--------------------------------------------------------------------------------
1 | module.exports = getMysqlSettings;
2 |
3 |
4 | const inquirer = require('inquirer');
5 | const fs = require('fs');
6 | const util = require('util');
7 | const writeFile = util.promisify(fs.writeFile);
8 | const term = require('terminal-kit').terminal;
9 |
10 |
11 | const credsFilePath = `${__dirname}/../../creds.json`;
12 | const allPrompts = [
13 | {
14 | type: 'input',
15 | name: 'host',
16 | message: 'Enter host',
17 | default: 'localhost',
18 | },
19 | {
20 | type: 'input',
21 | name: 'user',
22 | message: 'Enter user',
23 | default: 'charlotte',
24 | },
25 | {
26 | type: 'password',
27 | name: 'password',
28 | message: 'Enter password',
29 | default: 'Wilbur',
30 | },
31 | {
32 | type: 'input',
33 | name: 'database',
34 | message: 'Enter database',
35 | default: 'scraper',
36 | },
37 | {
38 | type: 'input',
39 | name: 'connectionLimit',
40 | message: 'Connection Limit',
41 | default: 10,
42 | filter: function(value){
43 | return parseInt(value, 10);
44 | },
45 | validate: function(value){
46 | var isValid = (value > 0 && value === parseInt(value, 10));
47 | return isValid || "Please enter an integer";
48 | },
49 | },
50 | ];
51 |
52 |
53 | let creds;
54 |
55 |
56 | async function getMysqlSettings(){
57 | // From memory
58 | if(creds && creds.mysql){
59 | return creds.mysql;
60 | }
61 | // From file...
62 | try {
63 | creds = require(credsFilePath);
64 | } catch(e){
65 | creds = {};
66 | }
67 | creds.mysql = creds.mysql || {};
68 | // ...does the object contain all the required properties?
69 | var prompts = allPrompts.filter(function(prompt){
70 | return !creds.mysql.hasOwnProperty(prompt.name);
71 | });
72 | if(!prompts.length){
73 | return creds.mysql;
74 | }
75 | // Prompt
76 | term(`^r❯^: ^+Please Configure MySQL^: ^-(you only need to do this once)^:\n`);
77 | var answers = await inquirer.prompt(prompts)
78 | .then(function(answers){
79 | return answers;
80 | })
81 | .catch(function(err){
82 | console.error(err);
83 | });
84 | Object.assign(creds.mysql, answers);
85 | // Write the file
86 | await writeFile(credsFilePath, JSON.stringify(creds, null, 4));
87 | term(`^r❯^: ^+MySQL settings saved to^: ^ccreds.json^:\n`);
88 | // Return
89 | return creds.mysql;
90 | }
91 |
--------------------------------------------------------------------------------
/app/args/getThreads.js:
--------------------------------------------------------------------------------
1 | module.exports = getThreads;
2 |
3 |
4 | const argv = require('yargs').argv;
5 | const inquirer = require('inquirer');
6 |
7 |
8 | let threads;
9 |
10 |
11 | async function getThreads(){
12 | // From memory
13 | if(threads){
14 | return threads;
15 | }
16 | // From the command-line
17 | if(argv.threads){
18 | threads = argv.threads;
19 | return threads;
20 | }
21 | // Prompt
22 | return inquirer.prompt([
23 | {
24 | type: 'input',
25 | name: 'threads',
26 | message: 'How many threads to run?',
27 | default: 10,
28 | filter: function(value){
29 | return parseInt(value, 10);
30 | },
31 | validate: function(value){
32 | var isValid = (value > 0 && value === parseInt(value, 10));
33 | return isValid || "Please enter an integer";
34 | }
35 | }
36 | ])
37 | .then(function(answers){
38 | threads = answers.threads;
39 | return threads;
40 | })
41 | .catch(function(err){
42 | console.error(err);
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/app/args/index.js:
--------------------------------------------------------------------------------
1 | module.exports.getAction = require('./getAction.js');
2 | module.exports.getDomain = require('./getDomain.js');
3 | module.exports.getMysqlSettings = require('./getMysqlSettings.js');
4 | module.exports.getMongoConnectionString = require('./getMongoConnectionString.js');
5 | module.exports.getThreads = require('./getThreads.js');
6 |
--------------------------------------------------------------------------------
/app/crawler/README.md:
--------------------------------------------------------------------------------
1 | # Browser Pool
2 |
3 |
4 |
5 | WikiPedia :: Pool (computer science)
6 |
7 | In computer science, a pool is a set of resources that are kept ready to use,
8 | rather than acquired on use and released afterwards. In this context, resources
9 | can refer to system resources such as file handles, which are external to a
10 | process, or internal resources such as objects. A pool client requests a
11 | resource from the pool and performs desired operations on the returned resource.
12 | When the client finishes its use of the resource, it is returned to the pool
13 | rather than released and lost.
14 |
15 | The pooling of resources can offer a significant performance boost in situations
16 | that have high cost associated with resource acquiring, high rate of the
17 | requests for resources, and a low overall count of simultaneously used resources.
18 | Pooling is also useful when the latency is a concern, because a pool offers
19 | predictable times required to obtain resources since they have already been
20 | acquired. These benefits are mostly true for system resources that require a
21 | system call, or remote resources that require a network communication, such as
22 | database connections, socket connections, threads, and memory allocation.
23 | Pooling is also useful for expensive-to-compute data, notably large graphic
24 | objects like fonts or bitmaps, acting essentially as a data cache or a
25 | memoization technique.
26 |
27 | Special cases of pools are connection pools, thread pools, and memory pools.
28 |
--------------------------------------------------------------------------------
/app/crawler/index.js:
--------------------------------------------------------------------------------
1 | module.exports.start = start;
2 |
3 |
4 | const hud = require('../hud/');
5 | const datastore = require('../mongo/');
6 | const sanitize = require("sanitize-filename");
7 | const puppeteer = require('puppeteer');
8 | const md5 = require('md5');
9 |
10 |
11 | let domainConfig;
12 | let browser;
13 | let runningThreads = 0;
14 | let maxThreads = 10;
15 |
16 |
17 | async function start(myDomainConfig, myNumThreads){
18 | domainConfig = myDomainConfig;
19 | maxThreads = myNumThreads;
20 | // Title
21 | hud.title(`Crawling ^c${domainConfig.domain}^:`);
22 | // Start keyboard control
23 | hud.keyboard.assign('UP', increaseThreads, 'Increase Threads');
24 | hud.keyboard.assign('DOWN', decreaseThreads, 'Decrease Threads');
25 | hud.keyboard.assign('p', pause, 'Pause');
26 | hud.keyboard.start();
27 | // Update the progress bar
28 | updateProgress();
29 | // Upsert the seed URLs
30 | hud.message(`upserting ${domainConfig.seedUrls.length} seed URLS`);
31 | await datastore.upsertNewUrls(domainConfig, domainConfig.seedUrls);
32 | // Initialise our browser and start crawling
33 | browser = await puppeteer.launch();
34 | crawlUrls();
35 | }
36 |
37 |
38 |
39 |
40 |
41 | async function updateProgress(){
42 | try {
43 | let progress = await datastore.getProgress(domainConfig.domain);
44 | hud.progress(progress.crawled, progress.total);
45 | } catch (e) {
46 | hud.error(e);
47 | } finally {
48 | setTimeout(updateProgress, 1000);
49 | }
50 | }
51 |
52 |
53 |
54 |
55 |
56 | async function getPage(){
57 | if(runningThreads < maxThreads){
58 | runningThreads ++;
59 | return await browser.newPage();
60 | }
61 | }
62 |
63 | async function releasePage(page){
64 | if(page){
65 | await page.close();
66 | runningThreads --;
67 | }
68 | }
69 |
70 | async function crawlUrls(){
71 | const page = await getPage();
72 | if(page){
73 | const urlObject = await datastore.getUncrawledUrl(domainConfig.domain);
74 | if(urlObject){
75 | crawlUrl(page, urlObject);
76 | // Recur
77 | setTimeout(crawlUrls, 100);
78 | } else {
79 | releasePage(page);
80 | }
81 | }
82 | }
83 |
84 | async function crawlUrl(page, urlObject){
85 | hud.urlState(urlObject.url, 'Parsing');
86 | try {
87 | /* Blocking images
88 | await page.setRequestInterception(true);
89 | page.on('request', request => {
90 | if (request.resourceType === 'image')
91 | request.abort();
92 | else
93 | request.continue();
94 | });
95 | */
96 | const response = await page.goto(urlObject.url, { waitUntil: 'networkidle' });
97 | const html = await page.content();
98 | // Webgraph Basics
99 | urlObject.status = response.status;
100 | urlObject.hash = md5(html);
101 | urlObject.links = await page.evaluate(function(){ // ...this runs in the context of the browser
102 | let links = [... document.querySelectorAll('a')];
103 | return links.map(function(link){
104 | return link.href;
105 | });
106 | });
107 | // Screenshot
108 | // const filename = sanitize(urlObject.url);
109 | // await page.screenshot({ path: `${__dirname}/../../images/${filename}.png` });
110 | // Parsing (domain specific)
111 | if(typeof domainConfig.parse === 'function'){
112 | await injectJQuery(page);
113 | urlObject.data = await domainConfig.parse(page, urlObject);
114 | }
115 | // Save
116 | await datastore.updateUrl(domainConfig, urlObject);
117 | } catch(e) {
118 | // Log errors
119 | hud.error(e);
120 | } finally {
121 | // Always tidy up
122 | hud.urlState(urlObject.url, false);
123 | await releasePage(page);
124 | // Spawn more crawlers
125 | crawlUrls();
126 | }
127 | }
128 |
129 |
130 |
131 |
132 | async function injectJQuery(page){
133 | await page.evaluate(() => {
134 | var jq = document.createElement("script")
135 | jq.setAttribute('type','text/javascript');
136 | jq.src = "https://code.jquery.com/jquery-3.2.1.min.js"
137 | return new Promise((resolve) => {
138 | jq.addEventListener("load", ()=> {
139 | resolve();
140 | });
141 | document.getElementsByTagName("head")[0].appendChild(jq);
142 | });
143 | })
144 | const watchDog = page.waitForFunction('window.jQuery !== undefined');
145 | await watchDog;
146 | }
147 |
148 |
149 |
150 |
151 |
152 | let pausedThreads;
153 | function increaseThreads(){
154 | maxThreads ++;
155 | crawlUrls();
156 | hud.message(`Max: ${maxThreads} threads`);
157 | }
158 | function decreaseThreads(){
159 | maxThreads --;
160 | hud.message(`Max: ${maxThreads} threads`);
161 | }
162 | function pause(){
163 | pausedThreads = maxThreads;
164 | maxThreads = 0;
165 | hud.keyboard.assign('p', unpause, 'Unpause');
166 | hud.message('Paused. Wait for threads to complete');
167 | }
168 | function unpause(){
169 | maxThreads = pausedThreads;
170 | crawlUrls();
171 | hud.keyboard.assign('p', pause, 'Pause');
172 | hud.message(`Unpaused. Max: ${maxThreads} threads`);
173 | }
174 |
--------------------------------------------------------------------------------
/app/hud/README.md:
--------------------------------------------------------------------------------
1 | # Heads-up Display (HUD)
2 |
3 | Renders a Heads-up Display with title, keyboard control, progress meter, urlStates, messages and errors.
4 |
5 | Uses [terminal-kit](https://github.com/cronvel/terminal-kit/) for the heavy lifting.
6 |
7 | Renders colors and styles with [terminal-kit Style Markup](https://github.com/cronvel/terminal-kit/blob/master/doc/low-level.md#style-markup).
8 |
9 | ## Example output
10 |
11 | This is without styling, I've added line numbers for reference.
12 |
13 |
14 | 0 Crawling oodavid.com
15 | 1 [CTRL+C] to exit, [UP] Increase Threads, [DOWN] Increase Threads, [p] Unpause
16 | 2
17 | 3 Progress [==================---------------------------------------------------] 26% [15022 / 61280] 1,068 / 47,326
18 | 4 Started: 21 Nov 20:38 Completed: 296 Rate: 5,530 / hour TTC: 8 hours
19 | 5
20 | 6 Loading http://domain.com/1
21 | 7 Loading http://domain.com/2
22 | 8 Loading http://domain.com/3
23 | 9 Processing http://domain.com/4
24 | 10 ------- http://----.--
25 | 11 Loading http://domain.com/6
26 | 12 Processing http://domain.com/7
27 | 13 Loading http://domain.com/8
28 | 14 ------- http://----.--
29 | 15 Loading http://domain.com/10
30 | 16 12 threads (4 threads are hidden)
31 | 17
32 | 18 Paused. Wait for threads to complete
33 | 19 ./error.log - 20 new errors - latest: 2017/11/14 11:40:58
34 |
35 |
36 | ## Usage
37 |
38 | ```js
39 | // line 0 - hud.title
40 | hud.title('Crawling ^goodavid.com^:');
41 |
42 | // line 1 - hud.keyboard
43 | hud.keyboard.assign('UP', increaseProcesses, 'Increase Processes');
44 | hud.keyboard.assign('DOWN', decreaseProcesses, 'Decrease Processes');
45 | hud.keyboard.assign('p', pause, 'Pause');
46 | hud.keyboard.start();
47 | // ...can be reassigned
48 | hud.keyboard.assign('p', unpause, 'Unpause');
49 |
50 | // line 3-4 - hud.progress
51 | hud.progress(15022, 61280);
52 |
53 | // lines 6-16 - hud.urlState
54 | for(var n=1;n<=20;n++){
55 | hud.urlState(`http://domain.com/${n}`, 'Loading');
56 | }
57 | hud.urlState(`http://domain.com/4`, 'Processing');
58 | hud.urlState(`http://domain.com/5`, false);
59 | hud.urlState(`http://domain.com/7`, 'Processing');
60 | hud.urlState(`http://domain.com/9`, false);
61 |
62 | // line 18 - hud.message
63 | hud.message('Paused - processes will spin down');
64 |
65 | // line 19 - hud.error
66 | hud.error(new Error('Oh dear!'));
67 | ```
68 |
--------------------------------------------------------------------------------
/app/hud/error.js:
--------------------------------------------------------------------------------
1 | module.exports = error;
2 |
3 |
4 | const writeLine = require('./writeLine.js');
5 | const fs = require('fs');
6 |
7 |
8 | const errorLog = fs.createWriteStream(`${__dirname}/../../error.log`, { flags: 'a' });
9 | let errors = 0;
10 | function error(e){
11 | // Write to file
12 | const now = new Date();
13 | errorLog.write(`${now}\n${e.stack}\n\n`);
14 | // Update the HUD
15 | errors ++;
16 | writeLine(19, `^-./error.log - ^:^r${errors} new errors^- - latest: ${now}`);
17 | }
18 |
--------------------------------------------------------------------------------
/app/hud/index.js:
--------------------------------------------------------------------------------
1 | module.exports.intro = require('./intro.js');
2 | module.exports.initOnce = require('./initOnce.js');
3 | module.exports.title = require('./title.js');
4 | module.exports.keyboard = require('./keyboard.js');
5 | module.exports.progress = require('./progress.js');
6 | module.exports.urlState = require('./urlState.js');
7 | module.exports.message = require('./message.js');
8 | module.exports.error = require('./error.js');
9 |
--------------------------------------------------------------------------------
/app/hud/initOnce.js:
--------------------------------------------------------------------------------
1 | module.exports = initOnce;
2 |
3 |
4 | const numLines = require('./numLines.js');
5 | const term = require('terminal-kit').terminal;
6 |
7 |
8 | let initRun = false;
9 | function initOnce(){
10 | if(initRun){ return; }
11 | initRun = true;
12 | console.log((new Array(numLines+1)).join('\n'));
13 | term.saveCursor();
14 | }
15 |
--------------------------------------------------------------------------------
/app/hud/intro.js:
--------------------------------------------------------------------------------
1 | module.exports = printIntro;
2 |
3 |
4 | const term = require('terminal-kit').terminal;
5 |
6 |
7 | function printIntro(){
8 | term(addSplashOfColor(`
9 | ∇ ____ ∇
10 | | : : | ·---------------------------- - -
11 | {| ♥ ♥ |} | Web Scraper; Crawler and Parser
12 | |___==___| | oodavid 2017
13 | / \\ ·---------------------------- - -
14 |
15 | `));
16 | }
17 |
18 |
19 | function addSplashOfColor(message){
20 | return message.replace(/([∇♥])/g, `^r$1^:`);
21 | }
22 |
--------------------------------------------------------------------------------
/app/hud/keyboard.js:
--------------------------------------------------------------------------------
1 | module.exports.assign = assign;
2 | module.exports.start = start;
3 |
4 |
5 | const term = require('terminal-kit').terminal;
6 | const writeLine = require('./writeLine.js');
7 |
8 |
9 | let keys = {
10 | // keyName: { command, description }
11 | };
12 | let isActive = false;
13 |
14 |
15 | function assign(keyName, command, description){
16 | keys[keyName] = { command, description }; // ES6 syntactic sugar
17 | renderKeys();
18 | }
19 |
20 |
21 | function start(){
22 | // Only start when inactive
23 | if(isActive){ return; }
24 | isActive = true;
25 | // Listen for input
26 | term.grabInput(true);
27 | term.on('key', function(keyName, matches, data){
28 | // Has that key been assigned?
29 | if(keys.hasOwnProperty(keyName)){
30 | keys[keyName].command();
31 | }
32 | // Manually handle the exit command
33 | if(keyName === 'CTRL_C'){
34 | terminate();
35 | return;
36 | }
37 | });
38 | // Render the keys to screen
39 | renderKeys();
40 | }
41 |
42 |
43 | function terminate(){
44 | term.grabInput(false);
45 | setTimeout(function(){
46 | process.exit();
47 | }, 100);
48 | }
49 |
50 |
51 | function renderKeys(){
52 | if(!isActive){ return; }
53 | let commands = ['[CTRL+C] to exit'];
54 | Object.entries(keys).forEach(function(value){
55 | commands.push(`[${value[0]}] ${value[1].description}`);
56 | });
57 | writeLine(1, `^-${commands.join(', ')}^:`);
58 | }
59 |
--------------------------------------------------------------------------------
/app/hud/message.js:
--------------------------------------------------------------------------------
1 | module.exports = message;
2 |
3 |
4 | const writeLine = require('./writeLine.js');
5 | const fs = require('fs');
6 |
7 |
8 | const messageLog = fs.createWriteStream(`${__dirname}/../../message.log`, { flags: 'a' });
9 | let timeoutId;
10 |
11 |
12 | function message(message, duration=5000){
13 | // Write to file
14 | const now = new Date();
15 | messageLog.write(`${now} - ${message}\n\n`);
16 | // Update the HUD
17 | writeLine(18, message);
18 | if(timeoutId){
19 | clearTimeout(timeoutId);
20 | }
21 | timeoutId = setTimeout(function(){
22 | writeLine(18, '');
23 | }, duration);
24 | }
25 |
--------------------------------------------------------------------------------
/app/hud/numLines.js:
--------------------------------------------------------------------------------
1 | module.exports = 20;
2 |
--------------------------------------------------------------------------------
/app/hud/progress.js:
--------------------------------------------------------------------------------
1 | module.exports = progress;
2 |
3 |
4 | const numLines = require('./numLines.js');
5 | const initOnce = require('./initOnce.js');
6 | const term = require('terminal-kit').terminal;
7 | const moment = require('moment');
8 |
9 |
10 | let session;
11 | let maxHistoryMs = 1*60*1000; // 1 minute
12 |
13 |
14 | function progress(completed, total){
15 | initOnce();
16 | calculateSessionTotals(completed, total);
17 | showProgressMeter();
18 | showVelocityAndEta();
19 | }
20 |
21 |
22 | function calculateSessionTotals(completed, total){
23 | const now = Date.now();
24 | let props = { completed, total, timestamp: now };
25 | if(!session){
26 | session = {
27 | started: now,
28 | initial: props,
29 | history: [],
30 | };
31 | }
32 | // Basics
33 | session.completed = props.completed;
34 | session.total = props.total;
35 | session.timestamp = now;
36 | session.duration = now - session.initial.timestamp;
37 | session.addedToTotal = total - session.initial.total;
38 | session.newlyCompleted = completed - session.initial.completed;
39 | session.remaining = total - completed;
40 | session.pctComplete = total ? (completed/total) : 0;
41 | // Manage history
42 | session.history.push(props);
43 | session.history = session.history.filter(function(item){
44 | return item.timestamp > (now - maxHistoryMs); // Trim older history items
45 | });
46 | // Velocity (time to complete one item, ms) - compare the oldest and newest history items
47 | const dTimestamp = props.timestamp - session.history[0].timestamp;
48 | const dCompleted = props.completed - session.history[0].completed;
49 | session.velocity = false;
50 | if(dCompleted > 0){
51 | session.velocity = (dTimestamp/dCompleted);
52 | }
53 | // Eta (time to complete the remaining items, ms)
54 | session.eta = false;
55 | if(session.velocity){
56 | session.eta = new Date(now+(session.remaining*session.velocity));
57 | }
58 | }
59 |
60 |
61 | // Progress [---------------------------------------------------------------------] 1% [362 / 27,534]
62 | function showProgressMeter(){
63 | // The meter
64 | var len = 70;
65 | var left = Array(Math.round(len*session.pctComplete)).join('=');
66 | var right = Array(1+len-Math.round(len*session.pctComplete)).join('-');
67 | var completed = session.completed.toLocaleString();
68 | var total = session.total.toLocaleString();
69 | // The string
70 | var pctStr = Math.floor(session.pctComplete * 100);
71 | var str = `Progress [^g${left}^:^-${right}^:] ${pctStr}% [^g${completed}^: / ${total}]`;
72 | // Output
73 | term.restoreCursor();
74 | term.column(0).move(0, 3-numLines).eraseLine();
75 | term(str);
76 | term.restoreCursor();
77 | }
78 |
79 |
80 | // Completed: 1,234, Velocity: 100 / hour, ETA: 2h 40
81 | function showVelocityAndEta(){
82 | const started = moment(session.started).format('D MMM HH:mm');
83 | const completed = session.newlyCompleted.toLocaleString();
84 | var ttc = '---';
85 | if(session.eta){
86 | ttc = moment().to(moment(session.eta), true);
87 | }
88 | var rate = '---';
89 | if(session.velocity){
90 | rate = Math.round((60*60*1000)/session.velocity).toLocaleString() + ' / hour';
91 | }
92 | var str = `^-Started: ^:${started} ^-Completed: ^:${completed} ^-Rate: ^:${rate} ^-TTC: ^:${ttc}`;
93 | // Output
94 | term.restoreCursor();
95 | term.column(0).move(0, 4-numLines).eraseLine();
96 | term(str);
97 | term.restoreCursor();
98 | }
99 |
--------------------------------------------------------------------------------
/app/hud/title.js:
--------------------------------------------------------------------------------
1 | module.exports = title;
2 |
3 |
4 | const writeLine = require('./writeLine.js');
5 | const term = require('terminal-kit').terminal;
6 |
7 |
8 | function title(title){
9 | writeLine(0, title);
10 | title = title.replace(/\^./g, ''); // Strip "Style Markup" ie: ^bBLUE^: becomes BLUE
11 | term.windowTitle(title);
12 | }
13 |
--------------------------------------------------------------------------------
/app/hud/urlState.js:
--------------------------------------------------------------------------------
1 | module.exports = urlState;
2 |
3 |
4 | const writeLine = require('./writeLine.js');
5 | const term = require('terminal-kit').terminal;
6 |
7 |
8 | let firstLine = 6;
9 | let numLines = 10;
10 |
11 |
12 | let allUrls = {}; // url: state
13 | let visibleUrls = new Array(numLines).fill(null); // url
14 |
15 |
16 | function urlState(url, state){
17 | var index = getVisibleIndex(url);
18 | if(state){
19 | allUrls[url] = state;
20 | if(index !== -1){
21 | visibleUrls[index] = url;
22 | let paddedState = (state+' ').substr(0, 11); // Fixed length
23 | writeLine(index+firstLine, `^-${paddedState}^: ${url}`);
24 | }
25 | } else {
26 | delete allUrls[url];
27 | if(index !== -1){
28 | visibleUrls[index] = null;
29 | writeLine(index+firstLine, '^-------- http://----.--^:');
30 | }
31 | }
32 | renderTotals();
33 | }
34 |
35 |
36 | // Returns the line that this url is already allocated to, or the first empty line (or -1)
37 | function getVisibleIndex(url){
38 | const index = visibleUrls.indexOf(url);
39 | if(index !== -1){
40 | return index;
41 | }
42 | return visibleUrls.indexOf(null);
43 | }
44 |
45 |
46 | // Render totals
47 | function renderTotals(){
48 | // Total threads
49 | const totalThreads = Object.keys(allUrls).length;
50 | let message = `^-${totalThreads} threads`;
51 | // Hidden threads
52 | const hiddenThreads = totalThreads - visibleUrls.filter(Boolean).length;
53 | if(hiddenThreads > 0){
54 | message += ` (${hiddenThreads} threads are hidden)`;
55 | }
56 | writeLine(numLines+firstLine, message);
57 | }
58 |
--------------------------------------------------------------------------------
/app/hud/writeLine.js:
--------------------------------------------------------------------------------
1 | module.exports = writeLine;
2 |
3 |
4 | const numLines = require('./numLines.js');
5 | const initOnce = require('./initOnce.js');
6 | const term = require('terminal-kit').terminal;
7 |
8 |
9 | const maxLength = 100;
10 |
11 |
12 | function writeLine(n, message){
13 | // Sanitize the message
14 | message = message.replace(/[\r\n]/g, ''); // Strip newlines
15 | message = message.substring(0, maxLength); // Clip the length
16 | // Write
17 | initOnce();
18 | term.restoreCursor();
19 | term.column(0).move(0, n-numLines).eraseLine();
20 | term(message);
21 | term.restoreCursor();
22 | }
23 |
--------------------------------------------------------------------------------
/app/mongo/README.md:
--------------------------------------------------------------------------------
1 | # MongoDB
2 |
3 | Don't run MongoDB locally, a decentralised database is more useful; a managed service is often easier.
4 |
5 | There are services available with a free tier:
6 |
7 | * [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) - Free tier
8 | * [MLab](https://mlab.com/) - Sandbox
9 |
10 | Each come with a their own limitations, usually around the amount of data that can be stored, number of connections and replication.
11 |
12 | Regardless, they're enough for testing and development.
13 |
14 | # Setup
15 |
16 | Once you've created a remote database (above), run this command to set up your collection indexes.
17 |
18 | ```shell
19 | node app/mongo/setup/index.js
20 | ```
21 |
22 | You can update this script to add more collections and indexes, as you see fit.
23 |
24 | The script will prompt you for your [MongoDB Connection String](https://docs.mongodb.com/manual/reference/connection-string/), this will be saved to `creds.json`, and is in the format:
25 |
26 | `mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]`
27 |
28 | ## Webgraph Data Structure
29 |
30 | We have one collection to represent the webgraph; `urls`.
31 |
32 | Each document will have the basic structure below, with indexes on:
33 |
34 | * url (unique)
35 | * domain, status
36 | * hash
37 |
38 | ```json
39 | {
40 | _id: 'MongoObjectId',
41 | url: 'http://example.com/scraped-url',
42 | domain: 'example.com',
43 | status: 200,
44 | hash: '865A922C002FCA69CF14B0818FCBEB05',
45 | html: '....',
46 | links: [
47 | 'http://example.com/about-us',
48 | 'http://example.com/contact',
49 | 'http://blog.example.com/',
50 | 'http://otherdomain.com/home'
51 | ]
52 | }
53 | ```
54 |
55 | # Handy Queries
56 |
57 | ```js
58 | // Total number of URLs in the system (for any domain)
59 | db.urls.find().count();
60 |
61 | // Get the highest priority url to parse
62 | db.urls.find({ status: { $exists: false }, domain: 'www.checkatrade.com' }).sort({ weight: -1 }).limit(1).pretty();
63 |
64 | // Get the number of processed URLs
65 | db.urls.find({ status: { $exists: true, $ne: 'wip' }, domain: 'www.checkatrade.com' }).count();
66 |
67 | // Get all URLs with data matching JamesPond
68 | db.urls.find({ 'data.id': 'JamesPond' }, { url: 1, domain: 1, data: 1 }).pretty()
69 |
70 | // Count the URLs of each state
71 | // https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum
72 | db.urls.aggregate([{"$group": {"_id": "$status", "count": {"$sum":1} }}])
73 | ```
74 |
75 | ### References
76 |
77 | * https://docs.mongodb.com/v2.8/tutorial/create-an-index/
78 | * http://thecodebarbarian.com/common-async-await-design-patterns-in-node.js.html
79 |
--------------------------------------------------------------------------------
/app/mongo/getDb.js:
--------------------------------------------------------------------------------
1 | module.exports = getDb;
2 |
3 | const args = require('../args/index.js');
4 | const MongoClient = require('mongodb').MongoClient;
5 |
6 |
7 | let db;
8 |
9 |
10 | async function getDb(){
11 | // Initial connect
12 | if(!db){
13 | const connectionString = await args.getMongoConnectionString();
14 | db = await MongoClient.connect(connectionString);
15 | }
16 | // Return from memory
17 | return db;
18 | }
19 |
--------------------------------------------------------------------------------
/app/mongo/index.js:
--------------------------------------------------------------------------------
1 | exports.getDb = require('./getDb.js');
2 | exports.init = require('./init.js');
3 |
4 |
5 | exports.upsertNewUrls = require('./webgraph/upsertNewUrls.js');
6 | exports.getUncrawledUrl = require('./webgraph/getUncrawledUrl.js');
7 | exports.updateUrl = require('./webgraph/updateUrl.js');
8 | exports.getProgress = require('./webgraph/getProgress.js');
9 |
--------------------------------------------------------------------------------
/app/mongo/init.js:
--------------------------------------------------------------------------------
1 | module.exports = init;
2 |
3 |
4 | const getDb = require('./getDb.js');
5 | const term = require('terminal-kit').terminal;
6 |
7 |
8 | async function init(){
9 | try {
10 | const db = await getDb();
11 | term(`^r❯^: Connected to MongoDB^:\n`);
12 | } catch (e){
13 | console.log(e);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/mongo/setup/index.js:
--------------------------------------------------------------------------------
1 | const getDb = require('../getDb.js');
2 |
3 |
4 | async function setup(){
5 | try {
6 | const db = await getDb();
7 | // All URLS must be unique
8 | await db.collection('urls').createIndex({ url: 1 }, { unique: true });
9 | // Optimizes the query "find urls from domain with status, in weight order"
10 | await db.collection('urls').createIndex({ domain: 1, status: 1, weight: -1 });
11 | // Optimizes the query "does this url give a duplicate hash?"
12 | await db.collection('urls').createIndex({ hash: 1 });
13 | // Allows us to Map Reduce our scraped data
14 | await db.collection('urls').createIndex({ 'data.id': 1 });
15 | // All done
16 | db.close();
17 | console.log('Mongo Setup Complete (you only need to run this once)');
18 | } catch (e){
19 | console.log(e);
20 | }
21 | }
22 |
23 |
24 | setup();
25 |
--------------------------------------------------------------------------------
/app/mongo/webgraph/getProgress.js:
--------------------------------------------------------------------------------
1 | module.exports = getProgress;
2 |
3 |
4 | const getDb = require('../getDb.js');
5 | const hud = require('../../hud/');
6 |
7 |
8 | async function getProgress(domain){
9 | try {
10 | const db = await getDb();
11 | // Response
12 | let response = { crawled: 0, total: 0 };
13 | // Total
14 | const totalCursor = await db.collection('urls').aggregate([
15 | { $match: { domain: domain } },
16 | { $count: "total" }
17 | ]).toArray();
18 | response.total = totalCursor.length ? totalCursor[0].total : 0;
19 | // Crawled
20 | const crawledCursor = await db.collection('urls').aggregate([
21 | { $match: { status: { $exists: true, $ne: 'wip' }, domain: domain } },
22 | { $count: "crawled" }
23 | ]).toArray();
24 | response.crawled = crawledCursor.length ? crawledCursor[0].crawled : 0;
25 | // Return
26 | return response;
27 | } catch (e){
28 | hud.error(e);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/mongo/webgraph/getUncrawledUrl.js:
--------------------------------------------------------------------------------
1 | module.exports = getUncrawledUrl;
2 |
3 |
4 | const getDb = require('../getDb.js');
5 | const hud = require('../../hud/');
6 |
7 |
8 | async function getUncrawledUrl(domain){
9 | try {
10 | const db = await getDb();
11 | // Get an URL with no status, update it, and return
12 | const filter = { status: { $exists: false }, domain: domain };
13 | const update = { $set: { status: 'wip' } };
14 | const options = { returnNewDocument: true, sort: { weight: -1 } };
15 | const response = await db.collection('urls').findOneAndUpdate(filter, update, options);
16 | return response.value; // This may be null!
17 | } catch (e){
18 | hud.error(e);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/mongo/webgraph/updateUrl.js:
--------------------------------------------------------------------------------
1 | module.exports = updateUrl;
2 |
3 |
4 | const getDb = require('../getDb.js');
5 | const upsertNewUrls = require('./upsertNewUrls.js');
6 | const hud = require('../../hud/');
7 |
8 |
9 | // Where urlObject is an object of format { _id, url, domain, status, html, hash, links, data }
10 | async function updateUrl(domainConfig, urlObject){
11 | try {
12 | const db = await getDb();
13 | // Update the object
14 | urlObject.updated = new Date();
15 | const filter = { _id: urlObject._id };
16 | const update = { $set: urlObject };
17 | await db.collection('urls').updateOne(filter, update);
18 | // Upsert the links
19 | await upsertNewUrls(domainConfig, urlObject.links);
20 | } catch (e){
21 | hud.error(e);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/mongo/webgraph/upsertNewUrls.js:
--------------------------------------------------------------------------------
1 | module.exports = upsertNewUrls;
2 |
3 |
4 | const parseUrl = require('url').parse;
5 | const getDb = require('../getDb.js');
6 | const hud = require('../../hud/');
7 |
8 |
9 | async function upsertNewUrls(domainConfig, urls){
10 | try {
11 | const db = await getDb();
12 | // Insert the URLs
13 | const options = {
14 | ordered: false, // This means that, when a single write fails, the operation continues with the remaining writes
15 | };
16 | let docs = parseUrlsIntoMongoDocs(domainConfig, urls);
17 | if(docs.length){
18 | await db.collection('urls').insertMany(docs, options);
19 | }
20 | } catch (e){
21 | // Ignore errors relating to duplicates
22 | if(!(e.name === 'MongoError' && e.code === 11000)){
23 | hud.error(e);
24 | }
25 | }
26 | }
27 |
28 |
29 | function parseUrlsIntoMongoDocs(domainConfig, urls){
30 | return urls
31 | .filter(function(url){
32 | return url.match(/^http/i); // Must start with HTTP (case insensitive)
33 | })
34 | .map(function(url){
35 | return url.split('#')[0]; // Strip hashes
36 | })
37 | .filter(function(url, index, self){
38 | return self.indexOf(url) === index; // Only unique URLs
39 | })
40 | .map(function(url){
41 | let weight = 0.5;
42 | if(typeof domainConfig.getUrlWeight === 'function'){
43 | weight = domainConfig.getUrlWeight(url);
44 | }
45 | return {
46 | url,
47 | domain: parseUrl(url).host, // Extract the domain
48 | weight,
49 | created: new Date()
50 | };
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/app/mysql/README.md:
--------------------------------------------------------------------------------
1 | ## MySQL (localhost) installation
2 |
3 | 1 - Go here
4 | https://dev.mysql.com/downloads/mysql/
5 | ...
6 | n - After running the installer you will see a message like this:
7 | > A temporary password is generated for root@localhost: abcd1234ABCD
8 | > If you lose this password, please consult the section How to Reset the Root Password in the MySQL reference manual.
9 | ...make note of the password!
10 | n - Test it is running with
11 | `mysqladmin -u root version`
12 |
13 | ### Automatically Starting
14 |
15 | MySQL will automatically start.
16 |
17 | You can access the server (on OSX) via **System Preferences**.
18 |
19 | https://dev.mysql.com/doc/refman/5.7/en/osx-installation-launchd.html
20 |
21 | ## Create our user
22 |
23 | ```
24 | mysql -u root -p
25 | ---
26 | CREATE DATABASE scraper;
27 | CREATE USER 'charlotte'@'localhost' IDENTIFIED BY 'Wilbur';
28 | GRANT ALL PRIVILEGES ON scraper.* TO 'charlotte'@'localhost';
29 | FLUSH PRIVILEGES;
30 | exit
31 | --
32 | mysql -u charlotte -p -D scraper
33 | SELECT DATABASE();
34 | exit
35 | ```
36 |
37 | ## Workbench installation
38 |
39 | 1 - Go here
40 | https://www.mysql.com/products/workbench/
41 | 2 - Then here
42 | https://dev.mysql.com/downloads/workbench/
43 | Select OS and click download
44 | 3 - No need to register;
45 | No thanks, just start my download.
46 |
47 | Exporting our EER Diagram `File > Export > Forward Engineer SQL CREATE Script`
48 |
49 |
50 | ## Create our database
51 |
52 | mysql -u charlotte -p scraper < ./scraper.sql
53 |
54 |
55 | # Useful statements
56 |
57 | ```mysql
58 | -- Recently updated URLs
59 | SELECT id, domain, url, status, created, updated, CHAR_LENGTH(html), hash FROM urls WHERE status IS NOT NULL and status != 'wip' ORDER BY updated DESC LIMIT 10;
60 |
61 | -- How many URLs per domain
62 | SELECT domain, COUNT(*) FROM urls GROUP BY domain;
63 |
64 | -- How many URLs of each status per domain
65 | SELECT domain, status, COUNT(*) FROM urls GROUP BY domain, status ORDER BY COUNT(*) DESC LIMIT 30;
66 |
67 | -- Finding duplicate pages for a given domain
68 | SELECT hash, COUNT(*) AS num FROM urls WHERE domain = 'domain.com' AND hash IS NOT NULL GROUP BY hash HAVING num > 1;
69 |
70 | -- Reset WIP urls
71 | UPDATE urls SET status = NULL WHERE status = 'wip';
72 | ```
73 |
--------------------------------------------------------------------------------
/app/mysql/index.js:
--------------------------------------------------------------------------------
1 | exports.query = require('./query.js');
2 | exports.init = require('./init.js');
3 |
4 |
5 | exports.upsertNewUrls = require('./webgraph/upsertNewUrls.js');
6 | exports.getUncrawledUrl = require('./webgraph/getUncrawledUrl.js');
7 | exports.updateUrl = require('./webgraph/updateUrl.js');
8 | exports.getProgress = require('./webgraph/getProgress.js');
9 |
--------------------------------------------------------------------------------
/app/mysql/init.js:
--------------------------------------------------------------------------------
1 | module.exports = init;
2 |
3 |
4 | const query = require('./query.js');
5 | const term = require('terminal-kit').terminal;
6 |
7 |
8 | async function init(){
9 | let rows = await query('SELECT DATABASE() AS db, USER() AS user;');
10 | term(`^r❯^: Connected to ^c${rows[0]['db']}^: with ^c${rows[0]['user']}^:\n`);
11 | }
12 |
--------------------------------------------------------------------------------
/app/mysql/query.js:
--------------------------------------------------------------------------------
1 | module.exports = query;
2 |
3 |
4 | const Promise = require('bluebird');
5 | const mysql = require('promise-mysql');
6 | const args = require('../args/index.js');
7 |
8 |
9 | let pool;
10 |
11 |
12 | async function query(sql, values){
13 | return await Promise.using(getConnection(), function(connection) {
14 | return connection
15 | .query(sql, values);
16 | });
17 | }
18 |
19 |
20 | async function getConnection(){
21 | const pool = await getPool();
22 | return pool
23 | .getConnection()
24 | .disposer(function(connection) {
25 | pool.releaseConnection(connection);
26 | });
27 | }
28 |
29 |
30 | async function getPool() {
31 | // From memory
32 | if(pool){
33 | return pool;
34 | }
35 | // Create the pool from settings
36 | let settings = await args.getMysqlSettings();
37 | settings.multipleStatements = true;
38 | pool = mysql.createPool(settings);
39 | return pool;
40 | }
41 |
--------------------------------------------------------------------------------
/app/mysql/setup/scraper.mwb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oodavid/puppeteer-scraper/286326c380eb026c730fccf337a0ff1a2bcda0f9/app/mysql/setup/scraper.mwb
--------------------------------------------------------------------------------
/app/mysql/setup/scraper.sql:
--------------------------------------------------------------------------------
1 | -- MySQL Script generated by MySQL Workbench
2 | -- Sat Nov 18 17:17:03 2017
3 | -- Model: New Model Version: 1.0
4 | SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
5 | SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
6 | SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL,ALLOW_INVALID_DATES';
7 |
8 | -- -----------------------------------------------------
9 | -- Schema scraper
10 | -- -----------------------------------------------------
11 | CREATE SCHEMA IF NOT EXISTS `scraper` DEFAULT CHARACTER SET utf8mb4 ;
12 | USE `scraper` ;
13 |
14 | -- -----------------------------------------------------
15 | -- Table `scraper`.`urls`
16 | -- -----------------------------------------------------
17 | CREATE TABLE IF NOT EXISTS `scraper`.`urls` (
18 | `id` INT NOT NULL AUTO_INCREMENT,
19 | `domain` VARCHAR(100) NULL,
20 | `url` VARCHAR(750) NULL,
21 | `hash` VARCHAR(32) NULL,
22 | `status` VARCHAR(3) NULL,
23 | `html` LONGTEXT NULL,
24 | `created` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
25 | `updated` DATETIME NULL,
26 | PRIMARY KEY (`id`),
27 | UNIQUE INDEX `url_UNIQUE` (`url` ASC),
28 | INDEX `domain_INDEX` (`domain` ASC),
29 | INDEX `hash_INDEX` (`hash` ASC),
30 | INDEX `status_INDEX` (`status` ASC),
31 | INDEX `created_INDEX` (`created` ASC),
32 | INDEX `updated_INDEX` (`updated` ASC))
33 | ENGINE = InnoDB;
34 |
35 |
36 | -- -----------------------------------------------------
37 | -- Table `scraper`.`links`
38 | -- -----------------------------------------------------
39 | CREATE TABLE IF NOT EXISTS `scraper`.`links` (
40 | `from_url_id` INT NOT NULL,
41 | `to_url_id` INT NOT NULL,
42 | INDEX `fk_links_from_url_id_idx` (`from_url_id` ASC),
43 | INDEX `fk_links_to_url_id_idx` (`to_url_id` ASC),
44 | UNIQUE INDEX `link_UNIQUE` (`from_url_id` ASC, `to_url_id` ASC),
45 | CONSTRAINT `fk_links_from_url_id`
46 | FOREIGN KEY (`from_url_id`)
47 | REFERENCES `scraper`.`urls` (`id`)
48 | ON DELETE NO ACTION
49 | ON UPDATE NO ACTION,
50 | CONSTRAINT `fk_links_to_url_id`
51 | FOREIGN KEY (`to_url_id`)
52 | REFERENCES `scraper`.`urls` (`id`)
53 | ON DELETE NO ACTION
54 | ON UPDATE NO ACTION)
55 | ENGINE = InnoDB;
56 |
57 |
58 | SET SQL_MODE=@OLD_SQL_MODE;
59 | SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
60 | SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
61 |
--------------------------------------------------------------------------------
/app/mysql/webgraph/getProgress.js:
--------------------------------------------------------------------------------
1 | module.exports = getProgress;
2 |
3 |
4 | const mysql = require('../mysql/');
5 |
6 |
7 | async function getProgress(domain){
8 | const sql = `
9 | SELECT COUNT(*) as total FROM urls WHERE domain = ?;
10 | SELECT COUNT(*) as crawled FROM urls WHERE domain = ? AND status IS NOT NULL AND status != 'wip';
11 | `;
12 | const values = [ domain, domain ];
13 | let rows = await mysql.query(sql, values);
14 | if(rows && rows[0] && rows[0][0] && rows[1] && rows[1][0]){
15 | return {
16 | total: rows[0][0].total,
17 | crawled: rows[1][0].crawled,
18 | };
19 | } else {
20 | return {
21 | total: 0,
22 | crawled: 0,
23 | };
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/mysql/webgraph/getUncrawledUrl.js:
--------------------------------------------------------------------------------
1 | module.exports = getUncrawledUrl;
2 |
3 |
4 | const mysql = require('../mysql/');
5 | const err = require('../hud/error.js');
6 |
7 |
8 | async function getUncrawledUrl(domain){
9 | const sql = `
10 | SELECT id, url, @id:=id FROM urls WHERE status IS NULL AND domain = ? LIMIT 1 FOR UPDATE;
11 | UPDATE urls SET status='wip', updated = NOW() WHERE id=@id;
12 | `;
13 | const values = [ domain ];
14 | let rows = await mysql.query(sql, values);
15 | if(rows && rows[0] && rows[0][0] && rows[0][0].id){
16 | return {
17 | id: rows[0][0].id,
18 | url: rows[0][0].url,
19 | };
20 | } else {
21 | return false;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/mysql/webgraph/updateUrl.js:
--------------------------------------------------------------------------------
1 | module.exports = updateUrl;
2 |
3 |
4 | const hud = require('../hud/');
5 | const mysql = require('../mysql/');
6 | const parseUrl = require('url').parse;
7 |
8 |
9 | // Where urlObject is an object of format { id, status, html, hash, links }
10 | async function updateUrl(domainConfig, urlObject){
11 | // Build up our SQL statement and values
12 | let sql = '';
13 | let values = [];
14 | // Run the query
15 | try {
16 | // Update the URL itself
17 | sql += 'UPDATE urls SET status = ?, html = ?, hash = ?, updated = NOW() WHERE id = ?;';
18 | values.push(... [ urlObject.status, urlObject.html, urlObject.hash, urlObject.id ]);
19 | // Create the links
20 | const links = parseLinks(urlObject.links);
21 | links.forEach(function(link){
22 | const domain = parseUrl(link).host;
23 | if(domain){
24 | sql += 'INSERT INTO urls (domain, url) VALUES (?, ?) ON DUPLICATE KEY UPDATE id=id;';
25 | values.push(... [ domain, link ]);
26 | sql += 'INSERT IGNORE INTO links (from_url_id, to_url_id) VALUES (?, (SELECT id FROM urls WHERE url=? LIMIT 1));';
27 | values.push(... [ urlObject.id, link ]);
28 | }
29 | });
30 | await mysql.query(sql, values);
31 | } catch(e){
32 | hud.error(e);
33 | hud.error(new Error(sql));
34 | hud.error(new Error(JSON.stringify(values, null, 2)));
35 | }
36 | }
37 |
38 |
39 | function parseLinks(links){
40 | // Only HTTP (and HTTPS)
41 | links = links.filter(function(link){
42 | return link.indexOf('http') === 0;
43 | });
44 | // Strip hashes
45 | links.forEach(function(value, index, arr){
46 | arr[index] = value.split('#')[0];
47 | });
48 | // Make sure the links are unique
49 | links = [... new Set([... links])];
50 | return links;
51 | }
52 |
--------------------------------------------------------------------------------
/app/mysql/webgraph/upsertNewUrls.js:
--------------------------------------------------------------------------------
1 | module.exports = upsertNewUrls;
2 |
3 |
4 | const mysql = require('../mysql/');
5 | const parseUrl = require('url').parse;
6 |
7 |
8 | async function upsertNewUrls(domainConfig, seedUrls){
9 | // The `query` module needs `values` in a specific format to trigger a bulk insert, see: https://stackoverflow.com/a/14259347/1122851
10 | let values = [
11 | seedUrls.map(function(url){
12 | const domain = parseUrl(url).host;
13 | return [ domain, url ];
14 | })
15 | ];
16 | return await mysql.query('INSERT INTO urls (domain, url) VALUES ? ON DUPLICATE KEY UPDATE id=id;', values);
17 | }
18 |
--------------------------------------------------------------------------------
/domains/checkatrade.com/index.js:
--------------------------------------------------------------------------------
1 | exports.domain = 'www.checkatrade.com';
2 | exports.seedUrls = getSeedUrls();
3 | exports.parse = parse;
4 | exports.getUrlWeight = getUrlWeight;
5 |
6 |
7 | // Return an array of initial Urls to parse
8 | function getSeedUrls(){
9 | return [
10 | 'http://www.checkatrade.com/',
11 | 'http://www.checkatrade.com/JamesPond/',
12 | 'http://www.checkatrade.com/JamesPond/Reviews.aspx',
13 | 'http://www.checkatrade.com/JamesPond/Gallery.aspx',
14 | 'http://www.checkatrade.com/JamesPond/Services.aspx',
15 | 'http://www.checkatrade.com/JamesPond/Checks.aspx',
16 | ];
17 | };
18 |
19 | // Parse the page, returning data
20 | async function parse(page, urlObject){
21 | const data = await page.evaluate(function(){
22 | var $ = window.$ || window.jQuery;
23 | // Blank data
24 | var data = {};
25 | // The "id" is simply the first part of the Url, ie: /BusinessName/Services = "BusinessName"
26 | var m = location.pathname.match(/^\/([^\/]*)\//i);
27 | var id = m ? m[1] : false;
28 | if(id){
29 | data.id = id;
30 | // Rip out every property from the contact card
31 | $('.contact-card [itemprop]').each(function(){
32 | var prop = $(this).attr('itemprop');
33 | // Ignore these wrapper elements
34 | if($.inArray(prop, ['aggregateRating']) !== -1){ return true; }
35 | // Values come in different forms
36 | var value;
37 | switch(prop){
38 | // Rating
39 | case "ratingValue": value = parseFloat($(this).text(), 10); break;
40 | case "reviewCount": value = parseInt($(this).text()); break;
41 | case "worstRating": value = parseInt($(this).attr('content')); break;
42 | case "bestRating": value = parseInt($(this).attr('content')); break;
43 | // By default grab the content
44 | default: value = $.trim($(this).text()); break;
45 | }
46 | // Add to the data, duplicate keys should create arrays (ie: telephone)
47 | if(data[prop] && !Array.isArray(data[prop])){
48 | data[prop] = [ data[prop] ];
49 | }
50 | if(Array.isArray(data[prop])){
51 | data[prop].push(value);
52 | } else {
53 | data[prop] = value;
54 | }
55 | });
56 | // Can we see the company id?
57 | var companyId = $('[data-company-id]');
58 | if(companyId.length){
59 | data.checkatradeId = parseInt(companyId.attr('data-company-id'));
60 | }
61 | // Some pages have a well-formatted "company" object, we'll have that
62 | if(window.cat && window.cat.dataHeaderData && window.cat.dataHeaderData.Companies.length){
63 | data.company = window.cat.dataHeaderData.Companies[0];
64 | }
65 | // Checks - I think they really just mean "checks" here, not "checksum" - how silly.
66 | var checks = $('.member-checksum li');
67 | if(checks.length){
68 | data.checks = [];
69 | checks.each(function(){
70 | data.checks.push($.trim($(this).text()));
71 | });
72 | }
73 | // Skills
74 | var skills = $('.skills-list > li');
75 | if(skills.length){
76 | data.skills = {};
77 | skills.each(function(){
78 | var heading = $.trim($(this).find('> span').text());
79 | data.skills[heading] = [];
80 | $(this).find('li').each(function(){
81 | data.skills[heading].push($.trim($(this).text()));
82 | });
83 | });
84 | }
85 | // Services
86 | var services = $('.member-services__serv-list > div');
87 | if(services.length){
88 | data.services = [];
89 | services.each(function(){
90 | data.services.push($.trim($(this).text()));
91 | });
92 | }
93 | // Map
94 | // http://www.checkatrade.com/BandKElectricalsLtd/Checks.aspx
95 | // VAT Number
96 | // Company Name
97 | // Company ID
98 | // Director
99 | }
100 | return data;
101 | });
102 | return data;
103 | }
104 |
105 |
106 | // Return the "weight" of an URL from 0-1
107 | function getUrlWeight(url){
108 | var weights = {
109 | '/Account/': 0, // Customer Sign-In
110 | '/Report.aspx': 0, // Printable report
111 | '/GiveFeedback/': 0, // Give feedback about a member
112 | '/Search/': 0.1, // Good for building the network, but I think there are infinite search variations exposed
113 | '/Gallery.aspx': 0.3, // Just photos of the member's work
114 | '/Reviews.aspx': 0.4, // Reviews about the member
115 | '/Reputation': 0.2 // Can't remember why I downgraded this...
116 | };
117 | // See if we can match it
118 | for(var key in weights){
119 | if(weights.hasOwnProperty(key)){
120 | if(url.indexOf(key) !== -1){
121 | return weights[key];
122 | }
123 | }
124 | }
125 | // Everything else is top priority
126 | return 1;
127 | };
128 |
--------------------------------------------------------------------------------
/domains/oodavid.com/index.js:
--------------------------------------------------------------------------------
1 | exports.domain = 'oodavid.com';
2 | exports.seedUrls = getSeedUrls();
3 | exports.getUrlWeight = getUrlWeight;
4 | exports.parseForData = parseForData;
5 | exports.throttle = 0;
6 |
7 | // Return an array of initial Urls to parse
8 | function getSeedUrls(){
9 | return [ 'https://oodavid.com/' ];
10 | };
11 |
12 | // Return the "weight" of an URL from 0-1.
13 | // ...this dictates the order that pages are scraped
14 | function getUrlWeight(url){
15 | var weights = {
16 | '/Account/': 0, // Customer Sign-In
17 | '/Report.aspx': 0, // Printable report
18 | '/GiveFeedback/': 0, // Give feedback about a member
19 | '/Search/': 0.1, // Good for building the network, but I think there are infinite search variations exposed
20 | '/Gallery.aspx': 0.3, // Just photos of the member's work
21 | '/Reviews.aspx': 0.4, // Reviews about the member
22 | '/Reputation': 0.2 // Can't remember why I downgraded this...
23 | };
24 | // See if we can match it
25 | for(var key in weights){
26 | if(weights.hasOwnProperty(key)){
27 | if(url.pathname.indexOf(key) !== -1){
28 | return weights[key];
29 | }
30 | }
31 | }
32 | // Everything else is top priority
33 | return 1;
34 | };
35 |
36 | // Return a { data } object to be stored with the Url
37 | // ...this runs in the window context, so console.log won't output to NodeJS (it operates in the horseman window)
38 | // ...this MUST have an `id` value to uniquely identify the lead
39 | function parseForData(){
40 | var $ = window.$ || window.jQuery;
41 | // Blank data
42 | var data = {};
43 | // The "id" is simply the first part of the Url, ie: /BusinessName/Services = "BusinessName"
44 | var m = location.pathname.match(/^\/([^\/]*)\//i);
45 | var id = m ? m[1] : false;
46 | if(id){
47 | data.id = id;
48 | // Rip out every property from the contact card
49 | $('.contact-card [itemprop]').each(function(){
50 | var prop = $(this).attr('itemprop');
51 | // Ignore these wrapper elements
52 | if($.inArray(prop, ['aggregateRating']) !== -1){ return true; }
53 | // Values come in different forms
54 | var value;
55 | switch(prop){
56 | // Rating
57 | case "ratingValue": value = parseFloat($(this).text(), 10); break;
58 | case "reviewCount": value = parseInt($(this).text()); break;
59 | case "worstRating": value = parseInt($(this).attr('content')); break;
60 | case "bestRating": value = parseInt($(this).attr('content')); break;
61 | // By default grab the content
62 | default: value = $.trim($(this).text()); break;
63 | }
64 | // Add to the data, duplicate keys should create arrays (ie: telephone)
65 | if(data[prop] && !Array.isArray(data[prop])){
66 | data[prop] = [ data[prop] ];
67 | }
68 | if(Array.isArray(data[prop])){
69 | data[prop].push(value);
70 | } else {
71 | data[prop] = value;
72 | }
73 | });
74 | // Can we see the company id?
75 | var companyId = $('[data-company-id]');
76 | if(companyId.length){
77 | data.checkatradeId = parseInt(companyId.attr('data-company-id'));
78 | }
79 | // Some pages have a well-formatted "company" object, we'll have that
80 | if(window.cat && window.cat.dataHeaderData && window.cat.dataHeaderData.Companies.length){
81 | data.company = window.cat.dataHeaderData.Companies[0];
82 | }
83 | // Checks - I think they really just mean "checks" here, not "checksum" - how silly.
84 | var checks = $('.member-checksum li');
85 | if(checks.length){
86 | data.checks = [];
87 | checks.each(function(){
88 | data.checks.push($.trim($(this).text()));
89 | });
90 | }
91 | // Skills
92 | var skills = $('.skills-list > li');
93 | if(skills){
94 | data.skills = {};
95 | skills.each(function(){
96 | var heading = $.trim($(this).find('> span').text());
97 | data.skills[heading] = [];
98 | $(this).find('li').each(function(){
99 | data.skills[heading].push($.trim($(this).text()));
100 | });
101 | });
102 | }
103 | // Services
104 | var services = $('.member-services__serv-list > div');
105 | if(services){
106 | data.services = [];
107 | services.each(function(){
108 | data.services.push($.trim($(this).text()));
109 | });
110 | }
111 | // Map
112 | // http://www.checkatrade.com/BandKElectricalsLtd/Checks.aspx
113 | // VAT Number
114 | // Company Name
115 | // Company ID
116 | // Director
117 | }
118 | return data;
119 | };
120 |
--------------------------------------------------------------------------------
/domains/template/template.js:
--------------------------------------------------------------------------------
1 | /*
2 | regex url matching
3 | getSeedUrls
4 | getUrlThrottle
5 | getUrlPriority
6 | parseHtml
7 | auth
8 | */
9 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const hud = require('./app/hud/');
2 | const args = require('./app/args/');
3 | const datastore = require('./app/mongo/');
4 | const term = require('terminal-kit').terminal;
5 |
6 |
7 | (async () => {
8 | hud.intro();
9 | // Connect to MySQL
10 | await datastore.init();
11 | // Get the arguments
12 | const action = await args.getAction();
13 | const domainName = await args.getDomain();
14 | const numThreads = await args.getThreads();
15 | term(`^r❯^: ^+To run this without prompts, use^:\n`);
16 | term(`^r❯^: ^-node index.js --action ${action} --domain ${domainName} --threads ${numThreads}^:\n`);
17 | // Read the config
18 | const domainConfig = require(`./domains/${domainName}/`);
19 | // Start the action
20 | if(action === 'crawl'){
21 | const crawler = require('./app/crawler/');
22 | crawler.start(domainConfig, numThreads);
23 | } else {
24 | const parser = require('./app/parser/');
25 | parser.start(domainConfig, numThreads);
26 | }
27 | })();
28 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "puppeteer-scraper",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "@types/mysql": {
8 | "version": "2.15.1",
9 | "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.1.tgz",
10 | "integrity": "sha512-CLdDYJMQjM3yFbCVcNAxplWX0WEPI6RQesJapPo9TbduGbrfielJJdMafslycAeC36OwgtWHE06YnwgzSKl3xw==",
11 | "requires": {
12 | "@types/node": "8.0.51"
13 | }
14 | },
15 | "@types/node": {
16 | "version": "8.0.51",
17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.51.tgz",
18 | "integrity": "sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ=="
19 | },
20 | "agent-base": {
21 | "version": "4.1.1",
22 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.1.1.tgz",
23 | "integrity": "sha512-yWGUUmCZD/33IRjG2It94PzixT8lX+47Uq8fjmd0cgQWITCMrJuXFaVIMnGDmDnZGGKAGdwTx8UGeU8lMR2urA==",
24 | "requires": {
25 | "es6-promisify": "5.0.0"
26 | }
27 | },
28 | "ajv": {
29 | "version": "5.2.3",
30 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz",
31 | "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=",
32 | "requires": {
33 | "co": "4.6.0",
34 | "fast-deep-equal": "1.0.0",
35 | "json-schema-traverse": "0.3.1",
36 | "json-stable-stringify": "1.0.1"
37 | }
38 | },
39 | "ansi-escapes": {
40 | "version": "2.0.0",
41 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-2.0.0.tgz",
42 | "integrity": "sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs="
43 | },
44 | "ansi-regex": {
45 | "version": "2.1.1",
46 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
47 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
48 | },
49 | "ansi-styles": {
50 | "version": "2.2.1",
51 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
52 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
53 | },
54 | "asn1": {
55 | "version": "0.2.3",
56 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
57 | "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y="
58 | },
59 | "assert-plus": {
60 | "version": "1.0.0",
61 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
62 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
63 | },
64 | "async-kit": {
65 | "version": "2.2.3",
66 | "resolved": "https://registry.npmjs.org/async-kit/-/async-kit-2.2.3.tgz",
67 | "integrity": "sha1-JkdRonndxfWbQZY4uAWuLEmFj7c=",
68 | "requires": {
69 | "nextgen-events": "0.9.9",
70 | "tree-kit": "0.5.26"
71 | },
72 | "dependencies": {
73 | "nextgen-events": {
74 | "version": "0.9.9",
75 | "resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-0.9.9.tgz",
76 | "integrity": "sha1-OaivxKK4RTiMV+LGu5cWcRmGo6A="
77 | }
78 | }
79 | },
80 | "async-limiter": {
81 | "version": "1.0.0",
82 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
83 | "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg=="
84 | },
85 | "asynckit": {
86 | "version": "0.4.0",
87 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
88 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
89 | },
90 | "aws-sign2": {
91 | "version": "0.7.0",
92 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
93 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
94 | },
95 | "aws4": {
96 | "version": "1.6.0",
97 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz",
98 | "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4="
99 | },
100 | "balanced-match": {
101 | "version": "1.0.0",
102 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
103 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
104 | },
105 | "bcrypt-pbkdf": {
106 | "version": "1.0.1",
107 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
108 | "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
109 | "optional": true,
110 | "requires": {
111 | "tweetnacl": "0.14.5"
112 | }
113 | },
114 | "bignumber.js": {
115 | "version": "4.0.4",
116 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.0.4.tgz",
117 | "integrity": "sha512-LDXpJKVzEx2/OqNbG9mXBNvHuiRL4PzHCGfnANHMJ+fv68Ads3exDVJeGDJws+AoNEuca93bU3q+S0woeUaCdg=="
118 | },
119 | "bluebird": {
120 | "version": "3.5.1",
121 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
122 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA=="
123 | },
124 | "boom": {
125 | "version": "4.3.1",
126 | "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
127 | "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
128 | "requires": {
129 | "hoek": "4.2.0"
130 | }
131 | },
132 | "brace-expansion": {
133 | "version": "1.1.8",
134 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz",
135 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=",
136 | "requires": {
137 | "balanced-match": "1.0.0",
138 | "concat-map": "0.0.1"
139 | }
140 | },
141 | "browser-request": {
142 | "version": "0.3.3",
143 | "resolved": "https://registry.npmjs.org/browser-request/-/browser-request-0.3.3.tgz",
144 | "integrity": "sha1-ns5bWsqJopkyJC4Yv5M975h2zBc="
145 | },
146 | "bson": {
147 | "version": "1.0.4",
148 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz",
149 | "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw="
150 | },
151 | "buffer-shims": {
152 | "version": "1.0.0",
153 | "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz",
154 | "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E="
155 | },
156 | "camelcase": {
157 | "version": "4.1.0",
158 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
159 | "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
160 | },
161 | "caseless": {
162 | "version": "0.12.0",
163 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
164 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
165 | },
166 | "chalk": {
167 | "version": "1.1.3",
168 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
169 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
170 | "requires": {
171 | "ansi-styles": "2.2.1",
172 | "escape-string-regexp": "1.0.5",
173 | "has-ansi": "2.0.0",
174 | "strip-ansi": "3.0.1",
175 | "supports-color": "2.0.0"
176 | }
177 | },
178 | "charenc": {
179 | "version": "0.0.2",
180 | "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
181 | "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc="
182 | },
183 | "cli-cursor": {
184 | "version": "2.1.0",
185 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
186 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
187 | "requires": {
188 | "restore-cursor": "2.0.0"
189 | }
190 | },
191 | "cli-width": {
192 | "version": "2.2.0",
193 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
194 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
195 | },
196 | "cliui": {
197 | "version": "3.2.0",
198 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
199 | "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
200 | "requires": {
201 | "string-width": "1.0.2",
202 | "strip-ansi": "3.0.1",
203 | "wrap-ansi": "2.1.0"
204 | },
205 | "dependencies": {
206 | "is-fullwidth-code-point": {
207 | "version": "1.0.0",
208 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
209 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
210 | "requires": {
211 | "number-is-nan": "1.0.1"
212 | }
213 | },
214 | "string-width": {
215 | "version": "1.0.2",
216 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
217 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
218 | "requires": {
219 | "code-point-at": "1.1.0",
220 | "is-fullwidth-code-point": "1.0.0",
221 | "strip-ansi": "3.0.1"
222 | }
223 | }
224 | }
225 | },
226 | "cloudant-follow": {
227 | "version": "0.13.0",
228 | "resolved": "https://registry.npmjs.org/cloudant-follow/-/cloudant-follow-0.13.0.tgz",
229 | "integrity": "sha1-fs6teQYbADmfXuNUA2jdYGV83Cs=",
230 | "requires": {
231 | "browser-request": "0.3.3",
232 | "debug": "2.6.9",
233 | "request": "2.81.0"
234 | },
235 | "dependencies": {
236 | "ajv": {
237 | "version": "4.11.8",
238 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz",
239 | "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=",
240 | "requires": {
241 | "co": "4.6.0",
242 | "json-stable-stringify": "1.0.1"
243 | }
244 | },
245 | "assert-plus": {
246 | "version": "0.2.0",
247 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
248 | "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ="
249 | },
250 | "aws-sign2": {
251 | "version": "0.6.0",
252 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
253 | "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8="
254 | },
255 | "boom": {
256 | "version": "2.10.1",
257 | "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
258 | "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
259 | "requires": {
260 | "hoek": "2.16.3"
261 | }
262 | },
263 | "cryptiles": {
264 | "version": "2.0.5",
265 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
266 | "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
267 | "requires": {
268 | "boom": "2.10.1"
269 | }
270 | },
271 | "form-data": {
272 | "version": "2.1.4",
273 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
274 | "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
275 | "requires": {
276 | "asynckit": "0.4.0",
277 | "combined-stream": "1.0.5",
278 | "mime-types": "2.1.17"
279 | }
280 | },
281 | "har-schema": {
282 | "version": "1.0.5",
283 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz",
284 | "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4="
285 | },
286 | "har-validator": {
287 | "version": "4.2.1",
288 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz",
289 | "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=",
290 | "requires": {
291 | "ajv": "4.11.8",
292 | "har-schema": "1.0.5"
293 | }
294 | },
295 | "hawk": {
296 | "version": "3.1.3",
297 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
298 | "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
299 | "requires": {
300 | "boom": "2.10.1",
301 | "cryptiles": "2.0.5",
302 | "hoek": "2.16.3",
303 | "sntp": "1.0.9"
304 | }
305 | },
306 | "hoek": {
307 | "version": "2.16.3",
308 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
309 | "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0="
310 | },
311 | "http-signature": {
312 | "version": "1.1.1",
313 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
314 | "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
315 | "requires": {
316 | "assert-plus": "0.2.0",
317 | "jsprim": "1.4.1",
318 | "sshpk": "1.13.1"
319 | }
320 | },
321 | "performance-now": {
322 | "version": "0.2.0",
323 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz",
324 | "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU="
325 | },
326 | "qs": {
327 | "version": "6.4.0",
328 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz",
329 | "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM="
330 | },
331 | "request": {
332 | "version": "2.81.0",
333 | "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz",
334 | "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=",
335 | "requires": {
336 | "aws-sign2": "0.6.0",
337 | "aws4": "1.6.0",
338 | "caseless": "0.12.0",
339 | "combined-stream": "1.0.5",
340 | "extend": "3.0.1",
341 | "forever-agent": "0.6.1",
342 | "form-data": "2.1.4",
343 | "har-validator": "4.2.1",
344 | "hawk": "3.1.3",
345 | "http-signature": "1.1.1",
346 | "is-typedarray": "1.0.0",
347 | "isstream": "0.1.2",
348 | "json-stringify-safe": "5.0.1",
349 | "mime-types": "2.1.17",
350 | "oauth-sign": "0.8.2",
351 | "performance-now": "0.2.0",
352 | "qs": "6.4.0",
353 | "safe-buffer": "5.1.1",
354 | "stringstream": "0.0.5",
355 | "tough-cookie": "2.3.3",
356 | "tunnel-agent": "0.6.0",
357 | "uuid": "3.1.0"
358 | }
359 | },
360 | "sntp": {
361 | "version": "1.0.9",
362 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
363 | "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
364 | "requires": {
365 | "hoek": "2.16.3"
366 | }
367 | }
368 | }
369 | },
370 | "co": {
371 | "version": "4.6.0",
372 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
373 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
374 | },
375 | "code-point-at": {
376 | "version": "1.1.0",
377 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
378 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
379 | },
380 | "color-convert": {
381 | "version": "1.9.0",
382 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz",
383 | "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=",
384 | "requires": {
385 | "color-name": "1.1.3"
386 | }
387 | },
388 | "color-name": {
389 | "version": "1.1.3",
390 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
391 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
392 | },
393 | "combined-stream": {
394 | "version": "1.0.5",
395 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz",
396 | "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=",
397 | "requires": {
398 | "delayed-stream": "1.0.0"
399 | }
400 | },
401 | "concat-map": {
402 | "version": "0.0.1",
403 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
404 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
405 | },
406 | "concat-stream": {
407 | "version": "1.6.0",
408 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
409 | "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
410 | "requires": {
411 | "inherits": "2.0.3",
412 | "readable-stream": "2.3.3",
413 | "typedarray": "0.0.6"
414 | }
415 | },
416 | "core-util-is": {
417 | "version": "1.0.2",
418 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
419 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
420 | },
421 | "cross-spawn": {
422 | "version": "5.1.0",
423 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
424 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
425 | "requires": {
426 | "lru-cache": "4.1.1",
427 | "shebang-command": "1.2.0",
428 | "which": "1.3.0"
429 | }
430 | },
431 | "crypt": {
432 | "version": "0.0.2",
433 | "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
434 | "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs="
435 | },
436 | "cryptiles": {
437 | "version": "3.1.2",
438 | "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
439 | "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
440 | "requires": {
441 | "boom": "5.2.0"
442 | },
443 | "dependencies": {
444 | "boom": {
445 | "version": "5.2.0",
446 | "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
447 | "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==",
448 | "requires": {
449 | "hoek": "4.2.0"
450 | }
451 | }
452 | }
453 | },
454 | "cwise-compiler": {
455 | "version": "1.1.3",
456 | "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
457 | "integrity": "sha1-9NZnQQ6FDToxOn0tt7HlBbsDTMU=",
458 | "requires": {
459 | "uniq": "1.0.1"
460 | }
461 | },
462 | "dashdash": {
463 | "version": "1.14.1",
464 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
465 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
466 | "requires": {
467 | "assert-plus": "1.0.0"
468 | }
469 | },
470 | "data-uri-to-buffer": {
471 | "version": "0.0.3",
472 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
473 | "integrity": "sha1-GK6XmmoMqZSwYlhTkW0mYruuCxo="
474 | },
475 | "dateformat": {
476 | "version": "3.0.2",
477 | "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.2.tgz",
478 | "integrity": "sha1-mk30v/FYrC80vGN6vbFUcWB+Flk="
479 | },
480 | "debug": {
481 | "version": "2.6.9",
482 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
483 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
484 | "requires": {
485 | "ms": "2.0.0"
486 | }
487 | },
488 | "decamelize": {
489 | "version": "1.2.0",
490 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
491 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
492 | },
493 | "delayed-stream": {
494 | "version": "1.0.0",
495 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
496 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
497 | },
498 | "ecc-jsbn": {
499 | "version": "0.1.1",
500 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
501 | "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
502 | "optional": true,
503 | "requires": {
504 | "jsbn": "0.1.1"
505 | }
506 | },
507 | "errs": {
508 | "version": "0.3.2",
509 | "resolved": "https://registry.npmjs.org/errs/-/errs-0.3.2.tgz",
510 | "integrity": "sha1-eYCZstvTfKK8dJ5TinwTB9C1BJk="
511 | },
512 | "es6-promise": {
513 | "version": "4.1.1",
514 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.1.1.tgz",
515 | "integrity": "sha512-OaU1hHjgJf+b0NzsxCg7NdIYERD6Hy/PEmFLTjw+b65scuisG3Kt4QoTvJ66BBkPZ581gr0kpoVzKnxniM8nng=="
516 | },
517 | "es6-promisify": {
518 | "version": "5.0.0",
519 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
520 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
521 | "requires": {
522 | "es6-promise": "4.1.1"
523 | }
524 | },
525 | "escape-string-regexp": {
526 | "version": "1.0.5",
527 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
528 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
529 | },
530 | "execa": {
531 | "version": "0.7.0",
532 | "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
533 | "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
534 | "requires": {
535 | "cross-spawn": "5.1.0",
536 | "get-stream": "3.0.0",
537 | "is-stream": "1.1.0",
538 | "npm-run-path": "2.0.2",
539 | "p-finally": "1.0.0",
540 | "signal-exit": "3.0.2",
541 | "strip-eof": "1.0.0"
542 | }
543 | },
544 | "extend": {
545 | "version": "3.0.1",
546 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
547 | "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
548 | },
549 | "external-editor": {
550 | "version": "2.0.5",
551 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.5.tgz",
552 | "integrity": "sha512-Msjo64WT5W+NhOpQXh0nOHm+n0RfU1QUwDnKYvJ8dEJ8zlwLrqXNTv5mSUTJpepf41PDJGyhueTw2vNZW+Fr/w==",
553 | "requires": {
554 | "iconv-lite": "0.4.19",
555 | "jschardet": "1.6.0",
556 | "tmp": "0.0.33"
557 | }
558 | },
559 | "extract-zip": {
560 | "version": "1.6.5",
561 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.5.tgz",
562 | "integrity": "sha1-maBnNbbqIOqbcF13ms/8yHz/BEA=",
563 | "requires": {
564 | "concat-stream": "1.6.0",
565 | "debug": "2.2.0",
566 | "mkdirp": "0.5.0",
567 | "yauzl": "2.4.1"
568 | },
569 | "dependencies": {
570 | "debug": {
571 | "version": "2.2.0",
572 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
573 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=",
574 | "requires": {
575 | "ms": "0.7.1"
576 | }
577 | },
578 | "ms": {
579 | "version": "0.7.1",
580 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz",
581 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg="
582 | }
583 | }
584 | },
585 | "extsprintf": {
586 | "version": "1.3.0",
587 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
588 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
589 | },
590 | "fast-deep-equal": {
591 | "version": "1.0.0",
592 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz",
593 | "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8="
594 | },
595 | "fd-slicer": {
596 | "version": "1.0.1",
597 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz",
598 | "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=",
599 | "requires": {
600 | "pend": "1.2.0"
601 | }
602 | },
603 | "figures": {
604 | "version": "2.0.0",
605 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
606 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
607 | "requires": {
608 | "escape-string-regexp": "1.0.5"
609 | }
610 | },
611 | "find-up": {
612 | "version": "2.1.0",
613 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
614 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
615 | "requires": {
616 | "locate-path": "2.0.0"
617 | }
618 | },
619 | "forever-agent": {
620 | "version": "0.6.1",
621 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
622 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
623 | },
624 | "form-data": {
625 | "version": "2.3.1",
626 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz",
627 | "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=",
628 | "requires": {
629 | "asynckit": "0.4.0",
630 | "combined-stream": "1.0.5",
631 | "mime-types": "2.1.17"
632 | }
633 | },
634 | "fs.realpath": {
635 | "version": "1.0.0",
636 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
637 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
638 | },
639 | "fuzzy": {
640 | "version": "0.1.3",
641 | "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz",
642 | "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg="
643 | },
644 | "get-caller-file": {
645 | "version": "1.0.2",
646 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
647 | "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U="
648 | },
649 | "get-pixels": {
650 | "version": "3.3.0",
651 | "resolved": "https://registry.npmjs.org/get-pixels/-/get-pixels-3.3.0.tgz",
652 | "integrity": "sha1-jZeVvq4YhQuED3SVgbrcBdPjbkE=",
653 | "requires": {
654 | "data-uri-to-buffer": "0.0.3",
655 | "jpeg-js": "0.1.2",
656 | "mime-types": "2.1.17",
657 | "ndarray": "1.0.18",
658 | "ndarray-pack": "1.2.1",
659 | "node-bitmap": "0.0.1",
660 | "omggif": "1.0.8",
661 | "parse-data-uri": "0.2.0",
662 | "pngjs": "2.3.1",
663 | "request": "2.83.0",
664 | "through": "2.3.8"
665 | }
666 | },
667 | "get-stream": {
668 | "version": "3.0.0",
669 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
670 | "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
671 | },
672 | "getpass": {
673 | "version": "0.1.7",
674 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
675 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
676 | "requires": {
677 | "assert-plus": "1.0.0"
678 | }
679 | },
680 | "glob": {
681 | "version": "7.1.2",
682 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
683 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
684 | "requires": {
685 | "fs.realpath": "1.0.0",
686 | "inflight": "1.0.6",
687 | "inherits": "2.0.3",
688 | "minimatch": "3.0.4",
689 | "once": "1.4.0",
690 | "path-is-absolute": "1.0.1"
691 | }
692 | },
693 | "har-schema": {
694 | "version": "2.0.0",
695 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
696 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
697 | },
698 | "har-validator": {
699 | "version": "5.0.3",
700 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz",
701 | "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=",
702 | "requires": {
703 | "ajv": "5.2.3",
704 | "har-schema": "2.0.0"
705 | }
706 | },
707 | "has-ansi": {
708 | "version": "2.0.0",
709 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
710 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
711 | "requires": {
712 | "ansi-regex": "2.1.1"
713 | }
714 | },
715 | "has-flag": {
716 | "version": "2.0.0",
717 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
718 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE="
719 | },
720 | "hawk": {
721 | "version": "6.0.2",
722 | "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz",
723 | "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==",
724 | "requires": {
725 | "boom": "4.3.1",
726 | "cryptiles": "3.1.2",
727 | "hoek": "4.2.0",
728 | "sntp": "2.0.2"
729 | }
730 | },
731 | "hoek": {
732 | "version": "4.2.0",
733 | "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz",
734 | "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ=="
735 | },
736 | "http-signature": {
737 | "version": "1.2.0",
738 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
739 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
740 | "requires": {
741 | "assert-plus": "1.0.0",
742 | "jsprim": "1.4.1",
743 | "sshpk": "1.13.1"
744 | }
745 | },
746 | "https-proxy-agent": {
747 | "version": "2.1.0",
748 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.1.0.tgz",
749 | "integrity": "sha512-/DTVSUCbRc6AiyOV4DBRvPDpKKCJh4qQJNaCgypX0T41quD9hp/PB5iUyx/60XobuMPQa9ce1jNV9UOUq6PnTg==",
750 | "requires": {
751 | "agent-base": "4.1.1",
752 | "debug": "2.6.9"
753 | }
754 | },
755 | "iconv-lite": {
756 | "version": "0.4.19",
757 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
758 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
759 | },
760 | "inflight": {
761 | "version": "1.0.6",
762 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
763 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
764 | "requires": {
765 | "once": "1.4.0",
766 | "wrappy": "1.0.2"
767 | }
768 | },
769 | "inherits": {
770 | "version": "2.0.3",
771 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
772 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
773 | },
774 | "inquirer": {
775 | "version": "3.3.0",
776 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
777 | "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
778 | "requires": {
779 | "ansi-escapes": "3.0.0",
780 | "chalk": "2.3.0",
781 | "cli-cursor": "2.1.0",
782 | "cli-width": "2.2.0",
783 | "external-editor": "2.0.5",
784 | "figures": "2.0.0",
785 | "lodash": "4.17.4",
786 | "mute-stream": "0.0.7",
787 | "run-async": "2.3.0",
788 | "rx-lite": "4.0.8",
789 | "rx-lite-aggregates": "4.0.8",
790 | "string-width": "2.1.1",
791 | "strip-ansi": "4.0.0",
792 | "through": "2.3.8"
793 | },
794 | "dependencies": {
795 | "ansi-escapes": {
796 | "version": "3.0.0",
797 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz",
798 | "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ=="
799 | },
800 | "ansi-regex": {
801 | "version": "3.0.0",
802 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
803 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
804 | },
805 | "ansi-styles": {
806 | "version": "3.2.0",
807 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
808 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
809 | "requires": {
810 | "color-convert": "1.9.0"
811 | }
812 | },
813 | "chalk": {
814 | "version": "2.3.0",
815 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
816 | "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
817 | "requires": {
818 | "ansi-styles": "3.2.0",
819 | "escape-string-regexp": "1.0.5",
820 | "supports-color": "4.5.0"
821 | }
822 | },
823 | "strip-ansi": {
824 | "version": "4.0.0",
825 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
826 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
827 | "requires": {
828 | "ansi-regex": "3.0.0"
829 | }
830 | },
831 | "supports-color": {
832 | "version": "4.5.0",
833 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
834 | "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
835 | "requires": {
836 | "has-flag": "2.0.0"
837 | }
838 | }
839 | }
840 | },
841 | "inquirer-autocomplete-prompt": {
842 | "version": "0.11.1",
843 | "resolved": "https://registry.npmjs.org/inquirer-autocomplete-prompt/-/inquirer-autocomplete-prompt-0.11.1.tgz",
844 | "integrity": "sha512-VM4eNiyRD4CeUc2cyKni+F8qgHwL9WC4LdOr+mEC85qP/QNsDV+ysVqUrJYhw1TmDQu1QVhc8hbaL7wfk8SJxw==",
845 | "requires": {
846 | "ansi-escapes": "2.0.0",
847 | "chalk": "1.1.3",
848 | "figures": "2.0.0",
849 | "inquirer": "3.1.1",
850 | "lodash": "4.17.4",
851 | "run-async": "2.3.0",
852 | "util": "0.10.3"
853 | },
854 | "dependencies": {
855 | "inquirer": {
856 | "version": "3.1.1",
857 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.1.1.tgz",
858 | "integrity": "sha512-H50sHQwgvvaTBd3HpKMVtL/u6LoHDvYym51gd7bGQe/+9HkCE+J0/3N5FJLfd6O6oz44hHewC2Pc2LodzWVafQ==",
859 | "requires": {
860 | "ansi-escapes": "2.0.0",
861 | "chalk": "1.1.3",
862 | "cli-cursor": "2.1.0",
863 | "cli-width": "2.2.0",
864 | "external-editor": "2.0.5",
865 | "figures": "2.0.0",
866 | "lodash": "4.17.4",
867 | "mute-stream": "0.0.7",
868 | "run-async": "2.3.0",
869 | "rx-lite": "4.0.8",
870 | "rx-lite-aggregates": "4.0.8",
871 | "string-width": "2.1.1",
872 | "strip-ansi": "3.0.1",
873 | "through": "2.3.8"
874 | }
875 | }
876 | }
877 | },
878 | "invert-kv": {
879 | "version": "1.0.0",
880 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz",
881 | "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
882 | },
883 | "iota-array": {
884 | "version": "1.0.0",
885 | "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
886 | "integrity": "sha1-ge9X/l0FgUzVjCSDYyqZwwoOgIc="
887 | },
888 | "is-buffer": {
889 | "version": "1.1.6",
890 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
891 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
892 | },
893 | "is-fullwidth-code-point": {
894 | "version": "2.0.0",
895 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
896 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
897 | },
898 | "is-promise": {
899 | "version": "2.1.0",
900 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
901 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
902 | },
903 | "is-stream": {
904 | "version": "1.1.0",
905 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
906 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
907 | },
908 | "is-typedarray": {
909 | "version": "1.0.0",
910 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
911 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
912 | },
913 | "isarray": {
914 | "version": "1.0.0",
915 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
916 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
917 | },
918 | "isexe": {
919 | "version": "2.0.0",
920 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
921 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
922 | },
923 | "isstream": {
924 | "version": "0.1.2",
925 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
926 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
927 | },
928 | "jpeg-js": {
929 | "version": "0.1.2",
930 | "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz",
931 | "integrity": "sha1-E1uZLAV1yYXPoPSUoyJ+0jhYPs4="
932 | },
933 | "jquery": {
934 | "version": "3.2.1",
935 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.2.1.tgz",
936 | "integrity": "sha1-XE2d5lKvbNCncBVKYxu6ErAVx4c="
937 | },
938 | "jsbn": {
939 | "version": "0.1.1",
940 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
941 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
942 | "optional": true
943 | },
944 | "jschardet": {
945 | "version": "1.6.0",
946 | "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.6.0.tgz",
947 | "integrity": "sha512-xYuhvQ7I9PDJIGBWev9xm0+SMSed3ZDBAmvVjbFR1ZRLAF+vlXcQu6cRI9uAlj81rzikElRVteehwV7DuX2ZmQ=="
948 | },
949 | "json-schema": {
950 | "version": "0.2.3",
951 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
952 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
953 | },
954 | "json-schema-traverse": {
955 | "version": "0.3.1",
956 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
957 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
958 | },
959 | "json-stable-stringify": {
960 | "version": "1.0.1",
961 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
962 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
963 | "requires": {
964 | "jsonify": "0.0.0"
965 | }
966 | },
967 | "json-stringify-safe": {
968 | "version": "5.0.1",
969 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
970 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
971 | },
972 | "jsonify": {
973 | "version": "0.0.0",
974 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
975 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
976 | },
977 | "jsprim": {
978 | "version": "1.4.1",
979 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
980 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
981 | "requires": {
982 | "assert-plus": "1.0.0",
983 | "extsprintf": "1.3.0",
984 | "json-schema": "0.2.3",
985 | "verror": "1.10.0"
986 | }
987 | },
988 | "keypress": {
989 | "version": "0.2.1",
990 | "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz",
991 | "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc="
992 | },
993 | "lcid": {
994 | "version": "1.0.0",
995 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz",
996 | "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=",
997 | "requires": {
998 | "invert-kv": "1.0.0"
999 | }
1000 | },
1001 | "locate-path": {
1002 | "version": "2.0.0",
1003 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
1004 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
1005 | "requires": {
1006 | "p-locate": "2.0.0",
1007 | "path-exists": "3.0.0"
1008 | }
1009 | },
1010 | "lodash": {
1011 | "version": "4.17.4",
1012 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
1013 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
1014 | },
1015 | "lru-cache": {
1016 | "version": "4.1.1",
1017 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
1018 | "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==",
1019 | "requires": {
1020 | "pseudomap": "1.0.2",
1021 | "yallist": "2.1.2"
1022 | }
1023 | },
1024 | "md5": {
1025 | "version": "2.2.1",
1026 | "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz",
1027 | "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=",
1028 | "requires": {
1029 | "charenc": "0.0.2",
1030 | "crypt": "0.0.2",
1031 | "is-buffer": "1.1.6"
1032 | }
1033 | },
1034 | "mem": {
1035 | "version": "1.1.0",
1036 | "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz",
1037 | "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=",
1038 | "requires": {
1039 | "mimic-fn": "1.1.0"
1040 | }
1041 | },
1042 | "mime": {
1043 | "version": "1.4.1",
1044 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
1045 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
1046 | },
1047 | "mime-db": {
1048 | "version": "1.30.0",
1049 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz",
1050 | "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE="
1051 | },
1052 | "mime-types": {
1053 | "version": "2.1.17",
1054 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz",
1055 | "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=",
1056 | "requires": {
1057 | "mime-db": "1.30.0"
1058 | }
1059 | },
1060 | "mimic-fn": {
1061 | "version": "1.1.0",
1062 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz",
1063 | "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg="
1064 | },
1065 | "minimatch": {
1066 | "version": "3.0.4",
1067 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
1068 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
1069 | "requires": {
1070 | "brace-expansion": "1.1.8"
1071 | }
1072 | },
1073 | "minimist": {
1074 | "version": "0.0.8",
1075 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
1076 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
1077 | },
1078 | "mkdirp": {
1079 | "version": "0.5.0",
1080 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz",
1081 | "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=",
1082 | "requires": {
1083 | "minimist": "0.0.8"
1084 | }
1085 | },
1086 | "moment": {
1087 | "version": "2.19.2",
1088 | "resolved": "https://registry.npmjs.org/moment/-/moment-2.19.2.tgz",
1089 | "integrity": "sha512-Rf6jiHPEfxp9+dlzxPTmRHbvoFXsh2L/U8hOupUMpnuecHQmI6cF6lUbJl3QqKPko1u6ujO+FxtcajLVfLpAtA=="
1090 | },
1091 | "mongodb": {
1092 | "version": "2.2.33",
1093 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.33.tgz",
1094 | "integrity": "sha1-tTfEcdNKZlG0jzb9vyl1A0Dgi1A=",
1095 | "requires": {
1096 | "es6-promise": "3.2.1",
1097 | "mongodb-core": "2.1.17",
1098 | "readable-stream": "2.2.7"
1099 | },
1100 | "dependencies": {
1101 | "es6-promise": {
1102 | "version": "3.2.1",
1103 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz",
1104 | "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q="
1105 | },
1106 | "readable-stream": {
1107 | "version": "2.2.7",
1108 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz",
1109 | "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=",
1110 | "requires": {
1111 | "buffer-shims": "1.0.0",
1112 | "core-util-is": "1.0.2",
1113 | "inherits": "2.0.3",
1114 | "isarray": "1.0.0",
1115 | "process-nextick-args": "1.0.7",
1116 | "string_decoder": "1.0.3",
1117 | "util-deprecate": "1.0.2"
1118 | }
1119 | }
1120 | }
1121 | },
1122 | "mongodb-core": {
1123 | "version": "2.1.17",
1124 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.17.tgz",
1125 | "integrity": "sha1-pBizN6FKFJkPtRC5I97mqBMXPfg=",
1126 | "requires": {
1127 | "bson": "1.0.4",
1128 | "require_optional": "1.0.1"
1129 | }
1130 | },
1131 | "ms": {
1132 | "version": "2.0.0",
1133 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1134 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
1135 | },
1136 | "mute-stream": {
1137 | "version": "0.0.7",
1138 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
1139 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
1140 | },
1141 | "mysql": {
1142 | "version": "2.15.0",
1143 | "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.15.0.tgz",
1144 | "integrity": "sha512-C7tjzWtbN5nzkLIV+E8Crnl9bFyc7d3XJcBAvHKEVkjrYjogz3llo22q6s/hw+UcsE4/844pDob9ac+3dVjQSA==",
1145 | "requires": {
1146 | "bignumber.js": "4.0.4",
1147 | "readable-stream": "2.3.3",
1148 | "safe-buffer": "5.1.1",
1149 | "sqlstring": "2.3.0"
1150 | }
1151 | },
1152 | "nano": {
1153 | "version": "6.4.2",
1154 | "resolved": "https://registry.npmjs.org/nano/-/nano-6.4.2.tgz",
1155 | "integrity": "sha1-ubD6geuRS4ucY0odVeh44PRbCb8=",
1156 | "requires": {
1157 | "cloudant-follow": "0.13.0",
1158 | "debug": "2.6.9",
1159 | "errs": "0.3.2",
1160 | "request": "2.83.0",
1161 | "underscore": "1.8.3"
1162 | }
1163 | },
1164 | "ndarray": {
1165 | "version": "1.0.18",
1166 | "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.18.tgz",
1167 | "integrity": "sha1-tg06cyJOxVXQ+qeXEeUCRI/T95M=",
1168 | "requires": {
1169 | "iota-array": "1.0.0",
1170 | "is-buffer": "1.1.6"
1171 | }
1172 | },
1173 | "ndarray-pack": {
1174 | "version": "1.2.1",
1175 | "resolved": "https://registry.npmjs.org/ndarray-pack/-/ndarray-pack-1.2.1.tgz",
1176 | "integrity": "sha1-jK6+qqJNXs9w/4YCBjeXfajuWFo=",
1177 | "requires": {
1178 | "cwise-compiler": "1.1.3",
1179 | "ndarray": "1.0.18"
1180 | }
1181 | },
1182 | "nextgen-events": {
1183 | "version": "0.10.2",
1184 | "resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-0.10.2.tgz",
1185 | "integrity": "sha512-P6efDoVOOJjVLOhwINq+aqhC2B3a9IxojbWMn9fTv2coDiyRaaGLSgWsm84wQHlQeuqVgqiLEE+xiHXf3EVN6w=="
1186 | },
1187 | "node-bitmap": {
1188 | "version": "0.0.1",
1189 | "resolved": "https://registry.npmjs.org/node-bitmap/-/node-bitmap-0.0.1.tgz",
1190 | "integrity": "sha1-GA6scAPgxwdhjvMTaPYvhLKmkJE="
1191 | },
1192 | "node-couchdb": {
1193 | "version": "1.2.0",
1194 | "resolved": "https://registry.npmjs.org/node-couchdb/-/node-couchdb-1.2.0.tgz",
1195 | "integrity": "sha1-6APixyNiF9CR3jfeIW2CgYOfCnw=",
1196 | "requires": {
1197 | "request": "2.83.0"
1198 | }
1199 | },
1200 | "npm-run-path": {
1201 | "version": "2.0.2",
1202 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
1203 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
1204 | "requires": {
1205 | "path-key": "2.0.1"
1206 | }
1207 | },
1208 | "number-is-nan": {
1209 | "version": "1.0.1",
1210 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
1211 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
1212 | },
1213 | "oauth-sign": {
1214 | "version": "0.8.2",
1215 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz",
1216 | "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM="
1217 | },
1218 | "omggif": {
1219 | "version": "1.0.8",
1220 | "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.8.tgz",
1221 | "integrity": "sha1-F483sqsLPXtG7ToORr0HkLWNNTA="
1222 | },
1223 | "once": {
1224 | "version": "1.4.0",
1225 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1226 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
1227 | "requires": {
1228 | "wrappy": "1.0.2"
1229 | }
1230 | },
1231 | "onetime": {
1232 | "version": "2.0.1",
1233 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
1234 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
1235 | "requires": {
1236 | "mimic-fn": "1.1.0"
1237 | }
1238 | },
1239 | "os-locale": {
1240 | "version": "2.1.0",
1241 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
1242 | "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==",
1243 | "requires": {
1244 | "execa": "0.7.0",
1245 | "lcid": "1.0.0",
1246 | "mem": "1.1.0"
1247 | }
1248 | },
1249 | "os-tmpdir": {
1250 | "version": "1.0.2",
1251 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
1252 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
1253 | },
1254 | "p-finally": {
1255 | "version": "1.0.0",
1256 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
1257 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
1258 | },
1259 | "p-limit": {
1260 | "version": "1.1.0",
1261 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz",
1262 | "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw="
1263 | },
1264 | "p-locate": {
1265 | "version": "2.0.0",
1266 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
1267 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
1268 | "requires": {
1269 | "p-limit": "1.1.0"
1270 | }
1271 | },
1272 | "parse-data-uri": {
1273 | "version": "0.2.0",
1274 | "resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
1275 | "integrity": "sha1-vwTYUd1ch7CrI45dAazklLYEtMk=",
1276 | "requires": {
1277 | "data-uri-to-buffer": "0.0.3"
1278 | }
1279 | },
1280 | "path-exists": {
1281 | "version": "3.0.0",
1282 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
1283 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
1284 | },
1285 | "path-is-absolute": {
1286 | "version": "1.0.1",
1287 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
1288 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
1289 | },
1290 | "path-key": {
1291 | "version": "2.0.1",
1292 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
1293 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
1294 | },
1295 | "pend": {
1296 | "version": "1.2.0",
1297 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
1298 | "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
1299 | },
1300 | "performance-now": {
1301 | "version": "2.1.0",
1302 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
1303 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
1304 | },
1305 | "pngjs": {
1306 | "version": "2.3.1",
1307 | "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-2.3.1.tgz",
1308 | "integrity": "sha1-EdHhK5y2TWPjDBQ6Mw9MH1Z9qF8="
1309 | },
1310 | "process-nextick-args": {
1311 | "version": "1.0.7",
1312 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
1313 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
1314 | },
1315 | "progress": {
1316 | "version": "2.0.0",
1317 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
1318 | "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8="
1319 | },
1320 | "promise-mysql": {
1321 | "version": "3.1.3",
1322 | "resolved": "https://registry.npmjs.org/promise-mysql/-/promise-mysql-3.1.3.tgz",
1323 | "integrity": "sha512-rtFSHKSLiT6x9MC2V1TXhXQw1++QY0O9t+iZIFf6tPcMF06EgUYlw7PD2aqZK6PxF4NyNrcUsqoJIRLRoCsxhQ==",
1324 | "requires": {
1325 | "@types/mysql": "2.15.1",
1326 | "bluebird": "3.5.1",
1327 | "mysql": "2.15.0"
1328 | }
1329 | },
1330 | "proxy-from-env": {
1331 | "version": "1.0.0",
1332 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz",
1333 | "integrity": "sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4="
1334 | },
1335 | "pseudomap": {
1336 | "version": "1.0.2",
1337 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
1338 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
1339 | },
1340 | "punycode": {
1341 | "version": "1.4.1",
1342 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
1343 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
1344 | },
1345 | "puppeteer": {
1346 | "version": "0.11.0",
1347 | "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-0.11.0.tgz",
1348 | "integrity": "sha512-/o8uY6VChG74B1HYBZkLDU6aIekWeOd85wez9YxB9SNbDnFNsUddv2F4xWuhwvQ4ab84vb+1VX4fxs2kBz3u8g==",
1349 | "requires": {
1350 | "debug": "2.6.9",
1351 | "extract-zip": "1.6.5",
1352 | "https-proxy-agent": "2.1.0",
1353 | "mime": "1.4.1",
1354 | "progress": "2.0.0",
1355 | "proxy-from-env": "1.0.0",
1356 | "rimraf": "2.6.2",
1357 | "ws": "3.2.0"
1358 | }
1359 | },
1360 | "qs": {
1361 | "version": "6.5.1",
1362 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
1363 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
1364 | },
1365 | "readable-stream": {
1366 | "version": "2.3.3",
1367 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
1368 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
1369 | "requires": {
1370 | "core-util-is": "1.0.2",
1371 | "inherits": "2.0.3",
1372 | "isarray": "1.0.0",
1373 | "process-nextick-args": "1.0.7",
1374 | "safe-buffer": "5.1.1",
1375 | "string_decoder": "1.0.3",
1376 | "util-deprecate": "1.0.2"
1377 | }
1378 | },
1379 | "request": {
1380 | "version": "2.83.0",
1381 | "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz",
1382 | "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==",
1383 | "requires": {
1384 | "aws-sign2": "0.7.0",
1385 | "aws4": "1.6.0",
1386 | "caseless": "0.12.0",
1387 | "combined-stream": "1.0.5",
1388 | "extend": "3.0.1",
1389 | "forever-agent": "0.6.1",
1390 | "form-data": "2.3.1",
1391 | "har-validator": "5.0.3",
1392 | "hawk": "6.0.2",
1393 | "http-signature": "1.2.0",
1394 | "is-typedarray": "1.0.0",
1395 | "isstream": "0.1.2",
1396 | "json-stringify-safe": "5.0.1",
1397 | "mime-types": "2.1.17",
1398 | "oauth-sign": "0.8.2",
1399 | "performance-now": "2.1.0",
1400 | "qs": "6.5.1",
1401 | "safe-buffer": "5.1.1",
1402 | "stringstream": "0.0.5",
1403 | "tough-cookie": "2.3.3",
1404 | "tunnel-agent": "0.6.0",
1405 | "uuid": "3.1.0"
1406 | }
1407 | },
1408 | "require-directory": {
1409 | "version": "2.1.1",
1410 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1411 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
1412 | },
1413 | "require-main-filename": {
1414 | "version": "1.0.1",
1415 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz",
1416 | "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE="
1417 | },
1418 | "require_optional": {
1419 | "version": "1.0.1",
1420 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
1421 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
1422 | "requires": {
1423 | "resolve-from": "2.0.0",
1424 | "semver": "5.4.1"
1425 | }
1426 | },
1427 | "resolve-from": {
1428 | "version": "2.0.0",
1429 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
1430 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
1431 | },
1432 | "restore-cursor": {
1433 | "version": "2.0.0",
1434 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
1435 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
1436 | "requires": {
1437 | "onetime": "2.0.1",
1438 | "signal-exit": "3.0.2"
1439 | }
1440 | },
1441 | "rimraf": {
1442 | "version": "2.6.2",
1443 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
1444 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
1445 | "requires": {
1446 | "glob": "7.1.2"
1447 | }
1448 | },
1449 | "run-async": {
1450 | "version": "2.3.0",
1451 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
1452 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
1453 | "requires": {
1454 | "is-promise": "2.1.0"
1455 | }
1456 | },
1457 | "rx-lite": {
1458 | "version": "4.0.8",
1459 | "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
1460 | "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ="
1461 | },
1462 | "rx-lite-aggregates": {
1463 | "version": "4.0.8",
1464 | "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
1465 | "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
1466 | "requires": {
1467 | "rx-lite": "4.0.8"
1468 | }
1469 | },
1470 | "safe-buffer": {
1471 | "version": "5.1.1",
1472 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
1473 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
1474 | },
1475 | "sanitize-filename": {
1476 | "version": "1.6.1",
1477 | "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz",
1478 | "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=",
1479 | "requires": {
1480 | "truncate-utf8-bytes": "1.0.2"
1481 | }
1482 | },
1483 | "semver": {
1484 | "version": "5.4.1",
1485 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
1486 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg=="
1487 | },
1488 | "set-blocking": {
1489 | "version": "2.0.0",
1490 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
1491 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
1492 | },
1493 | "shebang-command": {
1494 | "version": "1.2.0",
1495 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
1496 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
1497 | "requires": {
1498 | "shebang-regex": "1.0.0"
1499 | }
1500 | },
1501 | "shebang-regex": {
1502 | "version": "1.0.0",
1503 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
1504 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
1505 | },
1506 | "signal-exit": {
1507 | "version": "3.0.2",
1508 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
1509 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
1510 | },
1511 | "sntp": {
1512 | "version": "2.0.2",
1513 | "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz",
1514 | "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=",
1515 | "requires": {
1516 | "hoek": "4.2.0"
1517 | }
1518 | },
1519 | "sqlstring": {
1520 | "version": "2.3.0",
1521 | "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.0.tgz",
1522 | "integrity": "sha1-UluKT9Jtb3GqYegipsr5dtMa0qg="
1523 | },
1524 | "sshpk": {
1525 | "version": "1.13.1",
1526 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz",
1527 | "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=",
1528 | "requires": {
1529 | "asn1": "0.2.3",
1530 | "assert-plus": "1.0.0",
1531 | "bcrypt-pbkdf": "1.0.1",
1532 | "dashdash": "1.14.1",
1533 | "ecc-jsbn": "0.1.1",
1534 | "getpass": "0.1.7",
1535 | "jsbn": "0.1.1",
1536 | "tweetnacl": "0.14.5"
1537 | }
1538 | },
1539 | "string-kit": {
1540 | "version": "0.6.3",
1541 | "resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.6.3.tgz",
1542 | "integrity": "sha512-G2T92klsuE+S9mqdKQyWurFweNQV5X+FRzSKTqYHRdaVUN/4dL6urbYJJ+xb9ep/4XWm+4RNT8j3acncNhFRBg==",
1543 | "requires": {
1544 | "xregexp": "3.2.0"
1545 | }
1546 | },
1547 | "string-width": {
1548 | "version": "2.1.1",
1549 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
1550 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
1551 | "requires": {
1552 | "is-fullwidth-code-point": "2.0.0",
1553 | "strip-ansi": "4.0.0"
1554 | },
1555 | "dependencies": {
1556 | "ansi-regex": {
1557 | "version": "3.0.0",
1558 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
1559 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
1560 | },
1561 | "strip-ansi": {
1562 | "version": "4.0.0",
1563 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
1564 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
1565 | "requires": {
1566 | "ansi-regex": "3.0.0"
1567 | }
1568 | }
1569 | }
1570 | },
1571 | "string_decoder": {
1572 | "version": "1.0.3",
1573 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
1574 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
1575 | "requires": {
1576 | "safe-buffer": "5.1.1"
1577 | }
1578 | },
1579 | "stringstream": {
1580 | "version": "0.0.5",
1581 | "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz",
1582 | "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg="
1583 | },
1584 | "strip-ansi": {
1585 | "version": "3.0.1",
1586 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
1587 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
1588 | "requires": {
1589 | "ansi-regex": "2.1.1"
1590 | }
1591 | },
1592 | "strip-eof": {
1593 | "version": "1.0.0",
1594 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
1595 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
1596 | },
1597 | "supports-color": {
1598 | "version": "2.0.0",
1599 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
1600 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
1601 | },
1602 | "terminal-kit": {
1603 | "version": "1.14.1",
1604 | "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.14.1.tgz",
1605 | "integrity": "sha512-XBHeDrw84ehTejUWSQi1bK98xVr+K154m9LKU0absFbKVO+Q4g2iXxRKMXWS4YfxlgxE4MWlUhemw32TOQK8/w==",
1606 | "requires": {
1607 | "async-kit": "2.2.3",
1608 | "get-pixels": "3.3.0",
1609 | "ndarray": "1.0.18",
1610 | "nextgen-events": "0.10.2",
1611 | "string-kit": "0.6.3",
1612 | "tree-kit": "0.5.26"
1613 | }
1614 | },
1615 | "through": {
1616 | "version": "2.3.8",
1617 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
1618 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
1619 | },
1620 | "tmp": {
1621 | "version": "0.0.33",
1622 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
1623 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
1624 | "requires": {
1625 | "os-tmpdir": "1.0.2"
1626 | }
1627 | },
1628 | "tough-cookie": {
1629 | "version": "2.3.3",
1630 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz",
1631 | "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=",
1632 | "requires": {
1633 | "punycode": "1.4.1"
1634 | }
1635 | },
1636 | "tree-kit": {
1637 | "version": "0.5.26",
1638 | "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.5.26.tgz",
1639 | "integrity": "sha1-hXHIb6JNHbdU5bDLOn4J9B50qN8="
1640 | },
1641 | "truncate-utf8-bytes": {
1642 | "version": "1.0.2",
1643 | "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
1644 | "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=",
1645 | "requires": {
1646 | "utf8-byte-length": "1.0.4"
1647 | }
1648 | },
1649 | "tunnel-agent": {
1650 | "version": "0.6.0",
1651 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
1652 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
1653 | "requires": {
1654 | "safe-buffer": "5.1.1"
1655 | }
1656 | },
1657 | "tweetnacl": {
1658 | "version": "0.14.5",
1659 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
1660 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
1661 | "optional": true
1662 | },
1663 | "typedarray": {
1664 | "version": "0.0.6",
1665 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1666 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
1667 | },
1668 | "ultron": {
1669 | "version": "1.1.0",
1670 | "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz",
1671 | "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ="
1672 | },
1673 | "underscore": {
1674 | "version": "1.8.3",
1675 | "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
1676 | "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
1677 | },
1678 | "uniq": {
1679 | "version": "1.0.1",
1680 | "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
1681 | "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
1682 | },
1683 | "utf8-byte-length": {
1684 | "version": "1.0.4",
1685 | "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
1686 | "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E="
1687 | },
1688 | "util": {
1689 | "version": "0.10.3",
1690 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
1691 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
1692 | "requires": {
1693 | "inherits": "2.0.1"
1694 | },
1695 | "dependencies": {
1696 | "inherits": {
1697 | "version": "2.0.1",
1698 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
1699 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
1700 | }
1701 | }
1702 | },
1703 | "util-deprecate": {
1704 | "version": "1.0.2",
1705 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
1706 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
1707 | },
1708 | "uuid": {
1709 | "version": "3.1.0",
1710 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz",
1711 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g=="
1712 | },
1713 | "verror": {
1714 | "version": "1.10.0",
1715 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
1716 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
1717 | "requires": {
1718 | "assert-plus": "1.0.0",
1719 | "core-util-is": "1.0.2",
1720 | "extsprintf": "1.3.0"
1721 | }
1722 | },
1723 | "which": {
1724 | "version": "1.3.0",
1725 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
1726 | "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==",
1727 | "requires": {
1728 | "isexe": "2.0.0"
1729 | }
1730 | },
1731 | "which-module": {
1732 | "version": "2.0.0",
1733 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
1734 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
1735 | },
1736 | "wrap-ansi": {
1737 | "version": "2.1.0",
1738 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz",
1739 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=",
1740 | "requires": {
1741 | "string-width": "1.0.2",
1742 | "strip-ansi": "3.0.1"
1743 | },
1744 | "dependencies": {
1745 | "is-fullwidth-code-point": {
1746 | "version": "1.0.0",
1747 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
1748 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
1749 | "requires": {
1750 | "number-is-nan": "1.0.1"
1751 | }
1752 | },
1753 | "string-width": {
1754 | "version": "1.0.2",
1755 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
1756 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
1757 | "requires": {
1758 | "code-point-at": "1.1.0",
1759 | "is-fullwidth-code-point": "1.0.0",
1760 | "strip-ansi": "3.0.1"
1761 | }
1762 | }
1763 | }
1764 | },
1765 | "wrappy": {
1766 | "version": "1.0.2",
1767 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1768 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
1769 | },
1770 | "ws": {
1771 | "version": "3.2.0",
1772 | "resolved": "https://registry.npmjs.org/ws/-/ws-3.2.0.tgz",
1773 | "integrity": "sha512-hTS3mkXm/j85jTQOIcwVz3yK3up9xHgPtgEhDBOH3G18LDOZmSAG1omJeXejLKJakx+okv8vS1sopgs7rw0kVw==",
1774 | "requires": {
1775 | "async-limiter": "1.0.0",
1776 | "safe-buffer": "5.1.1",
1777 | "ultron": "1.1.0"
1778 | }
1779 | },
1780 | "xregexp": {
1781 | "version": "3.2.0",
1782 | "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-3.2.0.tgz",
1783 | "integrity": "sha1-yzYBmHv+JpW1hAAMGPHEqMMih44="
1784 | },
1785 | "y18n": {
1786 | "version": "3.2.1",
1787 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
1788 | "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
1789 | },
1790 | "yallist": {
1791 | "version": "2.1.2",
1792 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
1793 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
1794 | },
1795 | "yargs": {
1796 | "version": "10.0.3",
1797 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-10.0.3.tgz",
1798 | "integrity": "sha512-DqBpQ8NAUX4GyPP/ijDGHsJya4tYqLQrjPr95HNsr1YwL3+daCfvBwg7+gIC6IdJhR2kATh3hb61vjzMWEtjdw==",
1799 | "requires": {
1800 | "cliui": "3.2.0",
1801 | "decamelize": "1.2.0",
1802 | "find-up": "2.1.0",
1803 | "get-caller-file": "1.0.2",
1804 | "os-locale": "2.1.0",
1805 | "require-directory": "2.1.1",
1806 | "require-main-filename": "1.0.1",
1807 | "set-blocking": "2.0.0",
1808 | "string-width": "2.1.1",
1809 | "which-module": "2.0.0",
1810 | "y18n": "3.2.1",
1811 | "yargs-parser": "8.0.0"
1812 | }
1813 | },
1814 | "yargs-parser": {
1815 | "version": "8.0.0",
1816 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.0.0.tgz",
1817 | "integrity": "sha1-IdR2Mw5agieaS4gTRb8GYQLiGcY=",
1818 | "requires": {
1819 | "camelcase": "4.1.0"
1820 | }
1821 | },
1822 | "yauzl": {
1823 | "version": "2.4.1",
1824 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz",
1825 | "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=",
1826 | "requires": {
1827 | "fd-slicer": "1.0.1"
1828 | }
1829 | }
1830 | }
1831 | }
1832 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "puppeteer-scraper",
3 | "version": "1.0.0",
4 | "description": "A Better Scraper, with Puppeteer",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "David 'oodavid' King",
10 | "license": "ISC",
11 | "keywords": [
12 | "Puppeteer",
13 | "Chrome",
14 | "Headless",
15 | "Scrapping",
16 | "Node",
17 | "Distributed",
18 | "Proxy",
19 | "Proxies",
20 | "Throttle",
21 | "Throttling"
22 | ],
23 | "dependencies": {
24 | "bluebird": "^3.5.1",
25 | "dateformat": "^3.0.2",
26 | "fuzzy": "^0.1.3",
27 | "glob": "^7.1.2",
28 | "inquirer": "^3.3.0",
29 | "inquirer-autocomplete-prompt": "^0.11.1",
30 | "jquery": "^3.2.1",
31 | "keypress": "^0.2.1",
32 | "md5": "^2.2.1",
33 | "moment": "^2.19.2",
34 | "mongodb": "^2.2.33",
35 | "nano": "^6.4.2",
36 | "node-couchdb": "^1.2.0",
37 | "promise-mysql": "^3.1.3",
38 | "puppeteer": "^0.11.0",
39 | "sanitize-filename": "^1.6.1",
40 | "terminal-kit": "^1.14.1",
41 | "yargs": "^10.0.3"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------