├── 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 | 
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 |
23 |
24 |
25 |
Your happiness score is
26 |
27 | 46>
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
202 |
203 |
204 |
--------------------------------------------------------------------------------