Initial commit.

This commit is contained in:
Oliver Akins 2022-02-17 00:04:40 -06:00
parent 489a16c0b5
commit 3cb6f97610
No known key found for this signature in database
GPG key ID: 3C2014AF9457AF99
17 changed files with 1881 additions and 0 deletions

View 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
View 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
View 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

View 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;
};
},
};

View 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() );
},
};

View 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);
};
},
};

View 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
View 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
View 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
View 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[];
}

View 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;
};
};
}

View 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
View 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
View 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. */
}
}