├── github └── screenshot.png ├── .gitignore ├── consts.js ├── app ├── public │ └── main.css ├── src │ └── utils.js └── views │ ├── noauth.ejs │ └── index.html ├── new.instajoy ├── package.json ├── README.md ├── gulpfile.js └── app.js /github/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hr/instajoy/master/github/screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.* 3 | .env 4 | .DS_Store 5 | public/main.js 6 | .env.json 7 | .idea/ 8 | app/public/main.js -------------------------------------------------------------------------------- /consts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | endpoints: { 3 | faceAPI: 'https://westcentralus.api.cognitive.microsoft.com/face/v1.0/detect', 4 | instagramMedia: 'https://api.instagram.com/v1/users/self/media/recent/?access_token=' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /app/public/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Open Sans', sans-serif; 3 | } 4 | 5 | .container { 6 | margin-bottom: 20px; 7 | } 8 | 9 | .navbar-brand { 10 | margin: auto !important; 11 | text-align: center; 12 | font-size: 50px; 13 | color: #000; 14 | } 15 | 16 | .score { 17 | font-size: 20em; 18 | } 19 | 20 | .thumbs { 21 | width: 4.5em; 22 | height: 4.5em; 23 | margin: 0 0.4em 0 0.4em; 24 | } -------------------------------------------------------------------------------- /new.instajoy: -------------------------------------------------------------------------------- 1 | ______ __ _____ 2 | /\__ _\ /\ \__ /\___ \ 3 | \/_/\ \/ ___ ____\ \ ,_\ __ \/__/\ \ ___ __ __ 4 | \ \ \ /' _ `\ /',__\\ \ \/ /'__`\ _\ \ \ / __`\/\ \/\ \ 5 | \_\ \__/\ \/\ \/\__, `\\ \ \_/\ \_\.\_/\ \_\ \/\ \_\ \ \ \_\ \ 6 | /\_____\ \_\ \_\/\____/ \ \__\ \__/.\_\ \____/\ \____/\/`____ \ 7 | \/_____/\/_/\/_/\/___/ \/__/\/__/\/_/\/___/ \/___/ `/___/> \ 8 | /\___/ 9 | \/__/ 10 | 11 | Creating some joy... 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instajoy", 3 | "version": "0.1.0", 4 | "private": true, 5 | "main": "app.js", 6 | "scripts": { 7 | "start": "cat ./new.instajoy && node app.js", 8 | "dev": "cat ./new.instajoy && ./node_modules/.bin/gulp", 9 | "test": "echo \"TODO\"" 10 | }, 11 | "dependencies": { 12 | "axios": "^0.17.1", 13 | "body-parser": "^1.18.2", 14 | "cookie-parser": "^1.4.3", 15 | "ejs": "^2.5.7", 16 | "errorhandler": "^1.5.0", 17 | "express": "^4.16.2", 18 | "morgan": "^1.9.0", 19 | "request": "^2.83.0", 20 | "request-promise-native": "^1.0.5", 21 | "serve-favicon": "^2.4.5", 22 | "serve-static": "^1.13.1" 23 | }, 24 | "devDependencies": { 25 | "gulp": "^3.9.1", 26 | "gulp-env": "^0.4.0", 27 | "gulp-nodemon": "^2.2.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InstaJ😂y 2 | 3 | A NodeJS app made in less than 24hrs at the Facebook Hackathon 2018. 4 | 5 | It visualises the ups and downs of your life by doing sentimental analysis on your instagram posts. 6 | Gives you a happiness score that is based on how happy you have been with the aim of encouraging you to laugh 😂 more. 7 | 8 | ![InstaJoy screenshot](github/screenshot.png) 9 | 10 | # Development 11 | 12 | ## Pre-requirements 13 | Need the following environmental defined to run this. These can be defined in a `.env.json` file like so 14 | ```js 15 | { 16 | "MS_FACE_API_KEY": [microsoft cognitive services face API key], 17 | "IG_CLIENT_ID": [instagram client id], 18 | "IG_CLIENT_SECRET": [instagram client secret] 19 | } 20 | ``` 21 | ## Run 22 | After installing all the dependencies (`npm i`) just run 23 | ```sh 24 | npm run dev 25 | ``` 26 | -------------------------------------------------------------------------------- /app/src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Normalize a port into a number, string, or false. 4 | */ 5 | exports.normalizePort = function (val) { 6 | var port = parseInt(val, 10) 7 | 8 | if (isNaN(port)) { 9 | // named pipe 10 | return val 11 | } 12 | 13 | if (port >= 0) { 14 | // port number 15 | return port 16 | } 17 | 18 | return false 19 | } 20 | 21 | /** 22 | * Event listener for HTTP server "error" event. 23 | */ 24 | exports.onError = function (error) { 25 | if (error.syscall !== 'listen') { 26 | throw error 27 | } 28 | 29 | var bind = typeof port === 'string' ? 30 | 'Pipe ' + port : 31 | 'Port ' + port 32 | 33 | // handle specific listen errors with friendly messages 34 | switch (error.code) { 35 | case 'EACCES': 36 | console.error(bind + ' requires elevated privileges') 37 | process.exit(1) 38 | break 39 | case 'EADDRINUSE': 40 | console.error(bind + ' is already in use') 41 | process.exit(1) 42 | break 43 | default: 44 | throw error 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'), 2 | nodemon = require('gulp-nodemon'), 3 | env = require('gulp-env'), 4 | JSX_FILES = ['app/src/client/**/*.js'] 5 | 6 | function runCommand (command) { 7 | return function (cb) { 8 | exec(command, function (err, stdout, stderr) { 9 | console.log(stdout) 10 | console.log(stderr) 11 | cb(err) 12 | }) 13 | } 14 | } 15 | 16 | /* 17 | * GULP tasks 18 | */ 19 | 20 | gulp.task('default', ['nodemon']); 21 | 22 | /* Development */ 23 | gulp.task('nodemon', function (cb) { 24 | var called = false 25 | env({ 26 | file: '.env.json' 27 | }) 28 | return nodemon({ 29 | script: './app.js', 30 | watch: ['app.js'], 31 | ignore: [ 32 | 'test/', 33 | 'node_modules/' 34 | ], 35 | }) 36 | .on('start', function onStart () { 37 | // ensure start only got called once 38 | if (!called) { cb() } 39 | called = true 40 | }) 41 | .on('restart', function onRestart () { 42 | console.log('NODEMON: restarted server!') 43 | 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /app/views/noauth.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InstaJ😂y 6 | 7 | 8 | 9 | 10 | 11 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'), 2 | app = express(), 3 | logger = require('morgan'), 4 | errorHandler = require('errorhandler'), 5 | debug = require('debug')('server'), 6 | http = require('http'), 7 | cookieParser = require('cookie-parser'), 8 | bodyParser = require('body-parser'), 9 | utils = require('./app/src/utils'), 10 | consts = require('./consts'), 11 | axios = require('axios'), 12 | req = require('request-promise-native'), 13 | request = require('request'), 14 | BASE_PATH = `${__dirname}/app`, 15 | ENV = process.env.NODE_ENV || 'development', 16 | DEFAULT_PORT = 3001; 17 | 18 | /* Configuration */ 19 | app.set('views', `${BASE_PATH}/views`) 20 | app.engine('html', require('ejs') 21 | .renderFile); 22 | app.set('view engine', 'html'); 23 | app.use(bodyParser.json()) 24 | app.use(bodyParser.urlencoded({ 25 | extended: false 26 | })) 27 | app.use(cookieParser()) 28 | app.use('/assets', express.static(`${BASE_PATH}/public`)) 29 | 30 | if (ENV === 'development') { 31 | console.log('DEVELOPMENT env') 32 | app.use(errorHandler({ 33 | dumpExceptions: true, 34 | showStack: true 35 | })) 36 | app.use(logger('dev')) 37 | } else { 38 | console.log('PRODUCTION env') 39 | app.use(errorHandler()) 40 | app.use(logger()) 41 | } 42 | 43 | 44 | app.locals = { 45 | userAccessToken: process.env.TEST_ACCESS_TOKEN, 46 | data: null, 47 | fetching: false 48 | } 49 | 50 | /** 51 | * Get port from environment and use it for Express. 52 | */ 53 | const PORT = utils.normalizePort(process.env.PORT || DEFAULT_PORT) 54 | app.set('port', PORT) 55 | 56 | /** 57 | * Create HTTP server. 58 | */ 59 | const server = http.createServer(app) 60 | 61 | /** 62 | * Listen on provided port, on all network interfaces. 63 | */ 64 | 65 | server.listen(PORT) 66 | 67 | /** 68 | * Server event handling 69 | */ 70 | server.on('error', (err) => { 71 | throw err 72 | }) 73 | server.on('listening', (err) => { 74 | let addr = server.address() 75 | let bind = typeof addr === 'string' ? 76 | 'pipe ' + addr : 77 | 'port ' + addr.port 78 | debug('InstaJoy is alive on ' + bind) 79 | }) 80 | 81 | console.log('InstaJoy alive on http://localhost:' + PORT); 82 | 83 | app.get('/', function (req, res) { 84 | if (app.locals.userAccessToken) { 85 | if (!app.locals.data && !app.locals.fetching) { 86 | getImages() 87 | } 88 | console.log('app.locals.data:', app.locals.data); 89 | res.status(200) 90 | .render('index') 91 | } else { 92 | res.status(200) 93 | .render('noauth.ejs', { 94 | clientId: process.env.IG_CLIENT_ID 95 | }) 96 | } 97 | }) 98 | 99 | app.get('/data', function (req, res) { 100 | if (app.locals.data) { 101 | res.status(200) 102 | .json(app.locals.data); 103 | } else { 104 | res.status(404) 105 | .json({ error: 'data not yet fetched or processed' }); 106 | } 107 | }) 108 | 109 | app.get('/auth', function (req, res) { 110 | if (req.query.code) { 111 | console.log("Authorization code is: " + req.query.code); 112 | var formData = { 113 | 'client_id': process.env.IG_CLIENT_ID, 114 | 'client_secret': process.env.IG_CLIENT_SECRET, 115 | 'grant_type': 'authorization_code', 116 | 'redirect_uri': 'http://localhost:3001/auth', 117 | 'code': req.query.code 118 | } 119 | request.post({ 120 | url: 'https://api.instagram.com/oauth/access_token', 121 | formData: formData 122 | }, function (err, httpResponse, body) { 123 | if (err) { 124 | return console.error('Failed to get user access token', err); 125 | } 126 | var parsedBody = JSON.parse(body); 127 | 128 | app.locals.userAccessToken = parsedBody["access_token"]; 129 | console.log('Got user access token:', app.locals.userAccessToken); 130 | // redirect 131 | res.redirect('/'); 132 | // start processing user images 133 | getImages(); 134 | }); 135 | } else { 136 | // error 137 | res.redirect('/'); 138 | } 139 | 140 | }) 141 | 142 | function getImages() { 143 | app.locals.fetching = true; 144 | let count = 100000000000; 145 | request.get(consts.endpoints.instagramMedia + app.locals.userAccessToken + '&count=' + count, function (err, httpResponse, body) { 146 | if (err) { 147 | return console.error('Failed to get data', err); 148 | } 149 | let parsedBody = JSON.parse(body); 150 | let data = parsedBody["data"]; 151 | let images = data.filter(img => img.type === "image"); 152 | getData(images) 153 | .then((res) => { 154 | console.log('Got Data:', JSON.stringify(res)); 155 | app.locals.data = res; 156 | app.locals.fetching = false; 157 | }) 158 | }) 159 | } 160 | 161 | async function getData(imgs) { 162 | let images = imgs.map(img => img["images"]["standard_resolution"]["url"]); 163 | let thumbs = imgs.map(img => img["images"]["thumbnail"]["url"]); 164 | let times = imgs.map(img => img["created_time"]); 165 | let res = []; 166 | console.log('images:', images); 167 | console.log('times:', times); 168 | for (let i = 0; i < images.length; i++) { 169 | let imageSentiments = await getImageSentiments(images[i]); 170 | if (imageSentiments) { 171 | res.push({ 172 | emotion: imageSentiments, 173 | time: times[i], 174 | thumb: thumbs[i] 175 | }); 176 | } 177 | } 178 | return res; 179 | } 180 | 181 | async function getImageSentiments(image) { 182 | try { 183 | let imgSents = await axios({ 184 | method: 'post', 185 | url: consts.endpoints.faceAPI, 186 | data: '{"url": ' + '"' + image + '"}', 187 | headers: { 188 | 'Content-Type': 'application/json', 189 | 'Ocp-Apim-Subscription-Key': process.env.MS_FACE_API_KEY 190 | }, 191 | params: { 192 | 'returnFaceId': 'true', 193 | 'returnFaceLandmarks': 'false', 194 | 'returnFaceAttributes': 'emotion' 195 | }, 196 | }) 197 | 198 | let faceData = imgSents.data 199 | if (faceData.length <= 0) { 200 | console.log("No faces detected for ", image) 201 | return null; 202 | } 203 | 204 | console.log(JSON.stringify(faceData)); 205 | 206 | let modeEmotions = faceData.map(face => Object.keys(face['faceAttributes']['emotion']) 207 | .reduce((a, b) => face['faceAttributes']['emotion'][a] > face['faceAttributes']['emotion'][b] ? a : b)); 208 | let modeEmotion = mode(modeEmotions); 209 | return modeEmotion; 210 | } catch (err) { 211 | console.error(err); 212 | return null; 213 | } 214 | } 215 | 216 | function mode(array) { 217 | if (array.length == 0) 218 | return null; 219 | var modeMap = {}; 220 | var maxEl = array[0], 221 | maxCount = 1; 222 | for (var i = 0; i < array.length; i++) { 223 | var el = array[i]; 224 | if (modeMap[el] == null) 225 | modeMap[el] = 1; 226 | else 227 | modeMap[el]++; 228 | if (modeMap[el] > maxCount) { 229 | maxEl = el; 230 | maxCount = modeMap[el]; 231 | } 232 | } 233 | return maxEl; 234 | } 235 | 236 | module.exports = app -------------------------------------------------------------------------------- /app/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | InstaJ😂y 6 | 7 | 8 | 9 | 10 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 202 | 203 | 204 | --------------------------------------------------------------------------------