From c967a0325754049e698693abf5ab14b6f82e8c65 Mon Sep 17 00:00:00 2001 From: Justin Judd Date: Sun, 6 Jul 2025 22:09:08 -0700 Subject: [PATCH] Added recording to database of voice channel state changes. Cleaned up messaging in terminal logs, and added new message for switching voice channels. --- go.mod | 12 +++++- go.sum | 23 ++++++++++ main.go | 127 +++++++++++++++++++++++++++++++++++++++++++++++++------- 3 files changed, 145 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 2a83e5b..10c3f5d 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,18 @@ go 1.24.1 require ( 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect - golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sys v0.33.0 // 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 04715b6..7daff4b 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,37 @@ 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/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/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/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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI= +modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE= diff --git a/main.go b/main.go index e409dad..0f2db1c 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "database/sql" "flag" "fmt" "log" @@ -12,6 +13,8 @@ import ( "github.com/bwmarrin/discordgo" "github.com/joho/godotenv" + + _ "modernc.org/sqlite" ) var botToken string @@ -19,9 +22,11 @@ var announceChannelName string var cleanDelay = 30 var confFile string +var dbFile 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.Parse() } @@ -33,11 +38,51 @@ var joinMessages = []string{ "<@%s> is hanging out in <#%s>", } +type Channel struct { + GuildID, ChannelID string +} + +type Server struct { + *sql.DB +} + +func NewServer(dbFile string) (*Server, error) { + db, err := sql.Open("sqlite", dbFile) + if err != nil { + return nil, fmt.Errorf("unable to open db %q: %w", dbFile, err) + } + tx, err := db.Begin() + if err != nil { + return nil, fmt.Errorf("unable to work on db %q: %w", dbFile, err) + } + tx.Exec(`CREATE TABLE IF NOT EXISTS voice_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + guildID INTEGER, + guildName TEXT, + memberId INTEGER, + memberName TEXT, + channelID INTEGER, + channelName TEXT, + action TEXT, + timestamp TIMESTAMP + );`) + err = tx.Commit() + if err != nil { + return nil, fmt.Errorf("unable to create table: %w", err) + } + s := Server{DB: db} + return &s, nil +} + 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) + } // set values from env botToken = envs["BOT_TOKEN"] @@ -48,41 +93,88 @@ func main() { } } - s, err := discordgo.New("Bot " + botToken) + ds, err := discordgo.New("Bot " + botToken) if err != nil { log.Fatal(err) } // Log all of the servers tht this bot is installed in. - s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + ds.AddHandler(func(ds *discordgo.Session, r *discordgo.Ready) { for _, g := range r.Guilds { - g2, _ := s.Guild(g.ID) + g2, _ := ds.Guild(g.ID) log.Printf("Bot is connected to %q.", g2.Name) } + }) // Add different capability handlers: - s.AddHandler(voiceStatus) + ds.AddHandler(s.voiceStatus) - err = s.Open() + err = ds.Open() if err != nil { log.Fatalf("Cannot open the session: %v", err) } - defer s.Close() + defer ds.Close() stop := make(chan os.Signal, 1) signal.Notify(stop, os.Interrupt) <-stop } +type VoiceState int + +const ( + Joining VoiceState = iota + Switching + Leaving +) + +func (s VoiceState) String() string { + return []string{"Joined", "Switched", "Left"}[s] +} + // Handle for when someone joins a voice channel - send notification about them joining. -func voiceStatus(s *discordgo.Session, m *discordgo.VoiceStateUpdate) { - if m.BeforeUpdate != nil || m.VoiceState == nil { +func (s *Server) voiceStatus(ds *discordgo.Session, m *discordgo.VoiceStateUpdate) { + var state VoiceState + switch { + case len(m.ChannelID) == 0: + state = Leaving + case m.BeforeUpdate != nil: + state = Switching + } + + guild, _ := ds.Guild(m.VoiceState.GuildID) + + switch state { + case Leaving, Switching: + channel, _ := ds.Channel(m.BeforeUpdate.ChannelID) + log.Default().Printf("%q has left %q in %q", m.Member.DisplayName(), channel.Name, guild.Name) + + _, err := s.Exec("INSERT INTO voice_stats (guildID, guildName, memberID, memberName, channelID, channelName, action, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", guild.ID, guild.Name, m.VoiceState.UserID, m.VoiceState.Member.DisplayName(), channel.ID, channel.Name, "LEFT", time.Now()) + if err != nil { + log.Printf("unable to save log to db: %s", err) + } + } + + switch state { + case Joining, Switching: + channel, _ := ds.Channel(m.ChannelID) + log.Default().Printf("%q has joined %q in %q", m.Member.DisplayName(), channel.Name, guild.Name) + + _, err := s.Exec("INSERT INTO voice_stats (guildID, guildName, memberID, memberName, channelID, channelName, action, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", guild.ID, guild.Name, m.VoiceState.UserID, m.VoiceState.Member.DisplayName(), channel.ID, channel.Name, "JOIN", time.Now()) + if err != nil { + log.Printf("unable to save log to db: %s", err) + } + } + + switch state { + case Leaving: return } - channels, err := s.GuildChannels(m.VoiceState.GuildID) + + channels, err := ds.GuildChannels(m.GuildID) if err != nil { - log.Printf("Unable to get channels for Guild: %s", m.VoiceState.GuildID) + log.Printf("Unable to get channels for Guild: %s", m.GuildID) return } var announceChannel *discordgo.Channel @@ -92,18 +184,21 @@ func voiceStatus(s *discordgo.Session, m *discordgo.VoiceStateUpdate) { } } if announceChannel == nil { - log.Printf("Unable to get announce channel for Guild: %s", m.VoiceState.GuildID) + log.Printf("Unable to get announce channel for Guild: %s", m.GuildID) return } - msg := joinMessages[rand.IntN(len(joinMessages))] + 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 := s.ChannelMessageSend(announceChannel.ID, fmt.Sprintf(msg, m.VoiceState.UserID, m.VoiceState.ChannelID)) - g, _ := s.Guild(m.VoiceState.GuildID) - log.Default().Printf("%q has joined %q in %s", m.VoiceState.Member.DisplayName(), m.VoiceState.ChannelID, g.Name) + sent, err := ds.ChannelMessageSend(announceChannel.ID, msg) if err != nil { log.Printf("unable to send message: %s", err) } + time.AfterFunc(time.Second*time.Duration(cleanDelay), func() { - s.ChannelMessageDelete(sent.ChannelID, sent.ID) + ds.ChannelMessageDelete(sent.ChannelID, sent.ID) }) }