├── web_server ├── client │ ├── src │ │ ├── NewsPanel │ │ │ ├── NewsPanel.css │ │ │ └── NewsPanel.js │ │ ├── index.css │ │ ├── Login │ │ │ ├── LoginForm.css │ │ │ ├── LoginForm.js │ │ │ └── LoginPage.js │ │ ├── Signup │ │ │ ├── SignUpForm.css │ │ │ ├── SignUpForm.js │ │ │ └── SignUpPage.js │ │ ├── App │ │ │ ├── App.css │ │ │ ├── App.test.js │ │ │ └── App.js │ │ ├── index.js │ │ ├── Auth │ │ │ └── Auth.js │ │ ├── NewsCard │ │ │ ├── NewsCard.css │ │ │ └── NewsCard.js │ │ ├── routes.js │ │ └── Base │ │ │ └── Base.js │ ├── build │ │ ├── favicon.ico │ │ ├── static │ │ │ └── media │ │ │ │ ├── Roboto-Bold.eed9aab5.woff │ │ │ │ ├── Roboto-Thin.44b78f14.woff │ │ │ │ ├── Roboto-Bold.c0f1e4a4.woff2 │ │ │ │ ├── Roboto-Light.3c37aa69.woff2 │ │ │ │ ├── Roboto-Light.ea36cd9a.woff │ │ │ │ ├── Roboto-Medium.1561b424.woff2 │ │ │ │ ├── Roboto-Medium.cf4d60bc.woff │ │ │ │ ├── Roboto-Regular.3cf6adf6.woff │ │ │ │ ├── Roboto-Thin.1f35e6a1.woff2 │ │ │ │ └── Roboto-Regular.5136cbe6.woff2 │ │ ├── manifest.json │ │ ├── index.html │ │ ├── asset-manifest.json │ │ └── service-worker.js │ ├── public │ │ ├── favicon.ico │ │ ├── manifest.json │ │ └── index.html │ └── package.json └── server │ ├── views │ ├── index.jade │ ├── error.jade │ └── layout.jade │ ├── config │ └── config.json │ ├── public │ └── stylesheets │ │ └── style.css │ ├── rpc_client │ ├── rpc_client_test.js │ └── rpc_client.js │ ├── routes │ ├── index.js │ ├── news.js │ └── auth.js │ ├── models │ ├── main.js │ └── user.js │ ├── package.json │ ├── passport │ ├── signup_passport.js │ └── login_passport.js │ ├── middleware │ └── auth_checker.js │ ├── app.js │ ├── bin │ └── www │ └── package-lock.json ├── common ├── mongodb_client.pyc ├── cloudAMQP_client.pyc ├── news_api_client.pyc ├── news_topic_modeling_service_client.pyc ├── news_topic_modeling_service_client.py ├── mongodb_client.py ├── news_recommendation_service_client.py ├── news_api_client_test.py ├── news_topic_modeling_service_client_test.py ├── mongodb_client_test.py ├── cloudAMQP_client_test.py ├── news_api_client.py └── cloudAMQP_client.py ├── tap-news-architecture.png ├── backend_server ├── operations.pyc ├── service.py ├── operations_test.py └── operations.py ├── requirements.txt ├── news_pipeline ├── scrapers │ ├── cnn_news_scraper.pyc │ ├── cnn_news_scraper_test.py │ ├── cnn_news_scraper.py │ └── user_agents.txt ├── news_fetcher.py ├── news_monitor.py └── news_deduper.py ├── .gitignore ├── news_pipeline_launcher.sh ├── news_recommendation_service ├── news_classes.py ├── recommendation_service.py └── click_log_processor.py ├── news_topic_modeling_service ├── server │ ├── news_classes.py │ └── server.py ├── backfill.py └── trainer │ ├── news_class_trainer.py │ └── news_cnn_model.py └── README.md /web_server/client/src/NewsPanel/NewsPanel.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /common/mongodb_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/common/mongodb_client.pyc -------------------------------------------------------------------------------- /tap-news-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/tap-news-architecture.png -------------------------------------------------------------------------------- /common/cloudAMQP_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/common/cloudAMQP_client.pyc -------------------------------------------------------------------------------- /common/news_api_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/common/news_api_client.pyc -------------------------------------------------------------------------------- /backend_server/operations.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/backend_server/operations.pyc -------------------------------------------------------------------------------- /web_server/client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /web_server/client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/favicon.ico -------------------------------------------------------------------------------- /web_server/server/views/index.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= title 5 | p Welcome to #{title} 6 | -------------------------------------------------------------------------------- /web_server/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/public/favicon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dateutils 2 | lxml 3 | newspaper 4 | python-jsonrpc 5 | pymongo 6 | pika 7 | requests 8 | redis 9 | sklearn 10 | -------------------------------------------------------------------------------- /common/news_topic_modeling_service_client.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/common/news_topic_modeling_service_client.pyc -------------------------------------------------------------------------------- /news_pipeline/scrapers/cnn_news_scraper.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/news_pipeline/scrapers/cnn_news_scraper.pyc -------------------------------------------------------------------------------- /web_server/server/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Bold.eed9aab5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Bold.eed9aab5.woff -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Thin.44b78f14.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Thin.44b78f14.woff -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Bold.c0f1e4a4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Bold.c0f1e4a4.woff2 -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Light.3c37aa69.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Light.3c37aa69.woff2 -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Light.ea36cd9a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Light.ea36cd9a.woff -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Medium.1561b424.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Medium.1561b424.woff2 -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Medium.cf4d60bc.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Medium.cf4d60bc.woff -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Regular.3cf6adf6.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Regular.3cf6adf6.woff -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Thin.1f35e6a1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Thin.1f35e6a1.woff2 -------------------------------------------------------------------------------- /web_server/client/build/static/media/Roboto-Regular.5136cbe6.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ztqsteve/Tap-News/HEAD/web_server/client/build/static/media/Roboto-Regular.5136cbe6.woff2 -------------------------------------------------------------------------------- /web_server/server/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mongoDbUri": "mongodb://ztqsteve:Ztq50344630@ds141932.mlab.com:41932/tap-news-users", 3 | "jwtSecret": "a secret phrase!!" 4 | } 5 | -------------------------------------------------------------------------------- /web_server/server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /web_server/server/public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } 9 | -------------------------------------------------------------------------------- /web_server/client/src/Login/LoginForm.css: -------------------------------------------------------------------------------- 1 | .login-panel { 2 | margin: auto; 3 | width: 40%; 4 | } 5 | 6 | .error-message { 7 | padding-left: 20px; 8 | color: red; 9 | } 10 | -------------------------------------------------------------------------------- /web_server/client/src/Signup/SignUpForm.css: -------------------------------------------------------------------------------- 1 | .signup-panel { 2 | margin: auto; 3 | width: 40%; 4 | } 5 | 6 | .error-message { 7 | padding-left: 20px; 8 | color: red; 9 | } 10 | -------------------------------------------------------------------------------- /web_server/client/src/App/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | display: block; 7 | margin-left: auto; 8 | margin-right: auto; 9 | padding-top: 30px; 10 | width: 20%; 11 | } 12 | -------------------------------------------------------------------------------- /common/news_topic_modeling_service_client.py: -------------------------------------------------------------------------------- 1 | import pyjsonrpc 2 | 3 | URL = "http://localhost:6060" 4 | 5 | client = pyjsonrpc.HttpClient(url=URL) 6 | 7 | def classify(text): 8 | topic = client.classify(text) 9 | print("Topic: %s" % str(topic)) 10 | return topic 11 | -------------------------------------------------------------------------------- /common/mongodb_client.py: -------------------------------------------------------------------------------- 1 | from pymongo import MongoClient 2 | 3 | MONGO_DB_HOST = 'localhost' 4 | MONGO_DB_PORT = 27017 5 | DB_NAME = 'tap-news' 6 | 7 | client = MongoClient("%s:%d" % (MONGO_DB_HOST, MONGO_DB_PORT)) 8 | 9 | def get_db(db=DB_NAME): 10 | db = client[db] 11 | return db 12 | -------------------------------------------------------------------------------- /web_server/server/rpc_client/rpc_client_test.js: -------------------------------------------------------------------------------- 1 | var client = require('./rpc_client'); 2 | 3 | // invoke "getNewsSummariesForUser" 4 | client.getNewsSummariesForUser('test_user', 1, function(response) { 5 | console.assert(response != null); 6 | }) 7 | 8 | client.logNewsClickForUser('test_user', 'test_news'); 9 | -------------------------------------------------------------------------------- /web_server/client/src/App/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /web_server/client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import { browserHistory, Router } from 'react-router'; 5 | import routes from './routes'; 6 | 7 | ReactDOM.render(, 8 | document.getElementById('root')); 9 | -------------------------------------------------------------------------------- /common/news_recommendation_service_client.py: -------------------------------------------------------------------------------- 1 | import pyjsonrpc 2 | 3 | URL = 'http://localhost:5050/' 4 | 5 | client = pyjsonrpc.HttpClient(url=URL) 6 | 7 | def getPreferenceForUser(uerId): 8 | preference = client.call('getPreferenceForUser', userId) 9 | print 'Preference list: %s' % str(preference) 10 | return preference 11 | -------------------------------------------------------------------------------- /web_server/server/routes/index.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var path = require('path'); 4 | 5 | /* GET home page. */ 6 | router.get('/', function(req, res, next) { 7 | res.sendFile('index.html', { root: path.join(__dirname, '../../client/build/') }); 8 | }); 9 | 10 | module.exports = router; 11 | -------------------------------------------------------------------------------- /common/news_api_client_test.py: -------------------------------------------------------------------------------- 1 | import news_api_client as client 2 | 3 | def test_basic(): 4 | news = client.getNewsFromSource() 5 | print news 6 | assert len(news) > 0 7 | news = client.getNewsFromSource('bbc-news') 8 | assert len(news) > 0 9 | print 'test_basic passed!' 10 | 11 | if __name__ == '__main__': 12 | test_basic() 13 | -------------------------------------------------------------------------------- /common/news_topic_modeling_service_client_test.py: -------------------------------------------------------------------------------- 1 | import news_topic_modeling_service_client as client 2 | 3 | def test_basic(): 4 | newsTitle = "Pentagon might propose ground troops for Syria" 5 | topic = client.classify(newsTitle) 6 | assert topic == "Politics & Government" 7 | print('test_basic passed!') 8 | 9 | if __name__ == "__main__": 10 | test_basic() 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules/ 5 | build/ 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /news_pipeline_launcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | brew services start redis 3 | brew services start mongodb 4 | 5 | pip2 install -r requirements.txt 6 | 7 | cd news_pipeline 8 | python news_monitor.py & 9 | python news_fetcher.py & 10 | python news_deduper.py & 11 | 12 | echo "========================================================" 13 | read -p "PRESS [ENTER] TO TERMINATE PROCESSES." PRESSKEY 14 | 15 | kill $(jobs -p) 16 | -------------------------------------------------------------------------------- /web_server/client/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /web_server/client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /common/mongodb_client_test.py: -------------------------------------------------------------------------------- 1 | import mongodb_client as client 2 | 3 | def test_basic(): 4 | db = client.get_db('test') 5 | db.demo.drop() 6 | assert db.demo.count() == 0 7 | db.demo.insert({'test': '123'}) 8 | assert db.demo.count() == 1 9 | db.demo.drop() 10 | assert db.demo.count() == 0 11 | print('test_basic passed!') 12 | 13 | if __name__ == '__main__': 14 | test_basic() 15 | -------------------------------------------------------------------------------- /web_server/server/models/main.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | mongoose.set('useCreateIndex', true); 4 | module.exports.connect = (uri) => { 5 | mongoose.connect(uri, { useNewUrlParser: true }); 6 | 7 | mongoose.connection.on('error', (err) => { 8 | console.error(`Mongoose connection error: ${err}`); 9 | process.exit(1); 10 | }); 11 | 12 | // load modules. 13 | require('./user'); 14 | } 15 | -------------------------------------------------------------------------------- /news_recommendation_service/news_classes.py: -------------------------------------------------------------------------------- 1 | classes = [ 2 | 'Colleges & Schools', 3 | 'Environmental', 4 | 'World', 5 | 'Entertainment', 6 | 'Media', 7 | 'Politics & Government', 8 | 'Regional News', 9 | 'Religion', 10 | 'Sports', 11 | 'Technology', 12 | 'Traffic', 13 | 'Weather', 14 | 'Economic & Corp', 15 | 'Advertisements', 16 | 'Crime', 17 | 'Other', 18 | 'Magazine' 19 | ] 20 | -------------------------------------------------------------------------------- /news_pipeline/scrapers/cnn_news_scraper_test.py: -------------------------------------------------------------------------------- 1 | import cnn_news_scraper as scraper 2 | 3 | EXPECTED_NEWS = "The United States is not some banana republic with a two-tiered system of justice" 4 | CNN_NEWS_URL = "https://www.cnn.com/2018/09/04/politics/democrats-senators-donald-trump-jeff-sessions-tweet/index.html" 5 | 6 | def test_basic(): 7 | news = scraper.extract_news(CNN_NEWS_URL) 8 | 9 | assert EXPECTED_NEWS in news 10 | print('test_basic passed!') 11 | 12 | if __name__ == "__main__": 13 | test_basic() 14 | -------------------------------------------------------------------------------- /web_server/client/src/App/App.js: -------------------------------------------------------------------------------- 1 | import 'materialize-css/dist/css/materialize.min.css'; 2 | import 'materialize-css/dist/js/materialize.min.js'; 3 | 4 | import React, { Component } from 'react'; 5 | import './App.css'; 6 | 7 | import NewsPanel from '../NewsPanel/NewsPanel' 8 | class App extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 | 14 |
15 |
16 | ); 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /web_server/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tap-news", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "lodash": "^4.17.10", 7 | "materialize-css": "^0.100.2", 8 | "react": "^16.4.2", 9 | "react-dom": "^16.4.2", 10 | "react-router": "^3.2.1", 11 | "react-scripts": "1.1.5" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build", 16 | "test": "react-scripts test --env=jsdom", 17 | "eject": "react-scripts eject" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /news_topic_modeling_service/server/news_classes.py: -------------------------------------------------------------------------------- 1 | news_classes = { 2 | '1': 'Colleges & Schools', 3 | '2': 'Environmental', 4 | '3': 'World', 5 | '4': 'Entertainment', 6 | '5': 'Media', 7 | '6': 'Politics & Government', 8 | '7': 'Regional News', 9 | '8': 'Religion', 10 | '9': 'Sports', 11 | '10': 'Technology', 12 | '11': 'Traffic', 13 | '12': 'Weather', 14 | '13': 'Economic & Corp', 15 | '14': 'Advertisements', 16 | '15': 'Crime', 17 | '16': 'Other', 18 | '17': 'Magazine' 19 | } 20 | -------------------------------------------------------------------------------- /web_server/client/build/index.html: -------------------------------------------------------------------------------- 1 | React App
-------------------------------------------------------------------------------- /common/cloudAMQP_client_test.py: -------------------------------------------------------------------------------- 1 | from cloudAMQP_client import CloudAMQPClient 2 | 3 | CLOUDAMQP_URL = "amqp://ukgnvsto:6uetcub2OXNRdK-aepRsMD35Rec-wGCy@chimpanzee.rmq.cloudamqp.com/ukgnvsto" 4 | 5 | TEST_QUEUE_NAME = 'test' 6 | 7 | def test_basic(): 8 | client = CloudAMQPClient(CLOUDAMQP_URL, TEST_QUEUE_NAME) 9 | sentMsg = {'test': 'demo'} 10 | client.sendMessage(sentMsg) 11 | client.sleep(10) 12 | 13 | receivedMsg = client.getMessage() 14 | assert sentMsg == receivedMsg 15 | print 'test_basic passed!' 16 | 17 | if __name__ == '__main__': 18 | test_basic() 19 | -------------------------------------------------------------------------------- /common/news_api_client.py: -------------------------------------------------------------------------------- 1 | from newsapi import NewsApiClient 2 | 3 | NEWS_API_KEY = 'd161fdcfcaed4789989a8e3ead3a5077' 4 | 5 | CNN = 'cnn' 6 | DEFAULT_SOURCES = CNN 7 | 8 | def getNewsFromSource(source=DEFAULT_SOURCES): 9 | articles = [] 10 | # Init 11 | newsapi = NewsApiClient(api_key=NEWS_API_KEY) 12 | 13 | # /v2/top-headlines 14 | top_headlines = newsapi.get_top_headlines(sources=source) 15 | 16 | # Extract info from response 17 | if top_headlines is not None and top_headlines['status'] == 'ok': 18 | articles.extend(top_headlines['articles']) 19 | 20 | return articles 21 | -------------------------------------------------------------------------------- /web_server/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "nodemon ./bin/www" 7 | }, 8 | "dependencies": { 9 | "bcrypt": "^3.0.0", 10 | "body-parser": "^1.18.3", 11 | "cookie-parser": "~1.4.3", 12 | "cors": "^2.8.4", 13 | "debug": "~2.6.9", 14 | "express": "~4.16.0", 15 | "http-errors": "~1.6.2", 16 | "jade": "~1.11.0", 17 | "jayson": "^2.0.6", 18 | "jsonwebtoken": "^8.3.0", 19 | "mongoose": "^5.2.12", 20 | "morgan": "~1.9.0", 21 | "passport": "^0.4.0", 22 | "passport-local": "^1.0.0", 23 | "validator": "^10.7.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /web_server/client/src/Auth/Auth.js: -------------------------------------------------------------------------------- 1 | class Auth { 2 | static authenticateUser(token, email) { 3 | localStorage.setItem('token', token); 4 | localStorage.setItem('email', email); 5 | } 6 | 7 | static isUserAuthenticated() { 8 | return localStorage.getItem('token') !== null; 9 | } 10 | 11 | static deauthenticateUser() { 12 | localStorage.removeItem('token'); 13 | localStorage.removeItem('email'); 14 | } 15 | 16 | static getToken() { 17 | return localStorage.getItem('token'); 18 | } 19 | 20 | static getEmail() { 21 | return localStorage.getItem('email'); 22 | } 23 | } 24 | 25 | export default Auth; 26 | -------------------------------------------------------------------------------- /web_server/client/src/NewsCard/NewsCard.css: -------------------------------------------------------------------------------- 1 | .news-intro-col { 2 | display: inline-flex; 3 | color: black; 4 | height: 100%; 5 | } 6 | 7 | .news-intro-panel { 8 | margin: auto 5px; 9 | text-align: left; 10 | } 11 | 12 | .news-description { 13 | text-align: left; 14 | } 15 | 16 | .news-description p { 17 | font-size: 18px; 18 | } 19 | 20 | .news-chip { 21 | font-size: 18px; 22 | } 23 | 24 | .fill { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | overflow: hidden; 29 | } 30 | 31 | .fill img { 32 | padding-left: 20px; 33 | padding-right: 20px; 34 | flex-shrink: 0; 35 | min-width: 100%; 36 | max-height: 250px; 37 | object-fit: cover; 38 | } 39 | -------------------------------------------------------------------------------- /web_server/server/passport/signup_passport.js: -------------------------------------------------------------------------------- 1 | const User = require('mongoose').model('User'); 2 | const PassportLocalStrategy = require('passport-local').Strategy; 3 | 4 | module.exports = new PassportLocalStrategy( 5 | { 6 | usernameField: 'email', 7 | passwordField: 'password', 8 | passReqToCallback: true 9 | }, 10 | (req, email, password, done) => { 11 | const userData = { 12 | email: email.trim(), 13 | password: password 14 | }; 15 | 16 | const newUser = new User(userData); 17 | 18 | newUser.save(err => { 19 | console.log('Save new user!'); 20 | 21 | if (err) { 22 | return done(err); 23 | } 24 | 25 | return done(null); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /web_server/server/rpc_client/rpc_client.js: -------------------------------------------------------------------------------- 1 | var jayson = require('jayson'); 2 | 3 | var client = jayson.client.http({ 4 | port: 4040, 5 | hostname: 'localhost' 6 | }); 7 | 8 | function getNewsSummariesForUser(user_id, page_num, callback) { 9 | client.request('getNewsSummariesForUser',[user_id, page_num], (err, error, response) => { 10 | if (err) throw err; 11 | console.log(response); 12 | callback(response); 13 | }); 14 | } 15 | 16 | function logNewsClickForUser(user_id, news_id) { 17 | client.request('logNewsClickForUser', [user_id, news_id], (err,error,response) => { 18 | if (err) throw err; 19 | console.log(response); 20 | }); 21 | } 22 | 23 | module.exports = { 24 | getNewsSummariesForUser, 25 | logNewsClickForUser 26 | } 27 | -------------------------------------------------------------------------------- /backend_server/service.py: -------------------------------------------------------------------------------- 1 | import pyjsonrpc 2 | import operations 3 | 4 | SERVER_HOST = 'localhost' 5 | SERVER_PORT = 4040 6 | 7 | class RequestHandler(pyjsonrpc.HttpRequestHandler): 8 | 9 | @pyjsonrpc.rpcmethod 10 | def getNewsSummariesForUser(self, user_id, page_num): 11 | return operations.getNewsSummariesForUser(user_id, page_num) 12 | 13 | @pyjsonrpc.rpcmethod 14 | def logNewsClickForUser(self, user_id, news_id): 15 | return operations.logNewsClickForUser(user_id, news_id) 16 | 17 | 18 | http_server = pyjsonrpc.ThreadingHttpServer( 19 | server_address = (SERVER_HOST, SERVER_PORT), 20 | RequestHandlerClass = RequestHandler 21 | ) 22 | 23 | print('Starting HTTP server on {}:{}'.format(SERVER_HOST, SERVER_PORT)) 24 | http_server.serve_forever() 25 | -------------------------------------------------------------------------------- /news_topic_modeling_service/backfill.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # import common package in parent directory 5 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 6 | 7 | import mongodb_client 8 | import news_topic_modeling_service_client 9 | 10 | if __name__ == '__main__': 11 | db = mongodb_client.get_db() 12 | cursor = db['news'].find({}) 13 | count = 0 14 | for news in cursor: 15 | count += 1 16 | print(count) 17 | 18 | if 'class' not in news: 19 | print('Populating classes...') 20 | title = news['title'] 21 | topic = news_topic_modeling_service_client.classify(title) 22 | news['class'] = topic 23 | db['news'].replace_one({'digest': news['digest']}, news, upsert=True) 24 | -------------------------------------------------------------------------------- /web_server/server/routes/news.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var router = express.Router(); 3 | var rpc_client = require('../rpc_client/rpc_client') 4 | 5 | /* GET news listing. */ 6 | router.get('/userId/:userId/pageNum/:pageNum', function(req, res, next) { 7 | console.log('Fetching news...'); 8 | user_id = req.params['userId']; 9 | page_num = req.params['pageNum']; 10 | 11 | rpc_client.getNewsSummariesForUser(user_id, page_num, response => { 12 | res.json(response); 13 | }); 14 | }); 15 | 16 | /* Post news click event */ 17 | router.post('/userId/:userId/newsId/:newsId', (req, res, next) => { 18 | console.log('Logging news click...'); 19 | user_id = req.params['userId']; 20 | news_id = req.params['newsid']; 21 | 22 | rpc_client.logNewsClickForUser(user_id, news_id); 23 | res.status(200); 24 | }); 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /web_server/server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | const UserSchema = new mongoose.Schema({ 5 | email: { 6 | type: String, 7 | index: { unique: true } 8 | }, 9 | password: String, 10 | }); 11 | 12 | UserSchema.methods.comparePassword = function comparePassword(password, callback) { 13 | bcrypt.compare(password, this.password, callback); 14 | }; 15 | 16 | UserSchema.pre('save', function saveHook(next) { 17 | const user = this; 18 | 19 | return bcrypt.genSalt((saltError, salt) => { 20 | if (saltError) { 21 | return next(saltError); 22 | } 23 | 24 | return bcrypt.hash(user.password, salt, (hashError, hash) => { 25 | if (hashError) { 26 | return next(hashError); 27 | } 28 | 29 | user.password = hash; 30 | 31 | return next(); 32 | }); 33 | }); 34 | }); 35 | 36 | module.exports = mongoose.model('User', UserSchema); 37 | -------------------------------------------------------------------------------- /web_server/client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "main.css": "static/css/main.8e91f7e9.css", 3 | "main.css.map": "static/css/main.8e91f7e9.css.map", 4 | "main.js": "static/js/main.be2dd674.js", 5 | "main.js.map": "static/js/main.be2dd674.js.map", 6 | "static/media/Roboto-Bold.woff": "static/media/Roboto-Bold.eed9aab5.woff", 7 | "static/media/Roboto-Bold.woff2": "static/media/Roboto-Bold.c0f1e4a4.woff2", 8 | "static/media/Roboto-Light.woff": "static/media/Roboto-Light.ea36cd9a.woff", 9 | "static/media/Roboto-Light.woff2": "static/media/Roboto-Light.3c37aa69.woff2", 10 | "static/media/Roboto-Medium.woff": "static/media/Roboto-Medium.cf4d60bc.woff", 11 | "static/media/Roboto-Medium.woff2": "static/media/Roboto-Medium.1561b424.woff2", 12 | "static/media/Roboto-Regular.woff": "static/media/Roboto-Regular.3cf6adf6.woff", 13 | "static/media/Roboto-Regular.woff2": "static/media/Roboto-Regular.5136cbe6.woff2", 14 | "static/media/Roboto-Thin.woff": "static/media/Roboto-Thin.44b78f14.woff", 15 | "static/media/Roboto-Thin.woff2": "static/media/Roboto-Thin.1f35e6a1.woff2" 16 | } -------------------------------------------------------------------------------- /web_server/client/src/routes.js: -------------------------------------------------------------------------------- 1 | import Base from './Base/Base'; 2 | import App from './App/App'; 3 | import LoginPage from './Login/LoginPage'; 4 | import SignUpPage from './Signup/SignUpPage'; 5 | import Auth from './Auth/Auth'; 6 | 7 | 8 | const routes = { 9 | // base component (wrapper for the whole application). 10 | component: Base, 11 | childRoutes: [ 12 | 13 | { 14 | path: '/', 15 | getComponent: (location, callback) => { 16 | if (Auth.isUserAuthenticated()) { 17 | callback(null, App); 18 | } else { 19 | callback(null, LoginPage); 20 | } 21 | } 22 | }, 23 | 24 | { 25 | path: '/login', 26 | component: LoginPage 27 | }, 28 | 29 | { 30 | path: '/signup', 31 | component: SignUpPage 32 | }, 33 | 34 | { 35 | path: '/logout', 36 | onEnter: (nextState, replace) => { 37 | Auth.deauthenticateUser(); 38 | 39 | // change the current URL to / 40 | replace('/'); 41 | } 42 | } 43 | ] 44 | }; 45 | 46 | export default routes; 47 | -------------------------------------------------------------------------------- /web_server/server/middleware/auth_checker.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('mongoose').model('User'); 3 | const config = require('../config/config.json'); 4 | 5 | module.exports = (req, res, next) => { 6 | console.log('auth_checker: req: ' + req.headers); 7 | 8 | if (!req.headers.authorization) { 9 | return res.status(401).end(); 10 | } 11 | 12 | // get the last part from a authorization header string like "bearer token-value" 13 | const token = req.headers.authorization.split(' ')[1]; 14 | 15 | console.log('auth_checker: token: ' + token); 16 | 17 | // decode the token using a secret key-phrase 18 | return jwt.verify(token, config.jwtSecret, (err, decoded) => { 19 | // the 401 code is for unauthorized status 20 | if (err) { 21 | return res.status(401).end(); 22 | } 23 | 24 | const id = decoded.sub; 25 | 26 | // check if a user exists 27 | return User.findById(id, (userErr, user) => { 28 | if (userErr || !user) { 29 | return res.status(401).end(); 30 | } 31 | 32 | return next(); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /web_server/client/src/Base/Base.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import Auth from '../Auth/Auth'; 4 | 5 | class Base extends React.Component { 6 | render(){ 7 | return( 8 |
9 | 27 |
28 | {this.props.children} 29 |
30 | ) 31 | } 32 | 33 | } 34 | 35 | 36 | export default Base; 37 | -------------------------------------------------------------------------------- /common/cloudAMQP_client.py: -------------------------------------------------------------------------------- 1 | import pika 2 | import json 3 | 4 | class CloudAMQPClient: 5 | def __init__(self, cloud_amqp_url, queue_name): 6 | self.cloud_amqp_url = cloud_amqp_url 7 | self.queue_name = queue_name 8 | self.params = pika.URLParameters(cloud_amqp_url) 9 | self.params.socket_timeout = 3 10 | self.connection = pika.BlockingConnection(self.params) 11 | self.channel = self.connection.channel() 12 | self.channel.queue_declare(queue=queue_name) 13 | # send a message 14 | def sendMessage(self, message): 15 | self.channel.basic_publish(exchange='', 16 | routing_key=self.queue_name, 17 | body=json.dumps(message)) 18 | print "[X] Sent message to %s: %s" % (self.queue_name, message) 19 | return 20 | 21 | # get a message 22 | def getMessage(self): 23 | method_frame, header_frame, body = self.channel.basic_get(self.queue_name) 24 | if method_frame: 25 | print "[O] Received message from %s: %s" % (self.queue_name, body) 26 | self.channel.basic_ack(method_frame.delivery_tag) 27 | return json.loads(body) 28 | else: 29 | print "No message returned" 30 | return None 31 | 32 | # sleep 33 | def sleep(self, seconds): 34 | self.connection.sleep(seconds) 35 | -------------------------------------------------------------------------------- /news_pipeline/scrapers/cnn_news_scraper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import random 3 | import os 4 | 5 | from lxml import html 6 | 7 | GET_CNN_NEWS_XPATH = '''//div[@class='zn-body__paragraph']//text() | 8 | //div[@class='zn-body__paragraph speakable']//text() | 9 | //p[@class='zn-body__paragraph speakable']//text() | 10 | //p[@class='zn-body__paragraph']//text()''' 11 | 12 | # Load user agents 13 | USER_AGENTS_FILE = os.path.dirname(os.path.abspath(__file__)) + '/user_agents.txt' 14 | USER_AGENTS = [] 15 | 16 | 17 | with open(USER_AGENTS_FILE, 'r') as uaf: 18 | for ua in uaf.readlines(): 19 | if ua: 20 | USER_AGENTS.append(ua.strip()[1:-1]) 21 | 22 | random.shuffle(USER_AGENTS) 23 | 24 | def getHeaders(): 25 | ua = random.choice(USER_AGENTS) 26 | headers = { 27 | 'Connection': 'close', 28 | 'User-Agent': ua 29 | } 30 | return headers 31 | 32 | def extract_news(news_url): 33 | # Fetch .html 34 | session_requests = requests.session() 35 | response = session_requests.get(news_url, headers=getHeaders()) 36 | 37 | news = {} 38 | 39 | try: 40 | # Parse html 41 | tree = html.fromstring(response.content) 42 | # Extract information 43 | 44 | news = tree.xpath(GET_CNN_NEWS_XPATH) 45 | news = ''.join(news) 46 | except Exception as e: 47 | print e 48 | return {} 49 | 50 | return news 51 | -------------------------------------------------------------------------------- /web_server/server/app.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var cors = require('cors'); 3 | var createError = require('http-errors'); 4 | var express = require('express'); 5 | var path = require('path'); 6 | var passport = require('passport'); 7 | 8 | var auth = require('./routes/auth'); 9 | var index = require('./routes/index'); 10 | var news = require('./routes/news'); 11 | 12 | var app = express(); 13 | 14 | // view engine setup 15 | app.set('views', path.join(__dirname, '../client/build/')); 16 | app.set('view engine', 'jade'); 17 | app.use('/static', express.static(path.join(__dirname, '../client/build/static/'))); 18 | 19 | app.use(bodyParser.json()); 20 | //TODO: remove this after development is done 21 | app.use(cors()); 22 | 23 | 24 | var config = require('./config/config.json'); 25 | require('./models/main.js').connect(config.mongoDbUri); 26 | 27 | app.use(passport.initialize()); 28 | var localSignupStrategy = require('./passport/signup_passport'); 29 | var localLoginStrategy = require('./passport/login_passport'); 30 | passport.use('local-signup', localSignupStrategy); 31 | passport.use('local-login', localLoginStrategy); 32 | 33 | const authCheckMiddleWare = require('./middleware/auth_checker'); 34 | app.use('/news', authCheckMiddleWare); 35 | 36 | app.use('/', index); 37 | app.use('/auth', auth); 38 | app.use('/news', news); 39 | 40 | // catch 404 and forward to error handler 41 | app.use(function(req, res) { 42 | var err = createError(404) 43 | res.render('404 Not Found'); 44 | }); 45 | 46 | 47 | module.exports = app; 48 | -------------------------------------------------------------------------------- /news_recommendation_service/recommendation_service.py: -------------------------------------------------------------------------------- 1 | import operator 2 | import os 3 | import sys 4 | import pyjsonrpc 5 | 6 | # import common package in parent directory 7 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 8 | 9 | import mongodb_client 10 | 11 | PREFERENCE_MODEL_TABLE_NAME = "user_preference_model" 12 | 13 | SERVER_HOST = 'localhost' 14 | SERVER_PORT = 5050 15 | 16 | def isclose(a, b, rel_tol=1e-09, abs_tol=0.0): 17 | return abs(a-b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol) 18 | 19 | class RequestHandler(pyjsonrpc.HttpRequestHandler): 20 | @pyjsonrpc.rpcmethod 21 | def getPreferenceForUser(self, user_id): 22 | """ Get user's pereference in an ordered class list """ 23 | db = mongodb_client.get_db() 24 | model = db[PREFERENCE_MODEL_TABLE_NAME].find_one({'userId':user_id}) 25 | 26 | if model is None: 27 | return [] 28 | 29 | 30 | sorted_tuples = sorted(list(model['preference'].items()), key=operator.itemgetter(1), reverse=True) 31 | sorted_list = [x[0] for x in sorted_tuples] 32 | sorted_value_list = [x[1] for x in sorted_tuples] 33 | 34 | if isclose(float(sorted_value_list[0]), float(sorted_value_list[-1])): 35 | return [] 36 | 37 | return sorted_list 38 | 39 | 40 | # Threading HTTP Server 41 | http_server = pyjsonrpc.ThreadingHttpServer( 42 | server_address=(SERVER_HOST, SERVER_PORT), 43 | RequestHandlerClass = RequestHandler 44 | ) 45 | 46 | print("Starting HTTP server on %s:%d" % (SERVER_HOST, SERVER_PORT)) 47 | 48 | http_server.serve_forever() 49 | -------------------------------------------------------------------------------- /web_server/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /news_pipeline/news_fetcher.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from newspaper import Article 7 | 8 | # import common package in parent directory 9 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 10 | sys.path.append(os.path.join(os.path.dirname(__file__), 'scrapers')) 11 | 12 | from cloudAMQP_client import CloudAMQPClient 13 | import cnn_news_scraper 14 | 15 | DEDUPE_NEWS_TASK_QUEUE_URL = "amqp://xzbggvzn:jFOMw3MZ6hzq0htegQTkG2S7fQsrNby1@chimpanzee.rmq.cloudamqp.com/xzbggvzn" 16 | DEDUPE_NEWS_TASK_QUEUE_NAME = "tap-news-dedupe-news-task-queue" 17 | SCRAPE_NEWS_TASK_QUEUE_URL = "amqp://hzileycx:iJgtzvRMYnzkdFGSdUYSytpAsW6FYnT_@chimpanzee.rmq.cloudamqp.com/hzileycx" 18 | SCRAPE_NEWS_TASK_QUEUE_NAME = "tap-news-scrape-news-task-queue" 19 | 20 | SLEEP_TIME_IN_SECONDS = 5 21 | 22 | dedupe_news_queue_client = CloudAMQPClient(DEDUPE_NEWS_TASK_QUEUE_URL, DEDUPE_NEWS_TASK_QUEUE_NAME) 23 | scrape_news_queue_client = CloudAMQPClient(SCRAPE_NEWS_TASK_QUEUE_URL, SCRAPE_NEWS_TASK_QUEUE_NAME) 24 | 25 | def handle_message(msg): 26 | if not isinstance(msg, dict): 27 | print 'message is broken' 28 | return 29 | 30 | task = msg 31 | 32 | article = Article(task['url']) 33 | article.download() 34 | article.parse() 35 | 36 | task['text'] = article.text 37 | 38 | dedupe_news_queue_client.sendMessage(task) 39 | 40 | while True: 41 | # Fetch msg from queue_name 42 | if scrape_news_queue_client is not None: 43 | msg = scrape_news_queue_client.getMessage() 44 | if msg is not None: 45 | # Handle message 46 | try: 47 | handle_message(msg) 48 | except Exception as e: 49 | print e 50 | pass 51 | scrape_news_queue_client.sleep(SLEEP_TIME_IN_SECONDS) 52 | -------------------------------------------------------------------------------- /web_server/client/src/NewsCard/NewsCard.js: -------------------------------------------------------------------------------- 1 | import './NewsCard.css'; 2 | 3 | import React from 'react'; 4 | import Auth from '../Auth/Auth'; 5 | 6 | class NewsCard extends React.Component{ 7 | redirectToUrl(url) { 8 | this.sendClickLog(); 9 | window.open(url, '_blank'); 10 | } 11 | 12 | sendClickLog() { 13 | let url = `http://localhost:3000/news/userId/${Auth.getEmail()}/newsId/${this.props.news.digest}`; 14 | 15 | let request = new Request(encodeURI(url), { 16 | method: 'POST', 17 | headers: { 18 | 'Authorization': 'bearer ' + Auth.getToken() 19 | }, 20 | cache: 'no-store' 21 | }); 22 | 23 | fetch(request); 24 | } 25 | 26 | render() { 27 | return( 28 |
this.redirectToUrl(this.props.news.url)}> 29 |
30 |
31 | news 32 |
33 |
34 |
35 |
36 |

{this.props.news.title}

37 |
38 |

{this.props.news.description}

39 |
40 | {this.props.news.source.id != null &&
{this.props.news.source.name}
} 41 | {this.props.news.reason != null &&
{this.props.news.reason}
} 42 | {this.props.news.time != null &&
{this.props.news.time}
} 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 | ) 52 | } 53 | } 54 | 55 | export default NewsCard; 56 | -------------------------------------------------------------------------------- /web_server/server/passport/login_passport.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const User = require('mongoose').model('User'); 3 | const PassportLocalStrategy = require('passport-local').Strategy; 4 | const config = require('../config/config.json'); 5 | 6 | module.exports = new PassportLocalStrategy( 7 | { 8 | usernameField: 'email', 9 | passwordField: 'password', 10 | session: false, 11 | passReqToCallback: true 12 | }, 13 | (req, email, password, done) => { 14 | const userData = { 15 | email: email.trim(), 16 | password: password 17 | }; 18 | 19 | // find a user by email address 20 | return User.findOne( 21 | { email: userData.email }, (err, user) => { 22 | if (err) { 23 | return done(err); 24 | } 25 | 26 | if (!user) { 27 | const error = new Error('Incorrect email or password'); 28 | error.name = 'IncorrectCredentialsError'; 29 | return done(error); 30 | } 31 | 32 | // check if a hashed user's password is equal to a value saved in the database 33 | return user.comparePassword(userData.password, (passwordErr, isMatch) => { 34 | if (passwordErr) { 35 | return done(passwordErr); 36 | } 37 | 38 | if (!isMatch) { 39 | const error = new Error('Incorrect email or password'); 40 | error.name = 'IncorrectCredentialsError'; 41 | return done(error); 42 | } 43 | 44 | const payload = { 45 | sub: user._id 46 | }; 47 | 48 | // create a token string 49 | const token = jwt.sign(payload, config.jwtSecret); 50 | //const data = {}; 51 | 52 | return done(null, token, null); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /web_server/client/src/Login/LoginForm.js: -------------------------------------------------------------------------------- 1 | import './LoginForm.css'; 2 | 3 | import { Link } from 'react-router' 4 | import PropTypes from 'prop-types'; 5 | import React from 'react'; 6 | 7 | const LoginForm = ({ 8 | onSubmit, 9 | onChange, 10 | errors, 11 | user, 12 | }) => ( 13 |
14 |
15 |
16 |

Login

17 | {errors.summary &&

{errors.summary}

} 18 |
19 |
20 | 21 | 22 |
23 |
24 | {errors.email &&

{errors.email}

} 25 |
26 |
27 | 28 | 29 |
30 |
31 | {errors.password &&

{errors.password}

} 32 |
33 | 34 |
35 |
36 |

New to Tap News? Sign Up

37 |
38 |
39 |
40 |
41 | ); 42 | 43 | LoginForm.propTypes = { 44 | onSubmit: PropTypes.func.isRequired, 45 | onChange: PropTypes.func.isRequired, 46 | errors: PropTypes.object.isRequired, 47 | user: PropTypes.object.isRequired 48 | }; 49 | 50 | export default LoginForm; 51 | -------------------------------------------------------------------------------- /web_server/server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | var debug = require('debug')('server:server'); 9 | var http = require('http'); 10 | 11 | /** 12 | * Get port from environment and store in Express. 13 | */ 14 | 15 | var port = normalizePort(process.env.PORT || '3000'); 16 | app.set('port', port); 17 | 18 | /** 19 | * Create HTTP server. 20 | */ 21 | 22 | var server = http.createServer(app); 23 | 24 | /** 25 | * Listen on provided port, on all network interfaces. 26 | */ 27 | 28 | server.listen(port); 29 | server.on('error', onError); 30 | server.on('listening', onListening); 31 | 32 | /** 33 | * Normalize a port into a number, string, or false. 34 | */ 35 | 36 | function normalizePort(val) { 37 | var port = parseInt(val, 10); 38 | 39 | if (isNaN(port)) { 40 | // named pipe 41 | return val; 42 | } 43 | 44 | if (port >= 0) { 45 | // port number 46 | return port; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /** 53 | * Event listener for HTTP server "error" event. 54 | */ 55 | 56 | function onError(error) { 57 | if (error.syscall !== 'listen') { 58 | throw error; 59 | } 60 | 61 | var bind = typeof port === 'string' 62 | ? 'Pipe ' + port 63 | : 'Port ' + port; 64 | 65 | // handle specific listen errors with friendly messages 66 | switch (error.code) { 67 | case 'EACCES': 68 | console.error(bind + ' requires elevated privileges'); 69 | process.exit(1); 70 | break; 71 | case 'EADDRINUSE': 72 | console.error(bind + ' is already in use'); 73 | process.exit(1); 74 | break; 75 | default: 76 | throw error; 77 | } 78 | } 79 | 80 | /** 81 | * Event listener for HTTP server "listening" event. 82 | */ 83 | 84 | function onListening() { 85 | var addr = server.address(); 86 | var bind = typeof addr === 'string' 87 | ? 'pipe ' + addr 88 | : 'port ' + addr.port; 89 | debug('Listening on ' + bind); 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tap News 2 | ## Overview 3 | This is a real-time news scraping and recommendation system. I built a news pipeline to scraping real-time latest news from a bunch of news source such as CNN, BBC, ESPN and TechCrunch. Created a single-page web application to show the news to users, and recommend news to users based on users' click behavior on different news topics. 4 | ## Architecture 5 | 6 | 7 | ### News Pipeline 8 | News pipeline is composed by news monitor, web scraper and news deduper, news is sent and received between them by RabbitMQ which decouples these components. The news monitor use [News API](https://newsapi.org) to derive latest news and store news title MD5 digest into Redis to avoid sending same news to the message queue. The web scraper use a third party package [Newspaper](https://newspaper.readthedocs.io/en/latest/) to fetch corresponding news articles from offical news website. News depuper implements TF-IDF to calculate similarity of news to avoid storing same news from different news source into MongoDB. For similar news, only store the one published firstly. 9 | 10 | ### News Topic Modeling 11 | The news topic classification is implemented by Convolutional Neutral Nework(CNN) in TensorFlow and deployed online using the offline trained model. Manually label the news with 17 classes: `Colleges & Schools`, `Environmental`, `World`, `Entertainment`, `Media`, `Politics & Government`, `Regional News`, `Religion`, `Sports`, `Technology`, `Traffic`, `Weather`, `Economic & Corp`, `Advertisements`, `Crime`, `Magazine`, `Other`. 12 | 13 | ### News Recommendation 14 | I built a click log processor to implement a time decay model. If a news topic is clicked, p = (1-α)p + α, if not, p = (1-α)p, Where p is the selection probability, and α is the degree of weight decrease. The result of this is that the nth most recent selection will have a weight of (1-α)^n. Using a coefficient value of 0.05 as an example, the 10th most recent selection would only have half the weight of the most recent. Increasing α would bias towards more recent results more. 15 | 16 | When some news' topic with the most probability for that user, the webpage will show a "Recommend" tag to the user. 17 | -------------------------------------------------------------------------------- /news_topic_modeling_service/trainer/news_class_trainer.py: -------------------------------------------------------------------------------- 1 | import news_cnn_model 2 | import numpy as np 3 | import os 4 | import pandas as pd 5 | import pickle 6 | import shutil 7 | import tensorflow as tf 8 | 9 | from sklearn import metrics 10 | 11 | learn = tf.contrib.learn 12 | 13 | REMOVE_PREVIOUS_MODEL = True 14 | 15 | MODEL_OUTPUT_DIR = '../model/' 16 | DATA_SET_FILE = '../data/labeled_news.csv' 17 | VARS_FILE = '../model/vars' 18 | VOCAB_PROCESSOR_SAVE_FILE = '../model/vocab_procesor_save_file' 19 | MAX_DOCUMENT_LENGTH = 100 20 | N_CLASSES = 8 21 | 22 | # Training parms 23 | STEPS = 200 24 | 25 | def main(unused_argv): 26 | if REMOVE_PREVIOUS_MODEL: 27 | # Remove old model 28 | print("Removing previous model...") 29 | shutil.rmtree(MODEL_OUTPUT_DIR) 30 | os.mkdir(MODEL_OUTPUT_DIR) 31 | 32 | # Prepare training and testing data 33 | df = pd.read_csv(DATA_SET_FILE, header=None) 34 | train_df = df[0:400] 35 | test_df = df.drop(train_df.index) 36 | 37 | # x - news title, y - class 38 | x_train = train_df[1] 39 | y_train = train_df[0] 40 | x_test = test_df[1] 41 | y_test = test_df[0] 42 | 43 | # Process vocabulary 44 | vocab_processor = learn.preprocessing.VocabularyProcessor(MAX_DOCUMENT_LENGTH) 45 | x_train = np.array(list(vocab_processor.fit_transform(x_train))) 46 | x_test = np.array(list(vocab_processor.transform(x_test))) 47 | 48 | n_words = len(vocab_processor.vocabulary_) 49 | print('Total words: %d' % n_words) 50 | 51 | # Saving n_words and vocab_processor: 52 | with open(VARS_FILE, 'wb') as f: # needs to be opened in binary mode. 53 | pickle.dump(n_words, f) 54 | 55 | vocab_processor.save(VOCAB_PROCESSOR_SAVE_FILE) 56 | 57 | # Build model 58 | classifier = learn.Estimator( 59 | model_fn=news_cnn_model.generate_cnn_model(N_CLASSES, n_words), 60 | model_dir=MODEL_OUTPUT_DIR) 61 | 62 | # Train and predict 63 | classifier.fit(x_train, y_train, steps=STEPS) 64 | 65 | # Evaluate model 66 | y_predicted = [ 67 | p['class'] for p in classifier.predict(x_test, as_iterable=True) 68 | ] 69 | 70 | score = metrics.accuracy_score(y_test, y_predicted) 71 | print('Accuracy: {0:f}'.format(score)) 72 | 73 | if __name__ == '__main__': 74 | tf.app.run(main=main) 75 | -------------------------------------------------------------------------------- /web_server/client/src/Signup/SignUpForm.js: -------------------------------------------------------------------------------- 1 | import './SignUpForm.css'; 2 | 3 | import { Link } from 'react-router' 4 | import PropTypes from 'prop-types'; 5 | import React from 'react'; 6 | 7 | const SignUpForm = ({ 8 | onSubmit, 9 | onChange, 10 | errors, 11 | user, 12 | }) => ( 13 |
14 |
15 |
16 |

Sign Up

17 | {errors.summary &&

{errors.summary}

} 18 |
19 |
20 | 21 | 22 |
23 |
24 | {errors.email &&

{errors.email}

} 25 |
26 |
27 | 28 | 29 |
30 |
31 | {errors.password &&

{errors.password}

} 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 |
41 |
42 |

Already have an account? Login

43 |
44 |
45 |
46 |
47 | ); 48 | 49 | SignUpForm.propTypes = { 50 | onSubmit: PropTypes.func.isRequired, 51 | onChange: PropTypes.func.isRequired, 52 | errors: PropTypes.object.isRequired, 53 | user: PropTypes.object.isRequired 54 | }; 55 | 56 | export default SignUpForm; 57 | -------------------------------------------------------------------------------- /web_server/client/src/Login/LoginPage.js: -------------------------------------------------------------------------------- 1 | import Auth from '../Auth/Auth'; 2 | import LoginForm from './LoginForm'; 3 | import PropTypes from 'prop-types'; 4 | import React from 'react'; 5 | 6 | class LoginPage extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | 10 | this.state = { 11 | errors: {}, 12 | user: { 13 | email: '', 14 | password: '' 15 | } 16 | }; 17 | } 18 | 19 | processForm(event) { 20 | event.preventDefault(); 21 | 22 | const email = this.state.user.email; 23 | const password = this.state.user.password; 24 | 25 | console.log('email:', email); 26 | console.log('password:', password); 27 | 28 | // Post login data. 29 | const url = `http://${window.location.hostname}:3000/auth/login`; 30 | const request = new Request( 31 | url, 32 | { 33 | method:'POST', headers: { 34 | 'Accept': 'application/json', 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ 38 | email: email, 39 | password: password 40 | }) 41 | }); 42 | 43 | fetch(request).then(response => { 44 | if (response.status === 200) { 45 | this.setState({ errors:{}}); 46 | 47 | response.json().then(json => { 48 | console.log(json); 49 | Auth.authenticateUser(json.token, email); 50 | this.context.router.replace('/'); 51 | }); 52 | } else { 53 | console.log("Login failed"); 54 | response.json().then(json => { 55 | const errors = json.errors ? json.errors : {}; 56 | errors.summary = json.message; 57 | this.setState({errors}); 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | changeUser(event) { 64 | const field = event.target.name; 65 | const user = this.state.user; 66 | user[field] = event.target.value; 67 | 68 | this.setState({ 69 | user : user 70 | }); 71 | } 72 | 73 | render() { 74 | return ( 75 | this.processForm(e)} 77 | onChange={(e) => this.changeUser(e)} 78 | errors={this.state.errors} 79 | user={this.state.user} /> 80 | ); 81 | } 82 | } 83 | 84 | LoginPage.contextTypes = { 85 | router: PropTypes.object.isRequired 86 | } 87 | 88 | export default LoginPage; 89 | -------------------------------------------------------------------------------- /news_pipeline/news_monitor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import os 5 | import sys 6 | import redis 7 | import hashlib 8 | 9 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 10 | import news_api_client 11 | from cloudAMQP_client import CloudAMQPClient 12 | 13 | REDIS_HOST = 'localhost' 14 | REDIS_PORT = 6379 15 | 16 | NEWS_TIME_OUT_IN_SECONDS = 3600 * 24 17 | 18 | SCRAPE_NEWS_TASK_QUEUE_URL = "amqp://hzileycx:iJgtzvRMYnzkdFGSdUYSytpAsW6FYnT_@chimpanzee.rmq.cloudamqp.com/hzileycx" 19 | SCRAPE_NEWS_TASK_QUEUE_NAME = 'tap-news-scrape-news-task-queue' 20 | 21 | def concatSources(sourcesList): 22 | return ','.join(sourcesList) 23 | NEWS_SOURCES = concatSources(['cnn', 24 | 'bbc-news', 25 | 'bloomberg', 26 | 'espn', 27 | 'nbc-news', 28 | 'techcrunch', 29 | 'the-verge', 30 | 'the-wall-street-journal', 31 | 'the-new-york-times', 32 | 'abc-news', 33 | 'daily-mail', 34 | 'fox-sports', 35 | 'the-washington-post']) 36 | print(NEWS_SOURCES) 37 | 38 | redis_client = redis.StrictRedis(REDIS_HOST, REDIS_PORT) 39 | cloudAMQP_client = CloudAMQPClient(SCRAPE_NEWS_TASK_QUEUE_URL, SCRAPE_NEWS_TASK_QUEUE_NAME) 40 | SLEEP_TIME_IN_SECONDS = 10 41 | 42 | while True: 43 | news_list = news_api_client.getNewsFromSource(NEWS_SOURCES) 44 | 45 | num_of_new_news = 0 46 | 47 | for news in news_list: 48 | news_digest = hashlib.md5(news['title'].encode('utf-8')).digest().encode('base64') 49 | 50 | if redis_client.get(news_digest) is None: 51 | num_of_new_news += 1 52 | news['digest'] = news_digest 53 | 54 | if news['publishedAt'] is None: 55 | # format: YYYY-MM-DDTHH:MM:SSZ in UTC 56 | news['publishedAt'] = datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') 57 | 58 | redis_client.set(news_digest, news) 59 | redis_client.expire(news_digest, NEWS_TIME_OUT_IN_SECONDS) 60 | 61 | cloudAMQP_client.sendMessage(news) 62 | 63 | print 'Fetched %d new news.' %num_of_new_news 64 | 65 | cloudAMQP_client.sleep(SLEEP_TIME_IN_SECONDS) 66 | -------------------------------------------------------------------------------- /news_topic_modeling_service/trainer/news_cnn_model.py: -------------------------------------------------------------------------------- 1 | import tensorflow as tf 2 | 3 | EMBEDDING_SIZE = 40 4 | N_FILTERS = 10 5 | WINDOW_SIZE = 20 6 | FILTER_SHAPE1 = [WINDOW_SIZE, EMBEDDING_SIZE] 7 | FILTER_SHAPE2 = [WINDOW_SIZE, N_FILTERS] 8 | POOLING_WINDOW = 4 9 | POOLING_STRIDE = 2 10 | 11 | LEARNING_RATE = 0.05 12 | 13 | def generate_cnn_model(n_classes, n_words): 14 | """2 layer ConvNet to predict from sequence of words to a class.""" 15 | def cnn_model(features, target): 16 | # Convert indexes of words into embeddings. 17 | # This creates embeddings matrix of [n_words, EMBEDDING_SIZE] and then 18 | # maps word indexes of the sequence into [batch_size, sequence_length, 19 | # EMBEDDING_SIZE]. 20 | 21 | target = tf.one_hot(target, n_classes, 1, 0) 22 | word_vectors = tf.contrib.layers.embed_sequence( 23 | features, vocab_size=n_words, embed_dim=EMBEDDING_SIZE, scope='words') 24 | word_vectors = tf.expand_dims(word_vectors, 3) 25 | with tf.variable_scope('CNN_layer1'): 26 | # Apply Convolution filtering on input sequence. 27 | conv1 = tf.contrib.layers.convolution2d( 28 | word_vectors, N_FILTERS, FILTER_SHAPE1, padding='VALID') 29 | # Add a RELU for non linearity. 30 | conv1 = tf.nn.relu(conv1) 31 | # Max pooling across output of Convolution+Relu. 32 | pool1 = tf.nn.max_pool( 33 | conv1, 34 | ksize=[1, POOLING_WINDOW, 1, 1], 35 | strides=[1, POOLING_STRIDE, 1, 1], 36 | padding='SAME') 37 | # Transpose matrix so that n_filters from convolution becomes width. 38 | pool1 = tf.transpose(pool1, [0, 1, 3, 2]) 39 | with tf.variable_scope('CNN_layer2'): 40 | # Second level of convolution filtering. 41 | conv2 = tf.contrib.layers.convolution2d( 42 | pool1, N_FILTERS, FILTER_SHAPE2, padding='VALID') 43 | # Max across each filter to get useful features for classification. 44 | pool2 = tf.squeeze(tf.reduce_max(conv2, 1), squeeze_dims=[1]) 45 | 46 | # Apply regular WX + B and classification. 47 | logits = tf.contrib.layers.fully_connected(pool2, n_classes, activation_fn=None) 48 | loss = tf.contrib.losses.softmax_cross_entropy(logits, target) 49 | 50 | train_op = tf.contrib.layers.optimize_loss( 51 | loss, 52 | tf.contrib.framework.get_global_step(), 53 | optimizer='Adam', 54 | learning_rate=LEARNING_RATE) 55 | 56 | return ({ 57 | 'class': tf.argmax(logits, 1), 58 | 'prob': tf.nn.softmax(logits) 59 | }, loss, train_op) 60 | 61 | return cnn_model 62 | -------------------------------------------------------------------------------- /web_server/client/src/NewsPanel/NewsPanel.js: -------------------------------------------------------------------------------- 1 | import './NewsPanel.css'; 2 | import _ from 'lodash'; 3 | import React from 'react'; 4 | import Auth from '../Auth/Auth'; 5 | import NewsCard from '../NewsCard/NewsCard'; 6 | 7 | class NewsPanel extends React.Component{ 8 | constructor() { 9 | super(); 10 | this.state = {news: null, pageNum: 1, totalPages: 1, loadedAll: false}; 11 | this.handleScroll = this.handleScroll.bind(this); 12 | } 13 | 14 | componentDidMount() { 15 | this.loadMoreNews(); 16 | this.loadMoreNews = _.debounce(this.loadMoreNews, 1000); 17 | window.addEventListener('scroll', this.handleScroll); 18 | } 19 | 20 | handleScroll() { 21 | let scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop; 22 | if ((window.innerHeight + scrollY) >= (document.body.OffsetHeight - 50)) { 23 | console.log('Loading more news'); 24 | this.loadMoreNews(); 25 | } 26 | } 27 | 28 | loadMoreNews() { 29 | if (this.state.loadedAll === true) { 30 | return; 31 | } 32 | let url = `http://localhost:3000/news/userId/${Auth.getEmail()}/pageNum/${this.state.pageNum}`; 33 | let request = new Request(encodeURI(url), { 34 | method: 'GET', 35 | headers: { 36 | 'Authorization': 'bearer ' + Auth.getToken() 37 | }, 38 | cache: 'no-store' 39 | }); 40 | 41 | fetch(request) 42 | .then(res => res.json()) 43 | .then(news => { 44 | if (!news || news.length === 0) { 45 | this.setState({loadedAll: true}); 46 | } 47 | this.setState({ 48 | news: this.state.news? this.state.news.concat(news) : news, 49 | pageNum: this.state.pageNum + 1 50 | }); 51 | }); 52 | } 53 | 54 | renderNews() { 55 | const news_list = this.state.news.map((news) => { 56 | return( 57 | 58 | 59 | 60 | ); 61 | }); 62 | 63 | return ( 64 |
65 |
66 | {news_list} 67 |
68 |
69 | ); 70 | } 71 | 72 | render() { 73 | if (this.state.news) { 74 | return( 75 |
76 | {this.renderNews()} 77 |
78 | ); 79 | } else { 80 | return( 81 |
82 |
83 | Loading 84 |
85 |
86 | ); 87 | } 88 | } 89 | 90 | } 91 | 92 | export default NewsPanel; 93 | -------------------------------------------------------------------------------- /backend_server/operations_test.py: -------------------------------------------------------------------------------- 1 | import operations 2 | import sys 3 | import os 4 | 5 | # import common package in parent directory 6 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 7 | 8 | import mongodb_client 9 | from cloudAMQP_client import CloudAMQPClient 10 | 11 | CLICK_LOGS_TABLE_NAME = 'click_logs' 12 | LOG_CLICKS_TASK_QUEUE_URL = "amqp://pbsoegxa:ybQrjx9SwAVY5Icb083qQu0dJQ8IkIK7@chimpanzee.rmq.cloudamqp.com/pbsoegxa" 13 | LOG_CLICKS_TASK_QUEUE_NAME = "tap-news-log-clicks-task-queue" 14 | cloudAMQP_client = CloudAMQPClient(LOG_CLICKS_TASK_QUEUE_URL, LOG_CLICKS_TASK_QUEUE_NAME) 15 | 16 | def test_getOneNews_basic(): 17 | news = operations.getOneNews() 18 | print(news) 19 | assert news is not None 20 | print('test_getOneNews_basic passed!') 21 | 22 | def test_getNewsSummariesForUser_basic(): 23 | news = operations.getNewsSummariesForUser('test', 1) 24 | assert len(news) > 0 25 | print('test_getNewsSummariesForUser_basic passed!') 26 | 27 | def test_getNewsSummariesForUser_pagination(): 28 | news_page_1 = operations.getNewsSummariesForUser('test', 1) 29 | news_page_2 = operations.getNewsSummariesForUser('test', 2) 30 | 31 | # Assert that there is no dupe news in two pages. 32 | digests_page_1_set = set([news['digest'] for news in news_page_1]) 33 | digests_page_2_set = set([news['digest'] for news in news_page_2]) 34 | 35 | assert len(digests_page_1_set.intersection(digests_page_2_set)) == 0 36 | print('test_getNewsSummariesForUser_pagination passed!') 37 | 38 | def test_logNewsClickForUser_basic(): 39 | db = mongodb_client.get_db() 40 | db[CLICK_LOGS_TABLE_NAME].delete_many({'userId': 'test'}) 41 | 42 | operations.logNewsClickForUser('test', 'test_news') 43 | 44 | # Verify click logs written into MongoDB 45 | # Get most recent record in MongoDB 46 | record = list(db[CLICK_LOGS_TABLE_NAME].find().sort([('timestamp', -1)]).limit(1))[0] 47 | 48 | assert record is not None 49 | assert record['userId'] == 'test' 50 | assert record['newsId'] == 'test_news' 51 | assert record['timestamp'] is not None 52 | 53 | db[CLICK_LOGS_TABLE_NAME].delete_many({'userId': 'test'}) 54 | 55 | # Verify the message has been sent to queue 56 | msg = cloudAMQP_client.getMessage() 57 | assert msg is not None 58 | assert msg['userId'] == 'test' 59 | assert msg['newsId'] == 'test_news' 60 | assert msg['timestamp'] is not None 61 | 62 | print 'test_logNewsClickForUser_basic passed!' 63 | 64 | if __name__ == "__main__": 65 | test_getOneNews_basic() 66 | test_getNewsSummariesForUser_basic() 67 | test_getNewsSummariesForUser_pagination() 68 | test_logNewsClickForUser_basic() 69 | -------------------------------------------------------------------------------- /news_pipeline/news_deduper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import os 5 | import sys 6 | 7 | from dateutil import parser 8 | from sklearn.feature_extraction.text import TfidfVectorizer 9 | 10 | # import common package in parent directory 11 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 12 | 13 | import mongodb_client 14 | import news_topic_modeling_service_client 15 | 16 | from cloudAMQP_client import CloudAMQPClient 17 | 18 | DEDUPE_NEWS_TASK_QUEUE_URL = "amqp://xzbggvzn:jFOMw3MZ6hzq0htegQTkG2S7fQsrNby1@chimpanzee.rmq.cloudamqp.com/xzbggvzn" 19 | DEDUPE_NEWS_TASK_QUEUE_NAME = "tap-news-dedupe-news-task-queue" 20 | 21 | 22 | SLEEP_TIME_IN_SECONDS = 1 23 | 24 | NEWS_TABLE_NAME = 'news' 25 | 26 | SAME_NEWS_SIMILARITY_THRESHOLD = 0.8 27 | 28 | cloudAMQP_client = CloudAMQPClient(DEDUPE_NEWS_TASK_QUEUE_URL, DEDUPE_NEWS_TASK_QUEUE_NAME) 29 | 30 | 31 | def handle_message(msg): 32 | if msg is None or not isinstance(msg, dict) : 33 | return 34 | task = msg 35 | text = task['text'] 36 | if text is None: 37 | return 38 | 39 | # Get all recent news 40 | published_at = parser.parse(task['publishedAt']) 41 | published_at_day_begin = datetime.datetime(published_at.year, published_at.month, published_at.day, 0, 0, 0, 0) 42 | published_at_day_end = published_at_day_begin + datetime.timedelta(days=1) 43 | 44 | db = mongodb_client.get_db() 45 | recent_news_list = list(db[NEWS_TABLE_NAME].find({'publishedAt' : {'$gte': published_at_day_begin, 46 | '%lt' : published_at_day_end}})) 47 | 48 | if recent_news_list is not None and len(recent_news_list) > 0: 49 | documents = [str(news['text']) for news in recent_news_list] 50 | documents.insert(0, text) 51 | 52 | # Calculate similarity matrix 53 | tfidf = TfidfVectorizer().fit_transform(documents) 54 | pairwise_sim = tfidf * tfidf.T 55 | 56 | print(pairwise_sim.A) 57 | 58 | rows, _ = pairwise_sim.shape 59 | 60 | for row in range(1, rows): 61 | if pairwise_sim[row, 0] > SAME_NEWS_SIMILARITY_THRESHOLD: 62 | # Duplicated news, ignore. 63 | print 'Duplicated news, ignore.' 64 | return 65 | 66 | task['publishedAt'] = parser.parse(task['publishedAt']) 67 | 68 | # # Classify news 69 | # title = task['title'] 70 | # if title is not None: 71 | # topic = news_topic_modeling_service_client.classify(title) 72 | # task['class'] = topic 73 | 74 | db[NEWS_TABLE_NAME].replace_one({'digest': task['digest']}, task, upsert=True) 75 | 76 | while True: 77 | if cloudAMQP_client is not None: 78 | msg = cloudAMQP_client.getMessage() 79 | if msg is not None: 80 | # Parse and process the task 81 | try: 82 | handle_message(msg) 83 | except Exception as e: 84 | print(e) 85 | pass 86 | 87 | cloudAMQP_client.sleep(SLEEP_TIME_IN_SECONDS) 88 | -------------------------------------------------------------------------------- /web_server/client/src/Signup/SignUpPage.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import SignUpForm from './SignUpForm'; 4 | 5 | class SignUpPage extends React.Component { 6 | constructor(props, context) { 7 | super(props, context); 8 | 9 | this.state = { 10 | errors: {}, 11 | user: { 12 | email: '', 13 | password: '', 14 | confirm_password: '' 15 | } 16 | }; 17 | 18 | this.processForm = this.processForm.bind(this); 19 | this.changeUser = this.changeUser.bind(this); 20 | } 21 | 22 | processForm(event) { 23 | event.preventDefault(); 24 | 25 | const email = this.state.user.email; 26 | const password = this.state.user.password; 27 | const confirm_password = this.state.user.confirm_password; 28 | 29 | console.log('email:', email); 30 | console.log('password:', password); 31 | console.log('confirm_password:', confirm_password); 32 | 33 | if (password !== confirm_password) { 34 | return; 35 | } 36 | 37 | // Post registeration data 38 | fetch('http://localhost:3000/auth/signup',{ 39 | method:'POST', 40 | headers: { 41 | 'Accept': 'application/json', 42 | 'Content-Type': 'application/json', 43 | }, 44 | body: JSON.stringify({ 45 | email: this.state.user.email, 46 | password: this.state.user.password 47 | }) 48 | }).then(response => { 49 | if (response.status === 200) { 50 | this.setState({errors: {}}); 51 | 52 | // change the current URL to /login 53 | this.context.router.replace('/login'); 54 | } else { 55 | response.json().then(json => { 56 | console.log(json); 57 | const errors = json.errors ? json.errors : {}; 58 | errors.summary = json.message; 59 | console.log(this.state.errors); 60 | this.setState({errors}); 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | changeUser(event) { 67 | const field = event.target.name; 68 | const user = this.state.user; 69 | user[field] = event.target.value; 70 | 71 | this.setState({ 72 | user : user 73 | }); 74 | 75 | if (this.state.user.password !== this.state.user.confirm_password) { 76 | const errors = this.state.errors; 77 | errors.password = "Password and Confirm Password don't match."; 78 | this.setState({errors}); 79 | } else { 80 | const errors = this.state.errors; 81 | errors.password = ''; 82 | this.setState({errors}); 83 | } 84 | } 85 | 86 | render() { 87 | return ( 88 | this.processForm(e)} 90 | onChange={(e) => this.changeUser(e)} 91 | errors={this.state.errors} 92 | user={this.state.user} 93 | /> 94 | ); 95 | } 96 | } 97 | 98 | SignUpPage.contextTypes = { 99 | router: PropTypes.object.isRequired 100 | }; 101 | 102 | export default SignUpPage; 103 | -------------------------------------------------------------------------------- /backend_server/operations.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pickle 4 | import redis 5 | import sys 6 | 7 | from bson.json_util import dumps 8 | from datetime import datetime 9 | 10 | # import common package in parent directory 11 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 12 | 13 | import mongodb_client 14 | # import news_recommendation_service_client 15 | # 16 | from cloudAMQP_client import CloudAMQPClient 17 | 18 | NEWS_LIST_BATCH_SIZE = 10 19 | NEWS_LIMIT = 150 20 | USER_NEWS_TIME_OUT_IN_SECONDS = 60 21 | 22 | NEWS_TABLE_NAME = 'news' 23 | CLICK_LOGS_TABLE_NAME = 'click_logs' 24 | 25 | REDIS_HOST = 'localhost' 26 | REDIS_PORT = 6379 27 | 28 | redis_client = redis.StrictRedis(REDIS_HOST, REDIS_PORT, db=0) 29 | 30 | 31 | LOG_CLICKS_TASK_QUEUE_URL = "amqp://pbsoegxa:ybQrjx9SwAVY5Icb083qQu0dJQ8IkIK7@chimpanzee.rmq.cloudamqp.com/pbsoegxa" 32 | LOG_CLICKS_TASK_QUEUE_NAME = "tap-news-log-clicks-task-queue" 33 | cloudAMQP_client = CloudAMQPClient(LOG_CLICKS_TASK_QUEUE_URL, LOG_CLICKS_TASK_QUEUE_NAME) 34 | 35 | def getOneNews(): 36 | db = mongodb_client.get_db() 37 | news = db[NEWS_TABLE_NAME].find_one() 38 | return news 39 | 40 | 41 | def getNewsSummariesForUser(user_id, page_num): 42 | page_num = int(page_num) 43 | begin_index = (page_num - 1) * NEWS_LIST_BATCH_SIZE 44 | end_index = page_num * NEWS_LIST_BATCH_SIZE 45 | 46 | # The final list of news to be returned. 47 | sliced_news = [] 48 | db = mongodb_client.get_db() 49 | 50 | if redis_client.get(user_id) is not None: 51 | news_digests = pickle.loads(redis_client.get(user_id)) 52 | sliced_news_digest = news_digests[begin_index:end_index] 53 | sliced_news = list(db[NEWS_TABLE_NAME].find({'digest':{'$in':sliced_news_digest}})) 54 | else: 55 | total_news = list(db[NEWS_TABLE_NAME].find().sort([('publishedAt', -1)]).limit(NEWS_LIMIT)) 56 | total_news_digest = [x['digest'] for x in total_news] 57 | 58 | redis_client.set(user_id, pickle.dumps(total_news_digest)) 59 | redis_client.expire(user_id, USER_NEWS_TIME_OUT_IN_SECONDS) 60 | 61 | sliced_news = total_news[begin_index:end_index] 62 | 63 | # # Get preference for the user. 64 | # preference = news_recommendation_service_client.getPreferenceForUser(user_id) 65 | # topPrefence = None 66 | # 67 | # if preference is not None and len(preference) > 0: 68 | # topPrefence = preference[0] 69 | 70 | for news in sliced_news: 71 | # Remove text field to save bandwidth. 72 | del news['text'] 73 | if news['publishedAt'].date() == datetime.today().date(): 74 | news['time'] = 'today' 75 | # if news['class'] == topPrefence: 76 | # news['reason'] = "Recommend" 77 | 78 | return json.loads(dumps(sliced_news)) 79 | 80 | def logNewsClickForUser(user_id, news_id): 81 | message = {'userId': user_id, 'newsId': news_id, 'timestamp': datetime.utcnow()} 82 | 83 | db = mongodb_client.get_db() 84 | db[CLICK_LOGS_TABLE_NAME].insert(message) 85 | 86 | # Send log task to machine learning service for prediction 87 | message = {'userId': user_id, 'newsId': news_id, 'timestamp': str(datetime.utcnow())} 88 | cloudAMQP_client.sendMessage(message) 89 | -------------------------------------------------------------------------------- /news_topic_modeling_service/server/server.py: -------------------------------------------------------------------------------- 1 | import news_classes 2 | import numpy as np 3 | import os 4 | import pandas as pd 5 | import pickle 6 | import sys 7 | import tensorflow as tf 8 | import time 9 | 10 | import pyjsonrpc 11 | from tensorflow.contrib.learn.python.learn.estimators import model_fn 12 | from watchdog.observers import Observer 13 | from watchdog.events import FileSystemEventHandler 14 | 15 | # import packages in trainer 16 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'trainer')) 17 | import news_cnn_model 18 | 19 | learn = tf.contrib.learn 20 | 21 | SERVER_HOST = 'localhost' 22 | SERVER_PORT = 6060 23 | 24 | MODEL_DIR = '../model' 25 | MODEL_UPDATE_LAG_IN_SECONDS = 10 26 | 27 | N_CLASSES = 8 28 | 29 | VARS_FILE = '../model/vars' 30 | VOCAB_PROCESSOR_SAVE_FILE = '../model/vocab_procesor_save_file' 31 | 32 | n_words = 0 33 | 34 | MAX_DOCUMENT_LENGTH = 500 35 | vocab_processor = None 36 | 37 | classifier = None 38 | 39 | 40 | def restoreVars(): 41 | with open(VARS_FILE, 'rb') as f: 42 | global n_words 43 | n_words = pickle.load(f) 44 | 45 | global vocab_processor 46 | vocab_processor = learn.preprocessing.VocabularyProcessor.restore( 47 | VOCAB_PROCESSOR_SAVE_FILE) 48 | 49 | 50 | def loadModel(): 51 | global classifier 52 | classifier = learn.Estimator( 53 | model_fn=news_cnn_model.generate_cnn_model(N_CLASSES, n_words), 54 | model_dir=MODEL_DIR) 55 | # Prepare training and testing 56 | df = pd.read_csv('../data/labeled_news.csv', header=None) 57 | 58 | # TODO: fix this until https://github.com/tensorflow/tensorflow/issues/5548 is solved. 59 | # We have to call evaluate or predict at least once to make the restored Estimator work. 60 | train_df = df[0:1] 61 | x_train = train_df[1] 62 | x_train = np.array(list(vocab_processor.transform(x_train))) 63 | y_train = train_df[0] 64 | classifier.evaluate(x_train, y_train) 65 | 66 | print("Model update.") 67 | 68 | 69 | restoreVars() 70 | loadModel() 71 | 72 | print("Model loaded.") 73 | 74 | 75 | class ReloadModelHandler(FileSystemEventHandler): 76 | def on_any_event(self, event): 77 | # Reload model 78 | print("Model update detected. Loading new model.") 79 | time.sleep(MODEL_UPDATE_LAG_IN_SECONDS) 80 | retoreVars() 81 | loadModel() 82 | 83 | 84 | # Setup watchdog 85 | observer = Observer() 86 | observer.schedule(ReloadModelHandler(), path=MODEL_DIR, recursive=False) 87 | observer.start() 88 | 89 | class RequestHandler(pyjsonrpc.HttpRequestHandler): 90 | @pyjsonrpc.rpcmethod 91 | def classify(text): 92 | text_series = pd.Series([text]) 93 | predict_x = np.array(list(vocab_processor.transform(text_series))) 94 | print(predict_x) 95 | 96 | y_predicted = [ 97 | p['class'] for p in classifier.predict( 98 | predict_x, as_iterable=True) 99 | ] 100 | print(y_predicted[0]) 101 | topic = news_classes.class_map[str(y_predicted[0])] 102 | return topic 103 | 104 | 105 | # Threading RPC Server 106 | http_server = pyjsonrpc.ThreadingHttpServer( 107 | server_address = (SERVER_HOST, SERVER_PORT), 108 | RequestHandlerClass = RequestHandler 109 | ) 110 | 111 | print('Starting HTTP server on {}:{}'.format(SERVER_HOST, SERVER_PORT)) 112 | http_server.serve_forever() 113 | -------------------------------------------------------------------------------- /news_recommendation_service/click_log_processor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | Time decay model: 5 | 6 | If selected: 7 | p = (1-α)p + α 8 | 9 | If not: 10 | p = (1-α)p 11 | Where p is the selection probability, and α is the degree of weight decrease. 12 | The result of this is that the nth most recent selection will have a weight of 13 | (1-α)^n. Using a coefficient value of 0.05 as an example, the 10th most recent 14 | selection would only have half the weight of the most recent. Increasing epsilon 15 | would bias towards more recent results more. 16 | ''' 17 | 18 | import news_class 19 | import os 20 | import sys 21 | 22 | # import common package in parent directory 23 | sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'common')) 24 | 25 | import mongodb_client 26 | from cloudAMQP_client import CloudAMQPClient 27 | 28 | NUM_OF_CLASS = 17 29 | INITIAL_P = 1.0 / NUM_OF_CLASS 30 | ALPHA = 0.1 31 | 32 | SLEEP_TIME_IN_SECONDS = 3 33 | 34 | LOG_CLICKS_TASK_QUEUE_URL = "amqp://pbsoegxa:ybQrjx9SwAVY5Icb083qQu0dJQ8IkIK7@chimpanzee.rmq.cloudamqp.com/pbsoegxa" 35 | LOG_CLICKS_TASK_QUEUE_NAME = "tap-news-log-clicks-task-queue" 36 | 37 | PREFERENCE_MODEL_TABLE_NAME = "user_preference_model" 38 | NEWS_TABLE_NAME = "news" 39 | 40 | cloudAMQP_client = CloudAMQPClient(LOG_CLICKS_TASK_QUEUE_URL, LOG_CLICKS_TASK_QUEUE_NAME) 41 | 42 | def handle_message(msg): 43 | if msg is None or not isinstance(msg, dict): 44 | return 45 | 46 | if ('userId' not in msg 47 | or 'newsId' not in msg 48 | or 'timestamp' not in msg): 49 | return 50 | 51 | userId = msg['userId'] 52 | newsId = msg['newsId'] 53 | 54 | # Update user's preference 55 | db = mongodb_client.get_db() 56 | model = db[PREFERENCE_MODEL_TABLE_NAME].find_one({'userId':userId}) 57 | 58 | # If model not exists, create a new one 59 | if model is None: 60 | print("Creating preference model for new user: %s" % userId) 61 | news_model = {'userId' : userId} 62 | preference = {} 63 | for i in news_class.classes: 64 | preference[i] = float(INITIAL_P) 65 | news_model['preference'] = preference 66 | model = news_model 67 | 68 | print("Update prefernce model for user: %s" % userId) 69 | 70 | # Update model using time decay method. 71 | news = db[NEWS_TABLE_NAME].find_one({'digest': newsId}) 72 | if (news is None or 73 | 'class' not in news or 74 | news['class'] not in news_class.classes): 75 | print("SKipping processing...") 76 | return 77 | 78 | # Update the clicked one. 79 | click_class = news['class'] 80 | old_p = model['preference'][click_class] 81 | model['preference'][click_class] = float((1 - ALPHA) * old_p + ALPHA) 82 | 83 | # Update the non-clicked classes. 84 | for i, prob in model['preference'].items(): 85 | if not i == click_class: 86 | old_p = model['preference'][i] 87 | model['preference'][i] = float((1 - ALPHA) * old_p) 88 | 89 | db[PREFERENCE_MODEL_TABLE_NAME].replace_one({'userId': userId}, model, upsert=True) 90 | 91 | 92 | def run(): 93 | while True: 94 | if cloudAMQP_client is not None: 95 | msg = cloudAMQP_client.getMessage() 96 | if msg is not None: 97 | try: 98 | handle_message(msg) 99 | except Exception as e: 100 | print(e) 101 | pass 102 | 103 | cloudAMQP_client.sleep(SLEEP_TIME_IN_SECONDS) 104 | 105 | 106 | if __name__ == "__main__": 107 | run() 108 | -------------------------------------------------------------------------------- /web_server/client/build/service-worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var precacheConfig=[["/index.html","cf156459f047a1035d9efe9c73c00660"],["/static/css/main.8e91f7e9.css","a00546197b6ecbc4bf2322fbeb489b5d"],["/static/js/main.be2dd674.js","e68b61456c64fe438ace19944c771c58"],["/static/media/Roboto-Bold.c0f1e4a4.woff2","c0f1e4a4fdfb8048c72e86aadb2a247d"],["/static/media/Roboto-Bold.eed9aab5.woff","eed9aab5449cc9c8430d7d258108f602"],["/static/media/Roboto-Light.3c37aa69.woff2","3c37aa69cd77e6a53a067170fa8fe2e9"],["/static/media/Roboto-Light.ea36cd9a.woff","ea36cd9a0e9eee97012a67b8a4570d7b"],["/static/media/Roboto-Medium.1561b424.woff2","1561b424aaef2f704bbd89155b3ce514"],["/static/media/Roboto-Medium.cf4d60bc.woff","cf4d60bc0b1d4b2314085919a00e1724"],["/static/media/Roboto-Regular.3cf6adf6.woff","3cf6adf61054c328b1b0ddcd8f9ce24d"],["/static/media/Roboto-Regular.5136cbe6.woff2","5136cbe62a63604402f2fedb97f246f8"],["/static/media/Roboto-Thin.1f35e6a1.woff2","1f35e6a11d27d2e10d28946d42332dc5"],["/static/media/Roboto-Thin.44b78f14.woff","44b78f142603eb69f593ed4002ed7a4a"]],cacheName="sw-precache-v3-sw-precache-webpack-plugin-"+(self.registration?self.registration.scope:""),ignoreUrlParametersMatching=[/^utm_/],addDirectoryIndex=function(e,t){var a=new URL(e);return"/"===a.pathname.slice(-1)&&(a.pathname+=t),a.toString()},cleanResponse=function(t){return t.redirected?("body"in t?Promise.resolve(t.body):t.blob()).then(function(e){return new Response(e,{headers:t.headers,status:t.status,statusText:t.statusText})}):Promise.resolve(t)},createCacheKey=function(e,t,a,n){var r=new URL(e);return n&&r.pathname.match(n)||(r.search+=(r.search?"&":"")+encodeURIComponent(t)+"="+encodeURIComponent(a)),r.toString()},isPathWhitelisted=function(e,t){if(0===e.length)return!0;var a=new URL(t).pathname;return e.some(function(e){return a.match(e)})},stripIgnoredUrlParameters=function(e,a){var t=new URL(e);return t.hash="",t.search=t.search.slice(1).split("&").map(function(e){return e.split("=")}).filter(function(t){return a.every(function(e){return!e.test(t[0])})}).map(function(e){return e.join("=")}).join("&"),t.toString()},hashParamName="_sw-precache",urlsToCacheKeys=new Map(precacheConfig.map(function(e){var t=e[0],a=e[1],n=new URL(t,self.location),r=createCacheKey(n,hashParamName,a,/\.\w{8}\./);return[n.toString(),r]}));function setOfCachedUrls(e){return e.keys().then(function(e){return e.map(function(e){return e.url})}).then(function(e){return new Set(e)})}self.addEventListener("install",function(e){e.waitUntil(caches.open(cacheName).then(function(n){return setOfCachedUrls(n).then(function(a){return Promise.all(Array.from(urlsToCacheKeys.values()).map(function(t){if(!a.has(t)){var e=new Request(t,{credentials:"same-origin"});return fetch(e).then(function(e){if(!e.ok)throw new Error("Request for "+t+" returned a response with status "+e.status);return cleanResponse(e).then(function(e){return n.put(t,e)})})}}))})}).then(function(){return self.skipWaiting()}))}),self.addEventListener("activate",function(e){var a=new Set(urlsToCacheKeys.values());e.waitUntil(caches.open(cacheName).then(function(t){return t.keys().then(function(e){return Promise.all(e.map(function(e){if(!a.has(e.url))return t.delete(e)}))})}).then(function(){return self.clients.claim()}))}),self.addEventListener("fetch",function(t){if("GET"===t.request.method){var e,a=stripIgnoredUrlParameters(t.request.url,ignoreUrlParametersMatching),n="index.html";(e=urlsToCacheKeys.has(a))||(a=addDirectoryIndex(a,n),e=urlsToCacheKeys.has(a));var r="/index.html";!e&&"navigate"===t.request.mode&&isPathWhitelisted(["^(?!\\/__).*"],t.request.url)&&(a=new URL(r,self.location).toString(),e=urlsToCacheKeys.has(a)),e&&t.respondWith(caches.open(cacheName).then(function(e){return e.match(urlsToCacheKeys.get(a)).then(function(e){if(e)return e;throw Error("The cached response that was expected is missing.")})}).catch(function(e){return console.warn('Couldn\'t serve response for "%s" from cache: %O',t.request.url,e),fetch(t.request)}))}}); -------------------------------------------------------------------------------- /web_server/server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | const router = express.Router(); 4 | const validator = require('validator'); 5 | 6 | 7 | router.post('/signup', (req, res, next) => { 8 | const validationResult = validateSignupForm(req.body); 9 | if (!validationResult.success) { 10 | console.log('validationResult failed'); 11 | return res.status(400).json({ 12 | success: false, 13 | message: validationResult.message, 14 | errors: validationResult.errors 15 | }); 16 | } 17 | 18 | return passport.authenticate('local-signup', err => { 19 | if (err) { 20 | console.log(err); 21 | if (err.name === 'MongoError' && err.code === 11000) { 22 | // the 11000 Mongo code is for a duplication email error 23 | // the 409 HTTP status code is for conflict error 24 | return res.status(409).json({ 25 | success: false, 26 | message: 'Check the form for errors.', 27 | errors: { 28 | email: 'This email is already taken.' 29 | } 30 | }); 31 | } 32 | 33 | return res.status(400).json({ 34 | success: false, 35 | message: 'Could not process the form.' 36 | }); 37 | } 38 | 39 | return res.status(200).json({ 40 | success: true, 41 | message: 'You have successfully signed up! Now you should be able to log in.' 42 | }); 43 | })(req, res, next); 44 | }); 45 | 46 | router.post('/login', (req, res, next) => { 47 | const validationResult = validateLoginForm(req.body); 48 | if (!validationResult.success) { 49 | return res.status(400).json({ 50 | success: false, 51 | message: validationResult.message, 52 | errors: validationResult.errors 53 | }); 54 | } 55 | 56 | return passport.authenticate('local-login', (err, token, userData) => { 57 | if (err) { 58 | if (err.name === 'IncorrectCredentialsError') { 59 | return res.status(400).json({ 60 | success: false, 61 | message: err.message 62 | }); 63 | } 64 | 65 | return res.status(400).json({ 66 | success: false, 67 | message: 'Could not process the form: ' + err.message 68 | }); 69 | } 70 | 71 | return res.json({ 72 | success: true, 73 | message: 'You have successfully logged in!', 74 | token, 75 | user: userData 76 | }); 77 | })(req, res, next); 78 | }); 79 | 80 | function validateSignupForm(payload) { 81 | console.log(payload); 82 | const errors = {}; 83 | let isFormValid = true; 84 | let message = ''; 85 | 86 | if (!payload || typeof payload.email !== 'string' || !validator.isEmail(payload.email)) { 87 | isFormValid = false; 88 | errors.email = 'Please provide a correct email address.'; 89 | } 90 | 91 | if (!payload || typeof payload.password !== 'string' || payload.password.length < 8) { 92 | isFormValid = false; 93 | errors.password = 'Password must have at least 8 characters.'; 94 | } 95 | 96 | if (!isFormValid) { 97 | message = 'Check the form for errors.'; 98 | } 99 | 100 | return { 101 | success: isFormValid, 102 | message, 103 | errors 104 | }; 105 | } 106 | 107 | function validateLoginForm(payload) { 108 | console.log(payload); 109 | const errors = {}; 110 | let isFormValid = true; 111 | let message = ''; 112 | 113 | if (!payload || typeof payload.email !== 'string' || payload.email.trim().length === 0) { 114 | isFormValid = false; 115 | errors.email = 'Please provide your email address.'; 116 | } 117 | 118 | if (!payload || typeof payload.password !== 'string' || payload.password.length === 0) { 119 | isFormValid = false; 120 | errors.password = 'Please provide your password.'; 121 | } 122 | 123 | if (!isFormValid) { 124 | message = 'Check the form for errors.'; 125 | } 126 | 127 | return { 128 | success: isFormValid, 129 | message, 130 | errors 131 | }; 132 | } 133 | 134 | module.exports = router; 135 | -------------------------------------------------------------------------------- /news_pipeline/scrapers/user_agents.txt: -------------------------------------------------------------------------------- 1 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)" 2 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)" 3 | "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)" 4 | "Mozilla/4.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)" 5 | "Mozilla/1.22 (compatible; MSIE 10.0; Windows 3.1)" 6 | "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US))" 7 | "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)" 8 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)" 9 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7)" 10 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; Media Center PC 6.0; InfoPath.3; MS-RTC LM 8; Zune 4.7" 11 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; InfoPath.3; MS-RTC LM 8; .NET4.0C; .NET4.0E)" 12 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)" 13 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)" 14 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 4.0; Tablet PC 2.0; InfoPath.3; .NET4.0C; .NET4.0E)" 15 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0" 16 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0; chromeframe/11.0.696.57)" 17 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0) chromeframe/10.0.648.205" 18 | "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/5.0; chromeframe/11.0.696.57)" 19 | "Mozilla/5.0 ( ; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)" 20 | "Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 5.1; Trident/5.0)" 21 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 7.1; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C)" 22 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; AskTB5.5)" 23 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; InfoPath.2; .NET4.0C; .NET4.0E)" 24 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C)" 25 | "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/5.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; FDM; .NET CLR 1.1.4322; .NET4.0C; .NET4.0E; Tablet PC 2.0)" 26 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; Media Center PC 4.0; SLCC1; .NET CLR 3.0.04320)" 27 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 1.1.4322)" 28 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727)" 29 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727)" 30 | "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 5.0; Trident/4.0; InfoPath.1; SV1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 3.0.04506.30)" 31 | "Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; .NET CLR 2.0.34861; .NET CLR 3.0.3746.3218; .NET CLR 3.5.33652; msn OptimizedIE8;ENUS)" 32 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.2; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)" 33 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8)" 34 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; Media Center PC 6.0; InfoPath.2; MS-RTC LM 8" 35 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; Media Center PC 6.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C)" 36 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 3.5.30729; .NET CLR 3.0.30729; MS-RTC LM 8)" 37 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; InfoPath.2)" 38 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Zune 3.0)" 39 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; msn OptimizedIE8;ZHCN)" 40 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; InfoPath.3; .NET4.0C; .NET4.0E) chromeframe/8.0.552.224" 41 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; Zune 4.7; InfoPath.3)" 42 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8; .NET4.0C; .NET4.0E; Zune 4.7)" 43 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; MS-RTC LM 8)" 44 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; Zune 4.0)" 45 | "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.3; .NET4.0C; .NET4.0E; MS-RTC LM 8; Zune 4.7)" 46 | "Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre" 47 | "Mozilla/5.0 (X11; Linux x86_64; rv:2.2a1pre) Gecko/20100101 Firefox/4.2a1pre" 48 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110324 Firefox/4.2a1pre" 49 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.2a1pre) Gecko/20110323 Firefox/4.2a1pre" 50 | "Mozilla/5.0 (X11; Linux x86_64; rv:2.0b9pre) Gecko/20110111 Firefox/4.0b9pre" 51 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b9pre) Gecko/20101228 Firefox/4.0b9pre" 52 | "Mozilla/5.0 (Windows NT 5.1; rv:2.0b9pre) Gecko/20110105 Firefox/4.0b9pre" 53 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre" 54 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101213 Firefox/4.0b8pre" 55 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101128 Firefox/4.0b8pre" 56 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b8pre) Gecko/20101114 Firefox/4.0b8pre" 57 | "Mozilla/5.0 (Windows NT 5.1; rv:2.0b8pre) Gecko/20101127 Firefox/4.0b8pre" 58 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b8) Gecko/20100101 Firefox/4.0b8" 59 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0b7pre) Gecko/20100921 Firefox/4.0b7pre" 60 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20101111 Firefox/4.0b7" 61 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b7) Gecko/20100101 Firefox/4.0b7" 62 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre" 63 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0b6pre) Gecko/20100903 Firefox/4.0b6pre Firefox/4.0b6pre" 64 | "Mozilla/5.0 (X11; Linux x86_64; rv:2.0b4) Gecko/20100818 Firefox/4.0b4" 65 | "Mozilla/5.0 (X11; Linux i686; rv:2.0b3pre) Gecko/20100731 Firefox/4.0b3pre" 66 | "Mozilla/5.0 (Windows NT 5.2; rv:2.0b13pre) Gecko/20110304 Firefox/4.0b13pre" 67 | "Mozilla/5.0 (Windows NT 5.1; rv:2.0b13pre) Gecko/20110223 Firefox/4.0b13pre" 68 | "Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20110204 Firefox/4.0b12pre" 69 | "Mozilla/5.0 (X11; Linux i686; rv:2.0b12pre) Gecko/20100101 Firefox/4.0b12pre" 70 | "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre" 71 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110131 Firefox/4.0b11pre" 72 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110129 Firefox/4.0b11pre" 73 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b11pre) Gecko/20110128 Firefox/4.0b11pre" 74 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre" 75 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0b11pre) Gecko/20110126 Firefox/4.0b11pre" 76 | "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0b10pre) Gecko/20110118 Firefox/4.0b10pre" 77 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0b10pre) Gecko/20110113 Firefox/4.0b10pre" 78 | "Mozilla/5.0 (X11; Linux i686; rv:2.0b10) Gecko/20100101 Firefox/4.0b10" 79 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:2.0b10) Gecko/20110126 Firefox/4.0b10" 80 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0b10) Gecko/20110126 Firefox/4.0b10" 81 | "Mozilla/5.0 (X11; U; Linux x86_64; pl-PL; rv:2.0) Gecko/20110307 Firefox/4.0" 82 | "Mozilla/5.0 (X11; U; Linux i686; en-GB; rv:2.0) Gecko/20110404 Fedora/16-dev Firefox/4.0" 83 | "Mozilla/5.0 (X11; Arch Linux i686; rv:2.0) Gecko/20110321 Firefox/4.0" 84 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.3) Gecko/20100401 Firefox/4.0 (.NET CLR 3.5.30729)" 85 | "Mozilla/5.0 (Windows NT 6.1; rv:2.0) Gecko/20110319 Firefox/4.0" 86 | "Mozilla/5.0 (Windows NT 6.1; rv:1.9) Gecko/20100101 Firefox/4.0" 87 | "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/20121223 Ubuntu/9.25 (jaunty) Firefox/3.8" 88 | "Mozilla/5.0 (X11; U; Linux i686; pl-PL; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8" 89 | "Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8" 90 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.3) Gecko/20100401 Mozilla/5.0 (X11; U; Linux i686; it-IT; rv:1.9.0.2) Gecko/2008092313 Ubuntu/9.25 (jaunty) Firefox/3.8" 91 | "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.3a5pre) Gecko/20100526 Firefox/3.7a5pre" 92 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5" 93 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5" 94 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b5) Gecko/20091204 Firefox/3.6b5" 95 | "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2) Gecko/20091218 Firefox 3.6b5" 96 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; fr; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4 (.NET CLR 3.5.30729)" 97 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2b4) Gecko/20091124 Firefox/3.6b4" 98 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2b1) Gecko/20091014 Firefox/3.6b1 GTB5" 99 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090428 Firefox/3.6a1pre" 100 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2a1pre) Gecko/20090405 Firefox/3.6a1pre" 101 | "Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.9.2a1pre) Gecko/20090405 Ubuntu/9.04 (jaunty) Firefox/3.6a1pre" 102 | "Mozilla/5.0 (Windows; Windows NT 5.1; es-ES; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre" 103 | "Mozilla/5.0 (Windows; Windows NT 5.1; en-US; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre" 104 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.2a1pre) Gecko/20090402 Firefox/3.6a1pre (.NET CLR 3.5.30729)" 105 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.9) Gecko/20100915 Gentoo Firefox/3.6.9" 106 | "Mozilla/5.0 (X11; U; FreeBSD i386; en-US; rv:1.9.2.9) Gecko/20100913 Firefox/3.6.9" 107 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9 ( .NET CLR 3.5.30729; .NET CLR 4.0.20506)" 108 | "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-GB; rv:1.9.2.9) Gecko/20100824 Firefox/3.6.9" 109 | "Mozilla/5.0 (X11; U; OpenBSD i386; en-US; rv:1.9.2.8) Gecko/20101230 Firefox/3.6.8" 110 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100804 Gentoo Firefox/3.6.8" 111 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.8) Gecko/20100723 SUSE/3.6.8-0.1.1 Firefox/3.6.8" 112 | "Mozilla/5.0 (X11; U; Linux i686; zh-CN; rv:1.9.2.8) Gecko/20100722 Ubuntu/10.04 (lucid) Firefox/3.6.8" 113 | "Mozilla/5.0 (X11; U; Linux i686; ru; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8" 114 | "Mozilla/5.0 (X11; U; Linux i686; fi-FI; rv:1.9.2.8) Gecko/20100723 Ubuntu/10.04 (lucid) Firefox/3.6.8" 115 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.8) Gecko/20100727 Firefox/3.6.8" 116 | "Mozilla/5.0 (X11; U; Linux i686; de-DE; rv:1.9.2.8) Gecko/20100725 Gentoo Firefox/3.6.8" 117 | "Mozilla/5.0 (X11; U; FreeBSD i386; de-CH; rv:1.9.2.8) Gecko/20100729 Firefox/3.6.8" 118 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" 119 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-BR; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 GTB7.1" 120 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.8) Gecko/20100722 AskTbADAP/3.9.1.14019 Firefox/3.6.8" 121 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; he; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" 122 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; fr; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8 GTB7.1" 123 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-GB; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0C)" 124 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.8) Gecko/20100722 Firefox 3.6.8" 125 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; de; rv:1.9.2.3) Gecko/20121221 Firefox/3.6.8" 126 | "Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-TW; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" 127 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8" 128 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; tr; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 ( .NET CLR 3.5.30729; .NET4.0E" 129 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100809 Fedora/3.6.7-1.fc14 Firefox/3.6.7" 130 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.7) Gecko/20100723 Fedora/3.6.7-1.fc13 Firefox/3.6.7" 131 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.7) Gecko/20100726 CentOS/3.6-3.el5.centos Firefox/3.6.7" 132 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; hu; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 GTB7.1" 133 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.7 (.NET CLR 3.5.30729)" 134 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; pt-PT; rv:1.9.2.7) Gecko/20100713 Firefox/3.6.7 (.NET CLR 3.5.30729)" 135 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.1" 136 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 GTB7.0" 137 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6 (.NET CLR 3.5.30729)" 138 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.6) Gecko/20100628 Ubuntu/10.04 (lucid) Firefox/3.6.6" 139 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; pt-PT; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6" 140 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729)" 141 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 (.NET CLR 3.5.30729)" 142 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 GTB7.1" 143 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; nl; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6" 144 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; it; rv:1.9.2.6) Gecko/20100625 Firefox/3.6.6 ( .NET CLR 3.5.30729; .NET4.0E)" 145 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.4) Gecko/20100614 Ubuntu/10.04 (lucid) Firefox/3.6.4" 146 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.4) Gecko/20100625 Gentoo Firefox/3.6.4" 147 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 ( .NET CLR 3.5.30729)" 148 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; ru; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4" 149 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; ja; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.1" 150 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; cs; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)" 151 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4" 152 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; ja; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 ( .NET CLR 3.5.30729)" 153 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; fr; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)" 154 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4 (.NET CLR 3.5.30729)" 155 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100527 Firefox/3.6.4" 156 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4 ( .NET CLR 3.5.30729)" 157 | "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)" 158 | "Mozilla/5.0 (Windows; U; Windows NT 5.2; en-CA; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4" 159 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-TW; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 GTB7.0 ( .NET CLR 3.5.30729)" 160 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100513 Firefox/3.6.4 (.NET CLR 3.5.30729)" 161 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.2.4) Gecko/20100503 Firefox/3.6.4 ( .NET CLR 3.5.30729)" 162 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; nb-NO; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4 (.NET CLR 3.5.30729)" 163 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; ko; rv:1.9.2.4) Gecko/20100523 Firefox/3.6.4" 164 | "Mozilla/5.0 (Windows; U; Windows NT 5.1; cs; rv:1.9.2.4) Gecko/20100611 Firefox/3.6.4" 165 | "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.2.3pre) Gecko/20100405 Firefox/3.6.3plugin1 ( .NET CLR 3.5.30729)" 166 | "Mozilla/5.0 (X11; U; Linux x86_64; fr; rv:1.9.2.3) Gecko/20100403 Fedora/3.6.3-4.fc13 Firefox/3.6.3" 167 | "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100403 Firefox/3.6.3" 168 | "Mozilla/5.0 (X11; U; Linux x86_64; de; rv:1.9.2.3) Gecko/20100401 SUSE/3.6.3-1.1 Firefox/3.6.3" 169 | "Mozilla/5.0 (X11; U; Linux i686; ko-KR; rv:1.9.2.3) Gecko/20100423 Ubuntu/10.04 (lucid) Firefox/3.6.3" 170 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100404 Ubuntu/10.04 (lucid) Firefox/3.6.3" 171 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 GTB7.1" 172 | "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.3" 173 | -------------------------------------------------------------------------------- /web_server/server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "0.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "JSONStream": { 8 | "version": "1.3.4", 9 | "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz", 10 | "integrity": "sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg==", 11 | "requires": { 12 | "jsonparse": "^1.2.0", 13 | "through": ">=2.2.7 <3" 14 | } 15 | }, 16 | "accepts": { 17 | "version": "1.3.5", 18 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 19 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 20 | "requires": { 21 | "mime-types": "~2.1.18", 22 | "negotiator": "0.6.1" 23 | } 24 | }, 25 | "acorn": { 26 | "version": "2.7.0", 27 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", 28 | "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" 29 | }, 30 | "acorn-globals": { 31 | "version": "1.0.9", 32 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", 33 | "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", 34 | "requires": { 35 | "acorn": "^2.1.0" 36 | } 37 | }, 38 | "align-text": { 39 | "version": "0.1.4", 40 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 41 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 42 | "requires": { 43 | "kind-of": "^3.0.2", 44 | "longest": "^1.0.1", 45 | "repeat-string": "^1.5.2" 46 | } 47 | }, 48 | "amdefine": { 49 | "version": "1.0.1", 50 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 51 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" 52 | }, 53 | "array-flatten": { 54 | "version": "1.1.1", 55 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 56 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 57 | }, 58 | "asap": { 59 | "version": "1.0.0", 60 | "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", 61 | "integrity": "sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0=" 62 | }, 63 | "async": { 64 | "version": "2.6.1", 65 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 66 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 67 | "requires": { 68 | "lodash": "^4.17.10" 69 | } 70 | }, 71 | "basic-auth": { 72 | "version": "2.0.0", 73 | "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.0.tgz", 74 | "integrity": "sha1-AV2z81PgLlY3d1X5YnQuiYHnu7o=", 75 | "requires": { 76 | "safe-buffer": "5.1.1" 77 | } 78 | }, 79 | "bcrypt": { 80 | "version": "3.0.0", 81 | "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-3.0.0.tgz", 82 | "integrity": "sha512-gjicxsD4e5U3nH0EqiEb5y+fKpsZ7F52wcnmNfu45nxnolWVAYh7NgbdfilY+5x1v6cLspxmzz4hf+ju2pFxhA==", 83 | "requires": { 84 | "nan": "2.10.0", 85 | "node-pre-gyp": "0.10.2" 86 | }, 87 | "dependencies": { 88 | "abbrev": { 89 | "version": "1.1.1", 90 | "bundled": true 91 | }, 92 | "ansi-regex": { 93 | "version": "2.1.1", 94 | "bundled": true 95 | }, 96 | "aproba": { 97 | "version": "1.2.0", 98 | "bundled": true 99 | }, 100 | "are-we-there-yet": { 101 | "version": "1.1.5", 102 | "bundled": true, 103 | "requires": { 104 | "delegates": "^1.0.0", 105 | "readable-stream": "^2.0.6" 106 | } 107 | }, 108 | "balanced-match": { 109 | "version": "1.0.0", 110 | "bundled": true 111 | }, 112 | "brace-expansion": { 113 | "version": "1.1.11", 114 | "bundled": true, 115 | "requires": { 116 | "balanced-match": "^1.0.0", 117 | "concat-map": "0.0.1" 118 | } 119 | }, 120 | "chownr": { 121 | "version": "1.0.1", 122 | "bundled": true 123 | }, 124 | "code-point-at": { 125 | "version": "1.1.0", 126 | "bundled": true 127 | }, 128 | "concat-map": { 129 | "version": "0.0.1", 130 | "bundled": true 131 | }, 132 | "console-control-strings": { 133 | "version": "1.1.0", 134 | "bundled": true 135 | }, 136 | "core-util-is": { 137 | "version": "1.0.2", 138 | "bundled": true 139 | }, 140 | "debug": { 141 | "version": "2.6.9", 142 | "bundled": true, 143 | "requires": { 144 | "ms": "2.0.0" 145 | } 146 | }, 147 | "deep-extend": { 148 | "version": "0.6.0", 149 | "bundled": true 150 | }, 151 | "delegates": { 152 | "version": "1.0.0", 153 | "bundled": true 154 | }, 155 | "detect-libc": { 156 | "version": "1.0.3", 157 | "bundled": true 158 | }, 159 | "fs-minipass": { 160 | "version": "1.2.5", 161 | "bundled": true, 162 | "requires": { 163 | "minipass": "^2.2.1" 164 | } 165 | }, 166 | "fs.realpath": { 167 | "version": "1.0.0", 168 | "bundled": true 169 | }, 170 | "gauge": { 171 | "version": "2.7.4", 172 | "bundled": true, 173 | "requires": { 174 | "aproba": "^1.0.3", 175 | "console-control-strings": "^1.0.0", 176 | "has-unicode": "^2.0.0", 177 | "object-assign": "^4.1.0", 178 | "signal-exit": "^3.0.0", 179 | "string-width": "^1.0.1", 180 | "strip-ansi": "^3.0.1", 181 | "wide-align": "^1.1.0" 182 | } 183 | }, 184 | "glob": { 185 | "version": "7.1.2", 186 | "bundled": true, 187 | "requires": { 188 | "fs.realpath": "^1.0.0", 189 | "inflight": "^1.0.4", 190 | "inherits": "2", 191 | "minimatch": "^3.0.4", 192 | "once": "^1.3.0", 193 | "path-is-absolute": "^1.0.0" 194 | } 195 | }, 196 | "has-unicode": { 197 | "version": "2.0.1", 198 | "bundled": true 199 | }, 200 | "iconv-lite": { 201 | "version": "0.4.23", 202 | "bundled": true, 203 | "requires": { 204 | "safer-buffer": ">= 2.1.2 < 3" 205 | } 206 | }, 207 | "ignore-walk": { 208 | "version": "3.0.1", 209 | "bundled": true, 210 | "requires": { 211 | "minimatch": "^3.0.4" 212 | } 213 | }, 214 | "inflight": { 215 | "version": "1.0.6", 216 | "bundled": true, 217 | "requires": { 218 | "once": "^1.3.0", 219 | "wrappy": "1" 220 | } 221 | }, 222 | "inherits": { 223 | "version": "2.0.3", 224 | "bundled": true 225 | }, 226 | "ini": { 227 | "version": "1.3.5", 228 | "bundled": true 229 | }, 230 | "is-fullwidth-code-point": { 231 | "version": "1.0.0", 232 | "bundled": true, 233 | "requires": { 234 | "number-is-nan": "^1.0.0" 235 | } 236 | }, 237 | "isarray": { 238 | "version": "1.0.0", 239 | "bundled": true 240 | }, 241 | "minimatch": { 242 | "version": "3.0.4", 243 | "bundled": true, 244 | "requires": { 245 | "brace-expansion": "^1.1.7" 246 | } 247 | }, 248 | "minimist": { 249 | "version": "0.0.8", 250 | "bundled": true 251 | }, 252 | "minipass": { 253 | "version": "2.3.3", 254 | "bundled": true, 255 | "requires": { 256 | "safe-buffer": "^5.1.2", 257 | "yallist": "^3.0.0" 258 | }, 259 | "dependencies": { 260 | "safe-buffer": { 261 | "version": "5.1.2", 262 | "bundled": true 263 | }, 264 | "yallist": { 265 | "version": "3.0.2", 266 | "bundled": true 267 | } 268 | } 269 | }, 270 | "minizlib": { 271 | "version": "1.1.0", 272 | "bundled": true, 273 | "requires": { 274 | "minipass": "^2.2.1" 275 | } 276 | }, 277 | "mkdirp": { 278 | "version": "0.5.1", 279 | "bundled": true, 280 | "requires": { 281 | "minimist": "0.0.8" 282 | } 283 | }, 284 | "ms": { 285 | "version": "2.0.0", 286 | "bundled": true 287 | }, 288 | "needle": { 289 | "version": "2.2.1", 290 | "bundled": true, 291 | "requires": { 292 | "debug": "^2.1.2", 293 | "iconv-lite": "^0.4.4", 294 | "sax": "^1.2.4" 295 | } 296 | }, 297 | "node-pre-gyp": { 298 | "version": "0.10.2", 299 | "bundled": true, 300 | "requires": { 301 | "detect-libc": "^1.0.2", 302 | "mkdirp": "^0.5.1", 303 | "needle": "^2.2.0", 304 | "nopt": "^4.0.1", 305 | "npm-packlist": "^1.1.6", 306 | "npmlog": "^4.0.2", 307 | "rc": "^1.2.7", 308 | "rimraf": "^2.6.1", 309 | "semver": "^5.3.0", 310 | "tar": "^4" 311 | } 312 | }, 313 | "nopt": { 314 | "version": "4.0.1", 315 | "bundled": true, 316 | "requires": { 317 | "abbrev": "1", 318 | "osenv": "^0.1.4" 319 | } 320 | }, 321 | "npm-bundled": { 322 | "version": "1.0.3", 323 | "bundled": true 324 | }, 325 | "npm-packlist": { 326 | "version": "1.1.10", 327 | "bundled": true, 328 | "requires": { 329 | "ignore-walk": "^3.0.1", 330 | "npm-bundled": "^1.0.1" 331 | } 332 | }, 333 | "npmlog": { 334 | "version": "4.1.2", 335 | "bundled": true, 336 | "requires": { 337 | "are-we-there-yet": "~1.1.2", 338 | "console-control-strings": "~1.1.0", 339 | "gauge": "~2.7.3", 340 | "set-blocking": "~2.0.0" 341 | } 342 | }, 343 | "number-is-nan": { 344 | "version": "1.0.1", 345 | "bundled": true 346 | }, 347 | "object-assign": { 348 | "version": "4.1.1", 349 | "bundled": true 350 | }, 351 | "once": { 352 | "version": "1.4.0", 353 | "bundled": true, 354 | "requires": { 355 | "wrappy": "1" 356 | } 357 | }, 358 | "os-homedir": { 359 | "version": "1.0.2", 360 | "bundled": true 361 | }, 362 | "os-tmpdir": { 363 | "version": "1.0.2", 364 | "bundled": true 365 | }, 366 | "osenv": { 367 | "version": "0.1.5", 368 | "bundled": true, 369 | "requires": { 370 | "os-homedir": "^1.0.0", 371 | "os-tmpdir": "^1.0.0" 372 | } 373 | }, 374 | "path-is-absolute": { 375 | "version": "1.0.1", 376 | "bundled": true 377 | }, 378 | "process-nextick-args": { 379 | "version": "2.0.0", 380 | "bundled": true 381 | }, 382 | "rc": { 383 | "version": "1.2.8", 384 | "bundled": true, 385 | "requires": { 386 | "deep-extend": "^0.6.0", 387 | "ini": "~1.3.0", 388 | "minimist": "^1.2.0", 389 | "strip-json-comments": "~2.0.1" 390 | }, 391 | "dependencies": { 392 | "minimist": { 393 | "version": "1.2.0", 394 | "bundled": true 395 | } 396 | } 397 | }, 398 | "readable-stream": { 399 | "version": "2.3.5", 400 | "bundled": true, 401 | "requires": { 402 | "core-util-is": "~1.0.0", 403 | "inherits": "~2.0.3", 404 | "isarray": "~1.0.0", 405 | "process-nextick-args": "~2.0.0", 406 | "safe-buffer": "~5.1.1", 407 | "string_decoder": "~1.0.3", 408 | "util-deprecate": "~1.0.1" 409 | } 410 | }, 411 | "rimraf": { 412 | "version": "2.6.2", 413 | "bundled": true, 414 | "requires": { 415 | "glob": "^7.0.5" 416 | } 417 | }, 418 | "safe-buffer": { 419 | "version": "5.1.1", 420 | "bundled": true 421 | }, 422 | "safer-buffer": { 423 | "version": "2.1.2", 424 | "bundled": true 425 | }, 426 | "sax": { 427 | "version": "1.2.4", 428 | "bundled": true 429 | }, 430 | "semver": { 431 | "version": "5.5.0", 432 | "bundled": true 433 | }, 434 | "set-blocking": { 435 | "version": "2.0.0", 436 | "bundled": true 437 | }, 438 | "signal-exit": { 439 | "version": "3.0.2", 440 | "bundled": true 441 | }, 442 | "string-width": { 443 | "version": "1.0.2", 444 | "bundled": true, 445 | "requires": { 446 | "code-point-at": "^1.0.0", 447 | "is-fullwidth-code-point": "^1.0.0", 448 | "strip-ansi": "^3.0.0" 449 | } 450 | }, 451 | "string_decoder": { 452 | "version": "1.0.3", 453 | "bundled": true, 454 | "requires": { 455 | "safe-buffer": "~5.1.0" 456 | } 457 | }, 458 | "strip-ansi": { 459 | "version": "3.0.1", 460 | "bundled": true, 461 | "requires": { 462 | "ansi-regex": "^2.0.0" 463 | } 464 | }, 465 | "strip-json-comments": { 466 | "version": "2.0.1", 467 | "bundled": true 468 | }, 469 | "tar": { 470 | "version": "4.4.4", 471 | "bundled": true, 472 | "requires": { 473 | "chownr": "^1.0.1", 474 | "fs-minipass": "^1.2.5", 475 | "minipass": "^2.3.3", 476 | "minizlib": "^1.1.0", 477 | "mkdirp": "^0.5.0", 478 | "safe-buffer": "^5.1.2", 479 | "yallist": "^3.0.2" 480 | }, 481 | "dependencies": { 482 | "safe-buffer": { 483 | "version": "5.1.2", 484 | "bundled": true 485 | }, 486 | "yallist": { 487 | "version": "3.0.2", 488 | "bundled": true 489 | } 490 | } 491 | }, 492 | "util-deprecate": { 493 | "version": "1.0.2", 494 | "bundled": true 495 | }, 496 | "wide-align": { 497 | "version": "1.1.3", 498 | "bundled": true, 499 | "requires": { 500 | "string-width": "^1.0.2 || 2" 501 | } 502 | }, 503 | "wrappy": { 504 | "version": "1.0.2", 505 | "bundled": true 506 | } 507 | } 508 | }, 509 | "bluebird": { 510 | "version": "3.5.1", 511 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", 512 | "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" 513 | }, 514 | "body-parser": { 515 | "version": "1.18.3", 516 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", 517 | "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", 518 | "requires": { 519 | "bytes": "3.0.0", 520 | "content-type": "~1.0.4", 521 | "debug": "2.6.9", 522 | "depd": "~1.1.2", 523 | "http-errors": "~1.6.3", 524 | "iconv-lite": "0.4.23", 525 | "on-finished": "~2.3.0", 526 | "qs": "6.5.2", 527 | "raw-body": "2.3.3", 528 | "type-is": "~1.6.16" 529 | }, 530 | "dependencies": { 531 | "qs": { 532 | "version": "6.5.2", 533 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 534 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 535 | } 536 | } 537 | }, 538 | "bson": { 539 | "version": "1.0.9", 540 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", 541 | "integrity": "sha512-IQX9/h7WdMBIW/q/++tGd+emQr0XMdeZ6icnT/74Xk9fnabWn+gZgpE+9V+gujL3hhJOoNrnDVY7tWdzc7NUTg==" 542 | }, 543 | "buffer-equal-constant-time": { 544 | "version": "1.0.1", 545 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", 546 | "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" 547 | }, 548 | "bytes": { 549 | "version": "3.0.0", 550 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 551 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 552 | }, 553 | "camelcase": { 554 | "version": "1.2.1", 555 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 556 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" 557 | }, 558 | "center-align": { 559 | "version": "0.1.3", 560 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 561 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 562 | "requires": { 563 | "align-text": "^0.1.3", 564 | "lazy-cache": "^1.0.3" 565 | } 566 | }, 567 | "character-parser": { 568 | "version": "1.2.1", 569 | "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", 570 | "integrity": "sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY=" 571 | }, 572 | "clean-css": { 573 | "version": "3.4.28", 574 | "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", 575 | "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", 576 | "requires": { 577 | "commander": "2.8.x", 578 | "source-map": "0.4.x" 579 | }, 580 | "dependencies": { 581 | "commander": { 582 | "version": "2.8.1", 583 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", 584 | "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", 585 | "requires": { 586 | "graceful-readlink": ">= 1.0.0" 587 | } 588 | } 589 | } 590 | }, 591 | "cliui": { 592 | "version": "2.1.0", 593 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 594 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 595 | "requires": { 596 | "center-align": "^0.1.1", 597 | "right-align": "^0.1.1", 598 | "wordwrap": "0.0.2" 599 | }, 600 | "dependencies": { 601 | "wordwrap": { 602 | "version": "0.0.2", 603 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 604 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" 605 | } 606 | } 607 | }, 608 | "commander": { 609 | "version": "2.6.0", 610 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", 611 | "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" 612 | }, 613 | "constantinople": { 614 | "version": "3.0.2", 615 | "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", 616 | "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", 617 | "requires": { 618 | "acorn": "^2.1.0" 619 | } 620 | }, 621 | "content-disposition": { 622 | "version": "0.5.2", 623 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 624 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 625 | }, 626 | "content-type": { 627 | "version": "1.0.4", 628 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 629 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 630 | }, 631 | "cookie": { 632 | "version": "0.3.1", 633 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 634 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 635 | }, 636 | "cookie-parser": { 637 | "version": "1.4.3", 638 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.3.tgz", 639 | "integrity": "sha1-D+MfoZ0AC5X0qt8fU/3CuKIDuqU=", 640 | "requires": { 641 | "cookie": "0.3.1", 642 | "cookie-signature": "1.0.6" 643 | } 644 | }, 645 | "cookie-signature": { 646 | "version": "1.0.6", 647 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 648 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 649 | }, 650 | "cors": { 651 | "version": "2.8.4", 652 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.4.tgz", 653 | "integrity": "sha1-K9OB8usgECAQXNUOpZ2mMJBpRoY=", 654 | "requires": { 655 | "object-assign": "^4", 656 | "vary": "^1" 657 | } 658 | }, 659 | "css": { 660 | "version": "1.0.8", 661 | "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", 662 | "integrity": "sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c=", 663 | "requires": { 664 | "css-parse": "1.0.4", 665 | "css-stringify": "1.0.5" 666 | } 667 | }, 668 | "css-parse": { 669 | "version": "1.0.4", 670 | "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", 671 | "integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90=" 672 | }, 673 | "css-stringify": { 674 | "version": "1.0.5", 675 | "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", 676 | "integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE=" 677 | }, 678 | "debug": { 679 | "version": "2.6.9", 680 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 681 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 682 | "requires": { 683 | "ms": "2.0.0" 684 | } 685 | }, 686 | "decamelize": { 687 | "version": "1.2.0", 688 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 689 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 690 | }, 691 | "depd": { 692 | "version": "1.1.2", 693 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 694 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 695 | }, 696 | "destroy": { 697 | "version": "1.0.4", 698 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 699 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 700 | }, 701 | "ecdsa-sig-formatter": { 702 | "version": "1.0.10", 703 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz", 704 | "integrity": "sha1-HFlQAPBKiJffuFAAiSoPTDOvhsM=", 705 | "requires": { 706 | "safe-buffer": "^5.0.1" 707 | } 708 | }, 709 | "ee-first": { 710 | "version": "1.1.1", 711 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 712 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 713 | }, 714 | "encodeurl": { 715 | "version": "1.0.2", 716 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 717 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 718 | }, 719 | "es6-promise": { 720 | "version": "4.2.4", 721 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", 722 | "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" 723 | }, 724 | "es6-promisify": { 725 | "version": "5.0.0", 726 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 727 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 728 | "requires": { 729 | "es6-promise": "^4.0.3" 730 | } 731 | }, 732 | "escape-html": { 733 | "version": "1.0.3", 734 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 735 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 736 | }, 737 | "etag": { 738 | "version": "1.8.1", 739 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 740 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 741 | }, 742 | "express": { 743 | "version": "4.16.3", 744 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 745 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 746 | "requires": { 747 | "accepts": "~1.3.5", 748 | "array-flatten": "1.1.1", 749 | "body-parser": "1.18.2", 750 | "content-disposition": "0.5.2", 751 | "content-type": "~1.0.4", 752 | "cookie": "0.3.1", 753 | "cookie-signature": "1.0.6", 754 | "debug": "2.6.9", 755 | "depd": "~1.1.2", 756 | "encodeurl": "~1.0.2", 757 | "escape-html": "~1.0.3", 758 | "etag": "~1.8.1", 759 | "finalhandler": "1.1.1", 760 | "fresh": "0.5.2", 761 | "merge-descriptors": "1.0.1", 762 | "methods": "~1.1.2", 763 | "on-finished": "~2.3.0", 764 | "parseurl": "~1.3.2", 765 | "path-to-regexp": "0.1.7", 766 | "proxy-addr": "~2.0.3", 767 | "qs": "6.5.1", 768 | "range-parser": "~1.2.0", 769 | "safe-buffer": "5.1.1", 770 | "send": "0.16.2", 771 | "serve-static": "1.13.2", 772 | "setprototypeof": "1.1.0", 773 | "statuses": "~1.4.0", 774 | "type-is": "~1.6.16", 775 | "utils-merge": "1.0.1", 776 | "vary": "~1.1.2" 777 | }, 778 | "dependencies": { 779 | "body-parser": { 780 | "version": "1.18.2", 781 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 782 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 783 | "requires": { 784 | "bytes": "3.0.0", 785 | "content-type": "~1.0.4", 786 | "debug": "2.6.9", 787 | "depd": "~1.1.1", 788 | "http-errors": "~1.6.2", 789 | "iconv-lite": "0.4.19", 790 | "on-finished": "~2.3.0", 791 | "qs": "6.5.1", 792 | "raw-body": "2.3.2", 793 | "type-is": "~1.6.15" 794 | } 795 | }, 796 | "iconv-lite": { 797 | "version": "0.4.19", 798 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 799 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 800 | }, 801 | "raw-body": { 802 | "version": "2.3.2", 803 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 804 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 805 | "requires": { 806 | "bytes": "3.0.0", 807 | "http-errors": "1.6.2", 808 | "iconv-lite": "0.4.19", 809 | "unpipe": "1.0.0" 810 | }, 811 | "dependencies": { 812 | "depd": { 813 | "version": "1.1.1", 814 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 815 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 816 | }, 817 | "http-errors": { 818 | "version": "1.6.2", 819 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 820 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 821 | "requires": { 822 | "depd": "1.1.1", 823 | "inherits": "2.0.3", 824 | "setprototypeof": "1.0.3", 825 | "statuses": ">= 1.3.1 < 2" 826 | } 827 | }, 828 | "setprototypeof": { 829 | "version": "1.0.3", 830 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 831 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 832 | } 833 | } 834 | } 835 | } 836 | }, 837 | "eyes": { 838 | "version": "0.1.8", 839 | "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", 840 | "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" 841 | }, 842 | "finalhandler": { 843 | "version": "1.1.1", 844 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 845 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 846 | "requires": { 847 | "debug": "2.6.9", 848 | "encodeurl": "~1.0.2", 849 | "escape-html": "~1.0.3", 850 | "on-finished": "~2.3.0", 851 | "parseurl": "~1.3.2", 852 | "statuses": "~1.4.0", 853 | "unpipe": "~1.0.0" 854 | } 855 | }, 856 | "forwarded": { 857 | "version": "0.1.2", 858 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 859 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 860 | }, 861 | "fresh": { 862 | "version": "0.5.2", 863 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 864 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 865 | }, 866 | "graceful-readlink": { 867 | "version": "1.0.1", 868 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 869 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" 870 | }, 871 | "http-errors": { 872 | "version": "1.6.3", 873 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 874 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 875 | "requires": { 876 | "depd": "~1.1.2", 877 | "inherits": "2.0.3", 878 | "setprototypeof": "1.1.0", 879 | "statuses": ">= 1.4.0 < 2" 880 | } 881 | }, 882 | "iconv-lite": { 883 | "version": "0.4.23", 884 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", 885 | "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", 886 | "requires": { 887 | "safer-buffer": ">= 2.1.2 < 3" 888 | } 889 | }, 890 | "inherits": { 891 | "version": "2.0.3", 892 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 893 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 894 | }, 895 | "ipaddr.js": { 896 | "version": "1.8.0", 897 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", 898 | "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" 899 | }, 900 | "is-buffer": { 901 | "version": "1.1.6", 902 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 903 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 904 | }, 905 | "is-promise": { 906 | "version": "2.1.0", 907 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 908 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" 909 | }, 910 | "jade": { 911 | "version": "1.11.0", 912 | "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", 913 | "integrity": "sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0=", 914 | "requires": { 915 | "character-parser": "1.2.1", 916 | "clean-css": "^3.1.9", 917 | "commander": "~2.6.0", 918 | "constantinople": "~3.0.1", 919 | "jstransformer": "0.0.2", 920 | "mkdirp": "~0.5.0", 921 | "transformers": "2.1.0", 922 | "uglify-js": "^2.4.19", 923 | "void-elements": "~2.0.1", 924 | "with": "~4.0.0" 925 | } 926 | }, 927 | "jayson": { 928 | "version": "2.0.6", 929 | "resolved": "https://registry.npmjs.org/jayson/-/jayson-2.0.6.tgz", 930 | "integrity": "sha512-ZIzF3DZ3ig9rNeOLUzGUbpxjOMqrfUX9a2H+3cmQHIGdBk8nZMEmHVZNXwxNYchgx6UnB/CFV+miFEfOjmMBmA==", 931 | "requires": { 932 | "JSONStream": "^1.3.1", 933 | "commander": "^2.12.2", 934 | "es6-promisify": "^5.0.0", 935 | "eyes": "^0.1.8", 936 | "json-stringify-safe": "^5.0.1", 937 | "lodash": "^4.17.4", 938 | "uuid": "^3.2.1" 939 | }, 940 | "dependencies": { 941 | "commander": { 942 | "version": "2.17.1", 943 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", 944 | "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" 945 | } 946 | } 947 | }, 948 | "json-stringify-safe": { 949 | "version": "5.0.1", 950 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 951 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 952 | }, 953 | "jsonparse": { 954 | "version": "1.3.1", 955 | "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", 956 | "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" 957 | }, 958 | "jsonwebtoken": { 959 | "version": "8.3.0", 960 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.3.0.tgz", 961 | "integrity": "sha512-oge/hvlmeJCH+iIz1DwcO7vKPkNGJHhgkspk8OH3VKlw+mbi42WtD4ig1+VXRln765vxptAv+xT26Fd3cteqag==", 962 | "requires": { 963 | "jws": "^3.1.5", 964 | "lodash.includes": "^4.3.0", 965 | "lodash.isboolean": "^3.0.3", 966 | "lodash.isinteger": "^4.0.4", 967 | "lodash.isnumber": "^3.0.3", 968 | "lodash.isplainobject": "^4.0.6", 969 | "lodash.isstring": "^4.0.1", 970 | "lodash.once": "^4.0.0", 971 | "ms": "^2.1.1" 972 | }, 973 | "dependencies": { 974 | "ms": { 975 | "version": "2.1.1", 976 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 977 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 978 | } 979 | } 980 | }, 981 | "jstransformer": { 982 | "version": "0.0.2", 983 | "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", 984 | "integrity": "sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs=", 985 | "requires": { 986 | "is-promise": "^2.0.0", 987 | "promise": "^6.0.1" 988 | } 989 | }, 990 | "jwa": { 991 | "version": "1.1.6", 992 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.1.6.tgz", 993 | "integrity": "sha512-tBO/cf++BUsJkYql/kBbJroKOgHWEigTKBAjjBEmrMGYd1QMBC74Hr4Wo2zCZw6ZrVhlJPvoMrkcOnlWR/DJfw==", 994 | "requires": { 995 | "buffer-equal-constant-time": "1.0.1", 996 | "ecdsa-sig-formatter": "1.0.10", 997 | "safe-buffer": "^5.0.1" 998 | } 999 | }, 1000 | "jws": { 1001 | "version": "3.1.5", 1002 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.1.5.tgz", 1003 | "integrity": "sha512-GsCSexFADNQUr8T5HPJvayTjvPIfoyJPtLQBwn5a4WZQchcrPMPMAWcC1AzJVRDKyD6ZPROPAxgv6rfHViO4uQ==", 1004 | "requires": { 1005 | "jwa": "^1.1.5", 1006 | "safe-buffer": "^5.0.1" 1007 | } 1008 | }, 1009 | "kareem": { 1010 | "version": "2.2.1", 1011 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.2.1.tgz", 1012 | "integrity": "sha512-xpDFy8OxkFM+vK6pXy6JmH92ibeEFUuDWzas5M9L7MzVmHW3jzwAHxodCPV/BYkf4A31bVDLyonrMfp9RXb/oA==" 1013 | }, 1014 | "kind-of": { 1015 | "version": "3.2.2", 1016 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 1017 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 1018 | "requires": { 1019 | "is-buffer": "^1.1.5" 1020 | } 1021 | }, 1022 | "lazy-cache": { 1023 | "version": "1.0.4", 1024 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 1025 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" 1026 | }, 1027 | "lodash": { 1028 | "version": "4.17.10", 1029 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", 1030 | "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" 1031 | }, 1032 | "lodash.get": { 1033 | "version": "4.4.2", 1034 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 1035 | "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" 1036 | }, 1037 | "lodash.includes": { 1038 | "version": "4.3.0", 1039 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", 1040 | "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" 1041 | }, 1042 | "lodash.isboolean": { 1043 | "version": "3.0.3", 1044 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", 1045 | "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" 1046 | }, 1047 | "lodash.isinteger": { 1048 | "version": "4.0.4", 1049 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", 1050 | "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" 1051 | }, 1052 | "lodash.isnumber": { 1053 | "version": "3.0.3", 1054 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", 1055 | "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" 1056 | }, 1057 | "lodash.isplainobject": { 1058 | "version": "4.0.6", 1059 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", 1060 | "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" 1061 | }, 1062 | "lodash.isstring": { 1063 | "version": "4.0.1", 1064 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", 1065 | "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" 1066 | }, 1067 | "lodash.once": { 1068 | "version": "4.1.1", 1069 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", 1070 | "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" 1071 | }, 1072 | "longest": { 1073 | "version": "1.0.1", 1074 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 1075 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" 1076 | }, 1077 | "media-typer": { 1078 | "version": "0.3.0", 1079 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 1080 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 1081 | }, 1082 | "merge-descriptors": { 1083 | "version": "1.0.1", 1084 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 1085 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 1086 | }, 1087 | "methods": { 1088 | "version": "1.1.2", 1089 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 1090 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 1091 | }, 1092 | "mime": { 1093 | "version": "1.4.1", 1094 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 1095 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 1096 | }, 1097 | "mime-db": { 1098 | "version": "1.36.0", 1099 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", 1100 | "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" 1101 | }, 1102 | "mime-types": { 1103 | "version": "2.1.20", 1104 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", 1105 | "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", 1106 | "requires": { 1107 | "mime-db": "~1.36.0" 1108 | } 1109 | }, 1110 | "minimist": { 1111 | "version": "0.0.8", 1112 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1113 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" 1114 | }, 1115 | "mkdirp": { 1116 | "version": "0.5.1", 1117 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1118 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1119 | "requires": { 1120 | "minimist": "0.0.8" 1121 | } 1122 | }, 1123 | "mongodb": { 1124 | "version": "3.1.4", 1125 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.4.tgz", 1126 | "integrity": "sha512-BGUxo4a/p5KtZpOn6+z6iZXTHfDxKDvibHQap9uMJqQouwoszvTIO/QbVZkaSX3Spny0jtTEeHc0FwfpGbtEzA==", 1127 | "requires": { 1128 | "mongodb-core": "3.1.3", 1129 | "safe-buffer": "^5.1.2" 1130 | }, 1131 | "dependencies": { 1132 | "safe-buffer": { 1133 | "version": "5.1.2", 1134 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1135 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1136 | } 1137 | } 1138 | }, 1139 | "mongodb-core": { 1140 | "version": "3.1.3", 1141 | "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.3.tgz", 1142 | "integrity": "sha512-dISiV3zHGJTwZpg0xDhi9zCqFGMhA5kDPByHlcaEp09NSKfzHJ7XQbqVrL7qhki1U9PZHsmRfbFzco+6b1h2wA==", 1143 | "requires": { 1144 | "bson": "^1.1.0", 1145 | "require_optional": "^1.0.1", 1146 | "safe-buffer": "^5.1.2", 1147 | "saslprep": "^1.0.0" 1148 | }, 1149 | "dependencies": { 1150 | "bson": { 1151 | "version": "1.1.0", 1152 | "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", 1153 | "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" 1154 | }, 1155 | "safe-buffer": { 1156 | "version": "5.1.2", 1157 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1158 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1159 | } 1160 | } 1161 | }, 1162 | "mongoose": { 1163 | "version": "5.2.12", 1164 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.2.12.tgz", 1165 | "integrity": "sha512-yfQw4lbci12DvQrnc25DmP/g74vjhus5SaCliP5sbbzIIJPpeU1F3xscb4uPX26ygjPkl/NzppS65rILNv20Bg==", 1166 | "requires": { 1167 | "async": "2.6.1", 1168 | "bson": "~1.0.5", 1169 | "kareem": "2.2.1", 1170 | "lodash.get": "4.4.2", 1171 | "mongodb": "3.1.4", 1172 | "mongodb-core": "3.1.3", 1173 | "mongoose-legacy-pluralize": "1.0.2", 1174 | "mpath": "0.5.1", 1175 | "mquery": "3.2.0", 1176 | "ms": "2.0.0", 1177 | "regexp-clone": "0.0.1", 1178 | "safe-buffer": "5.1.2", 1179 | "sliced": "1.0.1" 1180 | }, 1181 | "dependencies": { 1182 | "safe-buffer": { 1183 | "version": "5.1.2", 1184 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1185 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1186 | } 1187 | } 1188 | }, 1189 | "mongoose-legacy-pluralize": { 1190 | "version": "1.0.2", 1191 | "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", 1192 | "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" 1193 | }, 1194 | "morgan": { 1195 | "version": "1.9.0", 1196 | "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.0.tgz", 1197 | "integrity": "sha1-0B+mxlhZt2/PMbPLU6OCGjEdgFE=", 1198 | "requires": { 1199 | "basic-auth": "~2.0.0", 1200 | "debug": "2.6.9", 1201 | "depd": "~1.1.1", 1202 | "on-finished": "~2.3.0", 1203 | "on-headers": "~1.0.1" 1204 | } 1205 | }, 1206 | "mpath": { 1207 | "version": "0.5.1", 1208 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.5.1.tgz", 1209 | "integrity": "sha512-H8OVQ+QEz82sch4wbODFOz+3YQ61FYz/z3eJ5pIdbMEaUzDqA268Wd+Vt4Paw9TJfvDgVKaayC0gBzMIw2jhsg==" 1210 | }, 1211 | "mquery": { 1212 | "version": "3.2.0", 1213 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.0.tgz", 1214 | "integrity": "sha512-qPJcdK/yqcbQiKoemAt62Y0BAc0fTEKo1IThodBD+O5meQRJT/2HSe5QpBNwaa4CjskoGrYWsEyjkqgiE0qjhg==", 1215 | "requires": { 1216 | "bluebird": "3.5.1", 1217 | "debug": "3.1.0", 1218 | "regexp-clone": "0.0.1", 1219 | "safe-buffer": "5.1.2", 1220 | "sliced": "1.0.1" 1221 | }, 1222 | "dependencies": { 1223 | "debug": { 1224 | "version": "3.1.0", 1225 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 1226 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 1227 | "requires": { 1228 | "ms": "2.0.0" 1229 | } 1230 | }, 1231 | "safe-buffer": { 1232 | "version": "5.1.2", 1233 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1234 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1235 | } 1236 | } 1237 | }, 1238 | "ms": { 1239 | "version": "2.0.0", 1240 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1241 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1242 | }, 1243 | "nan": { 1244 | "version": "2.10.0", 1245 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", 1246 | "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==" 1247 | }, 1248 | "negotiator": { 1249 | "version": "0.6.1", 1250 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 1251 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 1252 | }, 1253 | "object-assign": { 1254 | "version": "4.1.1", 1255 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1256 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1257 | }, 1258 | "on-finished": { 1259 | "version": "2.3.0", 1260 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 1261 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 1262 | "requires": { 1263 | "ee-first": "1.1.1" 1264 | } 1265 | }, 1266 | "on-headers": { 1267 | "version": "1.0.1", 1268 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz", 1269 | "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c=" 1270 | }, 1271 | "optimist": { 1272 | "version": "0.3.7", 1273 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", 1274 | "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", 1275 | "requires": { 1276 | "wordwrap": "~0.0.2" 1277 | } 1278 | }, 1279 | "parseurl": { 1280 | "version": "1.3.2", 1281 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 1282 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 1283 | }, 1284 | "passport": { 1285 | "version": "0.4.0", 1286 | "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.0.tgz", 1287 | "integrity": "sha1-xQlWkTR71a07XhgCOMORTRbwWBE=", 1288 | "requires": { 1289 | "passport-strategy": "1.x.x", 1290 | "pause": "0.0.1" 1291 | } 1292 | }, 1293 | "passport-local": { 1294 | "version": "1.0.0", 1295 | "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", 1296 | "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", 1297 | "requires": { 1298 | "passport-strategy": "1.x.x" 1299 | } 1300 | }, 1301 | "passport-strategy": { 1302 | "version": "1.0.0", 1303 | "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", 1304 | "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" 1305 | }, 1306 | "path-to-regexp": { 1307 | "version": "0.1.7", 1308 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 1309 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 1310 | }, 1311 | "pause": { 1312 | "version": "0.0.1", 1313 | "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", 1314 | "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" 1315 | }, 1316 | "promise": { 1317 | "version": "6.1.0", 1318 | "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", 1319 | "integrity": "sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY=", 1320 | "requires": { 1321 | "asap": "~1.0.0" 1322 | } 1323 | }, 1324 | "proxy-addr": { 1325 | "version": "2.0.4", 1326 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", 1327 | "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", 1328 | "requires": { 1329 | "forwarded": "~0.1.2", 1330 | "ipaddr.js": "1.8.0" 1331 | } 1332 | }, 1333 | "qs": { 1334 | "version": "6.5.1", 1335 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 1336 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 1337 | }, 1338 | "range-parser": { 1339 | "version": "1.2.0", 1340 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 1341 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 1342 | }, 1343 | "raw-body": { 1344 | "version": "2.3.3", 1345 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", 1346 | "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", 1347 | "requires": { 1348 | "bytes": "3.0.0", 1349 | "http-errors": "1.6.3", 1350 | "iconv-lite": "0.4.23", 1351 | "unpipe": "1.0.0" 1352 | } 1353 | }, 1354 | "regexp-clone": { 1355 | "version": "0.0.1", 1356 | "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-0.0.1.tgz", 1357 | "integrity": "sha1-p8LgmJH9vzj7sQ03b7cwA+aKxYk=" 1358 | }, 1359 | "repeat-string": { 1360 | "version": "1.6.1", 1361 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 1362 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 1363 | }, 1364 | "require_optional": { 1365 | "version": "1.0.1", 1366 | "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", 1367 | "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", 1368 | "requires": { 1369 | "resolve-from": "^2.0.0", 1370 | "semver": "^5.1.0" 1371 | } 1372 | }, 1373 | "resolve-from": { 1374 | "version": "2.0.0", 1375 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", 1376 | "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" 1377 | }, 1378 | "right-align": { 1379 | "version": "0.1.3", 1380 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 1381 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 1382 | "requires": { 1383 | "align-text": "^0.1.1" 1384 | } 1385 | }, 1386 | "safe-buffer": { 1387 | "version": "5.1.1", 1388 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1389 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 1390 | }, 1391 | "safer-buffer": { 1392 | "version": "2.1.2", 1393 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1394 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 1395 | }, 1396 | "saslprep": { 1397 | "version": "1.0.1", 1398 | "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.1.tgz", 1399 | "integrity": "sha512-ntN6SbE3hRqd45PKKadRPgA+xHPWg5lPSj2JWJdJvjTwXDDfkPVtXWvP8jJojvnm+rAsZ2b299C5NwZqq818EA==", 1400 | "optional": true 1401 | }, 1402 | "semver": { 1403 | "version": "5.5.1", 1404 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", 1405 | "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" 1406 | }, 1407 | "send": { 1408 | "version": "0.16.2", 1409 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 1410 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 1411 | "requires": { 1412 | "debug": "2.6.9", 1413 | "depd": "~1.1.2", 1414 | "destroy": "~1.0.4", 1415 | "encodeurl": "~1.0.2", 1416 | "escape-html": "~1.0.3", 1417 | "etag": "~1.8.1", 1418 | "fresh": "0.5.2", 1419 | "http-errors": "~1.6.2", 1420 | "mime": "1.4.1", 1421 | "ms": "2.0.0", 1422 | "on-finished": "~2.3.0", 1423 | "range-parser": "~1.2.0", 1424 | "statuses": "~1.4.0" 1425 | } 1426 | }, 1427 | "serve-static": { 1428 | "version": "1.13.2", 1429 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 1430 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 1431 | "requires": { 1432 | "encodeurl": "~1.0.2", 1433 | "escape-html": "~1.0.3", 1434 | "parseurl": "~1.3.2", 1435 | "send": "0.16.2" 1436 | } 1437 | }, 1438 | "setprototypeof": { 1439 | "version": "1.1.0", 1440 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 1441 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 1442 | }, 1443 | "sliced": { 1444 | "version": "1.0.1", 1445 | "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", 1446 | "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" 1447 | }, 1448 | "source-map": { 1449 | "version": "0.4.4", 1450 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", 1451 | "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", 1452 | "requires": { 1453 | "amdefine": ">=0.0.4" 1454 | } 1455 | }, 1456 | "statuses": { 1457 | "version": "1.4.0", 1458 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 1459 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 1460 | }, 1461 | "through": { 1462 | "version": "2.3.8", 1463 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1464 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 1465 | }, 1466 | "transformers": { 1467 | "version": "2.1.0", 1468 | "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", 1469 | "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", 1470 | "requires": { 1471 | "css": "~1.0.8", 1472 | "promise": "~2.0", 1473 | "uglify-js": "~2.2.5" 1474 | }, 1475 | "dependencies": { 1476 | "is-promise": { 1477 | "version": "1.0.1", 1478 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", 1479 | "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" 1480 | }, 1481 | "promise": { 1482 | "version": "2.0.0", 1483 | "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", 1484 | "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", 1485 | "requires": { 1486 | "is-promise": "~1" 1487 | } 1488 | }, 1489 | "source-map": { 1490 | "version": "0.1.43", 1491 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", 1492 | "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", 1493 | "requires": { 1494 | "amdefine": ">=0.0.4" 1495 | } 1496 | }, 1497 | "uglify-js": { 1498 | "version": "2.2.5", 1499 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", 1500 | "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", 1501 | "requires": { 1502 | "optimist": "~0.3.5", 1503 | "source-map": "~0.1.7" 1504 | } 1505 | } 1506 | } 1507 | }, 1508 | "type-is": { 1509 | "version": "1.6.16", 1510 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 1511 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 1512 | "requires": { 1513 | "media-typer": "0.3.0", 1514 | "mime-types": "~2.1.18" 1515 | } 1516 | }, 1517 | "uglify-js": { 1518 | "version": "2.8.29", 1519 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 1520 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 1521 | "requires": { 1522 | "source-map": "~0.5.1", 1523 | "uglify-to-browserify": "~1.0.0", 1524 | "yargs": "~3.10.0" 1525 | }, 1526 | "dependencies": { 1527 | "source-map": { 1528 | "version": "0.5.7", 1529 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 1530 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" 1531 | } 1532 | } 1533 | }, 1534 | "uglify-to-browserify": { 1535 | "version": "1.0.2", 1536 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 1537 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 1538 | "optional": true 1539 | }, 1540 | "unpipe": { 1541 | "version": "1.0.0", 1542 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1543 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1544 | }, 1545 | "utils-merge": { 1546 | "version": "1.0.1", 1547 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1548 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1549 | }, 1550 | "uuid": { 1551 | "version": "3.3.2", 1552 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 1553 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 1554 | }, 1555 | "validator": { 1556 | "version": "10.7.1", 1557 | "resolved": "https://registry.npmjs.org/validator/-/validator-10.7.1.tgz", 1558 | "integrity": "sha512-tbB5JrTczfeHKLw3PnFRzGFlF1xUAwSgXEDb66EuX1ffCirspYpDEZo3Vc9j38gPdL4JKrDc5UPFfgYiw1IWRQ==" 1559 | }, 1560 | "vary": { 1561 | "version": "1.1.2", 1562 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1563 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1564 | }, 1565 | "void-elements": { 1566 | "version": "2.0.1", 1567 | "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", 1568 | "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" 1569 | }, 1570 | "window-size": { 1571 | "version": "0.1.0", 1572 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 1573 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" 1574 | }, 1575 | "with": { 1576 | "version": "4.0.3", 1577 | "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", 1578 | "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", 1579 | "requires": { 1580 | "acorn": "^1.0.1", 1581 | "acorn-globals": "^1.0.3" 1582 | }, 1583 | "dependencies": { 1584 | "acorn": { 1585 | "version": "1.2.2", 1586 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", 1587 | "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" 1588 | } 1589 | } 1590 | }, 1591 | "wordwrap": { 1592 | "version": "0.0.3", 1593 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 1594 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" 1595 | }, 1596 | "yargs": { 1597 | "version": "3.10.0", 1598 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 1599 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 1600 | "requires": { 1601 | "camelcase": "^1.0.2", 1602 | "cliui": "^2.1.0", 1603 | "decamelize": "^1.0.0", 1604 | "window-size": "0.1.0" 1605 | } 1606 | } 1607 | } 1608 | } 1609 | --------------------------------------------------------------------------------