Initial commit
This commit is contained in:
commit
885efd959a
|
@ -0,0 +1,4 @@
|
||||||
|
.idea
|
||||||
|
shoGambler
|
||||||
|
plugins
|
||||||
|
config.json
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
)
|
|
@ -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=
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
go build -ldflags "-s -w" -buildmode=plugin -o bet.so main.go
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/bin/sh
|
||||||
|
go build -ldflags "-s -w" -buildmode=plugin -o diceroll.so main.go
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Privacy Policy</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Hey there! Here’s the deal with your data:</p>
|
||||||
|
|
||||||
|
<p>We’re 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. That’s it!</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>We don’t store your email address, and your name never touches our servers. It’s 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 we’re 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 you’ve 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>
|
Loading…
Reference in New Issue