Skip to content

Instantly share code, notes, and snippets.

@ajmadsen
Created April 15, 2020 23:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ajmadsen/9676c326f0e89a5fafb0bac7f831dc12 to your computer and use it in GitHub Desktop.
Save ajmadsen/9676c326f0e89a5fafb0bac7f831dc12 to your computer and use it in GitHub Desktop.
Voice Logging
package voicelog
import (
"errors"
"fmt"
"strings"
"time"
"github.com/bwmarrin/discordgo"
mgo "github.com/globalsign/mgo"
"github.com/globalsign/mgo/bson"
"github.com/sirupsen/logrus"
)
const (
flushTimeout = 1 * time.Second
logTimeFormat = "15:04:05.00"
)
var (
errMissingParams = errors.New("missing params")
)
type Options struct {
Session *discordgo.Session
Log *logrus.Entry
DB *mgo.Database
}
type Module struct {
*discordgo.Session
*logrus.Entry
*mgo.Collection
// maps GuildID to settings
settings map[string]*VoiceLogGuild
}
type VoiceLogSettings struct {
ID bson.ObjectId `bson:"_id"`
GuildID string `bson:"guild_id"`
LogChannelID string `bson:"log_channel_id"`
LocationStr string `bson:"location"`
}
type VoiceLogGuild struct {
*Module
VoiceLogSettings
location *time.Location
eventQueue chan *VoiceLogUpdate
// map of UserID to Channel
voiceState map[string]*discordgo.Channel
}
type VoiceLogUpdate struct {
*discordgo.VoiceStateUpdate
Time time.Time
}
func New(o *Options) *Module {
sess := o.DB.Session.Clone()
settings := make(map[string]*VoiceLogGuild)
m := &Module{
Session: o.Session,
Entry: o.Log.WithField("module", "voice_log"),
Collection: o.DB.C("voice_log").With(sess),
settings: settings,
}
// TODO: load settings
m.LoadSettings()
m.AddHandler(m.handleVoiceStateUpdate)
return m
}
func (m *Module) LoadSettings() error {
// Ensure we have a valid session
m.Collection.Database.Session.Refresh()
m.Info("loading settings")
defer m.Info("done loading settings")
var settings []VoiceLogSettings
err := m.Find(bson.M{}).All(&settings)
if err != nil {
m.Warnf("failed to load settings: %v", err)
return err
}
for _, s := range settings {
if s.GuildID == "" || s.LogChannelID == "" {
m.Warnf("setting %q is invalid: missing params", s.ID)
return errMissingParams
}
g := &VoiceLogGuild{
Module: m,
VoiceLogSettings: s,
eventQueue: make(chan *VoiceLogUpdate, 25),
voiceState: make(map[string]*discordgo.Channel),
}
if s.LocationStr == "" {
g.location = time.UTC
} else {
loc, err := time.LoadLocation(s.LocationStr)
if err != nil {
m.Warnf("could not parse location for guild %q: %v", s.GuildID, err)
loc = time.UTC
}
g.location = loc
}
m.settings[s.GuildID] = g
go g.Waiter()
}
return nil
}
func (m *Module) handleVoiceStateUpdate(s *discordgo.Session, u *discordgo.VoiceStateUpdate) {
g, ok := m.settings[u.GuildID]
if !ok {
return
}
g.eventQueue <- &VoiceLogUpdate{
VoiceStateUpdate: u,
Time: time.Now().In(g.location),
}
}
func (g *VoiceLogGuild) Waiter() {
var queue []*VoiceLogUpdate
timer := time.After(flushTimeout)
for {
select {
case u := <-g.eventQueue:
queue = append(queue, u)
case <-timer:
if len(queue) > 0 {
g.Flush(queue)
}
queue = queue[:0]
timer = time.After(flushTimeout)
}
}
}
func (g *VoiceLogGuild) Flush(queue []*VoiceLogUpdate) {
var msgs []string
for _, u := range queue {
m, err := g.State.Member(g.GuildID, u.UserID)
if err != nil {
g.Warnf("don't know aobut member %q in guild %q: %v", u.UserID, g.GuildID, err)
continue
}
action := "disconnected"
if u.ChannelID != "" {
ch, err := g.State.Channel(u.ChannelID)
if err != nil {
g.Warnf("don't know about channel %q in guild %q: %v", u.ChannelID, g.GuildID, err)
continue
}
if oldCh, ok := g.voiceState[u.UserID]; ok {
if oldCh.ID == u.ChannelID {
// some other update other than a channel move
continue
} else {
action = fmt.Sprintf("moves from voice channel `%s` to `%s`", oldCh.Name, ch.Name)
g.voiceState[u.UserID] = ch
}
} else {
action = fmt.Sprintf("joined voice channel `%s`", ch.Name)
g.voiceState[u.UserID] = ch
}
} else {
// remove user
delete(g.voiceState, u.UserID)
}
msgs = append(msgs, fmt.Sprintf("`[%s]` %s %s.", u.Time.Format(logTimeFormat), m.User.Mention(), action))
}
if len(msgs) == 0 {
// no messages to flush
return
}
embed := &discordgo.MessageEmbed{
Description: strings.Join(msgs, "\n"),
}
_, err := g.ChannelMessageSendEmbed(g.LogChannelID, embed)
if err != nil {
g.Warnf("could not send log message to %q: %v", g.LogChannelID, err)
return
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment