From 59da01b0e55696f86b532b9650903b5a48c70f6e Mon Sep 17 00:00:00 2001 From: Justin Judd Date: Sun, 27 Jul 2025 20:21:30 -0700 Subject: [PATCH] Move most configurations to a dedicated config file (using CUElang). Also added birthday notifier. --- .gitignore | 5 ++ go.mod | 17 +++- go.sum | 69 +++++++++++++++- main.go | 225 ++++++++++++++++++++++++++++++++++++++++++++--------- schema.cue | 31 ++++++++ 5 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 schema.cue diff --git a/.gitignore b/.gitignore index 5b90e79..fb1506c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ go.work.sum # env file .env +# config files +*.cue + +# database +data.db diff --git a/go.mod b/go.mod index 10c3f5d..497e770 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,28 @@ module dev.justinjudd.com/discord_bots go 1.24.1 require ( - github.com/bwmarrin/discordgo v0.29.0 // indirect + 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/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.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/crypto v0.37.0 // 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 ) diff --git a/go.sum b/go.sum index 7daff4b..2d6c0b2 100644 --- a/go.sum +++ b/go.sum @@ -1,37 +1,102 @@ +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= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +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/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/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +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/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= diff --git a/main.go b/main.go index 5dd0225..d081ee6 100644 --- a/main.go +++ b/main.go @@ -4,50 +4,98 @@ import ( "database/sql" "flag" "fmt" + "io" + "io/fs" "log" "math/rand/v2" "os" "os/signal" - "strconv" + "path/filepath" + "strings" "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", dbFile+"?_time_format=sqlite") + db, err := sql.Open("sqlite", filepath.Join(dataPath, dbFile)+"?_time_format=sqlite") if err != nil { return nil, fmt.Errorf("unable to open db %q: %w", dbFile, err) } @@ -70,28 +118,74 @@ func NewServer(dbFile string) (*Server, error) { if err != nil { return nil, fmt.Errorf("unable to create table: %w", err) } - s := Server{DB: db} + s := Server{DB: db, configs: map[string]Config{}} 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 { @@ -116,6 +210,10 @@ 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 @@ -135,6 +233,11 @@ 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: @@ -147,8 +250,6 @@ 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) @@ -176,33 +277,85 @@ func (s *Server) voiceStatus(ds *discordgo.Session, m *discordgo.VoiceStateUpdat return } - channels, err := ds.GuildChannels(m.GuildID) + ch, err := ds.Channel(server.VoiceChatAnnounce.AnnounceChannel) 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 } - msg := fmt.Sprintf(joinMessages[rand.IntN(len(joinMessages))], m.UserID, m.ChannelID) + messages := append(joinMessages, server.VoiceChatAnnounce.JoinMessages...) + + msg := fmt.Sprintf(messages[rand.IntN(len(messages))], 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(announceChannel.ID, msg) + sent, err := ds.ChannelMessageSend(ch.ID, msg) if err != nil { log.Printf("unable to send message: %s", err) } - time.AfterFunc(time.Second*time.Duration(cleanDelay), func() { + time.AfterFunc(time.Second*time.Duration(server.VoiceChatAnnounce.CleanUpDelay), 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 +} diff --git a/schema.cue b/schema.cue new file mode 100644 index 0000000..ca1ba64 --- /dev/null +++ b/schema.cue @@ -0,0 +1,31 @@ +// 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 + ... +} \ No newline at end of file