├── client ├── views │ └── config.pug ├── src │ ├── config.js │ └── components │ │ └── config │ │ ├── dialog.jsx │ │ ├── miniDashboard.jsx │ │ ├── dashboard.jsx │ │ ├── createUser.jsx │ │ ├── invites.jsx │ │ └── app.jsx └── public │ └── index.html ├── server ├── crawler │ ├── crawlerModel.js │ ├── intervalModel.js │ ├── crawlerController.js │ └── crawler.js ├── user │ ├── userModel.js │ └── userController.js ├── session │ ├── sessionModel.js │ └── sessionController.js ├── invite │ ├── inviteModel.js │ └── inviteController.js ├── endpoint │ ├── endpointModel.js │ └── endpointController.js └── server.js ├── webpack.config.js ├── bin ├── pull.sh └── start.sh ├── .gitignore ├── package.json └── README.md /client/views/config.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title LiveAPI - Config 4 | link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.11/semantic.min.css') 5 | body 6 | div#root 7 | script. 8 | const status='#{status}'; 9 | script(src='static/bundles/config.bundle.js') 10 | -------------------------------------------------------------------------------- /server/crawler/crawlerModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | mongoose.connect('mongodb://localhost:27017', { 4 | useMongoClient: true, 5 | }); 6 | 7 | mongoose.Promise = global.Promise; 8 | 9 | const crawlerSchema = new Schema({ 10 | endpoint: {type: String, required: true}, // config file 11 | scrape_date: {type: Date, default: Date.now(), required: true}, 12 | data: {type: Object, required: true}, // crawler 13 | }); 14 | 15 | module.exports = mongoose.model("Crawler", crawlerSchema); -------------------------------------------------------------------------------- /server/crawler/intervalModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | mongoose.connect('mongodb://localhost:27017', { 4 | useMongoClient: true, 5 | }); 6 | 7 | mongoose.Promise = global.Promise; 8 | 9 | const intervalSchema = new Schema({ 10 | endpoint: {type: String, required: true}, 11 | url: {type: String, required: true}, 12 | interval: {type: Number, required: true, default: 600000}, // 10 minute by default 13 | }); 14 | 15 | module.exports = mongoose.model("Interval", intervalSchema); -------------------------------------------------------------------------------- /client/src/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | //import 'semantic-ui-css/semantic.min.css'; 4 | import App from './components/config/app.jsx'; 5 | 6 | const randomGradient = () => { 7 | return 'background: #FFAFBD; background: -webkit-linear-gradient(to right, #ffc3a0, #FFAFBD);background: linear-gradient(to right, #ffc3a0, #FFAFBD);' 8 | } 9 | document.getElementById('root').setAttribute('style', randomGradient()); 10 | 11 | render( 12 | , 13 | document.getElementById('root') 14 | ); 15 | -------------------------------------------------------------------------------- /client/src/components/config/dialog.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Card, Input, Divider, Button, Form, Icon } from 'semantic-ui-react' 4 | 5 | const InfoDialog = (props) => { 6 | return ( 7 | 8 | 9 | 10 | {props.header} 11 | 12 | 13 | 14 | 15 | {props.linkText} 16 | 17 | 18 | ); 19 | } 20 | 21 | export default InfoDialog; -------------------------------------------------------------------------------- /server/user/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | mongoose.connect('mongodb://localhost:27017', { 4 | useMongoClient: true, 5 | }); 6 | 7 | mongoose.Promise = global.Promise; 8 | 9 | const SALT_WORK_FACTOR = 10; 10 | const bcrypt = require('bcryptjs'); 11 | 12 | const userSchema = new Schema({ 13 | username: {type: String, required: true, unique: true}, 14 | password: {type: String, required: true}, 15 | admin: {type: Boolean, default: false}, 16 | }); 17 | 18 | userSchema.pre('save', function(next, done) { 19 | this.password = bcrypt.hashSync(this.password, SALT_WORK_FACTOR); 20 | next(); 21 | }); 22 | 23 | module.exports = mongoose.model('User', userSchema); 24 | -------------------------------------------------------------------------------- /server/session/sessionModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | /** 5 | * Check out the `createdAt` field below. This is set up to use Mongo's automatic document 6 | * expiration service by giving the Mongoose schema the `expires` property. 7 | * After 30 seconds, the session will automatically be removed from the collection! 8 | * (actually, Mongo's cleanup service only runs once per minute so the session 9 | * could last up to 90 seconds before it's deleted, but still pretty cool!) 10 | */ 11 | const sessionSchema = new Schema({ 12 | cookieId: { type: String, required: true, unique: false }, 13 | createdAt: { type: Date, expires: 86400, default: Date.now } 14 | }); 15 | 16 | module.exports = mongoose.model('Session', sessionSchema); 17 | -------------------------------------------------------------------------------- /server/invite/inviteModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | /** 5 | * Check out the `createdAt` field below. This is set up to use Mongo's automatic document 6 | * expiration service by giving the Mongoose schema the `expires` property. 7 | * After 30 seconds, the session will automatically be removed from the collection! 8 | * (actually, Mongo's cleanup service only runs once per minute so the session 9 | * could last up to 90 seconds before it's deleted, but still pretty cool!) 10 | */ 11 | const sessionSchema = new Schema({ 12 | valid: { type: Boolean, required: true, default: true, }, 13 | creator: { type: String, required: false }, 14 | redeemer: { type: String, required: false } 15 | }); 16 | 17 | module.exports = mongoose.model('Invite', sessionSchema); 18 | -------------------------------------------------------------------------------- /client/src/components/config/miniDashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Card, Menu, Label } from 'semantic-ui-react'; 4 | import Invites from './invites.jsx'; 5 | 6 | const Dashboard = (props) => { 7 | let message = null; 8 | if (props.message) message = ; 9 | 10 | return ( 11 | 12 | 13 | 14 | LiveAPI Dashboard 15 | {message} 16 | 17 | 18 | 19 | 20 | 21 | 22 | Documentation 23 | 24 | 25 | ); 26 | } 27 | 28 | export default Dashboard; 29 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | // Components for '/config' route 7 | config: path.join(__dirname, 'client/src/config.js'), 8 | }, 9 | output: { 10 | path: path.join(__dirname, 'client/public/bundles'), 11 | filename: '[name].bundle.js' 12 | }, 13 | module: { 14 | loaders: [ 15 | { 16 | test: /.jsx?$/, 17 | loader: 'babel-loader', 18 | include: path.join(__dirname, 'client/src'), 19 | exclude: /node_modules/, 20 | query: { 21 | presets: [ 22 | 'react', 23 | ['env', { 24 | "modules": false, 25 | "targets": { 26 | "browsers": ["last 2 Chrome versions"] 27 | } 28 | }] 29 | ], 30 | 'plugins': [], 31 | } 32 | } 33 | ] 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
sample text in div 1 7 | 13 |
14 |
second div here with emphasized with deeper text
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /bin/pull.sh: -------------------------------------------------------------------------------- 1 | # The purpose of this script is to pull the LAS source form github and prepare it for installation 2 | 3 | # Install with: 4 | # curl -s https://raw.githubusercontent.com/Live-API/LAS/4b82aa830d5691f1815f5660d99d3198c3bc4849/bin/pull.sh | bash -s 5 | 6 | #!/bin/bash 7 | 8 | # Create a LAS directory 9 | #INSTALL_DIR='LiveAPI' 10 | #mkdir $INSTALL_DIR 11 | #cd $INSTALL_DIR 12 | 13 | # Install git 14 | 15 | # Mac OS 16 | if hash brew 2>/dev/null; then 17 | echo "Installing git with homebrew" 18 | brew install git 19 | else 20 | # Ubuntu/Debian 21 | if hash apt-get 2>/dev/null; then 22 | echo "Installing git with apt-get" 23 | sudo apt-get install -y git-all 24 | else 25 | # Enterprise Linus (e.g. Amazon Linux) 26 | if hash yum 2>/dev/null; then 27 | echo "Installing git with yum" 28 | sudo yum -y install git 29 | fi 30 | fi 31 | fi 32 | 33 | # Clone the repo 34 | echo "Cloning git repo" 35 | git clone -b master --single-branch https://github.com/live-api/las --depth 1 36 | 37 | cd las 38 | sudo bin/start.sh 39 | -------------------------------------------------------------------------------- /server/endpoint/endpointModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | mongoose.connect('mongodb://localhost:27017', { 4 | useMongoClient: true, 5 | }); 6 | 7 | mongoose.Promise = global.Promise; 8 | 9 | // Definition data from endpoint creation POST 10 | //{ 11 | // url: Starting URL of scrape 12 | // interval: Time between scrapes in seconds, used in IntervalModel 13 | // endpoint: Unique name of the endpoint and where the data can be retrieved from 14 | // text: Object of elements to scrape (e.g. {name: [...DOM paths]}) 15 | // images: Object of images to scrape 16 | // backgroundImages: Object of background images to scrape 17 | // pagination: Button to press after scraping a page 18 | //} 19 | 20 | const endpointSchema = new Schema({ 21 | endpoint: {type: String, required: true, unique: true}, 22 | creator: {type: String}, 23 | url: {type: String, default: Date.now(), required: true}, 24 | text: {type: Object, required: false}, 25 | images: {type: Object, required: false}, 26 | backgroundImages: {type: Object, required: false}, 27 | pagination: {type: Object, required: false}, 28 | }); 29 | 30 | module.exports = mongoose.model("Endpoint", endpointSchema); 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Database files 61 | db/ 62 | 63 | # Build files 64 | client/public/bundles 65 | 66 | # SSL Keys 67 | ssl/ 68 | 69 | # Hidden file for installation script to know which version is installed 70 | .LAS_status -------------------------------------------------------------------------------- /client/src/components/config/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Grid, Segment, Menu, Header, Label, Divider } from 'semantic-ui-react'; 4 | import Invites from './invites.jsx'; 5 | 6 | class Dashboard extends Component { 7 | 8 | // A larger dashboard that can be used when we have more content to put on it 9 | 10 | render() { 11 | let message = null; 12 | if (this.props.message) message = ; 13 | return ( 14 | 15 | 16 | 17 |
LiveAPI
18 | Home 19 | Messages 20 | {message} 21 |
22 | 23 | 24 | 25 | 26 | Home 27 | Messages 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 | ); 37 | } 38 | } 39 | 40 | export default Dashboard; 41 | -------------------------------------------------------------------------------- /server/session/sessionController.js: -------------------------------------------------------------------------------- 1 | const Session = require('./sessionModel'); 2 | 3 | const sessionController = {}; 4 | 5 | /** 6 | * isLoggedIn - find the appropriate session for this request in the database, then 7 | * verify whether or not the session is still valid. 8 | * 9 | * 10 | */ 11 | sessionController.isLoggedIn = async (req, res, next) => { 12 | try { 13 | // If session exists for current sid and if the session is not expired 14 | if (res.locals.userId = (await Session.findById(req.cookies.sid)).cookieId) next(); 15 | // Else respond with error status 16 | else res.status(401).send('Invalid or expired token') 17 | } 18 | catch (err) { 19 | console.log(err); 20 | res.status(401); 21 | res.send('Invalid or expired token') 22 | } 23 | }; 24 | 25 | /** 26 | * startSession - create a new Session model and then save the new session to the 27 | * database. 28 | * 29 | * 30 | */ 31 | sessionController.startSession = async (req, res, next) => { 32 | try { 33 | // Save the sid cookie to the db as a new session model instance 34 | const session = new Session({cookieId: res.locals.userId}); 35 | res.cookie('sid',(await session.save())._id); 36 | next(); 37 | } 38 | catch (err) { 39 | console.log('Error starting session', err) 40 | res.status(500); 41 | res.send(); 42 | } 43 | }; 44 | 45 | module.exports = sessionController; 46 | -------------------------------------------------------------------------------- /server/invite/inviteController.js: -------------------------------------------------------------------------------- 1 | const Invite = require('./inviteModel'); 2 | 3 | const inviteController = {}; 4 | 5 | // Generates an invite ID 6 | inviteController.createInvite = async (req, res, next) => { 7 | // Create a new invite and save the id 8 | try { res.locals.invite = { id: (await Invite.create({ creator: res.locals.userId }))._id } } 9 | // Else pass along the error 10 | catch (err) { res.locals.invite = { err }} 11 | next(); 12 | } 13 | 14 | // Updates invite with redeemer's ID 15 | inviteController.redeemInvite = async (req, res, next) => { 16 | // Update invite 17 | try { await Invite.findByIdAndUpdate(res.locals.inviteId, { redeemer: res.locals.userid }); next() } 18 | // Else pass along error 19 | catch (err) { res.status(500).send() } 20 | } 21 | 22 | // Checks if the current invite ID is valid 23 | inviteController.verifyInvite = async (req, res, next) => { 24 | try { 25 | // If invite exists and is valid and is not yet redeemed 26 | const invite = await Invite.findById(res.locals.inviteId); 27 | if (invite && invite.valid && !invite.redeemer) next(); 28 | // Else respond with error status 29 | else res.status(401).send('Invalid or expired invite') 30 | } 31 | catch (err) { 32 | console.log(err); 33 | res.status(401); 34 | res.send('Invalid or expired invite') 35 | } 36 | }; 37 | 38 | module.exports = inviteController; 39 | -------------------------------------------------------------------------------- /server/endpoint/endpointController.js: -------------------------------------------------------------------------------- 1 | const Endpoint = require('./endpointModel.js'); 2 | const Interval = require('./../crawler/intervalModel.js'); 3 | 4 | const endpointController = { 5 | 6 | // Retrieves an endpoint definition for a given endpoint name 7 | // If none found, returns null 8 | getEndpoint: async endpoint => { 9 | try { return await Endpoint.findOne({endpoint}); } 10 | catch (err) { return null; } 11 | }, 12 | 13 | // Express middleware 14 | // If successfully creates (upserts) endpoint in endpoints collection, responds with 200 15 | // Else responds with 400 16 | setEndpoint: async (req, res, next) => { 17 | try { 18 | console.log('Incoming endpoint definition: ', req.body); 19 | // Insert if the endpoint doesn't exist, update if it does 20 | const doc = await Endpoint.update( 21 | { endpoint: req.body.endpoint }, 22 | { 23 | endpoint: req.body.endpoint, 24 | creator: res.locals.userId, 25 | url: req.body.url, 26 | text: req.body.text, 27 | images: req.body.images, 28 | backgroundImages: req.body.backgroundImages, 29 | pagination: req.body.pagination, 30 | }, 31 | { upsert : true }); 32 | console.log('doc', doc); 33 | console.log('endpointController', req.body.endpoint); 34 | res.status(200); 35 | res.send(`Endpoint successfully created: /crawls/${req.body.endpoint}`); 36 | } 37 | catch (err) { 38 | console.log('Error saving endpoint: ', err); 39 | res.status(400) 40 | res.send(err); 41 | } 42 | next(); 43 | } 44 | 45 | } 46 | 47 | module.exports = endpointController; 48 | -------------------------------------------------------------------------------- /client/src/components/config/createUser.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Card, Input, Divider, Button, Form, Icon } from 'semantic-ui-react' 4 | 5 | class CreateUserDialog extends Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.state = {}; 10 | 11 | this.handleChange = this.handleChange.bind(this); 12 | this.handleSubmit = this.handleSubmit.bind(this); 13 | } 14 | 15 | handleChange(event) { 16 | const state = {}; 17 | state[event.target.name] = event.target.value; 18 | this.setState(state); 19 | } 20 | 21 | handleSubmit(event) { 22 | this.props.submission(this.state); 23 | event.preventDefault(); 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 30 | 31 | {this.props.description} 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
49 |
50 | ) 51 | } 52 | } 53 | 54 | export default CreateUserDialog; 55 | -------------------------------------------------------------------------------- /client/src/components/config/invites.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { render } from 'react-dom'; 3 | import axios from 'axios'; 4 | import CopyToClipboard from 'react-copy-to-clipboard'; 5 | import { Container, Button, Input } from 'semantic-ui-react' 6 | 7 | // A form for creation of invites 8 | class Invites extends Component { 9 | constructor() { 10 | super() 11 | this.generateInvite = this.generateInvite.bind(this); 12 | this.state = { inviteId: null }; 13 | this.domain = window.location.href.match(/(https?:\/\/[^\/]*)/)[0]; 14 | } 15 | 16 | async generateInvite() { 17 | const route = '/invites'; 18 | try { 19 | const response = (await axios.post(route, {})); 20 | if (response.status === 200) this.setState({ inviteId: response.data }); 21 | console.log(this.state.inviteId); 22 | } 23 | catch (err) { 24 | console.log(err); 25 | } 26 | } 27 | 28 | render() { 29 | const content = this.state.inviteId ? 'Copy Link' : 'Generate Invite Link'; 30 | const onClick = this.state.inviteId ? console.log : this.generateInvite; 31 | const otherAttrs = {}; 32 | const inviteUrl = `${this.domain}/invites/${this.state.inviteId}`; 33 | if (this.state.inviteId) otherAttrs.label = inviteUrl; 34 | 35 | const button =