├── 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 |
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 |
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 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------