├── __mocks__
├── styleMock.js
└── fileMock.js
├── .babelrc
├── test
└── jestsetup.js
├── src
├── Mobile.js
├── Panel.js
├── components
│ ├── App
│ │ ├── App.css
│ │ ├── CommandListContainer
│ │ │ ├── CommandListContainer.css
│ │ │ ├── CommandList
│ │ │ │ ├── CommandList.css
│ │ │ │ └── CommandList.js
│ │ │ └── CommandListContainer.js
│ │ ├── App.test.js
│ │ └── App.js
│ ├── ConfigPage
│ │ ├── Config.css
│ │ ├── ConfigPage.test.js
│ │ ├── ConfigContainer
│ │ │ ├── ConfigContainer.css
│ │ │ ├── ConfigCommand
│ │ │ │ ├── ConfigCommand.css
│ │ │ │ └── ConfigCommand.js
│ │ │ └── ConfigContainer.js
│ │ └── ConfigPage.js
│ └── Authentication
│ │ ├── Authentication.test.js
│ │ └── Authentication.js
├── VideoComponent.js
├── VideoOverlay.js
├── Config.js
└── LiveConfig.js
├── template.html
├── public
├── panel.html
├── config.html
├── mobile.html
├── video_overlay.html
├── live_config.html
└── video_component.html
├── bin
└── generate_cert.sh
├── .gitignore
├── index.js
├── package.json
├── README.md
└── webpack.config.js
/__mocks__/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
--------------------------------------------------------------------------------
/__mocks__/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub'
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "env",
4 | "react"
5 | ]
6 | }
--------------------------------------------------------------------------------
/test/jestsetup.js:
--------------------------------------------------------------------------------
1 | import Enzyme from 'enzyme'
2 | import Adapter from 'enzyme-adapter-react-16'
3 | Enzyme.configure({ adapter: new Adapter() })
--------------------------------------------------------------------------------
/src/Mobile.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "./components/App/App"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/Panel.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "./components/App/App"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/components/App/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | font-family: Arial, Helvetica, sans-serif;
3 | }
4 |
5 | .App-light{
6 | color:#232127;
7 | }
8 |
9 | .App-dark{
10 | color:#e5e3e8;
11 | }
--------------------------------------------------------------------------------
/src/VideoComponent.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "./components/App/App"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/VideoOverlay.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import App from "./components/App/App"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/components/ConfigPage/Config.css:
--------------------------------------------------------------------------------
1 | .Config{
2 | font-family: Arial, Helvetica, sans-serif;
3 | }
4 |
5 | .Config-light{
6 | color:#232127;
7 | }
8 |
9 | .Config-dark{
10 | color:#e5e3e8;
11 | }
--------------------------------------------------------------------------------
/src/Config.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import ConfigPage from "./components/ConfigPage/ConfigPage"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/LiveConfig.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import ReactDOM from "react-dom"
3 | import ConfigPage from "./components/ConfigPage/ConfigPage"
4 |
5 | ReactDOM.render(
6 | ,
7 | document.getElementById("root")
8 | )
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigPage.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme'
2 | import React from 'react'
3 | import ConfigPage from './ConfigPage'
4 |
5 | test('renders without failing', ()=>{
6 | let wrapper = shallow()
7 |
8 | expect(wrapper).toBeDefined()
9 | })
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigContainer/ConfigContainer.css:
--------------------------------------------------------------------------------
1 | .config-container-container{
2 | display:flex;
3 | flex-direction: column;
4 | }
5 |
6 | .config-container-form{
7 | margin-bottom:.5em;
8 | }
9 |
10 | .config-container-footer{
11 | margin-top:.5em;
12 | }
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigContainer/ConfigCommand/ConfigCommand.css:
--------------------------------------------------------------------------------
1 | .config-command-container{
2 | display:flex;
3 | }
4 |
5 | .config-command-actions{
6 | height: inherit;
7 | padding-right:1em;
8 | cursor: pointer;
9 | border-right: 1px solid;
10 | }
11 |
12 | .config-command-command{
13 | padding-left:1em;
14 | }
--------------------------------------------------------------------------------
/src/components/App/CommandListContainer/CommandListContainer.css:
--------------------------------------------------------------------------------
1 | .header{
2 | width:100%;
3 | display:flex;
4 | flex-direction:row;
5 | color: #e5e3e8;
6 | background-color:#6441A4;
7 | font-weight: 400;
8 | }
9 |
10 | .header > .header-text, .header > .header-expand-toggle{
11 | margin:.25em;
12 | }
13 |
14 | .header > .header-expand-toggle{
15 | margin-left:auto;
16 | }
--------------------------------------------------------------------------------
/src/components/App/CommandListContainer/CommandList/CommandList.css:
--------------------------------------------------------------------------------
1 | .command-container{
2 | display: block;
3 | padding-bottom:5px;
4 | padding-top:5px;
5 | margin: .5em;
6 | border-bottom: 1px solid;
7 | }
8 |
9 | .command-container:hover{
10 | cursor:pointer;
11 | }
12 |
13 | .command-header{
14 | width:100%;
15 | display:flex;
16 | flex-direction:row;
17 | }
18 |
19 | .command-header > .command-expand-toggle{
20 | margin-left:auto;
21 | }
--------------------------------------------------------------------------------
/template.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= htmlWebpackPlugin.options.title %>
8 |
9 |
10 |
11 |
14 |
15 |
--------------------------------------------------------------------------------
/public/panel.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Starter
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sample Config Page
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/mobile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sample Config Page
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/video_overlay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Starter
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/live_config.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Sample Config Page
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/public/video_component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | React Starter
8 |
9 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/components/App/App.test.js:
--------------------------------------------------------------------------------
1 | import { shallow } from 'enzyme'
2 | import React from 'react'
3 | import App from './App'
4 |
5 | test('renders without failing', ()=>{
6 | let wrapper = shallow()
7 |
8 | expect(wrapper).toBeDefined()
9 | })
10 |
11 | test('able to change theme based on context',()=>{
12 | let wrapper = shallow()
13 | let instance = wrapper.instance()
14 |
15 | expect(wrapper.state('theme')).toEqual('light')
16 | instance.contextUpdate({theme:'dark'},['theme'])
17 | expect(wrapper.state('theme')).toEqual('dark')
18 | })
19 |
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigContainer/ConfigCommand/ConfigCommand.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './ConfigCommand.css'
3 |
4 | export default class ConfigCommand extends React.Component{
5 | constructor(props){
6 | super(props)
7 | }
8 |
9 | render(){
10 | return(
11 |
12 |
this.props.delete(this.props.commandKey)}>
13 | Delete
14 |
15 |
16 | Command: {this.props.command.command}
17 | Description: {this.props.command.description}
18 |
19 |
20 | )
21 | }
22 | }
--------------------------------------------------------------------------------
/bin/generate_cert.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | NAME=${1:-server}
3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4 | openssl req \
5 | -newkey rsa:4096 \
6 | -days 1001 \
7 | -nodes \
8 | -x509 \
9 | -subj "/C=US/ST=California/L=San Francisco/O=LULZCorp/OU=web/CN=localhost" \
10 | -extensions SAN \
11 | -config <( cat $( [[ "Darwin" = "$(uname -s)" ]] && echo /System/Library/OpenSSL/openssl.cnf || echo /etc/ssl/openssl.cnf ) \
12 | <(printf "[SAN]\nsubjectAltName='DNS:localhost'")) \
13 | -keyout "${DIR}/${NAME}.key" \
14 | -out "${DIR}/${NAME}.crt"
15 |
16 | echo ""
17 | echo "* Generated $NAME.key and $NAME.crt files in local directory"
18 | echo ""
19 |
20 | if [[ "$OSTYPE" == "darwin"* ]]; then
21 | echo "* Installing cert into local Keychain."
22 | echo "* To see or modify, run 'Keychain Access' app and look in the 'System' Folder"
23 | sudo security add-trusted-cert -d -p ssl -r trustRoot -k "/Library/Keychains/System.keychain" "${DIR}/${NAME}.crt"
24 | else
25 | echo "* Please install and trust cert at conf/$NAME.crt"
26 | fi
27 | cd "$DIR"
28 | if [[ ! -d "${DIR}/../conf/" ]]; then
29 | mkdir "${DIR}/../conf/"
30 | fi
31 | mv ${NAME}.{key,crt} "${DIR}/../conf/"
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Configuration
2 | conf/*
3 |
4 | # Junk
5 | .DS_Store
6 |
7 | # Logs
8 | logs
9 | *.log
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (http://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Typescript v1 declaration files
46 | typings/
47 |
48 | # Optional npm cache directory
49 | .npm
50 |
51 | # Optional eslint cache
52 | .eslintcache
53 |
54 | # Optional REPL history
55 | .node_repl_history
56 |
57 | # Output of 'npm pack'
58 | *.tgz
59 |
60 | # Yarn Integrity file
61 | .yarn-integrity
62 |
63 | # dotenv environment variables file
64 | .env
65 |
66 | # Finalized files
67 | dist/
68 |
--------------------------------------------------------------------------------
/src/components/App/CommandListContainer/CommandList/CommandList.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | import './CommandList.css'
4 |
5 | export default class CommandList extends React.Component{
6 | constructor(props){
7 | super(props)
8 |
9 | this.state={
10 | expanded: false
11 | }
12 | }
13 |
14 | toggleDescription(){
15 | this.setState(prevState=>{
16 | return {
17 | expanded:!prevState.expanded
18 | }
19 | })
20 | }
21 |
22 | render(){
23 | return(
24 | this.toggleDescription()}>
25 |
26 |
27 | {this.props.command.command}
28 |
29 |
30 | {this.state.expanded ? "Collapse" : "Expand"}
31 |
32 |
33 |
34 | {this.state.expanded ? this.props.command.description : ""}
35 |
36 |
37 | )
38 | }
39 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const HtmlWebpackPlugin = require('html-webpack-plugin')
2 |
3 | let entryPoints = {
4 | VideoComponent:{
5 | path:"./src/VideoComponent.js",
6 | outputHtml:"video_component.html",
7 | build:true
8 | },
9 | VideoOverlay:{
10 | path:"./src/VideoOverlay.js",
11 | outputHtml:"video_overlay.html",
12 | build:true
13 | },
14 | Panel:{
15 | path:"./src/Panel.js",
16 | outputHtml:"panel.html",
17 | build:true
18 | },
19 | Config:{
20 | path:"./src/Config.js",
21 | outputHtml:"config.html",
22 | build:true
23 | },
24 | LiveConfig:{
25 | path:"./src/LiveConfig.js",
26 | outputHtml:"live_config.html",
27 | build:true
28 | },
29 | Mobile:{
30 | path:"./src/Mobile.js",
31 | outputHtml:"mobile.html",
32 | build:true
33 | }
34 | }
35 |
36 | let entry = {}
37 | let output = []
38 | for(name in entryPoints){
39 | if(entryPoints[name].build){
40 | entry[name]=entryPoints[name].path
41 | output.push(new HtmlWebpackPlugin({
42 | inject:true,
43 | chunks:name,
44 | template:'./template.html',
45 | filename:entryPoints[name].outputHtml
46 | }))
47 | }
48 | }
49 | console.log(entry)
50 | console.log(output)
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-frontend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "jest",
8 | "build": "webpack --mode production",
9 | "start": "webpack-dev-server --mode development",
10 | "cert": "./bin/generate_cert.sh server",
11 | "host": "webpack-dev-server --mode development --devrig"
12 | },
13 | "keywords": [],
14 | "author": "",
15 | "license": "ISC",
16 | "devDependencies": {
17 | "babel-cli": "^6.26.0",
18 | "babel-core": "^6.26.0",
19 | "babel-jest": "^23.4.2",
20 | "babel-loader": "^7.1.4",
21 | "babel-preset-env": "^1.7.0",
22 | "babel-preset-react": "^6.24.1",
23 | "css-loader": "^0.28.11",
24 | "enzyme": "^3.4.1",
25 | "enzyme-adapter-react-16": "^1.2.0",
26 | "file-loader": "^1.1.11",
27 | "html-webpack-plugin": "^3.2.0",
28 | "jest": "^23.5.0",
29 | "style-loader": "^0.21.0",
30 | "webpack": "^4.6.0",
31 | "webpack-cli": "^3.1.0",
32 | "webpack-dev-server": "^3.1.3"
33 | },
34 | "dependencies": {
35 | "jsonwebtoken": "^8.3.0",
36 | "react": "^16.3.2",
37 | "react-dom": "^16.3.2"
38 | },
39 | "jest": {
40 | "setupFiles": [
41 | "/test/jestsetup.js"
42 | ],
43 | "moduleNameMapper": {
44 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js",
45 | "\\.(css|less)$": "/__mocks__/styleMock.js"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/App/CommandListContainer/CommandListContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import CommandList from './CommandList/CommandList'
3 |
4 | import './CommandListContainer.css'
5 |
6 | export default class CommandListContainer extends React.Component{
7 | constructor(props){
8 | super(props)
9 |
10 | this.state={
11 | expanded:this.props.panel,
12 | panel:this.props.panel
13 | }
14 | }
15 |
16 | expandCommands(){
17 | this.setState((prevstate)=>{
18 | return {expanded:!prevstate.expanded}
19 | })
20 | }
21 |
22 | render(){
23 | return (
24 |
25 |
26 |
27 | Bot Commander
28 |
29 | {!this.state.panel ?
30 |
this.expandCommands()}>
31 | {this.state.expanded ? 'expanded': 'hidden'}
32 |
33 | :
34 |
35 |
36 | }
37 |
38 |
39 | {this.state.expanded ?
40 | this.props.commands ?
41 | this.props.commands.map((v,i)=>{
42 | return (
)
43 | })
44 | : ''
45 | : ''
46 | }
47 |
48 | )
49 | }
50 | }
--------------------------------------------------------------------------------
/src/components/Authentication/Authentication.test.js:
--------------------------------------------------------------------------------
1 | import Authentication from './Authentication'
2 | import "isomorphic-fetch"
3 |
4 | let normalToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjU5NzMxOTAsIm9wYXF1ZV91c2VyX2lkIjoiVVJpZ0FCQzEyIiwiY2hhbm5lbF9pZCI6ImRlbW9fY2hhbm5lbCIsInJvbGUiOiJ2aWV3ZXIiLCJwdWJzdWJfcGVybXMiOnsibGlzdGVuIjpbImJyb2FkY2FzdCIsImdsb2JhbCJdLCJzZW5kIjpbImJyb2FkY2FzdCJdfSwidXNlcl9pZCI6Inh4eHh4eHh4eCIsImlhdCI6MTUzNDQzNzE5MH0.O2gmsiVIUegWVMwS_mbRU1SF6cdBmHJ5E7JLmsV9AcY'
5 | let modToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjU5NzMxOTAsIm9wYXF1ZV91c2VyX2lkIjoiVVJpZ0FCQzEyIiwiY2hhbm5lbF9pZCI6ImRlbW9fY2hhbm5lbCIsInJvbGUiOiJicm9hZGNhc3RlciIsInB1YnN1Yl9wZXJtcyI6eyJsaXN0ZW4iOlsiYnJvYWRjYXN0IiwiZ2xvYmFsIl0sInNlbmQiOlsiYnJvYWRjYXN0Il19LCJ1c2VyX2lkIjoieHh4eHh4eHh4IiwiaWF0IjoxNTM0NDM3MTkwfQ.KhwwsrhcsPanRyaPL6dO2knTD0JMXcP38oVqDgjt5Mk'
6 |
7 | let auth = new Authentication()
8 |
9 | test('able to create new Authenciation instance', ()=>{
10 | expect(auth).toBeDefined()
11 | })
12 |
13 | test('able to set a token', ()=>{
14 | auth.setToken(normalToken,'U12345678')
15 | expect(auth.isAuthenticated()).toEqual(true)
16 | })
17 |
18 |
19 | describe('makeCall tests', ()=>{
20 | test('able to call a test URL', async ()=>{
21 | let response = await auth.makeCall('https://twitch.tv/')
22 | expect(response.status).toEqual(200)
23 | })
24 |
25 | test('rejects when no credentials', ()=>{
26 | auth.setToken('','')
27 | return expect(auth.makeCall('https://twitch.tv/')).rejects.toEqual('Unauthorized')
28 | })
29 |
30 | test('rejects on invalid response',()=>{
31 | auth.setToken('abc123','U12345678')
32 | return expect(auth.makeCall('htts://api')).rejects.toBeDefined()
33 | })
34 |
35 | test('rejecsts on bad credentials',async ()=>{
36 | return expect(auth.makeCall('https://google.com')).rejects.toBeDefined()
37 | })
38 | })
39 |
40 | describe('moderator tests', ()=>{
41 | test('returns valid mod status',()=>{
42 | auth.setToken(modToken,'ABC123')
43 | expect(auth.isModerator()).toEqual(true)
44 |
45 | auth.setToken(normalToken,'ABC123')
46 | expect(auth.isModerator()).toEqual(false)
47 | })
48 | })
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Bot Commander Config Service Example
2 |
3 | ## Background
4 |
5 | This is a quick example of the new config service, and how to build an extension with the service that does not rely on a backend server (EBS).
6 |
7 | ## Requirements
8 |
9 | There is only one requirement to use this example.
10 |
11 | * Node.JS LTS or greater.
12 |
13 | You may also find that using `yarn` is easier than `npm`, so we do recommend installing that as well by running:
14 | ```
15 | npm i -g yarn
16 | ```
17 | in an elevated command line interface.
18 |
19 | ## First time Usage
20 |
21 | ### [Developer Rig](https://dev.twitch.tv/docs/extensions/rig/) Usage
22 |
23 | If you are using the developer rig and have used this as your basis for your extension, please ignore the below steps- the developer rig has taken care of it for you!
24 |
25 | ### Local Development
26 |
27 | If you're wanting to develop this locally, use the below instructions.
28 | To use this, simply clone the repository into the folder of your choice.
29 |
30 | For example, to clone this into a `` folder, simply run the following in a commandline interface:
31 | ```
32 | git clone
33 | ```
34 |
35 | Next, do the following:
36 |
37 | 1. Change directories into the cloned folder.
38 | 2. Run `yarn install` to install all prerequisite packages needed to run the template.
39 | 3. Run `yarn cert` to generate the needed certificates. This allows the server to be run over HTTPS vs. HTTP.
40 | 4. Run `yarn start` to run the sample. If everything works, you should be be able to go to the developer rig, create a panel view, and see `Hello world!`
41 |
42 | ## Usage
43 |
44 | To build your finalized React JS files, simply run `yarn build` to build the various webpacked files.
45 |
46 | ## File Structure
47 |
48 | The file structure in the template is laid out with the following:
49 |
50 | ### bin
51 |
52 | The `/bin` folder holds the cert generation script.
53 |
54 | ### conf
55 |
56 | The `/conf` folder holds the generated certs after the cert generation script runs.
57 |
58 | ### dist
59 |
60 | `/dist` holds the final JS files after building.
61 |
62 | ### public
63 |
64 | `/public` houses the static HTML files used for your code's entrypoint.
65 |
66 | ### src
67 |
68 | This folder houses all source code and relevant files (such as images). Each React class/component is given a folder to house all associated files (such as associated CSS).
69 |
70 | Below this folder, the structure is much simpler.
71 |
72 | This would be:
73 |
74 | `
75 | components
76 | -\App
77 | --\App.js
78 | --\App.test.js
79 | --\App.css
80 | -\Authentication
81 | --\Authentication.js
82 | ...
83 | `
84 |
85 | Each component is under the `components` folder.
86 |
--------------------------------------------------------------------------------
/src/components/Authentication/Authentication.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 |
3 | /**
4 | * Helper class for authentication against an EBS service. Allows the storage of a token to be accessed across componenents.
5 | */
6 | export default class Authentication{
7 |
8 | constructor(token, opaque_id){
9 | this.state={
10 | token,
11 | opaque_id,
12 | user_id:false,
13 | isMod:false,
14 | role:""
15 | }
16 | }
17 |
18 | // this does guarantee the user is a moderator- this is fairly simple to bypass - so pass the JWT and verify
19 | // server-side that this is true. This, however, allows you to render client-side UI for users without holding on a backend to verify the JWT.
20 | isModerator(){
21 | return this.state.isMod
22 | }
23 |
24 | // similar to mod status, this isn't always verifyable, so have your backend verify before proceeding.
25 | hasSharedId(){
26 | return !!this.state.user_id
27 | }
28 |
29 | getUserId(){
30 | return this.state.user_id
31 | }
32 |
33 | // set the token in the Authentication componenent state
34 | setToken(token,opaque_id){
35 | let mod = false
36 | let role = ""
37 | let user_id = ""
38 |
39 | try {
40 | let decoded = jwt.decode(token)
41 |
42 | if(decoded.role === 'broadcaster' || decoded.role === 'moderator'){
43 | mod = true
44 | }
45 |
46 | user_id = decoded.user_id
47 | role = decoded.role
48 | } catch (e) {
49 | console.log('Invalid token.')
50 | token=''
51 | opaque_id=''
52 | }
53 |
54 | this.state={
55 | token,
56 | opaque_id,
57 | isMod:mod,
58 | user_id,
59 | role
60 | }
61 | }
62 |
63 | // checks to ensure there is a valid token in the state
64 | isAuthenticated(){
65 | if(this.state.token && this.state.opaque_id){
66 | return true
67 | }else{
68 | return false
69 | }
70 | }
71 |
72 | /**
73 | * Makes a call against a given endpoint using a specific method.
74 | *
75 | * Returns a Promise with the Request() object per fetch documentation.
76 | *
77 | */
78 |
79 | makeCall(url, method="GET"){
80 | return new Promise((resolve, reject)=>{
81 | if(this.isAuthenticated()){
82 | let headers={
83 | 'Content-Type':'application/json',
84 | 'Authorization': `Bearer ${this.state.token}`
85 | }
86 |
87 | fetch(url,
88 | {
89 | method,
90 | headers,
91 | })
92 | .then(response=>resolve(response))
93 | .catch(e=>reject(e))
94 | }else{
95 | reject('Unauthorized')
96 | }
97 | })
98 | }
99 | }
--------------------------------------------------------------------------------
/src/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Authentication from '../Authentication/Authentication'
3 | import CommandListContainer from './CommandListContainer/CommandListContainer'
4 |
5 | import './App.css'
6 |
7 | export default class App extends React.Component{
8 | constructor(props){
9 | super(props)
10 | this.Authentication = new Authentication()
11 |
12 | this.anchor = this.queryParamParse().anchor
13 | //if the extension is running on twitch or dev rig, set the shorthand here. otherwise, set to null.
14 | this.twitch = window.Twitch ? window.Twitch.ext : null
15 | this.state={
16 | finishedLoading:false,
17 | theme:'light',
18 | commands:[]
19 | }
20 | }
21 |
22 | queryParamParse(){
23 | let query = window.location.search
24 | let obj = {}
25 | query.substring(1).split('&').map(v=>{
26 | let s = v.split('=')
27 | obj[s[0]] = decodeURIComponent(s[1])
28 | })
29 |
30 | return obj
31 | }
32 |
33 | contextUpdate(context, delta){
34 | if(delta.includes('theme')){
35 | this.setState(()=>{
36 | return {theme:context.theme}
37 | })
38 | }
39 | }
40 |
41 | componentDidMount(){
42 | if(this.twitch){
43 | this.twitch.onAuthorized((auth)=>{
44 | this.Authentication.setToken(auth.token, auth.userId)
45 | if(!this.state.finishedLoading){
46 | this.setState(()=>{
47 | return {finishedLoading:true}
48 | })
49 | }
50 | })
51 |
52 | this.twitch.onContext((context,delta)=>{
53 | this.contextUpdate(context,delta)
54 | })
55 |
56 | this.twitch.configuration.onChanged(()=>{
57 | let config = this.twitch.configuration.broadcaster ? this.twitch.configuration.broadcaster.content : ''
58 | try{
59 | config = JSON.parse(config)
60 | }catch(e){
61 | config = ''
62 | }
63 |
64 | this.setState(()=>{
65 | return{
66 | commands:config
67 | }
68 | })
69 | })
70 | }
71 | }
72 |
73 | componentWillUnmount(){
74 | if(this.twitch){
75 | this.twitch.unlisten('broadcast', ()=>console.log('successfully unlistened'))
76 | }
77 | }
78 |
79 | render(){
80 | if(this.state.finishedLoading && this.state.commands.length > 0){
81 | return (
82 |
83 |
84 |
85 | )
86 | }else{
87 | return (
88 |
89 | No commands configured.
90 |
91 | )
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require("path")
3 | const webpack = require("webpack")
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 |
6 | // defines where the bundle file will live
7 | const bundlePath = path.resolve(__dirname, "dist/")
8 |
9 | module.exports = (_env,argv)=> {
10 | let entryPoints = {
11 | VideoComponent:{
12 | path:"./src/VideoComponent.js",
13 | outputHtml:"video_component.html",
14 | build:false
15 | },
16 | VideoOverlay:{
17 | path:"./src/VideoOverlay.js",
18 | outputHtml:"video_overlay.html",
19 | build:false
20 | },
21 | Panel:{
22 | path:"./src/Panel.js",
23 | outputHtml:"panel.html",
24 | build:true
25 | },
26 | Config:{
27 | path:"./src/Config.js",
28 | outputHtml:"config.html",
29 | build:true
30 | },
31 | LiveConfig:{
32 | path:"./src/LiveConfig.js",
33 | outputHtml:"live_config.html",
34 | build:false
35 | },
36 | Mobile:{
37 | path:"./src/Mobile.js",
38 | outputHtml:"mobile.html",
39 | build:true
40 | }
41 | }
42 |
43 | let entry = {}
44 |
45 | // edit webpack plugins here!
46 | let plugins = [
47 | new webpack.HotModuleReplacementPlugin()
48 | ]
49 |
50 | for(name in entryPoints){
51 | if(entryPoints[name].build){
52 | entry[name]=entryPoints[name].path
53 | if(argv.mode==='production'){
54 | plugins.push(new HtmlWebpackPlugin({
55 | inject:true,
56 | chunks:[name],
57 | template:'./template.html',
58 | filename:entryPoints[name].outputHtml
59 | }))
60 | }
61 | }
62 | }
63 |
64 | let config={
65 | //entry points for webpack- remove if not used/needed
66 | entry,
67 | optimization: {
68 | minimize: false // neccessary to pass Twitch's review process
69 | },
70 | module: {
71 | rules: [
72 | {
73 | test: /\.(js|jsx)$/,
74 | exclude: /(node_modules|bower_components)/,
75 | loader: 'babel-loader',
76 | options: { presets: ['env'] }
77 | },
78 | {
79 | test: /\.css$/,
80 | use: [ 'style-loader', 'css-loader' ]
81 | },
82 | {
83 | test: /\.(jpe?g|png|gif|svg)$/i,
84 | loader: "file-loader",
85 | options:{
86 | name:"img/[name].[ext]"
87 | }
88 | }
89 | ]
90 | },
91 | resolve: { extensions: ['*', '.js', '.jsx'] },
92 | output: {
93 | filename: "[name].bundle.js",
94 | path:bundlePath
95 | },
96 | plugins
97 | }
98 | if(argv.mode==='development'){
99 | config.devServer = {
100 | contentBase: path.join(__dirname,'public'),
101 | host:argv.devrig ? 'localhost.rig.twitch.tv' : 'localhost',
102 | headers: {
103 | 'Access-Control-Allow-Origin': '*'
104 | },
105 | port: 8080
106 | }
107 | if(fs.existsSync(path.resolve(__dirname,'conf/server.key'))){
108 | config.devServer.https = {
109 | key:fs.readFileSync(path.resolve(__dirname,'conf/server.key')),
110 | cert:fs.readFileSync(path.resolve(__dirname,'conf/server.crt'))
111 | }
112 | }
113 | }
114 |
115 | return config;
116 | }
117 |
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigPage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Authentication from '../Authentication/Authentication'
3 | import ConfigContainer from './ConfigContainer/ConfigContainer'
4 |
5 | import './Config.css'
6 |
7 | export default class ConfigPage extends React.Component{
8 | constructor(props){
9 | super(props)
10 | this.Authentication = new Authentication()
11 |
12 | //if the extension is running on twitch or dev rig, set the shorthand here. otherwise, set to null.
13 | this.twitch = window.Twitch ? window.Twitch.ext : null
14 | this.state={
15 | finishedLoading:false,
16 | theme:'light',
17 | commands:[]
18 | }
19 | }
20 |
21 | contextUpdate(context, delta){
22 | if(delta.includes('theme')){
23 | this.setState(()=>{
24 | return {theme:context.theme}
25 | })
26 | }
27 | }
28 |
29 | componentDidMount(){
30 | // do config page setup as needed here
31 | if(this.twitch){
32 | this.twitch.onAuthorized((auth)=>{
33 | this.Authentication.setToken(auth.token, auth.userId)
34 | if(!this.state.finishedLoading){
35 | // if the component hasn't finished loading (as in we've not set up after getting a token), let's set it up now.
36 |
37 | // now we've done the setup for the component, let's set the state to true to force a rerender with the correct data.
38 | this.setState(()=>{
39 | return {finishedLoading:true}
40 | })
41 | }
42 | })
43 |
44 | this.twitch.onContext((context,delta)=>{
45 | this.contextUpdate(context,delta)
46 | })
47 |
48 | this.twitch.configuration.onChanged(()=>{
49 | let config = this.twitch.configuration.broadcaster ? this.twitch.configuration.broadcaster.content : []
50 | try{
51 | config = JSON.parse(config)
52 | }catch(e){
53 | config = []
54 | }
55 |
56 | this.setState(()=>{
57 | return{
58 | commands:config
59 | }
60 | })
61 | })
62 | }
63 | }
64 |
65 | saveConfig(commands){
66 | this.twitch.configuration.set('broadcaster', '1.0', JSON.stringify(commands))
67 |
68 | this.setState(prevState=>{
69 | return{
70 | commands
71 | }
72 | })
73 | }
74 |
75 | render(){
76 | if(this.state.finishedLoading && this.Authentication.isModerator()){
77 | return(
78 |
79 |
80 | this.saveConfig(commands)}/>
81 |
82 |
83 | )
84 | }
85 | else{
86 | return(
87 |
88 |
89 | Loading...
90 |
91 |
92 | )
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/src/components/ConfigPage/ConfigContainer/ConfigContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ConfigCommand from './ConfigCommand/ConfigCommand'
3 |
4 | import './ConfigContainer.css'
5 |
6 | export default class ConfigContainer extends React.Component{
7 | constructor(props){
8 | super(props)
9 |
10 | this.state = {
11 | commands:this.props.commands
12 | }
13 | }
14 |
15 | onSubmit(event) {
16 | event.preventDefault()
17 |
18 | if(this.state.commands.length <= 5){
19 | this.setState(prevState=>{
20 | let commands = prevState.commands
21 | commands.push({
22 | command:this.state.command,
23 | description:this.state.description,
24 | date:new Date()
25 | })
26 | return {
27 | commands
28 | }
29 | })
30 | this.commandInput.value = ""
31 | this.descriptionInput.value = ""
32 | }
33 | }
34 |
35 | handleInputChange(event) {
36 | const target = event.target;
37 | const value = target.value;
38 | const name = target.name;
39 |
40 | this.setState({
41 | [name]: value
42 | });
43 | }
44 |
45 | deleteCommand(key){
46 | this.setState(prevState=>{
47 | return{
48 | commands:prevState.commands.filter(old=>old.date != key)}
49 | }
50 | )
51 | }
52 |
53 | render(){
54 | return(
55 |
56 |
57 |
82 |
83 |
84 |
85 | {this.state.commands.map((v,i)=>{
86 | return this.deleteCommand(key)}/>
87 | })}
88 |
89 |
90 | this.props.saveConfig(this.state.commands)}
93 | disabled={this.state.commands.length===0}
94 | value="Save commands!"
95 | />
96 |
97 |
98 | )
99 | }
100 | }
--------------------------------------------------------------------------------