Compare commits
	
		
			No commits in common. "2dd919a08fc0ab3848b8e7a28f43f28e161d35d1" and "f7924261de2ce2fb9bec4d11c78378d7c11a0503" have entirely different histories.
		
	
	
		
			2dd919a08f
			...
			f7924261de
		
	
		
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -25,8 +25,3 @@ go.work.sum
 | 
			
		||||
# env file
 | 
			
		||||
.env
 | 
			
		||||
 | 
			
		||||
# config files
 | 
			
		||||
*.cue
 | 
			
		||||
 | 
			
		||||
# database
 | 
			
		||||
data.db
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								go.mod
									
									
									
									
									
								
							@ -3,28 +3,19 @@ module dev.justinjudd.com/discord_bots
 | 
			
		||||
go 1.24.1
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	cuelang.org/go v0.13.2
 | 
			
		||||
	github.com/bwmarrin/discordgo v0.29.0
 | 
			
		||||
	github.com/joho/godotenv v1.5.1
 | 
			
		||||
	modernc.org/sqlite v1.38.0
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/cockroachdb/apd/v3 v3.2.1 // indirect
 | 
			
		||||
	github.com/bwmarrin/discordgo v0.29.0 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.1 // indirect
 | 
			
		||||
	github.com/google/uuid v1.6.0 // indirect
 | 
			
		||||
	github.com/gorilla/websocket v1.4.2 // indirect
 | 
			
		||||
	github.com/joho/godotenv v1.5.1 // indirect
 | 
			
		||||
	github.com/mattn/go-isatty v0.0.20 // indirect
 | 
			
		||||
	github.com/ncruces/go-strftime v0.1.9 // indirect
 | 
			
		||||
	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 | 
			
		||||
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 | 
			
		||||
	golang.org/x/crypto v0.37.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
 | 
			
		||||
	golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
 | 
			
		||||
	golang.org/x/net v0.39.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.33.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.24.0 // indirect
 | 
			
		||||
	gopkg.in/yaml.v3 v3.0.1 // indirect
 | 
			
		||||
	modernc.org/libc v1.65.10 // indirect
 | 
			
		||||
	modernc.org/mathutil v1.7.1 // indirect
 | 
			
		||||
	modernc.org/memory v1.11.0 // indirect
 | 
			
		||||
	modernc.org/sqlite v1.38.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										69
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								go.sum
									
									
									
									
									
								
							@ -1,102 +1,37 @@
 | 
			
		||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20250304105642-27e071d2c9b1 h1:Dmbd5Q+ENb2C6carvwrMsrOUwJ9X9qfL5JdW32gYAHo=
 | 
			
		||||
cuelabs.dev/go/oci/ociregistry v0.0.0-20250304105642-27e071d2c9b1/go.mod h1:dqrnoZx62xbOZr11giMPrWbhlaV8euHwciXZEy3baT8=
 | 
			
		||||
cuelang.org/go v0.13.2 h1:SagzeEASX4E2FQnRbItsqa33sSelrJjQByLqH9uZCE8=
 | 
			
		||||
cuelang.org/go v0.13.2/go.mod h1:8MoQXu+RcXsa2s9mebJN1HJ1orVDc9aI9/yKi6Dzsi4=
 | 
			
		||||
github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
 | 
			
		||||
github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
 | 
			
		||||
github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg=
 | 
			
		||||
github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc=
 | 
			
		||||
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/emicklei/proto v1.14.0 h1:WYxC0OrBuuC+FUCTZvb8+fzEHdZMwLEF+OnVfZA3LXU=
 | 
			
		||||
github.com/emicklei/proto v1.14.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
 | 
			
		||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
 | 
			
		||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
 | 
			
		||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 | 
			
		||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 | 
			
		||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
 | 
			
		||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 | 
			
		||||
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/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 | 
			
		||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 | 
			
		||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 | 
			
		||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 | 
			
		||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 | 
			
		||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 | 
			
		||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 | 
			
		||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 | 
			
		||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
 | 
			
		||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 | 
			
		||||
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/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 | 
			
		||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
 | 
			
		||||
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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 | 
			
		||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 | 
			
		||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
 | 
			
		||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
 | 
			
		||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 | 
			
		||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20250129171521-feedd8250727 h1:A8EM8fVuYc0qbVMw9D6EiKdKTIm1SmLvAWcCc2mipGY=
 | 
			
		||||
github.com/protocolbuffers/txtpbfmt v0.0.0-20250129171521-feedd8250727/go.mod h1:VmWrOlMnBZNtToCWzRlZlIXcJqjo0hS5dwQbRD62gL8=
 | 
			
		||||
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 | 
			
		||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 | 
			
		||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
 | 
			
		||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
 | 
			
		||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
 | 
			
		||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 | 
			
		||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
 | 
			
		||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
 | 
			
		||||
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
 | 
			
		||||
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 | 
			
		||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
 | 
			
		||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
 | 
			
		||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 | 
			
		||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 | 
			
		||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 | 
			
		||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
 | 
			
		||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
 | 
			
		||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 | 
			
		||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
 | 
			
		||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 | 
			
		||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
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.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
 | 
			
		||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
 | 
			
		||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
 | 
			
		||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
 | 
			
		||||
modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
 | 
			
		||||
modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
 | 
			
		||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
 | 
			
		||||
modernc.org/libc v1.65.10 h1:ZwEk8+jhW7qBjHIT+wd0d9VjitRyQef9BnzlzGwMODc=
 | 
			
		||||
modernc.org/libc v1.65.10/go.mod h1:StFvYpx7i/mXtBAfVOjaU0PWZOvIRoZSgXhrwXzr8Po=
 | 
			
		||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
 | 
			
		||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
 | 
			
		||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
 | 
			
		||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
 | 
			
		||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
 | 
			
		||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
 | 
			
		||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
 | 
			
		||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
 | 
			
		||||
modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
 | 
			
		||||
modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
 | 
			
		||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
 | 
			
		||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
 | 
			
		||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 | 
			
		||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										225
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										225
									
								
								main.go
									
									
									
									
									
								
							@ -4,98 +4,50 @@ import (
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"math/rand/v2"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"cuelang.org/go/cue"
 | 
			
		||||
	"cuelang.org/go/cue/cuecontext"
 | 
			
		||||
	"github.com/bwmarrin/discordgo"
 | 
			
		||||
	"github.com/joho/godotenv"
 | 
			
		||||
 | 
			
		||||
	_ "embed"
 | 
			
		||||
 | 
			
		||||
	_ "modernc.org/sqlite"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var botToken string
 | 
			
		||||
var announceChannelName string
 | 
			
		||||
var cleanDelay = 30
 | 
			
		||||
 | 
			
		||||
var confFile string
 | 
			
		||||
var dbFile string
 | 
			
		||||
var dataPath string
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	flag.StringVar(&confFile, "conf", ".env", ".env file w/ config variables")
 | 
			
		||||
	flag.StringVar(&dbFile, "db", "data.db", "db to store logs of events in")
 | 
			
		||||
	flag.StringVar(&dataPath, "data", "./", "file path for storing data")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//go:embed schema.cue
 | 
			
		||||
var schemaFile string
 | 
			
		||||
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Server            string
 | 
			
		||||
	VoiceChatAnnounce struct {
 | 
			
		||||
		Enable          bool
 | 
			
		||||
		AnnounceChannel string
 | 
			
		||||
		JoinMessages    []string
 | 
			
		||||
		CleanUpDelay    int
 | 
			
		||||
	}
 | 
			
		||||
	BirthdayAnnounce struct {
 | 
			
		||||
		Enable          bool
 | 
			
		||||
		AnnounceChannel string
 | 
			
		||||
		Birthdays       []Birthday
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c Config) String() string {
 | 
			
		||||
	s := strings.Builder{}
 | 
			
		||||
	s.WriteString(fmt.Sprintf("Guild ID: %s\n", c.Server))
 | 
			
		||||
	if c.VoiceChatAnnounce.Enable {
 | 
			
		||||
		s.WriteString("\tVoice Chat Accounce: ✅ \n")
 | 
			
		||||
		s.WriteString(fmt.Sprintf("\t\tTo Channel: %s\n", c.VoiceChatAnnounce.AnnounceChannel))
 | 
			
		||||
		s.WriteString(fmt.Sprintf("\t\t %d Custom messages\n", len(c.VoiceChatAnnounce.JoinMessages)))
 | 
			
		||||
	} else {
 | 
			
		||||
		s.WriteString("\tVoice Chat Accounce: ❌ \n")
 | 
			
		||||
	}
 | 
			
		||||
	if c.BirthdayAnnounce.Enable {
 | 
			
		||||
		s.WriteString("\tBirthday Accounce: ✅ \n")
 | 
			
		||||
		s.WriteString(fmt.Sprintf("\t\tTo Channel: %s\n", c.BirthdayAnnounce.AnnounceChannel))
 | 
			
		||||
		s.WriteString(fmt.Sprintf("\t\t %d Birthdays to Announce\n", len(c.BirthdayAnnounce.Birthdays)))
 | 
			
		||||
	} else {
 | 
			
		||||
		s.WriteString("\tBirthday Accounce: ❌ \n")
 | 
			
		||||
	}
 | 
			
		||||
	return s.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Birthday struct {
 | 
			
		||||
	Name   string //Optional
 | 
			
		||||
	Member string
 | 
			
		||||
	Date   string // Format MM/DD
 | 
			
		||||
	server string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var joinMessages = []string{
 | 
			
		||||
	"<@%s> has joined <#%s>",
 | 
			
		||||
	"<@%s> has joined <#%s>; Join in for some nerd talk",
 | 
			
		||||
	"<#%[2]s> is the place to be! <@%[1]s> just joined",
 | 
			
		||||
	"<#%[2]s> just got a bit cooler, <@%[1]s> is now in",
 | 
			
		||||
	"<@%s> is hanging out in <#%s>",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Channel struct {
 | 
			
		||||
	GuildID, ChannelID string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Server struct {
 | 
			
		||||
	*sql.DB
 | 
			
		||||
	configs map[string]Config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(dbFile string) (*Server, error) {
 | 
			
		||||
	db, err := sql.Open("sqlite", filepath.Join(dataPath, dbFile)+"?_time_format=sqlite")
 | 
			
		||||
	db, err := sql.Open("sqlite", dbFile+"?_time_format=sqlite")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to open db %q: %w", dbFile, err)
 | 
			
		||||
	}
 | 
			
		||||
@ -118,74 +70,28 @@ func NewServer(dbFile string) (*Server, error) {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to create table: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	s := Server{DB: db, configs: map[string]Config{}}
 | 
			
		||||
	s := Server{DB: db}
 | 
			
		||||
	return &s, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) AddConfig(c Config) {
 | 
			
		||||
	s.configs[c.Server] = c
 | 
			
		||||
	log.Printf("Added server: \n%s\n", c)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	envs, err := godotenv.Read(confFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Unable to get environment variables: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	s, err := NewServer(dbFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("can't create server: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx := cuecontext.New()
 | 
			
		||||
	schema := ctx.CompileString(schemaFile)
 | 
			
		||||
 | 
			
		||||
	vfs := os.DirFS(dataPath)
 | 
			
		||||
	err = fs.WalkDir(vfs, ".", func(path string, d fs.DirEntry, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal(err)
 | 
			
		||||
		}
 | 
			
		||||
		if filepath.Ext(d.Name()) != ".cue" {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		if d.Name() == "schema.cue" {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		f, err := vfs.Open(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		data, err := io.ReadAll(f)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		conf := ctx.CompileBytes(data, cue.Scope(schema))
 | 
			
		||||
		if err := conf.Validate(); err != nil {
 | 
			
		||||
			return fmt.Errorf("unable to validate config %q: %w", path, err)
 | 
			
		||||
		}
 | 
			
		||||
		cfg := Config{}
 | 
			
		||||
		fields, err := conf.Fields()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		fields.Next()
 | 
			
		||||
		if err := fields.Value().Decode(&cfg); err != nil {
 | 
			
		||||
			return fmt.Errorf("unable to decode config %q: %w", path, err)
 | 
			
		||||
		}
 | 
			
		||||
		s.AddConfig(cfg)
 | 
			
		||||
		fmt.Printf("Loaded conf from %q\n", path)
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Error(s) loading conf files: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	time.Sleep(500 * time.Millisecond)
 | 
			
		||||
	envs, err := godotenv.Read(filepath.Join(dataPath, confFile))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatalf("Unable to get environment variables: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// set values from env
 | 
			
		||||
	botToken = envs["BOT_TOKEN"]
 | 
			
		||||
	announceChannelName = envs["ANNOUNCE_CHANNEL"]
 | 
			
		||||
	if delay, ok := envs["CLEAN_DELAY"]; ok {
 | 
			
		||||
		if d2, err := strconv.Atoi(delay); err == nil {
 | 
			
		||||
			cleanDelay = d2
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ds, err := discordgo.New("Bot " + botToken)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
@ -210,10 +116,6 @@ func main() {
 | 
			
		||||
	}
 | 
			
		||||
	defer ds.Close()
 | 
			
		||||
 | 
			
		||||
	if err = s.setupBirthdayWatch(ds); err != nil {
 | 
			
		||||
		log.Fatalf("can't setup birthday watcher: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stop := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(stop, os.Interrupt)
 | 
			
		||||
	<-stop
 | 
			
		||||
@ -233,11 +135,6 @@ func (s VoiceState) String() string {
 | 
			
		||||
 | 
			
		||||
// Handle for when someone joins a voice channel - send notification about them joining.
 | 
			
		||||
func (s *Server) voiceStatus(ds *discordgo.Session, m *discordgo.VoiceStateUpdate) {
 | 
			
		||||
	guild, _ := ds.Guild(m.VoiceState.GuildID)
 | 
			
		||||
	server := s.configs[guild.ID]
 | 
			
		||||
	if !server.VoiceChatAnnounce.Enable {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var state VoiceState
 | 
			
		||||
	switch {
 | 
			
		||||
	case len(m.ChannelID) == 0:
 | 
			
		||||
@ -250,6 +147,8 @@ func (s *Server) voiceStatus(ds *discordgo.Session, m *discordgo.VoiceStateUpdat
 | 
			
		||||
		state = Switching
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	guild, _ := ds.Guild(m.VoiceState.GuildID)
 | 
			
		||||
 | 
			
		||||
	switch state {
 | 
			
		||||
	case Leaving, Switching:
 | 
			
		||||
		channel, _ := ds.Channel(m.BeforeUpdate.ChannelID)
 | 
			
		||||
@ -277,85 +176,33 @@ func (s *Server) voiceStatus(ds *discordgo.Session, m *discordgo.VoiceStateUpdat
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ch, err := ds.Channel(server.VoiceChatAnnounce.AnnounceChannel)
 | 
			
		||||
	channels, err := ds.GuildChannels(m.GuildID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("Unable to get channels for Guild: %s", m.GuildID)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var announceChannel *discordgo.Channel
 | 
			
		||||
	for _, c := range channels {
 | 
			
		||||
		if c.Name == announceChannelName {
 | 
			
		||||
			announceChannel = c
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if announceChannel == nil {
 | 
			
		||||
		log.Printf("Unable to get announce channel for Guild: %s", m.GuildID)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	messages := append(joinMessages, server.VoiceChatAnnounce.JoinMessages...)
 | 
			
		||||
 | 
			
		||||
	msg := fmt.Sprintf(messages[rand.IntN(len(messages))], m.UserID, m.ChannelID)
 | 
			
		||||
	msg := fmt.Sprintf(joinMessages[rand.IntN(len(joinMessages))], m.UserID, m.ChannelID)
 | 
			
		||||
	switch state {
 | 
			
		||||
	case Switching:
 | 
			
		||||
		msg = fmt.Sprintf("<@%s> has left <#%s> to join <#%s>", m.UserID, m.BeforeUpdate.ChannelID, m.ChannelID)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sent, err := ds.ChannelMessageSend(ch.ID, msg)
 | 
			
		||||
	sent, err := ds.ChannelMessageSend(announceChannel.ID, msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Printf("unable to send message: %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	time.AfterFunc(time.Second*time.Duration(server.VoiceChatAnnounce.CleanUpDelay), func() {
 | 
			
		||||
	time.AfterFunc(time.Second*time.Duration(cleanDelay), func() {
 | 
			
		||||
		ds.ChannelMessageDelete(sent.ChannelID, sent.ID)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) setupBirthdayWatch(ds *discordgo.Session) error {
 | 
			
		||||
	birthdays := map[string][]Birthday{} // Use a list in case there are collisions (See birthday paradox)
 | 
			
		||||
	for _, guild := range s.configs {
 | 
			
		||||
		if !guild.BirthdayAnnounce.Enable {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		for _, b := range guild.BirthdayAnnounce.Birthdays {
 | 
			
		||||
			b.server = guild.Server
 | 
			
		||||
			birthdays[b.Date] = append(birthdays[b.Date], b)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	targetHour := 7
 | 
			
		||||
	targetMinute := 35
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	location, err := time.LoadLocation("America/Los_Angeles")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to get timezone data: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	targetTime := time.Date(now.Year(), now.Month(), now.Day(), targetHour, targetMinute, 0, 0, location)
 | 
			
		||||
	if now.After(targetTime) {
 | 
			
		||||
		targetTime = targetTime.Add(24 * time.Hour)
 | 
			
		||||
	}
 | 
			
		||||
	initialDelay := targetTime.Sub(now)
 | 
			
		||||
	log.Printf("Waiting for %s for starting birthday watcher", initialDelay)
 | 
			
		||||
	birthdayMatcher := func(ds *discordgo.Session) {
 | 
			
		||||
		dateString := time.Now().Format("01/02")
 | 
			
		||||
		log.Printf("Looking for matching birthdays on %s", dateString)
 | 
			
		||||
		if bd, ok := birthdays[dateString]; ok {
 | 
			
		||||
			for _, b := range bd {
 | 
			
		||||
				log.Printf("It is %q's birthday today (%s)", b.Member, b.Date)
 | 
			
		||||
				if err := announceBirthday(ds, b.server, s.configs[b.server].BirthdayAnnounce.AnnounceChannel, b.Member); err != nil {
 | 
			
		||||
					log.Printf("Error w/ announcing: %v", err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	time.AfterFunc(initialDelay, func() {
 | 
			
		||||
 | 
			
		||||
		log.Printf("Birthday watcher initiated")
 | 
			
		||||
		// Run now, but also set up a daily job
 | 
			
		||||
		birthdayMatcher(ds)
 | 
			
		||||
		c := time.Tick(24 * time.Hour)
 | 
			
		||||
		for range c {
 | 
			
		||||
			birthdayMatcher(ds)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func announceBirthday(ds *discordgo.Session, guildID, channelID, userId string) error {
 | 
			
		||||
	_, err := ds.ChannelMessageSend(channelID, fmt.Sprintf("Happy Birthday to <@%s>!!", userId))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("unable to send message: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										31
									
								
								schema.cue
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								schema.cue
									
									
									
									
									
								
							@ -1,31 +0,0 @@
 | 
			
		||||
// Initially generated by cue get go.
 | 
			
		||||
 | 
			
		||||
//cue:generate cue get go dev.justinjudd.com/discord_bots
 | 
			
		||||
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
#Config: {
 | 
			
		||||
	Server: string
 | 
			
		||||
	VoiceChatAnnounce?: {
 | 
			
		||||
		Enable:          bool | *false
 | 
			
		||||
		AnnounceChannel: string | *""
 | 
			
		||||
		JoinMessages: [...string] @go(,[]string)
 | 
			
		||||
		CleanUpDelay: int | *30
 | 
			
		||||
	} @go(,struct{Enable bool; AnnounceChannel string; JoinMessages []string; CleanUpDelay int})
 | 
			
		||||
	BirthdayAnnounce?: {
 | 
			
		||||
		Enable:          bool | *false
 | 
			
		||||
		AnnounceChannel: string | *""
 | 
			
		||||
		Birthdays: [...#Birthday] @go(,[]Birthday)
 | 
			
		||||
	} @go(,struct{Enable bool; AnnounceChannel string; Birthdays []Birthday})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Birthday: {
 | 
			
		||||
	Name:   string
 | 
			
		||||
	Member: string
 | 
			
		||||
	Date:   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#Enabled: {
 | 
			
		||||
    Enable: true
 | 
			
		||||
    ...
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user