Initial commit

This commit is contained in:
Tracker-Friendly 2024-10-20 17:12:11 +01:00
commit 885efd959a
18 changed files with 1939 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
shoGambler
plugins
config.json

23
LICENSE.md Normal file
View File

@ -0,0 +1,23 @@
# The Restrictive Non-Commercial License
## License Text
1. **Use and Redistribution**: You are permitted to use and redistribute and create derivative works of this software, for non-commercial purposes only, provided that you comply with the terms of this license.
2. **Commercial Use**: This software may not be used for any commercial purposes without explicit permission from the author.
3. **Redistribution Terms**: Redistribution of this software is only permitted under the terms of this license.
4. **Copyright**: All derivative works remain under the copyright of the original author, as stated in the copyright notice below.
5. **License for Derivative Works**: All derivative works must be licensed under the same terms as this license, including the same copyright notice.
6. **Permission Waiver**: Any of these conditions may be waived if you have obtained explicit permission from the author.
7. **Cease of Use**: Upon request by the copyright holder, you must immediately cease all use of the software and refrain from any future use, distribution, or creation of derivative works unless otherwise agreed upon.
8. **Disclaimer**: This software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and non-infringement, to the extent permitted by law.
## Copyright Notice
Copyright (c) 2024, Arzumify

9
build.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/sh
go build -ldflags "-s -w"
cd plugins-src/diceroll || exit
./build.sh
mv diceroll.so ../../plugins/diceroll.so
cd ../bet || exit
./build.sh
mv bet.so ../../plugins/bet.so
echo Done

52
go.mod Normal file
View File

@ -0,0 +1,52 @@
module shoGambler
go 1.23.0
require (
github.com/MicahParks/keyfunc/v3 v3.3.5
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.2.0
modernc.org/sqlite v1.32.0
)
require (
github.com/MicahParks/jwkset v0.5.19 // indirect
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

139
go.sum Normal file
View File

@ -0,0 +1,139 @@
github.com/MicahParks/jwkset v0.5.19 h1:XZCsgJv05DBCvxEHYEHlSafqiuVn5ESG0VRB331Fxhw=
github.com/MicahParks/jwkset v0.5.19/go.mod h1:q8ptTGn/Z9c4MwbcfeCDssADeVQb3Pk7PnVxrvi+2QY=
github.com/MicahParks/keyfunc/v3 v3.3.5 h1:7ceAJLUAldnoueHDNzF8Bx06oVcQ5CfJnYwNt1U3YYo=
github.com/MicahParks/keyfunc/v3 v3.3.5/go.mod h1:SdCCyMJn/bYqWDvARspC6nCT8Sk74MjuAY22C7dCST8=
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

38
lib/main.go Normal file
View File

@ -0,0 +1,38 @@
package lib
import (
"math/big"
"github.com/gin-gonic/gin"
)
type Date struct {
DaysSinceEpoch uint64
}
type PluginData struct {
Name string
CanReturnPoints bool
CanAddPoints bool
CanCheckMod bool
OnDataReturn string
CanAcceptArbitraryPointAmount bool
RecommendedPoints *big.Int
PluginHTML string
PluginScript string
ApiCode func(*gin.Context, ApiInput) (*big.Int, error)
HasExtraAPI bool
ExtraAPICode func(*gin.Context)
}
type ApiInput struct {
InputPoints *big.Int
AddPointsFunction func(string, *big.Int)
ChannelID string
OptionalData string
}
type DateAndStream struct {
Date Date
Stream *big.Int
}

818
main.go Normal file
View File

@ -0,0 +1,818 @@
package main
import (
"shoGambler/lib"
"errors"
"log"
"os"
"plugin"
"strconv"
"strings"
"time"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"html/template"
"io/fs"
"math/big"
"net/http"
"path/filepath"
"github.com/MicahParks/keyfunc/v3"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
_ "modernc.org/sqlite"
)
func getYTChID(token string) (string, int, error) {
log.Println("[WARN] Scary, we are expending a Google API credit!")
// Ask Google for the user's channel ID
request, err := http.NewRequest("GET", "https://www.googleapis.com/youtube/v3/channels?mine=true", nil)
if err != nil {
return "", 500, errors.New("error creating Google auth request")
}
// Set the Authorization header
request.Header.Set("Authorization", "Bearer "+token)
// Send the request
response, err := http.DefaultClient.Do(request)
if err != nil {
return "", 500, errors.New("error sending Google auth request")
}
// Check the status code
if response.StatusCode != 200 {
return "", response.StatusCode, errors.New("error getting Google auth response")
}
// Read the response
var responseJSON map[string]interface{}
err = json.NewDecoder(response.Body).Decode(&responseJSON)
if err != nil {
return "", 500, errors.New("error decoding Google auth response")
}
// Get the user's channel ID
channelID, ok := responseJSON["items"].([]interface{})[0].(map[string]interface{})["id"].(string)
if !ok {
return "", 400, errors.New("error getting channel ID")
}
return channelID, 200, nil
}
func giveUserPoints(channelID string, points *big.Int) {
// Add the points to the userPoints
var existingPoints []byte
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&existingPoints)
if errors.Is(err, sql.ErrNoRows) {
_, err := conn.Exec("INSERT INTO users (channelID, points) VALUES (?, ?)", channelID, points.Bytes())
if err != nil {
log.Fatal("[FATAL] Error adding user to database: ", err)
}
} else if err != nil {
log.Fatal("[FATAL] Error querying database: ", err)
} else {
_, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", new(big.Int).Add(points, new(big.Int).SetBytes(existingPoints)).Bytes(), channelID)
if err != nil {
log.Fatal("[FATAL] Error updating user points: ", err)
}
}
}
func subtractUserPoints(channelID string, points *big.Int) {
// Subtract the points from the userPoints
var existingPoints []byte
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&existingPoints)
if errors.Is(err, sql.ErrNoRows) {
_, err := conn.Exec("INSERT INTO users (channelID, points) VALUES (?, ?)", channelID, new(big.Int).Neg(points).Bytes())
if err != nil {
log.Fatal("[FATAL] Error adding user to database: ", err)
}
} else if err != nil {
log.Fatal("[FATAL] Error querying database: ", err)
} else {
_, err := conn.Exec("UPDATE users SET points = ? WHERE channelID = ?", new(big.Int).Add(new(big.Int).SetBytes(existingPoints), new(big.Int).Neg(points)).Bytes(), channelID)
if err != nil {
log.Fatal("[FATAL] Error updating user points: ", err)
}
}
}
func getUserPoints(channelID string) *big.Int {
// Get the user's points
var points []byte
err := conn.QueryRow("SELECT points FROM users WHERE channelID = ?", channelID).Scan(&points)
if errors.Is(err, sql.ErrNoRows) {
return big.NewInt(0)
} else if err != nil {
log.Fatal("[FATAL] Error querying database: ", err)
}
return new(big.Int).SetBytes(points)
}
func userIsModerator(accessToken string) bool {
// Get the channel ID
channelID, ok := userSessions[accessToken]
if !ok {
return false
}
if channelID != "UCHlTEt24Yb4ylJFuWz7hXIw" {
// Check if the user is a moderator
var channelIDCheck string
err := conn.QueryRow("SELECT channelID FROM moderators WHERE channelID = ?", channelID).Scan(&channelIDCheck)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false
} else {
log.Fatal("[FATAL] Error querying database: ", err)
return false
}
} else {
return channelID == channelIDCheck
}
} else {
// Bro it's literally shounic, of course they're a moderator
return true
}
}
func checkForChatMessages(liveChatID string) {
for {
if streaming == true {
// Check for chat messages
log.Println("[INFO] Scary, we are expending a Google API credit (on stream)!")
// Keeping this in a comment so the compiler doesn't remember it:
key, err := base64.StdEncoding.DecodeString(configFile.ApiKey)
if err != nil {
log.Println("[ERROR] Error decoding API key: ", err)
}
response, err := http.Get("https://www.googleapis.com/youtube/v3/liveChat/messages?liveChatId=" + liveChatID + "&part=snippet,authorDetails&key=" + string(key))
if err != nil {
log.Println("[ERROR] Error getting chat messages: ", err)
return
}
// Read the response
var responseJSON map[string]interface{}
err = json.NewDecoder(response.Body).Decode(&responseJSON)
if err != nil {
log.Println("[ERROR] Error decoding chat messages: ", err)
return
}
// Check the status code
if response.StatusCode != 200 {
log.Println("[ERROR] Error getting chat messages: ", response.Status, responseJSON)
return
} else {
// Iterate through each live chat message
for _, item := range responseJSON["items"].([]interface{}) {
log.Println("[INFO] Processing message from ", item.(map[string]interface{})["authorDetails"].(map[string]interface{})["displayName"].(string))
earliestSentMessage, ok := earliestSentMsg[item.(map[string]interface{})["authorDetails"].(map[string]interface{})["channelId"].(string)]
publishedTime, err := time.Parse(time.RFC3339Nano, item.(map[string]interface{})["snippet"].(map[string]interface{})["publishedAt"].(string))
if err != nil {
log.Println("[ERROR] Error parsing time: ", err)
} else if !ok || publishedTime.Before(earliestSentMessage) {
if publishedTime.After(streamingSince) {
log.Println("[INFO] New message from ", item.(map[string]interface{})["authorDetails"].(map[string]interface{})["displayName"].(string))
earliestSentMsg[item.(map[string]interface{})["authorDetails"].(map[string]interface{})["channelId"].(string)] = publishedTime
}
}
}
// Close the response body
err := response.Body.Close()
if err != nil {
log.Println("[ERROR] Error closing response body: ", err)
}
// Wait for the rate because Google likes to screw us over
time.Sleep(time.Second * time.Duration(configFile.Rate))
}
} else {
return
}
}
}
var (
plugins []lib.PluginData
conn *sql.DB
earliestSentMsg = make(map[string]time.Time)
userSessions = make(map[string]string)
streamingSince time.Time
configFile config
streaming bool
)
type config struct {
ApiKey string `json:"key"`
Rate int `json:"rate"`
}
func main() {
// Connect to the database
var err error
conn, err = sql.Open("sqlite", "database.db")
if err != nil {
log.Fatal("[FATAL] Error connecting to database: ", err)
}
// Read in config.json
configBytes, err := os.ReadFile("config.json")
if err != nil {
log.Fatal("[FATAL] Error reading config.json: ", err)
}
// Parse the JSON
err = json.Unmarshal(configBytes, &configFile)
if err != nil {
log.Fatal("[FATAL] Error parsing config.json: ", err)
}
// Create the blacklist table if it doesn't exist
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS blacklist (nonce TEXT NOT NULL UNIQUE)")
if err != nil {
log.Fatal("[FATAL] Error creating blacklist table: ", err)
}
// Create the plugins table if it doesn't exist
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS plugins (pluginName TEXT UNIQUE, pointsOverride BLOB)")
if err != nil {
log.Fatal("[FATAL] Error creating plugins table: ", err)
}
// Create the moderator table if it doesn't exist
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS moderators (channelID TEXT NOT NULL UNIQUE, isStreamer BOOLEAN NOT NULL DEFAULT FALSE)")
if err != nil {
log.Fatal("[FATAL] Error creating moderators table: ", err)
}
// Create the user table if it doesn't exist
_, err = conn.Exec("CREATE TABLE IF NOT EXISTS users (channelID TEXT NOT NULL UNIQUE, points BLOB NOT NULL, sub TEXT NOT NULL UNIQUE)")
if err != nil {
log.Fatal("[FATAL] Error creating users table: ", err)
}
// Set up the JWT verification
keyVerifyFunction, err := keyfunc.NewDefault([]string{"https://www.googleapis.com/oauth2/v3/certs"})
if err != nil {
log.Fatal("[FATAL] Error setting up JWT verification: ", err)
}
// Set up plugins
err = filepath.WalkDir("plugins", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Ignore directories
if d.IsDir() {
return nil
}
// Load the plugin
gamblePlugin, err := plugin.Open(path)
if err != nil {
return err
}
// Run plugin.Metadata
metadata, err := gamblePlugin.Lookup("Metadata")
if err != nil {
return err
}
// Declare it as a function
metadataFunc, ok := metadata.(func() (lib.PluginData, error))
if !ok {
return errors.New("metadata is not a function")
}
// Call the function
data, err := metadataFunc()
// Give them the add points function if they want it
if data.CanAddPoints {
addPoints, err := gamblePlugin.Lookup("SetAddPointsFunc")
if err != nil {
return err
}
addPointsFunc, ok := addPoints.(func(func(string, *big.Int)))
if !ok {
return errors.New("addPoints is not a function")
}
addPointsFunc(giveUserPoints)
}
// Give them the check moderator function if they want it
if data.CanCheckMod {
checkMod, err := gamblePlugin.Lookup("SetCheckModFunc")
if err != nil {
return err
}
checkModFunc, ok := checkMod.(func(func(string) bool))
if !ok {
return errors.New("checkMod is not a function")
}
checkModFunc(userIsModerator)
}
// Append the plugin data to the plugins slice
plugins = append(plugins, data)
return nil
})
if err != nil {
log.Fatal("[FATAL] Error setting up plugins: ", err)
}
// Set up the router
gin.SetMode(gin.ReleaseMode)
router := gin.New()
// Set up the routes
router.Static("/static", "./static")
router.LoadHTMLGlob("./templates/*")
// Define routes for each plugin
for _, pluginData := range plugins {
log.Println("[INFO] Setting up plugin", pluginData.Name)
// Try to see if there is a point cost override
pointCost := pluginData.RecommendedPoints
var pointCostBytes []byte
err := conn.QueryRow("SELECT pointsOverride FROM plugins WHERE pluginName = ?", pluginData.Name).Scan(&pointCostBytes)
if err == nil {
pointCost = new(big.Int).SetBytes(pointCostBytes)
} else if !errors.Is(err, sql.ErrNoRows) {
log.Fatal("[FATAL] Error querying database: ", err)
}
router.GET("/"+pluginData.Name, func(c *gin.Context) {
var costsSupported string
if pluginData.CanAcceptArbitraryPointAmount {
costsSupported = "true"
} else {
costsSupported = "false"
}
var canReturn string
if pluginData.CanReturnPoints {
canReturn = "true"
} else {
canReturn = "false"
}
c.HTML(200, "plugin.html", gin.H{
"Name": pluginData.Name,
"Cost": pointCost,
"PluginHTML": template.HTML(pluginData.PluginHTML),
"PluginScript": template.JS(pluginData.PluginScript),
"CanReturn": canReturn,
"MultipleCostsSupported": costsSupported,
"OnDataReturn": template.JS(pluginData.OnDataReturn),
})
})
router.POST("/api/"+pluginData.Name, func(c *gin.Context) {
var data map[string]interface{}
err := c.BindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// Get the user's access token
accessToken := c.GetHeader("Authorization")
if accessToken == "" {
c.JSON(400, gin.H{"error": "No token provided"})
return
}
// Get the user's channel ID
channelID, ok := userSessions[accessToken]
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
var inputPoints *big.Int
if pluginData.CanAcceptArbitraryPointAmount {
// Get the points
points, ok := data["points"].(string)
if !ok {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// Parse the points
inputPoints, ok = new(big.Int).SetString(points, 10)
if !ok {
c.JSON(400, gin.H{"error": "Invalid points"})
return
}
// Subtract the point cost from the user's points
userPointAmount := getUserPoints(channelID)
if userPointAmount == big.NewInt(0) {
c.JSON(400, gin.H{"error": "No points"})
return
}
remaining := new(big.Int).Sub(userPointAmount, inputPoints)
if remaining.Cmp(big.NewInt(0)) == 1 {
subtractUserPoints(channelID, inputPoints)
} else {
c.JSON(400, gin.H{"error": "Not enough points, want " + inputPoints.String() + " have " + userPointAmount.String() + ", would leave you with " + remaining.String()})
return
}
} else {
// Subtract the point cost from the user's points
userPointAmount := getUserPoints(channelID)
if userPointAmount == big.NewInt(0) {
c.JSON(400, gin.H{"error": "No points"})
return
}
remaining := new(big.Int).Sub(userPointAmount, pointCost)
if remaining.Cmp(big.NewInt(0)) == 1 {
subtractUserPoints(channelID, pointCost)
} else {
c.JSON(400, gin.H{"error": "Not enough points, want " + pointCost.String() + " have " + userPointAmount.String() + ", would leave you with " + remaining.String()})
return
}
}
optionalData, ok := data["optional"].(string)
if !ok {
optionalData = "none"
}
if pluginData.CanReturnPoints {
profit, err := pluginData.ApiCode(c, lib.ApiInput{
InputPoints: inputPoints,
OptionalData: optionalData,
ChannelID: channelID,
})
if err != nil {
c.JSON(424, gin.H{"error": err.Error()})
return
}
giveUserPoints(channelID, profit)
c.JSON(200, gin.H{"profit": profit.String()})
} else {
_, err := pluginData.ApiCode(c, lib.ApiInput{
InputPoints: inputPoints,
OptionalData: optionalData,
ChannelID: channelID,
})
if err != nil {
c.JSON(424, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"success": "true"})
}
})
if pluginData.HasExtraAPI {
router.POST("/api/extra/"+pluginData.Name, pluginData.ExtraAPICode)
}
}
// Define the route for /api/claimUnclaimedPoints
router.POST("/api/claimUnclaimedPoints", func(c *gin.Context) {
// Look for the token in the userMap
accessToken := c.GetHeader("Authorization")
id, ok := userSessions[accessToken]
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Get the users earliest sent message
earliestSentMessage, ok := earliestSentMsg[id]
if !ok {
c.JSON(400, gin.H{"error": "No messages sent"})
return
}
// Get the second difference between the earliest sent message and now
secondDifference := int64(time.Now().Sub(earliestSentMessage).Seconds())
// Clear the earliest sent message
delete(earliestSentMsg, id)
// Issue the points
giveUserPoints(id, big.NewInt(secondDifference))
// Return the points
c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
})
// Define the route for /api/getUnclaimedPoints
router.GET("/api/getUnclaimedPoints", func(c *gin.Context) {
// Look for the token in the userMap
accessToken := c.GetHeader("Authorization")
id, ok := userSessions[accessToken]
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Get the users earliest sent message
earliestSentMessage, ok := earliestSentMsg[id]
if !ok {
c.JSON(200, gin.H{"points": "0"})
return
}
// Get the second difference between the earliest sent message and now
secondDifference := int64(time.Now().Sub(earliestSentMessage).Seconds())
// Return the points
c.JSON(200, gin.H{"points": strconv.FormatInt(secondDifference, 10)})
})
// Define the route for /api/getPlugins
router.GET("/api/getPlugins", func(c *gin.Context) {
var pluginJSON []map[string]interface{}
for _, pluginData := range plugins {
// Try to see if there is a point cost override
pointCost := pluginData.RecommendedPoints
var pointCostBytes []byte
err := conn.QueryRow("SELECT pointsOverride FROM plugins WHERE pluginName = ?", pluginData.Name).Scan(&pointCostBytes)
if err == nil {
pointCost = new(big.Int).SetBytes(pointCostBytes)
} else if !errors.Is(err, sql.ErrNoRows) {
log.Fatal("[FATAL] Error querying database: ", err)
}
// Append the plugin data to the pluginJSON slice
var costsSupported string
if pluginData.CanAcceptArbitraryPointAmount {
costsSupported = "true"
} else {
costsSupported = "false"
}
pluginJSON = append(pluginJSON, map[string]interface{}{
"Name": pluginData.Name,
"CanReturnPoints": pluginData.CanReturnPoints,
"Cost": pointCost,
"MultipleCostsSupported": costsSupported,
})
}
c.JSON(200, pluginJSON)
})
// Define the route for /api/getPoints
router.GET("/api/getPoints", func(c *gin.Context) {
// Look for the token in the userMap
accessToken := c.GetHeader("Authorization")
id, ok := userSessions[accessToken]
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Get the user's points
points := getUserPoints(id)
c.JSON(200, gin.H{"points": points.String()})
})
// Ugh, why do we have to do this legally
router.POST("/api/delete", func(c *gin.Context) {
// Look for the token in the userMap
accessToken := c.GetHeader("Authorization")
id, ok := userSessions[accessToken]
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Delete everything from every map with the user's channel ID
delete(earliestSentMsg, id)
_, err := conn.Exec("DELETE FROM users WHERE channelID = ?", id)
if err != nil {
log.Fatal("[FATAL] Error deleting user: ", err)
}
// Delete all access tokens with the user's channel ID
for key, value := range userSessions {
if value == id {
delete(userSessions, key)
}
}
c.JSON(200, "Data will be deleted in the time it takes for the garbage collector to do its thing")
})
// Define the route for /api/startStream
router.POST("/api/startStream", func(c *gin.Context) {
var data map[string]interface{}
err := c.BindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// Authenticate the user
accessToken := c.GetHeader("Authorization")
if accessToken == "" {
c.JSON(400, gin.H{"error": "No token provided"})
return
}
// Check if the user is a moderator
if !userIsModerator(accessToken) {
c.JSON(403, gin.H{"error": "You must be a moderator to start a stream"})
return
}
// Set the live status
streaming = true
streamingSince = time.Now()
liveChatID, ok := data["liveChatID"].(string)
if !ok {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// Start the chat message checker
log.Println("Starting chat message checker: ", liveChatID)
go checkForChatMessages(liveChatID)
// Return 200
c.JSON(200, gin.H{"message": "Stream started"})
})
// Define the route for /api/endStream
router.POST("/api/endStream", func(c *gin.Context) {
// Authenticate the user
accessToken := c.GetHeader("Authorization")
if accessToken == "" {
c.JSON(400, gin.H{"error": "No token provided"})
return
}
// Check if the user is a moderator
if !userIsModerator(accessToken) {
c.JSON(403, gin.H{"error": "You must be a moderator to end a stream"})
return
}
// Set the live status
streaming = false
// Clear all unclaimed points
for key := range earliestSentMsg {
delete(earliestSentMsg, key)
}
// Return 200
c.JSON(200, gin.H{"message": "Stream ended"})
})
router.POST("/api/authorize", func(c *gin.Context) {
var data map[string]interface{}
err := c.BindJSON(&data)
if err != nil {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
// Check if it's a valid google token via JWT
accessToken, ok := data["idToken"].(string)
if !ok {
c.JSON(400, gin.H{"error": "Invalid JSON"})
return
}
parsedToken, err := jwt.Parse(accessToken, keyVerifyFunction.Keyfunc)
if err != nil {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Get the claims
claims, ok := parsedToken.Claims.(jwt.MapClaims)
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Check if the user is already registered
var channelID string
err = conn.QueryRow("SELECT channelID FROM users WHERE sub = ?", claims["sub"]).Scan(&channelID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Get the at_hash
atHash, ok := claims["at_hash"].(string)
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Get the access token
accessToken, ok = data["accessToken"].(string)
if !ok {
c.JSON(403, gin.H{"error": "Invalid token"})
return
}
// Hash the access token
hashedAccessToken := sha256.Sum256([]byte(accessToken))
// Check if the hash matches the at_hash
if strings.ReplaceAll(base64.URLEncoding.EncodeToString(hashedAccessToken[:16]), "=", "") != atHash {
c.JSON(403, gin.H{"error": "Non-matching access token"})
return
}
// Get the user's channel ID
channelID, response, err := getYTChID(accessToken)
if err != nil {
c.JSON(response, gin.H{"error": "Error getting channel ID"})
return
}
_, err = conn.Exec("INSERT INTO users (channelID, sub, points) VALUES (?, ?, ?)", channelID, claims["sub"], big.NewInt(0).Bytes())
if err != nil {
log.Fatal("[FATAL] Error registering user: ", err)
}
} else {
log.Fatal("[FATAL] Error querying database: ", err)
}
}
// Create a new random session token
sessionToken := make([]byte, 32)
_, err = rand.Read(sessionToken)
if err != nil {
log.Fatal("[FATAL] Error generating session token: ", err)
}
// Hex encode the session token
sessionTokenHex := hex.EncodeToString(sessionToken)
// Add the session token to the userSessions map
userSessions[sessionTokenHex] = channelID
// Return the session token
c.JSON(200, gin.H{"sessionToken": sessionTokenHex})
})
// Now some static routes
router.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{})
})
router.GET("/admin", func(c *gin.Context) {
c.HTML(200, "admin.html", gin.H{})
})
router.GET("/login", func(c *gin.Context) {
c.HTML(200, "login.html", gin.H{})
})
router.GET("/privacy", func(c *gin.Context) {
c.HTML(200, "privacy.html", gin.H{})
})
router.GET("/tos", func(c *gin.Context) {
c.HTML(200, "tos.html", gin.H{})
})
// Start the server
var address string
if len(os.Args) < 2 {
address = ":8080"
} else {
address = os.Args[1]
}
log.Println("[INFO] Start server on " + address)
err = router.Run(address)
if err != nil {
log.Fatal("[FATAL] Error starting server: ", err)
}
}

2
plugins-src/bet/build.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
go build -ldflags "-s -w" -buildmode=plugin -o bet.so main.go

252
plugins-src/bet/main.go Normal file
View File

@ -0,0 +1,252 @@
package main
import (
"errors"
"math/big"
"shoGambler/lib"
"time"
"github.com/gin-gonic/gin"
)
type bet struct {
amount *big.Int
answer string
}
var (
bets = make(map[string]bet)
possibleAnswers []string
bettingIsOpen = false
question string
addPoints func(string, *big.Int)
userIsModerator func(string) bool
timeToBet time.Time
)
func Metadata() (lib.PluginData, error) {
return lib.PluginData{
Name: "bet-on-it",
CanReturnPoints: false,
RecommendedPoints: big.NewInt(10),
CanAcceptArbitraryPointAmount: true,
PluginHTML: `
<h1>Bet on it</h1>
<p>Vote for what you think will happen</p>
<p>If you lose the bet, you lose your points</p>
<p>If you win the bet, you get double your points back</p>
<p id="question"></p>
<p id="possible"></p>
<p id="timer"></p>
<button id="bet" disabled>Bet</button>
`,
PluginScript: `
async function updateTimer(time) {
while (true) {
let timeLeft = time - Math.floor(Date.now() / 1000);
if (timeLeft <= 0) {
document.getElementById("timer").innerText = "Betting is now closed";
document.getElementById("bet").disabled = true;
} else {
document.getElementById("timer").innerText = "Betting closes in " + timeLeft + " seconds";
}
await new Promise(r => setTimeout(r, 1000));
}
}
let data
fetch("/api/extra/bet-on-it", {
method: "POST",
body: JSON.stringify({
Action: "getCurrentBet",
}),
headers: {
"Content-Type": "application/json",
},
})
.then(async (response) => {
if (response.status == 206) {
alert("There is not a running bet");
window.location.href = "/";
} else if (response.status != 200) {
alert("Error: " + response.statusText);
window.location.href = "/";
}
data = await response.json();
document.getElementById("question").innerText = data["question"];
document.getElementById("possible").innerText = data["possible"].join(", ");
updateTimer(data["timeToBet"]);
document.getElementById("bet").disabled = false;
})
document.getElementById("bet").addEventListener("click", async () => {
let points = BigInt(0);
try {
points = BigInt(prompt("How many points do you want to spend?"));
} catch (e) {
alert("Invalid number");
return;
}
let candidate = prompt(data["question"] + data["possible"].join(", ") + "(case sensitive)");
if (data["possible"].includes(candidate)) {
sendCost(points, candidate);
} else {
alert("That's not an option!")
}
})
</script>
`,
ApiCode: ApiCode,
HasExtraAPI: true,
ExtraAPICode: ExtraAPICode,
OnDataReturn: "alert('Bet placed. Good luck!')",
CanAddPoints: true,
CanCheckMod: true,
}, nil
}
func ApiCode(_ *gin.Context, input lib.ApiInput) (*big.Int, error) {
if time.Now().Before(timeToBet) {
// See which option the user bet on
candidate := input.OptionalData
// Add the user's bet to the map
validBet := false
for _, possibleAnswer := range possibleAnswers {
if candidate == possibleAnswer {
validBet = true
break
}
}
if !validBet {
return nil, errors.New("invalid bet")
} else {
bets[input.ChannelID] = bet{
amount: input.InputPoints,
answer: candidate,
}
return nil, nil
}
} else {
return nil, errors.New("betting is closed")
}
}
func ExtraAPICode(c *gin.Context) {
var data map[string]interface{}
err := c.BindJSON(&data)
if err != nil {
c.JSON(400, gin.H{
"error": "Invalid JSON",
})
return
}
action, ok := data["Action"].(string)
if !ok {
c.JSON(400, gin.H{
"error": "Invalid action",
})
return
}
switch action {
case "getCurrentBet":
if bettingIsOpen {
c.JSON(200, gin.H{
"question": question,
"possible": possibleAnswers,
"timeToBet": timeToBet.Unix(),
})
return
} else {
c.JSON(206, gin.H{
"error": "There is not a running bet",
})
return
}
case "startBet":
if bettingIsOpen {
c.JSON(206, gin.H{
"error": "There is already a running bet",
})
return
} else {
accessToken := c.GetHeader("Authorization")
if !userIsModerator(accessToken) || accessToken == "" {
c.JSON(403, gin.H{
"error": "You must be a moderator to start a bet",
})
return
}
question, ok = data["question"].(string)
if !ok {
c.JSON(400, gin.H{
"error": "Invalid question",
})
return
}
possibleAnswersJSON, ok := data["possible"].([]interface{})
if !ok {
c.JSON(400, gin.H{
"error": "Invalid possible answers",
})
return
}
possibleAnswers = make([]string, len(possibleAnswersJSON))
for i, possibleAnswer := range possibleAnswersJSON {
possibleAnswers[i], ok = possibleAnswer.(string)
if !ok {
c.JSON(400, gin.H{
"error": "Invalid possible answer",
})
return
}
}
timeToBet = time.Unix(int64(data["timeToBet"].(float64)), 0)
bettingIsOpen = true
c.JSON(200, gin.H{
"success": true,
})
return
}
case "endBet":
if bettingIsOpen {
accessToken := c.GetHeader("Authorization")
if !userIsModerator(accessToken) || accessToken == "" {
c.JSON(403, gin.H{
"error": "You must be a moderator to end a bet",
})
return
}
bettingIsOpen = false
// Give the winners double their points
for channelID, bet := range bets {
if bet.answer == data["answer"].(string) {
addPoints(channelID, new(big.Int).Mul(bet.amount, big.NewInt(2)))
}
}
// Clear the bets
bets = make(map[string]bet)
question = ""
possibleAnswers = nil
// Return success
c.JSON(200, gin.H{
"success": true,
})
return
}
default:
c.JSON(400, gin.H{
"error": "Invalid action",
})
return
}
}
func SetAddPointsFunc(addPointsFunc func(string, *big.Int)) {
addPoints = addPointsFunc
}
func SetCheckModFunc(userIsModeratorFunc func(string) bool) {
userIsModerator = userIsModeratorFunc
}

2
plugins-src/diceroll/build.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
go build -ldflags "-s -w" -buildmode=plugin -o diceroll.so main.go

View File

@ -0,0 +1,86 @@
package main
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"shoGambler/lib"
"github.com/gin-gonic/gin"
)
func Metadata() (lib.PluginData, error) {
return lib.PluginData{
Name: "dice-roll",
// It does return points
CanReturnPoints: true,
// It does not add points outside out /api/dice-roll
CanAddPoints: false,
// It does not need an arbitrary API, it's fine within the constraints of the plugin API
HasExtraAPI: false,
// It does not need an arbitrary API, so this is nil
ExtraAPICode: nil,
// It recommends 10 points to be spent
RecommendedPoints: big.NewInt(10),
// You can input arbitrary point amounts
CanAcceptArbitraryPointAmount: true,
// Very simple HTML
PluginHTML: `
<h1>Dice Roll</h1>
<p>Roll a dice</p>
<button id="roll">Roll</button>
`,
// Very simple script
PluginScript: `
document.getElementById("roll").addEventListener("click", async () => {
let points = BigInt(0);
try {
points = BigInt(prompt("How many points do you want to spend?"));
} catch (e) {
alert("Invalid number");
return;
}
sendCost(points);
})
</script>
`,
// The API code is the function ApiCode
ApiCode: ApiCode,
// If the plugin html says the plugin has returned data rather than points, it's an error
OnDataReturn: "alert('Error: this plugin should be returning points'); throw new Error('Error: this plugin should be returning points')",
}, nil
}
func ApiCode(_ *gin.Context, input lib.ApiInput) (*big.Int, error) {
// Roll a die
diceRoll, err := rand.Int(rand.Reader, big.NewInt(6))
if err != nil {
return nil, err
}
// 1, 2 and 3 - lose all points
// 4 and 5 - keep points
// 6 - win 125% points
if input.InputPoints != nil {
switch diceRoll.Uint64() + 1 {
case 1, 2, 3:
// Lose all points
return big.NewInt(0), nil
case 4, 5:
// Keep points
return input.InputPoints, nil
case 6:
// Win 125% points
result, _ := new(big.Float).Mul(new(big.Float).SetInt(input.InputPoints), big.NewFloat(1.25)).Int(nil)
return result, nil
}
return nil, errors.New("dice roll out of range: " + diceRoll.String())
} else {
fmt.Println(input.InputPoints)
return nil, errors.New("input points is nil")
}
}

8
static/js/jwt.min.js vendored Normal file

File diff suppressed because one or more lines are too long

99
templates/admin.html Normal file
View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Panel</title>
</head>
<body>
<h1>Admin Panel</h1>
<p>Note: this admin panel will only work for Shounic!</p>
<button id="startPoll">Start Bet</button>
<button id="endPoll">End Bet</button>
<button id="startStream">Start Stream</button>
<button id="endStream">End Stream</button>
<script>
document.getElementById('startPoll').addEventListener('click', async () => {
let question = prompt("What is the question of the bet?")
let outcomes = prompt("What outcomes can the poll have (comma seperated)?")
let time = prompt("How long should the poll last (in seconds)?")
let response = await fetch("/api/extra/bet-on-it", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
},
body: JSON.stringify({
"Action": "startBet",
"question": question,
"possible": outcomes.split(", "),
"timeToBet": Math.floor(new Date().getTime() / 1000) + parseInt(time)
})
})
if (response.ok) {
alert("Poll started!")
} else {
alert("Error starting poll")
}
})
document.getElementById('endPoll').addEventListener('click', async () => {
let response = await fetch("/api/extra/bet-on-it", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
},
body: JSON.stringify({
"Action": "endBet",
"answer": prompt("What was the result of the bet (case sensitive)?")
})
})
if (response.ok) {
alert("Poll ended!")
} else {
alert("Error ending poll")
}
})
document.getElementById('startStream').addEventListener('click', async () => {
// First, fetch the broadcasts from YouTube
let response = await fetch("https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet&mine=true", {
headers: {
"Authorization": "Bearer " + localStorage.getItem("oauthToken")
}
})
let data = await response.json()
let liveChatID = data.items[0]["snippet"]["liveChatId"]
// Now that we have the live chat ID, we can start the stream
let response2 = await fetch("/api/startStream", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
},
body: JSON.stringify({
"liveChatID": liveChatID
})
})
if (response2.ok) {
alert("Stream started!")
} else {
alert("Error starting stream")
}
})
document.getElementById('endStream').addEventListener('click', async () => {
let response = await fetch("/api/endStream", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
}
})
if (response.ok) {
alert("Stream ended!")
} else {
alert("Error ending stream")
}
})
</script>
</body>
</html>

140
templates/index.html Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gamble</title>
<script src="/static/js/jwt.min.js"></script>
</head>
<body>
<p>Shounic's un-styled ultra-ugly gambling site</p>
<p id="unclaimedPoints">Loading...</p>
<p>Click the button to claim unclaimed points</p>
<button id="claimPoints">Claim Points</button>
<p id="pointAmount">Loading...</p>
<p>Here are the current activities:</p>
<ul id="fillIn">
<!-- Fill in the activities here with JavaScript -->
</ul>
<a href="/privacy">Privacy Policy</a>
<a href="/tos">Terms of Service</a>
<script>
async function main() {
function isUserInEU() {
// Updated list of time zones that are in the European Union
const euTimeZones = [
'Europe/Andorra',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Helsinki',
'Europe/Kiev',
'Europe/Lisbon',
'Europe/Ljubljana',
'Europe/Madrid',
'Europe/Malta',
'Europe/Monaco',
'Europe/Oslo',
'Europe/Paris',
'Europe/Prague',
'Europe/Rome',
'Europe/San_Marino',
'Europe/Stockholm',
'Europe/Tirane',
'Europe/Vaduz',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Zurich'
];
// Get the user's time zone
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Check if the user's time zone is in the EU list
return euTimeZones.includes(userTimeZone);
}
// Warn EU users that this website is not GDPR-compliant (in the EU)
if (isUserInEU()) {
if (!confirm("This website is not GDPR compliant in the EU (it is in the UK). It is compliant with every clause except the location one. If you use this service from within the EU, you are doing so at your own risk. Do you want to continue?")) {
// Redirect the user to the EU website (haha)
window.location.href = "https://europa.eu/"
}
}
if (localStorage.getItem("accessToken") === null) {
window.location.href = "/login"
}
let fillIn = document.getElementById("fillIn")
await fetch("/api/getPlugins", {
method: "GET",
headers: {
"Authorization": localStorage.getItem("accessToken")
}
}).then(async (response) => {
let data = await response.json()
for (let i = 0; i < data.length; i++) {
let li = document.createElement("li")
let a = document.createElement("a")
a.href = "/" + data[i]["Name"]
a.innerText = data[i]["Name"]
li.appendChild(a)
fillIn.appendChild(li)
}
})
document.getElementById('claimPoints').addEventListener('click', async () => {
let response = await fetch('/api/claimUnclaimedPoints', {
"method": "POST",
"headers": {
"Authorization": localStorage.getItem("accessToken")
}})
if (response.ok) {
alert("Claimed " + data['points'] + " points")
window.location.reload()
} else {
alert("Error claiming points")
}
})
}
async function updatePoints() {
while (true) {
let response = await fetch('/api/getPoints', {
"method": "GET",
"headers": {
"Authorization": localStorage.getItem("accessToken")
}
})
let data = await response.json()
document.getElementById('pointAmount').innerText = "You have " + data['points'] + " points"
response = await fetch('/api/getUnclaimedPoints', {
"method": "GET",
"headers": {
"Authorization": localStorage.getItem("accessToken")
}
})
if (!response.ok) {
document.getElementById('unclaimedPoints').innerText = "Error getting unclaimed points"
return
}
data = await response.json()
document.getElementById('unclaimedPoints').innerText = "You have " + data['points'] + " unclaimed points"
// Wait for a second before updating the points again
await new Promise(r => setTimeout(r, 1000))
}
}
main()
updatePoints()
</script>
</body>

170
templates/login.html Normal file
View File

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log in with Google</title>
</head>
<body>
<h1 id="text">Log in with Google</h1>
<button id="authorize">Authorize</button>
<button id="delete">Delete every last byte of my data</button>
<p>By logging in, you agree to our <a href="/privacy">Privacy Policy</a> and <a href="/tos">Terms of Service</a></p>
<script>
// Configuration
const clientId = '165295772598-ua5imd4pduoapsh0bnu96lul2j6bhsul.apps.googleusercontent.com';
const redirectUri = 'https://sho.ailur.dev/login';
const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
const userinfoEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json';
// Generate a random code verifier
function generateCodeVerifier() {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
const length = 128;
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
.map((x) => charset[x % charset.length])
.join("");
}
// Create a code challenge from the code verifier using SHA-256
async function createCodeChallenge(codeVerifier) {
const buffer = new TextEncoder().encode(codeVerifier);
const hashArrayBuffer = await crypto.subtle.digest('SHA-256', buffer);
return btoa(String.fromCharCode(...new Uint8Array(hashArrayBuffer)))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
// Authorization function with PKCE
document.getElementById('authorize').addEventListener('click', () => {
const codeVerifier = generateCodeVerifier();
localStorage.setItem('codeVerifier', codeVerifier); // Store code verifier
createCodeChallenge(codeVerifier)
.then((codeChallenge) => {
window.location.href = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&scope=https://www.googleapis.com/auth/youtube.readonly%20https://www.googleapis.com/auth/userinfo.profile`;
})
.catch((error) => {
console.error('Error generating code challenge:', error);
});
})
// Delete every last byte of user data
document.getElementById('delete').addEventListener('click', async () => {
document.getElementById("text").innerText = "Deleting every last byte of your data..."
let response = await fetch("/api/delete", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
}
})
if (response.status !== 200) {
document.getElementById("text").innerText = "Theres probably an outage or something. Try again later. Can't say I didn't try."
} else {
localStorage.clear()
document.getElementById("text").innerText = "Deleted every last byte of your data, even from memory (well, when the garbage collector kicks in)."
}
})
// Parse the authorization code from the URL
function parseCodeFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('code');
}
// Exchange authorization code for access token
async function exchangeCodeForToken(code) {
const codeVerifier = localStorage.getItem('codeVerifier'); // Retrieve code verifier
const formData = new URLSearchParams();
formData.append('client_id', String(clientId));
formData.append('code', String(code));
formData.append('redirect_uri', String(redirectUri));
formData.append('grant_type', 'authorization_code');
formData.append('code_verifier', String(codeVerifier));
// Google, for some godforsaken reason, wants client_secret during a PKCE flow.
// What the hell? That's not remotely compatible with the PKCE RFC.
// I hope to god that Google is still doing PKCE properly even though they're asking for client_secret.
// #### you, Google, the hate is absolutely justified.
formData.append('client_secret', String('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'))
let response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: formData
});
if (response.status !== 200) {
console.error('Failed to exchange code for token:', response.status, await response.text());
return;
}
const data = await response.json();
console.log('Successfully exchanged code for token:', data);
const accessToken = data['access_token'];
// Request userinfo with ACCESS TOKEN in bearer format
// For some other godforsaken reason, they want the access token, not the id token.
// #### you, Google.
// THAT'S NOT HOW USERINFO WORKS, GOOGLE. YOU IDIOT!
fetch(userinfoEndpoint, {
headers: {
"Authorization": "Bearer " + accessToken
}
})
.then((response) => {
async function doStuff() {
if (response.status === 200) {
const userinfoData = await response.json();
console.log("Userinfo:", userinfoData);
console.log(accessToken)
console.log("User:", userinfoData["name"]);
console.log("Sub:", userinfoData["id"]);
localStorage.removeItem("codeVerifier")
document.getElementById("text").innerText = "Authenticated, " + userinfoData.name + ", now logging into the server..."
await fetch("/api/authorize", {
method: "POST",
body: JSON.stringify({
"idToken": data['id_token'],
"accessToken": accessToken,
}),
}).then(response => {
response.json().then(data => {
if (response.status === 200) {
localStorage.setItem("accessToken", data["sessionToken"])
localStorage.setItem("user", userinfoData["name"])
localStorage.setItem("oauthToken", accessToken)
localStorage.setItem("sub", userinfoData["id"])
window.location.href = "/"
} else {
document.getElementById("text").innerText = "Authentication failed"
}
})
});
} else {
document.getElementById("text").innerText = "Authentication failed"
}
}
doStuff()
});
}
// Main function to handle OAuth2 flow
async function main() {
if (localStorage.getItem("user") !== null) {
document.getElementById("text").innerText = "Welcome back, " + localStorage.getItem("user") + ". Aren't you already logged in?"
}
const code = parseCodeFromUrl();
if (code) {
await exchangeCodeForToken(code);
}
}
// Call the main function on page load
main();
</script>
</body>
</html>

62
templates/plugin.html Normal file
View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ .Name }}</title>
<!-- Pass through the cost of the plugin to the page -->
<meta id="cost" content="{{ .Cost }}">
<meta id="multipleCostsSupported" content="{{ .MultipleCostsSupported }}">
<meta id="name" content="{{ .Name }}">
<meta id="canReturnTokens" content="{{ .CanReturn }}">
<script src="/static/js/jwt.min.js"></script>
<script>
async function sendCost(amount, optional) {
if (document.getElementById('multipleCostsSupported').content !== 'true') {
amount = BigInt(document.getElementById('cost').content)
}
// Save updated points to local storage
let response = await fetch('/api/' + document.getElementById('name').content, {
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": localStorage.getItem("accessToken")
},
"body": JSON.stringify({
"points": amount.toString(),
"optional": optional || "none"
})
})
if (response.status === 200) {
let data = await response.json()
if (document.getElementById('canReturnTokens').content === 'true') {
alert("You got " + data['profit'] + " points")
} else {
{{ .OnDataReturn }}
}
} else {
let data = await response.json()
alert("Failed to activate plugin: " + data['error'])
}
}
document.addEventListener('DOMContentLoaded', async () => {
let response = await fetch('/api/getPoints', {
"method": "GET",
"headers": {
"Authorization": localStorage.getItem("accessToken")
}
})
let data = await response.json()
document.getElementById('pointAmount').innerText = "You have " + data['points'] + " points"
})
</script>
</head>
<body>
{{ .PluginHTML }}
<p id="pointAmount">Loading...</p>
<script>
{{ .PluginScript }}
</script>
</body>
</html>

9
templates/privacy.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Privacy Policy</title>
</head>
<body>
We do not sell your data. Full stop. Your personal information never leaves your device. The only thing we ever use your google account for is to get your YouTube channel ID, and to display your name. We never store your email address, and your name never gets on our servers. If you would like us to delete all your data, immediately, simply press the delete account button on the login page (this may not work during outages).
</body>

26
templates/tos.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Privacy Policy</title>
</head>
<body>
<p>Hey there! Heres the deal with your data:</p>
<p>Were all about keeping things simple. We only use your Google account to:</p>
<ul>
<li>Grab your YouTube channel ID so we can make our service better.</li>
<li>Show your name on the client-side, so you can see it. Thats it!</li>
</ul>
<p>We dont store your email address, and your name never touches our servers. Its all on your device and stays there.</p>
<p>Want to delete your data? Hit the "Delete Account" button on the login page. It might not work during outages, but were doing our best.</p>
<p>We are very sorry, but I don't own servers in the EU and therefore cannot comply with the EU GDPR. It is compliant with the UK one and everything except the location thing in the EU one. Very sorry, but we cannot support any EU users at this time. If you use this service from within the EU, you are doing so at your own risk.</p>
<p>If youve got any questions or just want to chat, hit us up at @arzumify on discord.</p>
<p>Thanks for reading! Now go enjoy the app without worrying about your data.</p>
</body>
</html>