Move most configurations to a dedicated config file (using CUElang). Also added birthday notifier.

This commit is contained in:
Justin Judd 2025-07-27 20:21:30 -07:00
parent f7924261de
commit 59da01b0e5
5 changed files with 305 additions and 42 deletions

5
.gitignore vendored
View File

@ -25,3 +25,8 @@ go.work.sum
# env file
.env
# config files
*.cue
# database
data.db

17
go.mod
View File

@ -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
)

69
go.sum
View File

@ -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=

225
main.go
View File

@ -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
}

31
schema.cue Normal file
View File

@ -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
...
}