├── README.md ├── public ├── favicon.ico ├── fail.html ├── success.html ├── style.css └── code.html ├── api ├── code.js └── endpoint.js ├── package.json ├── LICENSE ├── .gitignore └── index.js /README.md: -------------------------------------------------------------------------------- 1 | # khan-vr-api 2 | 3 | Endpoint to study Khan Academy in Virtual Reality -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kaelinator/khan-vr-api/master/public/favicon.ico -------------------------------------------------------------------------------- /api/code.js: -------------------------------------------------------------------------------- 1 | 2 | const Promise = require('bluebird') 3 | const khan = require('khan')(process.env.CONSUMER_KEY, process.env.CONSUMER_SECRET) 4 | 5 | let codes = {} 6 | 7 | const isTokenAlive = ({ oauth_token, oauth_token_secret }) => 8 | khan(oauth_token_secret, oauth_token).user() 9 | .then(() => true) 10 | .catch(() => console.log('error') || false) 11 | 12 | 13 | module.exports = { 14 | 15 | add: (code, tokens) => { 16 | codes[code] = tokens 17 | }, 18 | 19 | get: code => codes[code], 20 | 21 | refresh: () => { 22 | Promise.resolve(Object.entries(codes)) 23 | .filter(pair => isTokenAlive(pair[1])) 24 | .reduce((obj, pair) => ({ ...obj, [pair[0]]: pair[1] }), {}) 25 | .then(obj => { code = obj }) // yikes 26 | }, 27 | 28 | isTokenAlive 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "khan-vr-api", 3 | "version": "1.0.0", 4 | "description": "Study Khan Academy in Vr\\", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Kaelinator/khan-vr-api.git" 13 | }, 14 | "author": "Kael Kirk", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/Kaelinator/khan-vr-api/issues" 18 | }, 19 | "homepage": "https://github.com/Kaelinator/khan-vr-api#readme", 20 | "dependencies": { 21 | "bluebird": "^3.5.1", 22 | "dotenv": "^6.0.0", 23 | "express": "^4.16.3", 24 | "khan": "git+https://github.com/Kaelinator/khan.git", 25 | "serve-favicon": "^2.5.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api/endpoint.js: -------------------------------------------------------------------------------- 1 | 2 | const code = require('./code') 3 | const Promise = require('bluebird') 4 | 5 | const get = (fetcher, funcName) => { 6 | 7 | switch (funcName) { 8 | case 'user': 9 | return fetcher.user() 10 | 11 | case 'userExercises': 12 | return fetcher.userExercises() 13 | 14 | default: 15 | return Promise.reject(new Error(`funcName '${funcName}' is not valid`)) 16 | } 17 | 18 | } 19 | 20 | module.exports = khan => (req, res) => { 21 | 22 | const tokens = code.get(req.query['code']) 23 | 24 | if (!tokens) 25 | res.json({ success: false }) 26 | 27 | 28 | const fetcher = khan(tokens['oauth_token_secret'], tokens['oauth_token']) 29 | 30 | get(fetcher, req.params['func']) 31 | .then(data => res.json({ success: true, data })) 32 | .catch(err => { 33 | 34 | console.log(err) 35 | res.json({ success: false }) 36 | }) 37 | } -------------------------------------------------------------------------------- /public/fail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Khan VR Academy 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Khan VR Academy 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kael Kirk 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | data/ 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | 2 | body, html { 3 | height: 100%; 4 | font-family: sans-serif; 5 | } 6 | 7 | body { 8 | margin: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | } 12 | 13 | div.wrapper { 14 | display: grid; 15 | height: 100%; 16 | grid-template-columns: 1fr 1fr 1fr; 17 | grid-template-rows: 1fr 5fr 1fr; 18 | } 19 | 20 | div.wrapper > div { 21 | display: block; 22 | grid-column: 2 / 3; 23 | grid-row: 2 / 3; 24 | } 25 | 26 | div.contents { 27 | display: grid; 28 | grid-template-columns: 1fr 5fr 1fr; 29 | color: #444; 30 | } 31 | 32 | div#header { 33 | text-align: center; 34 | grid-column: 2 / 3; 35 | grid-row: 1 / 2; 36 | } 37 | 38 | form { 39 | grid-column: 1 / 4; 40 | grid-row: 2 / 3; 41 | } 42 | 43 | div#form { 44 | display: grid; 45 | grid-template-columns: 1fr 3fr 1fr; 46 | grid-template-rows: repeat(1fr, 3); 47 | } 48 | 49 | div#input-code-wrap > input { 50 | width: 100%; 51 | } 52 | 53 | div.fine-print { 54 | color: #999; 55 | padding-top: 5%; 56 | padding-bottom: 5%; 57 | grid-column: 1 / 4; 58 | } 59 | 60 | div.fine-print > small > a { 61 | color: inherit; 62 | } 63 | 64 | div#submit-wrap { 65 | grid-column: 1 / 4; 66 | } 67 | 68 | input#go { 69 | font-size: 1.25em; 70 | padding: 10px; 71 | width: 100%; 72 | border: 2px solid #FFF; 73 | border-radius: 25px; 74 | color: #FFF; 75 | background: linear-gradient(rgba(112, 178, 6, 1.00), rgba(101, 161, 6, 1.00)); 76 | } 77 | 78 | input#go:focus { 79 | box-shadow: 0 0 0 1px rgba(107, 170, 6, 1.00); 80 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const favicon = require('serve-favicon') 4 | const path = require('path') 5 | require('dotenv').config() 6 | const khan = require('khan')(process.env.CONSUMER_KEY, process.env.CONSUMER_SECRET) 7 | const code = require('./api/code') 8 | 9 | app.use(express.static('public')) 10 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico'))) 11 | 12 | app.get('/', (req, res) => { 13 | 14 | res.sendFile(path.join(__dirname, 'public', 'code.html')) 15 | }) 16 | 17 | app.get('/oauth', (req, res) => { 18 | 19 | khan 20 | .requestToken(`${process.env.OAUTH_CALLBACK}?code=${req.query.code}`) 21 | .then(tokens => { 22 | res.redirect(`https://www.khanacademy.org/api/auth2/authorize?oauth_token=${tokens['oauth_token']}`) 23 | }) 24 | }) 25 | 26 | app.get('/oauth/callback', (req, res) => { 27 | 28 | khan.accessToken(req.query['oauth_token'], req.query['oauth_verifier'], req.query['oauth_token_secret']) 29 | .then(tokens => code.add(req.query['code'], tokens) || tokens) // yikes 30 | .then(code.isTokenAlive) 31 | .then(alive => 32 | res.sendFile(path.join(__dirname, 'public', alive && 'success.html' || 'fail.html')) 33 | ) 34 | .catch(() => res.sendFile(path.join(__dirname, 'public', 'fail.html'))) 35 | }) 36 | 37 | app.get('/api', (req, res) => { 38 | 39 | const tokens = code.get(req.query['code']) 40 | 41 | if (!tokens) 42 | res.json({ success: false }) 43 | 44 | khan(tokens['oauth_token_secret'], tokens['oauth_token']) 45 | .user() 46 | .then(() => res.json({ success: true })) 47 | .catch(err => { 48 | 49 | console.log(err) 50 | res.json({ success: false }) 51 | }) 52 | }) 53 | 54 | app.get('/api/:func', require('./api/endpoint')(khan)) 55 | 56 | setInterval(code.refresh, 5000) 57 | 58 | app.listen(process.env.PORT || 3000) -------------------------------------------------------------------------------- /public/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Khan VR Academy 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | 21 |
22 |
23 |
24 | 25 | Get your code by going into 26 | Khan VR Academy, and selecting "Get Code". 27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | Khan VR Academy, with all repositories and contributors associated, have no affiliation with the Khan Academy organization 36 | with the exception of an API OAuth consumer registration. 37 | 38 |
39 | 41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | --------------------------------------------------------------------------------