├── test ├── unit.js ├── index.js └── supertest.js ├── .gitignore ├── client ├── img │ └── CNC-screen.png ├── js │ └── index.js ├── index.html └── css │ └── statuspage.css ├── .eslintrc.js ├── server ├── server.js └── watson.js ├── README.md ├── LICENSE └── package.json /test/unit.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | require('./unit'); 2 | require('./supertest'); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules 3 | .DS_Store 4 | client/index_old.html -------------------------------------------------------------------------------- /client/img/CNC-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/travishuff/Company-News-Cruncher/HEAD/client/img/CNC-screen.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import" 5 | ] 6 | }; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const path = require('path'); 4 | const bodyParser = require('body-parser'); 5 | const cache = require('apicache').middleware; 6 | 7 | const { getData, getNews, getTicker } = require('./watson'); 8 | 9 | app.use((req, res, next) => { 10 | res.header("Access-Control-Allow-Origin", "*"); 11 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 12 | next(); 13 | }); 14 | 15 | app.use(express.static(path.join(__dirname, '../client/' ))); 16 | 17 | app.use(bodyParser.urlencoded({ extended: true })); 18 | 19 | app.get('/', (req, res) => { 20 | res.sendFile(path.join(__dirname, './../client/index.html')) 21 | }); 22 | 23 | app.post('/getNews', cache('2 minutes'), getNews, getData); 24 | 25 | app.post('/getTicker', cache('1 minutes'), getTicker); 26 | 27 | app.listen(3000); 28 | 29 | module.exports = app; 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Company News Cruncher 2 | Automatically crunch the news on any company and return sentiment analysis on major ideas, people and places. 3 | 4 | *Attn:* Bluemix Alchemy_language API is being replaced by [Natural Language Understanding (NLU)](https://www.ibm.com/blogs/bluemix/2017/02/hello-nlu/) March 2017, with EOL March 2018. 5 | 6 | 7 | ![Company News Cruncher Screenshot](client/img/CNC-screen.png) 8 | 9 | 10 | ## Dependencies 11 | [Express](https://github.com/expressjs/express) 12 | [body-parser](https://github.com/expressjs/body-parser) 13 | [apicache](https://github.com/kwhitley/apicache) 14 | [jQuery](http://jquery.com) 15 | 16 | ## Author 17 | [Travis Huff](huff.travis@gmail.com) 18 | 19 | ## Support 20 | Tested in Chrome 55 & Node 6/7. 21 | GitHub Issues: 22 | 23 | ## Contributions 24 | ❤️ Contributions welcome! 25 | 26 | ## License 27 | [MIT](https://github.com/travishuff/razorframe/blob/master/LICENSE) 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Travis Huff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "company-news-cruncher", 3 | "version": "1.0.0", 4 | "description": "An intelligent news analysis tool for any company's newsflow.", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon server/server.js", 8 | "test": "export NODE_ENV=test && mocha --timeout 8000 test/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/travishuff/company-news-cruncher.git" 13 | }, 14 | "author": "Travis Huff", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/travishuff/company-news-cruncher/issues" 18 | }, 19 | "homepage": "https://github.com/travishuff/company-news-cruncher#readme", 20 | "dependencies": { 21 | "apicache": "^0.3.4", 22 | "body-parser": "^1.15.2", 23 | "express": "^4.14.0", 24 | "path": "^0.12.7", 25 | "request": "^2.76.0", 26 | "watson-developer-cloud": "^2.7.0" 27 | }, 28 | "devDependencies": { 29 | "chai": "^3.5.0", 30 | "eslint": "^3.13.0", 31 | "eslint-config-airbnb-base": "^11.0.0", 32 | "eslint-plugin-import": "^2.2.0", 33 | "mocha": "^3.2.0", 34 | "sinon": "^1.17.7", 35 | "supertest": "^2.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/supertest.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const request = require('supertest'); 3 | const app = require('../server/server'); 4 | const PORT = process.env.PORT || 3000; 5 | const HOST = `http://localhost:${PORT}`; 6 | 7 | describe('Route integration', () => { 8 | 9 | describe('/', () => { 10 | 11 | describe('GET', () => { 12 | it('responds with 200 status and text/html content type', done => { 13 | request(HOST) 14 | .get('/') 15 | .expect('Content-Type', /text\/html/) 16 | .expect(200, done); 17 | }); 18 | }); 19 | 20 | }); 21 | 22 | describe('/getNews', () => { 23 | 24 | describe('GET', () => { 25 | it('responds with 200 status and "text/html; charset=utf-8" content type', done => { 26 | request(HOST) 27 | .get('/getNews') 28 | .expect('Content-Type', 'text/html; charset=utf-8') 29 | .expect(200, done); 30 | }); 31 | }); 32 | 33 | describe('POST', () => { 34 | it('responds to valid request with 200 status and application/json content type', done => { 35 | request(HOST) 36 | .post('/getNews') 37 | .send({FB: 'FB'}) 38 | .expect('Content-Type', /application\/json/) 39 | .expect(200, done); 40 | }); 41 | 42 | it('responds to invalid request with 400 status and error message in body', done => { 43 | request(HOST) 44 | .post('/getNews') 45 | .send({}) 46 | .expect(404, done); 47 | }); 48 | }); 49 | 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /client/js/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready((e) => { 2 | let today = Date(); 3 | $('#date').append(today); 4 | 5 | $('#message-button').on('click', getAI); 6 | $('#company').on('keypress', (e) => { 7 | if (e.keyCode === 13) getAI(); 8 | }); 9 | 10 | function getAI() { 11 | let company = $('#company').val(); 12 | $('#company').val(''); 13 | 14 | $.ajax({ 15 | method: "POST", 16 | url: "/getNews", 17 | data: company, 18 | }) 19 | .done(msg => { 20 | $('.root').empty(); 21 | console.log(msg); 22 | // write if statements to check for existence of concepts 23 | $(".root").append(`

Title: ${msg.title}

24 |

Sentiment: ${msg.docSentiment.type}

25 |

score: ${msg.docSentiment.score}

26 |

concept 1: ${msg.concepts[0].text}

27 |

relevance: ${msg.concepts[0].relevance}

`); 28 | }); 29 | } 30 | 31 | $('#ticker-button').on('click', getTicker); 32 | $('#ticker').on('keypress', (e) => { 33 | if (e.keyCode === 13) getTicker(); 34 | }); 35 | 36 | function getTicker() { 37 | let ticker = $('#ticker').val(); 38 | $('#ticker').val(''); 39 | 40 | $.ajax({ 41 | method: "POST", 42 | url: "/getTicker", 43 | data: ticker, 44 | }) 45 | .done(msg => { 46 | $('.root2').empty(); 47 | let parsed = msg.slice(3); 48 | parsed = JSON.parse(parsed); 49 | console.log(parsed); 50 | $(".root2").append(`

${parsed[0].t} ${parsed[0].l}

51 |

${parsed[0].lt}

`); 52 | }); 53 | } 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Company News Cruncher 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |

Company News Cruncher

16 |
17 | 18 |
19 | 20 |
21 |
22 |
AI-powered News Analysis
23 |
24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
Ticker
33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 |
41 |
Date
42 |
43 |
powered by IBM Bluemix Cognitive Solutions
44 | software design and engineering by Travis Huff
45 |
46 | 47 |
48 | 49 |
50 | 51 | -------------------------------------------------------------------------------- /server/watson.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | var watson = require('watson-developer-cloud'); 3 | var alchemy_language = watson.alchemy_language({ 4 | api_key: '6e3edf7b9c31fb749ea18b50bbb0a6f28731df30' 5 | }); 6 | 7 | const watsonController = { 8 | 9 | getData: (req, res, next) => { 10 | console.log('into getData:'); 11 | let parameters = { 12 | extract: 'title,concepts,doc-sentiment', // can also add: entities,taxonomy, 13 | // sentiment: 1, 14 | maxRetrieve: 1, 15 | url: req.urlArr[0] // Set which article to analyze here. 16 | }; 17 | 18 | alchemy_language.combined(parameters, function (err, response) { 19 | if (err) 20 | console.error('error:', err); 21 | else 22 | console.log('into alchemy request:', response); 23 | 24 | res.json(response); 25 | }); 26 | // next(); 27 | }, 28 | 29 | getNews: (req, res, next) => { 30 | console.log('accepted request: ', res.statusCode); 31 | let company = Object.keys(req.body)[0]; // get the company from the form input 32 | console.log(`Company request: ${company}`); 33 | 34 | const APIKEY = '6e3edf7b9c31fb749ea18b50bbb0a6f28731df30'; 35 | let url = `https://gateway-a.watsonplatform.net/calls/data/GetNews?outputMode=json&start=now-1d&end=now&count=1&q.enriched.url.title=${company}&return=enriched.url.url,enriched.url.title&apikey=${APIKEY}`; // SET # OF ARTICLES HERE 36 | // TREND ANALYSIS # of articles // https://gateway-a.watsonplatform.net/calls/data/GetNews?outputMode=json&start=now-7d&end=now&timeSlice=1d&q.enriched.url.entities.entity=|text=${company},type=Company|&apikey=${APIKEY} 37 | 38 | request(url, (error, response, html) => { 39 | if (error) console.error(error); 40 | console.log(response.body); 41 | let parsed = JSON.parse(response.body); 42 | let urlArr = []; 43 | 44 | // SET # OF ARTICLES HERE 45 | for (let i = 0; i < 1; i++) { 46 | urlArr.push(parsed.result.docs[i].source.enriched.url.url); 47 | } 48 | console.log(urlArr); 49 | req.urlArr = urlArr; 50 | next(); 51 | }); 52 | }, 53 | 54 | getTicker: (req, res, next) => { 55 | console.log('response status:', res.statusCode); 56 | let ticker = Object.keys(req.body)[0]; 57 | console.log(`ticker request: ${ticker}`); 58 | 59 | let url = `http://finance.google.com/finance/info?client=ig&q=NASDAQ%3A${ticker}`; 60 | 61 | request(url, (error, response, html) => { 62 | if (error) console.error(error); 63 | console.log(response.body); 64 | res.json(response.body); 65 | }); 66 | 67 | } 68 | 69 | }; 70 | 71 | module.exports = watsonController; 72 | -------------------------------------------------------------------------------- /client/css/statuspage.css: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.2 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select, 46 | textarea { 47 | margin: 0; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | } 53 | 54 | * { 55 | box-sizing: inherit; 56 | } 57 | 58 | *:before, *:after { 59 | box-sizing: inherit; 60 | } 61 | 62 | img, 63 | embed, 64 | object, 65 | audio, 66 | video { 67 | height: auto; 68 | max-width: 100%; 69 | } 70 | 71 | html { 72 | background-color: whitesmoke; 73 | font-size: 1em; 74 | -moz-osx-font-smoothing: grayscale; 75 | -webkit-font-smoothing: antialiased; 76 | min-width: 300px; 77 | overflow-x: hidden; 78 | overflow-y: scroll; 79 | text-rendering: optimizeLegibility; 80 | } 81 | 82 | article, 83 | aside, 84 | figure, 85 | footer, 86 | header, 87 | hgroup, 88 | section { 89 | display: block; 90 | } 91 | 92 | body, 93 | button, 94 | input, 95 | select, 96 | textarea { 97 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif; 98 | } 99 | 100 | body { 101 | color: #4a4a4a; 102 | font-size: 1rem; 103 | font-weight: 400; 104 | line-height: 1.428571428571429; 105 | } 106 | 107 | a { 108 | color: #00d1b2; 109 | cursor: pointer; 110 | text-decoration: none; 111 | -webkit-transition: none 86ms ease-out; 112 | transition: none 86ms ease-out; 113 | } 114 | 115 | a:hover { 116 | color: #363636; 117 | } 118 | 119 | hr { 120 | background-color: #dbdbdb; 121 | border: none; 122 | display: block; 123 | height: 1px; 124 | margin: 20px 0; 125 | } 126 | 127 | img { 128 | max-width: 100%; 129 | } 130 | 131 | input[type="checkbox"], 132 | input[type="radio"] { 133 | vertical-align: baseline; 134 | } 135 | 136 | small { 137 | font-size: 11px; 138 | } 139 | 140 | span { 141 | font-style: inherit; 142 | font-weight: inherit; 143 | } 144 | 145 | strong { 146 | color: #363636; 147 | font-weight: 700; 148 | } 149 | /* ----------------------------- */ 150 | 151 | body { 152 | background-color: lightgrey; 153 | min-height: 100vh; 154 | } 155 | 156 | h1 { 157 | font-size: 2em; 158 | } 159 | 160 | .body-container { 161 | padding:50px; 162 | max-width: 968px; 163 | 164 | } 165 | 166 | .section { 167 | margin-top:40px; 168 | border: 2px solid black; 169 | border-radius: 5px; 170 | background-color: whitesmoke; 171 | } 172 | 173 | .status-header { 174 | padding: 40px 40px 80px 40px; 175 | } 176 | .status-header .indicator { 177 | display: inline-block; 178 | width: 15px; 179 | height: 15px; 180 | border-radius: 100%; 181 | margin-right: 20px; 182 | background: #17d766; 183 | opacity: 0.8; 184 | filter:alpha(opacity=80); 185 | } 186 | .status-header .title { 187 | display: inline-block; 188 | } 189 | .status-header .subtitle { 190 | display: block; 191 | margin-left: 35px; 192 | } 193 | 194 | .status-uptime { 195 | border-top: 1px solid black; 196 | border-bottom: 1px solid black; 197 | padding: 5px; 198 | margin: 5px; 199 | } 200 | .status-uptime .title { 201 | display: inline-block; 202 | font-size: 14px; 203 | color: #bbb; 204 | } 205 | .status-uptime .uptime { 206 | display: inline-block; 207 | margin-left: 60px; 208 | font-size: 14px; 209 | color: #bbb; 210 | } 211 | --------------------------------------------------------------------------------