Initial commit.
This commit is contained in:
parent
489a16c0b5
commit
3cb6f97610
17 changed files with 1881 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
*.log
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
data/
|
||||||
|
**/config.toml
|
||||||
26
server/config.template.toml
Normal file
26
server/config.template.toml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[server]
|
||||||
|
host = ""
|
||||||
|
port = 6969
|
||||||
|
|
||||||
|
[server.auth]
|
||||||
|
enabled = false
|
||||||
|
# username = ""
|
||||||
|
# password = ""
|
||||||
|
|
||||||
|
|
||||||
|
[log]
|
||||||
|
level = "info"
|
||||||
|
|
||||||
|
[twitch]
|
||||||
|
client_id = ""
|
||||||
|
client_secret = ""
|
||||||
|
|
||||||
|
[twitch.auth]
|
||||||
|
redirect_uri = "/twitch/login/callback"
|
||||||
|
base_url = "https://id.twitch.tv/oauth2"
|
||||||
|
scopes = [
|
||||||
|
"channel:read:polls",
|
||||||
|
"channel:manage:polls",
|
||||||
|
"channel:read:predictions",
|
||||||
|
"channel:manage:predictions"
|
||||||
|
]
|
||||||
33
server/package.json
Normal file
33
server/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "Oliver Akins",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/glob": "^7.2.0",
|
||||||
|
"@types/hapi__basic": "^5.1.2",
|
||||||
|
"@types/hapi__hapi": "^20.0.10",
|
||||||
|
"@types/hapi__inert": "^5.2.3",
|
||||||
|
"@types/node": "^17.0.17"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hapi/basic": "^6.0.0",
|
||||||
|
"@hapi/boom": "^9.1.4",
|
||||||
|
"@hapi/hapi": "^20.2.1",
|
||||||
|
"@hapi/inert": "^6.0.5",
|
||||||
|
"axios": "^0.24.0",
|
||||||
|
"glob": "^7.2.0",
|
||||||
|
"module-alias": "^2.2.2",
|
||||||
|
"path": "^0.12.7",
|
||||||
|
"toml": "^3.0.0",
|
||||||
|
"tslog": "^3.3.2"
|
||||||
|
},
|
||||||
|
"_moduleAliases": {
|
||||||
|
"~": "./dist"
|
||||||
|
}
|
||||||
|
}
|
||||||
506
server/pnpm-lock.yaml
generated
Normal file
506
server/pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,506 @@
|
||||||
|
lockfileVersion: 5.3
|
||||||
|
|
||||||
|
specifiers:
|
||||||
|
'@hapi/basic': ^6.0.0
|
||||||
|
'@hapi/boom': ^9.1.4
|
||||||
|
'@hapi/hapi': ^20.2.1
|
||||||
|
'@hapi/inert': ^6.0.5
|
||||||
|
'@types/glob': ^7.2.0
|
||||||
|
'@types/hapi__basic': ^5.1.2
|
||||||
|
'@types/hapi__hapi': ^20.0.10
|
||||||
|
'@types/hapi__inert': ^5.2.3
|
||||||
|
'@types/node': ^17.0.17
|
||||||
|
axios: ^0.24.0
|
||||||
|
glob: ^7.2.0
|
||||||
|
module-alias: ^2.2.2
|
||||||
|
path: ^0.12.7
|
||||||
|
toml: ^3.0.0
|
||||||
|
tslog: ^3.3.2
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
'@hapi/basic': 6.0.0
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hapi': 20.2.1
|
||||||
|
'@hapi/inert': 6.0.5
|
||||||
|
axios: 0.24.0
|
||||||
|
glob: 7.2.0
|
||||||
|
module-alias: 2.2.2
|
||||||
|
path: 0.12.7
|
||||||
|
toml: 3.0.0
|
||||||
|
tslog: 3.3.2
|
||||||
|
|
||||||
|
devDependencies:
|
||||||
|
'@types/glob': 7.2.0
|
||||||
|
'@types/hapi__basic': 5.1.2
|
||||||
|
'@types/hapi__hapi': 20.0.10
|
||||||
|
'@types/hapi__inert': 5.2.3
|
||||||
|
'@types/node': 17.0.17
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
/@hapi/accept/5.0.2:
|
||||||
|
resolution: {integrity: sha512-CmzBx/bXUR8451fnZRuZAJRlzgm0Jgu5dltTX/bszmR2lheb9BpyN47Q1RbaGTsvFzn0PXAEs+lXDKfshccYZw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/ammo/5.0.1:
|
||||||
|
resolution: {integrity: sha512-FbCNwcTbnQP4VYYhLNGZmA76xb2aHg9AMPiy18NZyWMG310P5KdFGyA9v2rm5ujrIny77dEEIkMOwl0Xv+fSSA==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/b64/5.0.0:
|
||||||
|
resolution: {integrity: sha512-ngu0tSEmrezoiIaNGG6rRvKOUkUuDdf4XTPnONHGYfSGRmDqPZX5oJL6HAdKTo1UQHECbdB4OzhWrfgVppjHUw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
|
||||||
|
/@hapi/basic/6.0.0:
|
||||||
|
resolution: {integrity: sha512-nWWSXNCq3WptnP3To2c8kfQiRFDUnd9FQOcMS0B85y1x/m12c0hhp+VdmK60BMe44k6WIog1n6g8f9gZOagqBg==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/boom/9.1.4:
|
||||||
|
resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
|
||||||
|
/@hapi/bounce/2.0.0:
|
||||||
|
resolution: {integrity: sha512-JesW92uyzOOyuzJKjoLHM1ThiOvHPOLDHw01YV8yh5nCso7sDwJho1h0Ad2N+E62bZyz46TG3xhAi/78Gsct6A==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/bourne/2.0.0:
|
||||||
|
resolution: {integrity: sha512-WEezM1FWztfbzqIUbsDzFRVMxSoLy3HugVcux6KDDtTqzPsLE8NDRHfXvev66aH1i2oOKKar3/XDjbvh/OUBdg==}
|
||||||
|
|
||||||
|
/@hapi/call/8.0.1:
|
||||||
|
resolution: {integrity: sha512-bOff6GTdOnoe5b8oXRV3lwkQSb/LAWylvDMae6RgEWWntd0SHtkYbQukDHKlfaYtVnSAgIavJ0kqszF/AIBb6g==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/catbox-memory/5.0.1:
|
||||||
|
resolution: {integrity: sha512-QWw9nOYJq5PlvChLWV8i6hQHJYfvdqiXdvTupJFh0eqLZ64Xir7mKNi96d5/ZMUAqXPursfNDIDxjFgoEDUqeQ==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/catbox/11.1.1:
|
||||||
|
resolution: {integrity: sha512-u/8HvB7dD/6X8hsZIpskSDo4yMKpHxFd7NluoylhGrL6cUfYxdQPnvUp9YU2C6F9hsyBVLGulBd9vBN1ebfXOQ==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/podium': 4.1.3
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/content/5.0.2:
|
||||||
|
resolution: {integrity: sha512-mre4dl1ygd4ZyOH3tiYBrOUBzV7Pu/EOs8VLGf58vtOEECWed8Uuw6B4iR9AN/8uQt42tB04qpVaMyoMQh0oMw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/cryptiles/5.1.0:
|
||||||
|
resolution: {integrity: sha512-fo9+d1Ba5/FIoMySfMqPBR/7Pa29J2RsiPrl7bkwo5W5o+AN1dAYQRi4SPrPwwVxVGKjgLOEWrsvt1BonJSfLA==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
|
||||||
|
/@hapi/file/2.0.0:
|
||||||
|
resolution: {integrity: sha512-WSrlgpvEqgPWkI18kkGELEZfXr0bYLtr16iIN4Krh9sRnzBZN6nnWxHFxtsnP684wueEySBbXPDg/WfA9xJdBQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/hapi/20.2.1:
|
||||||
|
resolution: {integrity: sha512-OXAU+yWLwkMfPFic+KITo+XPp6Oxpgc9WUH+pxXWcTIuvWbgco5TC/jS8UDvz+NFF5IzRgF2CL6UV/KLdQYUSQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/accept': 5.0.2
|
||||||
|
'@hapi/ammo': 5.0.1
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bounce': 2.0.0
|
||||||
|
'@hapi/call': 8.0.1
|
||||||
|
'@hapi/catbox': 11.1.1
|
||||||
|
'@hapi/catbox-memory': 5.0.1
|
||||||
|
'@hapi/heavy': 7.0.1
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/mimos': 6.0.0
|
||||||
|
'@hapi/podium': 4.1.3
|
||||||
|
'@hapi/shot': 5.0.5
|
||||||
|
'@hapi/somever': 3.0.1
|
||||||
|
'@hapi/statehood': 7.0.3
|
||||||
|
'@hapi/subtext': 7.0.3
|
||||||
|
'@hapi/teamwork': 5.1.0
|
||||||
|
'@hapi/topo': 5.1.0
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/heavy/7.0.1:
|
||||||
|
resolution: {integrity: sha512-vJ/vzRQ13MtRzz6Qd4zRHWS3FaUc/5uivV2TIuExGTM9Qk+7Zzqj0e2G7EpE6KztO9SalTbiIkTh7qFKj/33cA==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/hoek/9.2.1:
|
||||||
|
resolution: {integrity: sha512-gfta+H8aziZsm8pZa0vj04KO6biEiisppNgA1kbJvFrrWu9Vm7eaUEy76DIxsuTaWvti5fkJVhllWc6ZTE+Mdw==}
|
||||||
|
|
||||||
|
/@hapi/inert/6.0.5:
|
||||||
|
resolution: {integrity: sha512-eVAdUVhJLmmXLM/Zt7u5H5Vzazs9GKe4zfPK2b97ePHEfs3g/AQkxHfYQjJqMy11hvyB7a21Z6rBEA0R//dtXw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/ammo': 5.0.1
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bounce': 2.0.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
lru-cache: 6.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/iron/6.0.0:
|
||||||
|
resolution: {integrity: sha512-zvGvWDufiTGpTJPG1Y/McN8UqWBu0k/xs/7l++HVU535NLHXsHhy54cfEMdW7EjwKfbBfM9Xy25FmTiobb7Hvw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/b64': 5.0.0
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bourne': 2.0.0
|
||||||
|
'@hapi/cryptiles': 5.1.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
|
||||||
|
/@hapi/mimos/6.0.0:
|
||||||
|
resolution: {integrity: sha512-Op/67tr1I+JafN3R3XN5DucVSxKRT/Tc+tUszDwENoNpolxeXkhrJ2Czt6B6AAqrespHoivhgZBWYSuANN9QXg==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
mime-db: 1.51.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/nigel/4.0.2:
|
||||||
|
resolution: {integrity: sha512-ht2KoEsDW22BxQOEkLEJaqfpoKPXxi7tvabXy7B/77eFtOyG5ZEstfZwxHQcqAiZhp58Ae5vkhEqI03kawkYNw==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/vise': 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/pez/5.0.3:
|
||||||
|
resolution: {integrity: sha512-mpikYRJjtrbJgdDHG/H9ySqYqwJ+QU/D7FXsYciS9P7NYBXE2ayKDAy3H0ou6CohOCaxPuTV4SZ0D936+VomHA==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/b64': 5.0.0
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/content': 5.0.2
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/nigel': 4.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/podium/4.1.3:
|
||||||
|
resolution: {integrity: sha512-ljsKGQzLkFqnQxE7qeanvgGj4dejnciErYd30dbrYzUOF/FyS/DOF97qcrT3bhoVwCYmxa6PEMhxfCPlnUcD2g==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/teamwork': 5.1.0
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
|
||||||
|
/@hapi/shot/5.0.5:
|
||||||
|
resolution: {integrity: sha512-x5AMSZ5+j+Paa8KdfCoKh+klB78otxF+vcJR/IoN91Vo2e5ulXIW6HUsFTCU+4W6P/Etaip9nmdAx2zWDimB2A==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/somever/3.0.1:
|
||||||
|
resolution: {integrity: sha512-4ZTSN3YAHtgpY/M4GOtHUXgi6uZtG9nEZfNI6QrArhK0XN/RDVgijlb9kOmXwCR5VclDSkBul9FBvhSuKXx9+w==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/bounce': 2.0.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/statehood/7.0.3:
|
||||||
|
resolution: {integrity: sha512-pYB+pyCHkf2Amh67QAXz7e/DN9jcMplIL7Z6N8h0K+ZTy0b404JKPEYkbWHSnDtxLjJB/OtgElxocr2fMH4G7w==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bounce': 2.0.0
|
||||||
|
'@hapi/bourne': 2.0.0
|
||||||
|
'@hapi/cryptiles': 5.1.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/iron': 6.0.0
|
||||||
|
'@hapi/validate': 1.1.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/subtext/7.0.3:
|
||||||
|
resolution: {integrity: sha512-CekDizZkDGERJ01C0+TzHlKtqdXZxzSWTOaH6THBrbOHnsr3GY+yiMZC+AfNCypfE17RaIakGIAbpL2Tk1z2+A==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bourne': 2.0.0
|
||||||
|
'@hapi/content': 5.0.2
|
||||||
|
'@hapi/file': 2.0.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/pez': 5.0.3
|
||||||
|
'@hapi/wreck': 17.1.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/teamwork/5.1.0:
|
||||||
|
resolution: {integrity: sha512-llqoQTrAJDTXxG3c4Kz/uzhBS1TsmSBa/XG5SPcVXgmffHE1nFtyLIK0hNJHCB3EuBKT84adzd1hZNY9GJLWtg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
|
/@hapi/topo/5.1.0:
|
||||||
|
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
|
||||||
|
/@hapi/validate/1.1.3:
|
||||||
|
resolution: {integrity: sha512-/XMR0N0wjw0Twzq2pQOzPBZlDzkekGcoCtzO314BpIEsbXdYGthQUbxgkGDf4nhk1+IPDAsXqWjMohRQYO06UA==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/topo': 5.1.0
|
||||||
|
|
||||||
|
/@hapi/vise/4.0.0:
|
||||||
|
resolution: {integrity: sha512-eYyLkuUiFZTer59h+SGy7hUm+qE9p+UemePTHLlIWppEd+wExn3Df5jO04bFQTm7nleF5V8CtuYQYb+VFpZ6Sg==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@hapi/wreck/17.1.0:
|
||||||
|
resolution: {integrity: sha512-nx6sFyfqOpJ+EFrHX+XWwJAxs3ju4iHdbB/bwR8yTNZOiYmuhA8eCe7lYPtYmb4j7vyK/SlbaQsmTtUrMvPEBw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/bourne': 2.0.0
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@sideway/address/4.1.3:
|
||||||
|
resolution: {integrity: sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@sideway/formula/3.0.0:
|
||||||
|
resolution: {integrity: sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@sideway/pinpoint/2.0.0:
|
||||||
|
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/glob/7.2.0:
|
||||||
|
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/minimatch': 3.0.5
|
||||||
|
'@types/node': 17.0.17
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__basic/5.1.2:
|
||||||
|
resolution: {integrity: sha512-sqoQ34nwmRlNgQ5fdHHnghyVzs23aC2d30l9G1sKvdrh4PDV22bnQ+8fBCjoItT+0gGSuXIoG4qdoVZGBjH5EQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/hapi__hapi': 20.0.10
|
||||||
|
joi: 17.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__catbox/10.2.4:
|
||||||
|
resolution: {integrity: sha512-A6ivRrXD5glmnJna1UAGw87QNZRp/vdFO9U4GS+WhOMWzHnw+oTGkMvg0g6y1930CbeheGOCm7A1qHsqH7AXqg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__hapi/20.0.10:
|
||||||
|
resolution: {integrity: sha512-Nt/SY/20/JAlHhbgH616j0g18vsANR9OWoyMdQcytlW6o7TBN+wRgf0MB8AgzjYpuzQam5oTiqyED9WwHmQKYQ==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/boom': 9.1.4
|
||||||
|
'@hapi/iron': 6.0.0
|
||||||
|
'@hapi/podium': 4.1.3
|
||||||
|
'@types/hapi__catbox': 10.2.4
|
||||||
|
'@types/hapi__mimos': 4.1.4
|
||||||
|
'@types/hapi__shot': 4.1.2
|
||||||
|
'@types/node': 17.0.17
|
||||||
|
joi: 17.6.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__inert/5.2.3:
|
||||||
|
resolution: {integrity: sha512-I1mWQrEc7oMqGtofT0rwBgRBCBurz0wNzbq8QZsHWR+aXM0bk1j9GA6zwyGIeO53PNl2C1c2kpXlc084xCV+Tg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/hapi__hapi': 20.0.10
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__mimos/4.1.4:
|
||||||
|
resolution: {integrity: sha512-i9hvJpFYTT/qzB5xKWvDYaSXrIiNqi4ephi+5Lo6+DoQdwqPXQgmVVOZR+s3MBiHoFqsCZCX9TmVWG3HczmTEQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/mime-db': 1.43.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/hapi__shot/4.1.2:
|
||||||
|
resolution: {integrity: sha512-8wWgLVP1TeGqgzZtCdt+F+k15DWQvLG1Yv6ZzPfb3D5WIo5/S+GGKtJBVo2uNEcqabP5Ifc71QnJTDnTmw1axA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 17.0.17
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/mime-db/1.43.1:
|
||||||
|
resolution: {integrity: sha512-kGZJY+R+WnR5Rk+RPHUMERtb2qBRViIHCBdtUrY+NmwuGb8pQdfTqQiCKPrxpdoycl8KWm2DLdkpoSdt479XoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/minimatch/3.0.5:
|
||||||
|
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/node/17.0.17:
|
||||||
|
resolution: {integrity: sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/axios/0.24.0:
|
||||||
|
resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.14.8
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/balanced-match/1.0.2:
|
||||||
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/brace-expansion/1.1.11:
|
||||||
|
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||||
|
dependencies:
|
||||||
|
balanced-match: 1.0.2
|
||||||
|
concat-map: 0.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/buffer-from/1.1.2:
|
||||||
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/concat-map/0.0.1:
|
||||||
|
resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/follow-redirects/1.14.8:
|
||||||
|
resolution: {integrity: sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/fs.realpath/1.0.0:
|
||||||
|
resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/glob/7.2.0:
|
||||||
|
resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==}
|
||||||
|
dependencies:
|
||||||
|
fs.realpath: 1.0.0
|
||||||
|
inflight: 1.0.6
|
||||||
|
inherits: 2.0.4
|
||||||
|
minimatch: 3.0.5
|
||||||
|
once: 1.4.0
|
||||||
|
path-is-absolute: 1.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inflight/1.0.6:
|
||||||
|
resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=}
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
wrappy: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inherits/2.0.3:
|
||||||
|
resolution: {integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/inherits/2.0.4:
|
||||||
|
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/joi/17.6.0:
|
||||||
|
resolution: {integrity: sha512-OX5dG6DTbcr/kbMFj0KGYxuew69HPcAE3K/sZpEV2nP6e/j/C0HV+HNiBPCASxdx5T7DMoa0s8UeHWMnb6n2zw==}
|
||||||
|
dependencies:
|
||||||
|
'@hapi/hoek': 9.2.1
|
||||||
|
'@hapi/topo': 5.1.0
|
||||||
|
'@sideway/address': 4.1.3
|
||||||
|
'@sideway/formula': 3.0.0
|
||||||
|
'@sideway/pinpoint': 2.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/lru-cache/6.0.0:
|
||||||
|
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
yallist: 4.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/mime-db/1.51.0:
|
||||||
|
resolution: {integrity: sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/minimatch/3.0.5:
|
||||||
|
resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==}
|
||||||
|
dependencies:
|
||||||
|
brace-expansion: 1.1.11
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/module-alias/2.2.2:
|
||||||
|
resolution: {integrity: sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/once/1.4.0:
|
||||||
|
resolution: {integrity: sha1-WDsap3WWHUsROsF9nFC6753Xa9E=}
|
||||||
|
dependencies:
|
||||||
|
wrappy: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/path-is-absolute/1.0.1:
|
||||||
|
resolution: {integrity: sha1-F0uSaHNVNP+8es5r9TpanhtcX18=}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/path/0.12.7:
|
||||||
|
resolution: {integrity: sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=}
|
||||||
|
dependencies:
|
||||||
|
process: 0.11.10
|
||||||
|
util: 0.10.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/process/0.11.10:
|
||||||
|
resolution: {integrity: sha1-czIwDoQBYb2j5podHZGn1LwW8YI=}
|
||||||
|
engines: {node: '>= 0.6.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/source-map-support/0.5.21:
|
||||||
|
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
source-map: 0.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/source-map/0.6.1:
|
||||||
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/toml/3.0.0:
|
||||||
|
resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/tslog/3.3.2:
|
||||||
|
resolution: {integrity: sha512-K+XduMfa9+yiHE/vxbUD/dL7RAbw9yIfi9tMjQj3uQ8evkPRKkmw0mQgEkzmueyg23hJHGaOQmDnCEZoKEws+w==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dependencies:
|
||||||
|
source-map-support: 0.5.21
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/util/0.10.4:
|
||||||
|
resolution: {integrity: sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==}
|
||||||
|
dependencies:
|
||||||
|
inherits: 2.0.3
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/wrappy/1.0.2:
|
||||||
|
resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/yallist/4.0.0:
|
||||||
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
dev: false
|
||||||
48
server/src/endpoints/auth/twitch/callback.ts
Normal file
48
server/src/endpoints/auth/twitch/callback.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { TwitchAuth } from "~/utils/TwitchAuth";
|
||||||
|
import { config, log, twitchAuths } from "~/main";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/twitch/login/callback`,
|
||||||
|
options: { auth: false },
|
||||||
|
async handler(request: Request, _: ResponseToolkit): Promise<any> {
|
||||||
|
log.silly(`An authentication request is being processed`);
|
||||||
|
try {
|
||||||
|
|
||||||
|
let code: string = request.query.code;
|
||||||
|
|
||||||
|
let qs = new URLSearchParams();
|
||||||
|
qs.set("client_id", config.twitch.client_id);
|
||||||
|
qs.set("client_secret", config.twitch.client_secret);
|
||||||
|
qs.set("redirect_uri", config.twitch.auth.redirect_uri);
|
||||||
|
qs.set("grant_type", "authorization_code");
|
||||||
|
qs.set("code", code);
|
||||||
|
|
||||||
|
let r = await axios.post(
|
||||||
|
config.twitch.auth.base_url + "/token?" + qs.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (r.status !== 200) {
|
||||||
|
return _.response({
|
||||||
|
message: "Something went wrong while request access from Twitch",
|
||||||
|
}).code(403);
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth = new TwitchAuth(r.data);
|
||||||
|
let userdata = await auth.request<any>(`GET`, `/users`);
|
||||||
|
let user = userdata.data[0];
|
||||||
|
auth.channel = user.login;
|
||||||
|
auth.bid = user.id;
|
||||||
|
|
||||||
|
twitchAuths[auth.channel as string] = auth;
|
||||||
|
|
||||||
|
return _.response({
|
||||||
|
message: `Setup auth correctly for ${auth.channel}, you can leave this page now.`,
|
||||||
|
}).code(200);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(err);
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
19
server/src/endpoints/auth/twitch/redirect.ts
Normal file
19
server/src/endpoints/auth/twitch/redirect.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { config, log } from "~/main";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/twitch/login`,
|
||||||
|
options: { auth: false },
|
||||||
|
async handler(_: Request, h: ResponseToolkit): Promise<any> {
|
||||||
|
log.silly(`A new authentication process has begun`);
|
||||||
|
|
||||||
|
let qs = new URLSearchParams();
|
||||||
|
|
||||||
|
qs.set("client_id", config.twitch.client_id);
|
||||||
|
qs.set("redirect_uri", config.twitch.auth.redirect_uri);
|
||||||
|
qs.set("response_type", "code");
|
||||||
|
qs.set("scope", config.twitch.auth.scopes.join(" "));
|
||||||
|
|
||||||
|
return h.redirect( config.twitch.auth.base_url + "/authorize?" + qs.toString() );
|
||||||
|
},
|
||||||
|
};
|
||||||
36
server/src/endpoints/polls/create.ts
Normal file
36
server/src/endpoints/polls/create.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { log, twitchAuths } from "~/main";
|
||||||
|
import boom from "@hapi/boom";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `POST`, path: `/twitch/{user}/poll`,
|
||||||
|
options: { auth: false },
|
||||||
|
async handler(request: Request, h: ResponseToolkit): Promise<any> {
|
||||||
|
log.debug(`Making a Twitch poll!`);
|
||||||
|
let user = request.params.user;
|
||||||
|
|
||||||
|
// Assert user existance
|
||||||
|
if (twitchAuths[user] == null) {
|
||||||
|
throw boom.notFound("Invalid user");
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth = twitchAuths[user];
|
||||||
|
let pollData = request.payload as any;
|
||||||
|
|
||||||
|
pollData.broadcaster_id = auth.bid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let r = await auth.request<any>("POST", "/polls", {
|
||||||
|
data: pollData,
|
||||||
|
});
|
||||||
|
|
||||||
|
let poll = r.data[0];
|
||||||
|
|
||||||
|
return h.response({
|
||||||
|
poll_id: poll.id,
|
||||||
|
})
|
||||||
|
} catch (err: any) {
|
||||||
|
throw boom.internal(err.response.message);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
23
server/src/endpoints/site/channel_polls.ts
Normal file
23
server/src/endpoints/site/channel_polls.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Request, ResponseToolkit } from "@hapi/hapi";
|
||||||
|
import { twitchAuths, log } from "~/main";
|
||||||
|
import boom from "@hapi/boom";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
method: `GET`, path: `/dashboard/{user}`,
|
||||||
|
options: { auth: 'simple' },
|
||||||
|
async handler(request: Request, h: ResponseToolkit): Promise<any> {
|
||||||
|
let user = request.params.user;
|
||||||
|
|
||||||
|
if (twitchAuths[user] == null) {
|
||||||
|
throw boom.notFound("Invalid user");
|
||||||
|
};
|
||||||
|
|
||||||
|
let uri = path.join(process.cwd(), `../site/${user}.html`);
|
||||||
|
log.silly(`Filepath: ${uri}`)
|
||||||
|
|
||||||
|
return h.file(uri, {
|
||||||
|
confine: false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
73
server/src/main.ts
Normal file
73
server/src/main.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
// Filepath alias resolution
|
||||||
|
import "module-alias/register";
|
||||||
|
|
||||||
|
import { clean_exit } from "~/utils/clean_exit";
|
||||||
|
import { init_webserver } from "~/webserver";
|
||||||
|
import { Logger } from "tslog";
|
||||||
|
import toml from "toml";
|
||||||
|
import fs from "fs";
|
||||||
|
import { TwitchAuth } from "./utils/TwitchAuth";
|
||||||
|
|
||||||
|
|
||||||
|
// Load the config from disk
|
||||||
|
if (!fs.existsSync(`config.toml`)) {
|
||||||
|
console.log(`Please create the config and edit it then run the server again.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
export const config: Config = toml.parse(fs.readFileSync(`config.toml`, `utf-8`));
|
||||||
|
|
||||||
|
|
||||||
|
// Setup the logger with the appropriate settings
|
||||||
|
export const log = new Logger({
|
||||||
|
displayFilePath: `hidden`,
|
||||||
|
displayFunctionName: false,
|
||||||
|
displayDateTime: true,
|
||||||
|
displayLogLevel: true,
|
||||||
|
minLevel: config.log.level,
|
||||||
|
name: config.log.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Load the database
|
||||||
|
if (!fs.existsSync(`data/db.json`)) {
|
||||||
|
log.info(`Can't find database file, creating default`);
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(`data/db.json`, `{"authed_channels": {}}`);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`Unable to create the default database, make sure the data directory exists.`);
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const db: Database = JSON.parse(
|
||||||
|
fs.readFileSync(`data/db.json`, `utf-8`)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const twitchAuths: {[index: string]: TwitchAuth} = {};
|
||||||
|
|
||||||
|
|
||||||
|
// Signal listeners to save persistent storage
|
||||||
|
process.on(`SIGINT`, clean_exit);
|
||||||
|
process.on(`SIGTERM`, clean_exit);
|
||||||
|
process.on(`uncaughtException`, clean_exit);
|
||||||
|
|
||||||
|
|
||||||
|
// Setup the utilities that are needed throughout the system
|
||||||
|
async function init() {
|
||||||
|
|
||||||
|
// Restore all the auth classes for users.
|
||||||
|
for (var token_data of db.authed_channels) {
|
||||||
|
if (token_data.channel) {
|
||||||
|
twitchAuths[token_data.channel] = new TwitchAuth(token_data);
|
||||||
|
} else {
|
||||||
|
let auth = new TwitchAuth(token_data);
|
||||||
|
let userdata = await auth.request<any>(`GET`, `/users`);
|
||||||
|
auth.channel = userdata.data[0].login;
|
||||||
|
|
||||||
|
twitchAuths[auth.channel as string] = auth;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
await init_webserver();
|
||||||
|
};
|
||||||
|
|
||||||
|
init();
|
||||||
24
server/src/types/Config.d.ts
vendored
Normal file
24
server/src/types/Config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
interface Config {
|
||||||
|
server: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
auth: {
|
||||||
|
enabled: boolean;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
twitch: {
|
||||||
|
client_id: string;
|
||||||
|
client_secret: string;
|
||||||
|
auth: {
|
||||||
|
redirect_uri: string;
|
||||||
|
base_url: string;
|
||||||
|
scopes: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
log: {
|
||||||
|
name: string;
|
||||||
|
level: "silly" | "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||||
|
};
|
||||||
|
};
|
||||||
13
server/src/types/Database.d.ts
vendored
Normal file
13
server/src/types/Database.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
interface TokenData {
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
refresh_token: string;
|
||||||
|
channel?: string;
|
||||||
|
scope: string;
|
||||||
|
token_type: string;
|
||||||
|
bid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
authed_channels: any[];
|
||||||
|
}
|
||||||
160
server/src/utils/TwitchAuth.ts
Normal file
160
server/src/utils/TwitchAuth.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import { config, log, twitchAuths } from "~/main";
|
||||||
|
import axios, { Method } from "axios";
|
||||||
|
|
||||||
|
export class TwitchAuth {
|
||||||
|
private _channel: string | undefined;
|
||||||
|
private _token: string;
|
||||||
|
private _refresh_token: string;
|
||||||
|
private token_type: string;
|
||||||
|
private scope: string;
|
||||||
|
private expires_in: number;
|
||||||
|
private invalidated: boolean = false;
|
||||||
|
private _bid: string | undefined;
|
||||||
|
|
||||||
|
constructor(token_data: TokenData) {
|
||||||
|
|
||||||
|
this._token = token_data.access_token;
|
||||||
|
this._refresh_token = token_data.refresh_token;
|
||||||
|
|
||||||
|
this.token_type = token_data.token_type;
|
||||||
|
this.scope = token_data.scope;
|
||||||
|
this.expires_in = token_data.expires_in;
|
||||||
|
|
||||||
|
if (token_data?.channel == null) {
|
||||||
|
log.silly("Unknown channel authenticated, not adding to the twitchAuths.");
|
||||||
|
} else {
|
||||||
|
log.silly("Known channel authenticated.");
|
||||||
|
this._channel = token_data.channel;
|
||||||
|
twitchAuths[this._channel] = this;
|
||||||
|
};
|
||||||
|
if (token_data?.bid == null) {
|
||||||
|
log.silly("Channel auth doesn't contain broadcaster ID");
|
||||||
|
} else {
|
||||||
|
this._bid = token_data.bid;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Get the authenticated user's channel name. */
|
||||||
|
private async getChannel() {
|
||||||
|
try {
|
||||||
|
log.debug(`Requesting user info`)
|
||||||
|
let r = await this.request<any>("GET", "/users");
|
||||||
|
this._channel = r.data.data[0].login;
|
||||||
|
if (this._channel) {
|
||||||
|
twitchAuths[this._channel] = this;
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`User info errored`)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.getChannel();
|
||||||
|
}, 2000);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The channel login name that this authentication is for */
|
||||||
|
get channel() { return this._channel };
|
||||||
|
set channel(value: string | undefined) {
|
||||||
|
if (!this._channel) {
|
||||||
|
this._channel = value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The authenticated user's broadcaster ID */
|
||||||
|
get bid() { return this._bid };
|
||||||
|
set bid(value: string | undefined) {
|
||||||
|
if (!this._bid) {
|
||||||
|
this._bid = value
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The token used to make requests for this channel */
|
||||||
|
get token() { return `Bearer ${this._token}` };
|
||||||
|
|
||||||
|
private async refresh() {
|
||||||
|
|
||||||
|
let qs = new URLSearchParams({
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
refresh_token: this._refresh_token,
|
||||||
|
client_id: config.twitch.client_id,
|
||||||
|
client_secret: config.twitch.client_secret,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
let r = await this.request<TokenData>(
|
||||||
|
"POST",
|
||||||
|
config.twitch.auth.base_url + "/token?" + encodeURIComponent(qs.toString()),
|
||||||
|
);
|
||||||
|
this._refresh_token = r.refresh_token;
|
||||||
|
this._token = r.access_token,
|
||||||
|
this.expires_in = r.expires_in;
|
||||||
|
this.scope = r.scope;
|
||||||
|
this.token_type = r.token_type;
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`Could not refresh the token for ${this._channel}`)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public saveData(): TokenData {
|
||||||
|
return {
|
||||||
|
access_token: this._token,
|
||||||
|
refresh_token: this._refresh_token,
|
||||||
|
token_type: this.token_type,
|
||||||
|
expires_in: this.expires_in,
|
||||||
|
scope: this.scope,
|
||||||
|
channel: this.channel,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a request to Twitch on behalf of the authenticated user.
|
||||||
|
*
|
||||||
|
* @param method The HTTP method to make the request with
|
||||||
|
* @param url The URL to make the request to Twitch with. Relative URLs are supported.
|
||||||
|
* @param conf The Axios RequestConfig, without method, url, or baseURL
|
||||||
|
* @returns The data returned from the Twitch API
|
||||||
|
*/
|
||||||
|
public async request<T>(method: Method, url: string, conf:any={}): Promise<T> {
|
||||||
|
|
||||||
|
if (this.invalidated) {
|
||||||
|
throw new Error("Can't make a request with an invalidated token");
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
let headers = {
|
||||||
|
"Client-Id": config.twitch.client_id,
|
||||||
|
"Authorization": this.token, // Bearer <token>
|
||||||
|
};
|
||||||
|
if (conf?.headers != null) {
|
||||||
|
headers = {
|
||||||
|
...headers,
|
||||||
|
...conf.headers,
|
||||||
|
};
|
||||||
|
delete conf.headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
headers,
|
||||||
|
baseURL: "https://api.twitch.tv/helix",
|
||||||
|
...(conf ?? {})
|
||||||
|
});
|
||||||
|
|
||||||
|
return r.data as T;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response) {
|
||||||
|
|
||||||
|
// global error handling,
|
||||||
|
switch (err.response.status) {
|
||||||
|
case 403:
|
||||||
|
this.refresh();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error(err);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
log.error(err);
|
||||||
|
throw err;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
24
server/src/utils/clean_exit.ts
Normal file
24
server/src/utils/clean_exit.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { db, log, twitchAuths } from "~/main";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
export function clean_exit() {
|
||||||
|
log.info(`Exiting the program cleanly`);
|
||||||
|
|
||||||
|
db.authed_channels = [];
|
||||||
|
for (var channel in twitchAuths) {
|
||||||
|
log.debug(`Saving ${channel}'s auth information into the database file.`);
|
||||||
|
db.authed_channels.push(twitchAuths[channel].saveData());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attempt to write the persistent storage to the database
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
`./data/db.json`,
|
||||||
|
JSON.stringify(db)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`Couldn't save program storage properly, writing to log.`, db);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
};
|
||||||
63
server/src/webserver.ts
Normal file
63
server/src/webserver.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { ResponseToolkit, Server, ServerRoute } from "@hapi/hapi";
|
||||||
|
import { config, log } from "~/main";
|
||||||
|
import basic from "@hapi/basic";
|
||||||
|
import inert from "@hapi/inert";
|
||||||
|
import glob from "glob";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export async function init_webserver() {
|
||||||
|
const server = new Server({
|
||||||
|
port: config.server.port,
|
||||||
|
});
|
||||||
|
await server.register(inert);
|
||||||
|
|
||||||
|
|
||||||
|
await server.register(basic);
|
||||||
|
server.auth.strategy(`simple`, `basic`, {
|
||||||
|
// @ts-expect-error
|
||||||
|
async validate(r: Request, user: string, password: string, h: ResponseToolkit) {
|
||||||
|
|
||||||
|
if (!config.server.auth.enabled) {
|
||||||
|
return {
|
||||||
|
isValid: true,
|
||||||
|
credentials: {
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Assert usernames match if it was set in the config
|
||||||
|
if (config.server.auth.username != null) {
|
||||||
|
if (user !== config.server.auth.username) {
|
||||||
|
return {
|
||||||
|
isValid: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: config.server.auth.password === password,
|
||||||
|
credentials: { user, password }
|
||||||
|
};
|
||||||
|
},
|
||||||
|
allowEmptyUsername: true,
|
||||||
|
});
|
||||||
|
server.auth.default(`simple`);
|
||||||
|
|
||||||
|
|
||||||
|
// Register all the endpoints that we need for the server functionality
|
||||||
|
let files = glob.sync(
|
||||||
|
`endpoints/**/!(*.map)`,
|
||||||
|
{ cwd: __dirname, nodir: true }
|
||||||
|
);
|
||||||
|
for (var file of files) {
|
||||||
|
let route: ServerRoute = (await import(path.join(__dirname, file))).default;
|
||||||
|
log.debug(`Registering route: ${route.method} ${route.path}`);
|
||||||
|
server.route(route);
|
||||||
|
};
|
||||||
|
|
||||||
|
server.start().then(() => {
|
||||||
|
log.info(`Server running on: ${config.server.host}:${config.server.port}`);
|
||||||
|
});
|
||||||
|
};
|
||||||
74
server/tsconfig.json
Normal file
74
server/tsconfig.json
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
"sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./dist", /* Redirect output structure to the directory. */
|
||||||
|
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
"noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||||
|
"paths": {
|
||||||
|
"~/*": [ "./src/*" ]
|
||||||
|
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
}
|
||||||
|
}
|
||||||
377
site/alkali_metal.html
Normal file
377
site/alkali_metal.html
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CGE Poll Manager</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" async></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background: #2c2f32;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#app {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.poll-data {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #202225;
|
||||||
|
border-radius: 7px;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-data > h4 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-data.important {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>CGE Poll Quick-Creator</h2>
|
||||||
|
<hr>
|
||||||
|
<div id="app">
|
||||||
|
<div
|
||||||
|
v-for="poll in polls"
|
||||||
|
class="poll-data"
|
||||||
|
:class="poll.important ? 'important' : ''"
|
||||||
|
>
|
||||||
|
<h4>{{poll.name}}</h4>
|
||||||
|
<p>
|
||||||
|
Duration: {{poll.data.duration}} seconds
|
||||||
|
<br>
|
||||||
|
Choices:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="choice in poll.data.choices">
|
||||||
|
{{choice.title}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
@click.stop="start_poll(poll.data)"
|
||||||
|
>
|
||||||
|
Start Poll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
let app = new Vue({
|
||||||
|
el: "#app",
|
||||||
|
methods: {
|
||||||
|
async start_poll(poll_data) {
|
||||||
|
try {
|
||||||
|
let r = await axios.post("/twitch/alkali_metal/poll", poll_data);
|
||||||
|
alert("Poll should've been created successfully.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Something went wrong starting the poll, check to see if one is made already.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
polls: [
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice (FIRST POLL)",
|
||||||
|
important: true,
|
||||||
|
data: {
|
||||||
|
title: "Which Region?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (NO TIE)",
|
||||||
|
important: true,
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (12 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (23 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (34 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (13 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (14 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (24 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (123 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (124 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (134 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (234 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
377
site/czechgamesedition.html
Normal file
377
site/czechgamesedition.html
Normal file
|
|
@ -0,0 +1,377 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CGE Poll Manager</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js" async></script>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
background: #2c2f32;
|
||||||
|
color: white;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#app {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.poll-data {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #202225;
|
||||||
|
border-radius: 7px;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-data > h4 {
|
||||||
|
margin: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-data.important {
|
||||||
|
border-color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2>CGE Poll Quick-Creator</h2>
|
||||||
|
<hr>
|
||||||
|
<div id="app">
|
||||||
|
<div
|
||||||
|
v-for="poll in polls"
|
||||||
|
class="poll-data"
|
||||||
|
:class="poll.important ? 'important' : ''"
|
||||||
|
>
|
||||||
|
<h4>{{poll.name}}</h4>
|
||||||
|
<p>
|
||||||
|
Duration: {{poll.data.duration}} seconds
|
||||||
|
<br>
|
||||||
|
Choices:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="choice in poll.data.choices">
|
||||||
|
{{choice.title}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button
|
||||||
|
@click.stop="start_poll(poll.data)"
|
||||||
|
>
|
||||||
|
Start Poll
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
let app = new Vue({
|
||||||
|
el: "#app",
|
||||||
|
methods: {
|
||||||
|
async start_poll(poll_data) {
|
||||||
|
try {
|
||||||
|
let r = await axios.post("/twitch/czechgamesedition/poll", poll_data);
|
||||||
|
alert("Poll should've been created successfully.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert("Something went wrong starting the poll, check to see if one is made already.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
polls: [
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice (FIRST POLL)",
|
||||||
|
important: true,
|
||||||
|
data: {
|
||||||
|
title: "Which Region?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (NO TIE)",
|
||||||
|
important: true,
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (12 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (23 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (34 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (13 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (14 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "1B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Section Choice (24 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Section?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4B"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 45,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (123 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (124 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (134 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wavelength Region Choice Tie-Breaker (234 TIE)",
|
||||||
|
data: {
|
||||||
|
title: "Which Region (Tie-Breaker)?",
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
title: "2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
duration: 30,
|
||||||
|
bits_voting_enabled: true,
|
||||||
|
bits_per_vote: 10,
|
||||||
|
channel_points_voting_enabled: true,
|
||||||
|
channel_points_per_vote: 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue