diff --git a/server/Gopkg.lock b/server/Gopkg.lock index e372e8f..39b87d9 100644 --- a/server/Gopkg.lock +++ b/server/Gopkg.lock @@ -18,12 +18,12 @@ version = "v1.1.0" [[projects]] - digest = "1:99c79dc08249968d203bf4210220c01bd8250301600f5daf0a81f4c5f184f8d9" + digest = "1:edf119770bbfa2586be839aa1a4dd3ab75a678e5921ee10e6914353af0966acc" name = "github.com/bwmarrin/discordgo" packages = ["."] pruneopts = "UT" - revision = "befbea878e0f1d0bb21be6723b6cf6db2689f6a8" - version = "v0.22.0" + revision = "c27ad65527ecbc264c674cd3d0e85bb09de942e3" + version = "v0.23.2" [[projects]] digest = "1:3ee1d175a75b911a659fbd860060874c4f503e793c5870d13e5a0ede529a63cf" diff --git a/server/Gopkg.toml b/server/Gopkg.toml index 75baf33..bdc2a2c 100644 --- a/server/Gopkg.toml +++ b/server/Gopkg.toml @@ -26,7 +26,7 @@ [[constraint]] name = "github.com/bwmarrin/discordgo" - version = "0.22.0" + version = "0.23.1" [[constraint]] name = "github.com/gin-gonic/gin" diff --git a/server/bot/bot.go b/server/bot/bot.go index a08fcfd..58bc7a7 100644 --- a/server/bot/bot.go +++ b/server/bot/bot.go @@ -39,6 +39,7 @@ func Start(token string) *discordgo.Session { // add bot handlers _session.AddHandler(bothandlers.SoundsHandler) + _session.AddHandler(bothandlers.VoiceStateHandler) _session.AddHandler(bothandlers.LoggerHandler) _session.AddHandler(func(_s *discordgo.Session, m *discordgo.MessageCreate) { if m.Content == config.Config.BotPrefix+"restart" { diff --git a/server/bothandlers/sounds.go b/server/bothandlers/sounds.go index 679b910..fb57bd8 100644 --- a/server/bothandlers/sounds.go +++ b/server/bothandlers/sounds.go @@ -33,16 +33,15 @@ var ActiveConnections = make(map[string]*AudioConnection) // AudioConnection - type AudioConnection struct { - Guild *discordgo.Guild `json:"guild"` - Session *discordgo.Session `json:"-"` - VoiceConnection *discordgo.VoiceConnection `json:"-"` - CurrentChannel *discordgo.Channel `json:"current_channel"` - Sounds map[string]*AudioClip `json:"-"` - SoundQueue chan string `json:"-"` - VoiceClipQueue chan *discordgo.Packet `json:"-"` - SoundPlayingLock bool `json:"-"` - AudioListenerLock bool `json:"-"` - Mutex *sync.Mutex `json:"-"` // mutex for single audio connection + Guild *discordgo.Guild `json:"guild"` + Session *discordgo.Session `json:"-"` + CurrentChannel *discordgo.Channel `json:"current_channel"` + Sounds map[string]*AudioClip `json:"-"` + SoundQueue chan string `json:"-"` + VoiceClipQueue chan *discordgo.Packet `json:"-"` + SoundPlayingLock bool `json:"-"` + AudioListenerLock bool `json:"-"` + Mutex *sync.Mutex `json:"-"` // mutex for single audio connection } // AudioClip - @@ -52,9 +51,31 @@ type AudioClip struct { Content [][]byte } -// SoundsHandler - -func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) { +// VoiceStateHandler - when users enter voice channels +func VoiceStateHandler(s *discordgo.Session, v *discordgo.VoiceStateUpdate) { + if conn := ActiveConnections[v.GuildID]; conn != nil { + voiceConnection := conn.getVoiceConnection() + + if voiceConnection.Ready && voiceConnection.ChannelID == v.VoiceState.ChannelID { + + user, err := model.UserGet(db.GetConn(), v.VoiceState.UserID) + + if err != nil { + log.Error(err) + return + } + + if user.VoiceJoinSound != nil { + time.Sleep(time.Second) + conn.PlayAudio(*user.VoiceJoinSound, nil, nil) + } + } + } +} + +// SoundsHandler - play sounds +func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) { // get channel state to get guild id c, err := s.State.Channel(m.ChannelID) if err != nil { @@ -119,18 +140,20 @@ func (conn *AudioConnection) handleMessage(m *discordgo.MessageCreate) { } } -// dismiss bot from currnet channel if it's in one +// dismiss bot from current channel if it's in one func (conn *AudioConnection) dismiss() { - if conn.VoiceConnection != nil && !conn.SoundPlayingLock && len(conn.SoundQueue) == 0 { - conn.VoiceConnection.Disconnect() + voiceConnection := conn.getVoiceConnection() + if voiceConnection != nil && !conn.SoundPlayingLock && len(conn.SoundQueue) == 0 { + voiceConnection.Disconnect() } } // summon bot to channel that user is currently in func (conn *AudioConnection) summon(m *discordgo.MessageCreate) { + voiceConnection := conn.getVoiceConnection() // Join the channel the user issued the command from if not in it - if conn.VoiceConnection == nil || conn.VoiceConnection.ChannelID != m.ChannelID { + if voiceConnection == nil || voiceConnection.ChannelID != m.ChannelID { var err error @@ -159,27 +182,22 @@ func (conn *AudioConnection) summon(m *discordgo.MessageCreate) { log.Error(err) return } - if _, ok := conn.Session.VoiceConnections[c.GuildID]; ok { - conn.VoiceConnection = conn.Session.VoiceConnections[c.GuildID] - } else { - log.Error("Voice connection not found on discord object") - return - } // set the current channel conn.CurrentChannel = c - // start listening to audio if not locked - if !conn.AudioListenerLock { - go conn.startAudioListener() - } + go conn.startAudioListener() } } } } -// play a random sound clip +func (conn *AudioConnection) getVoiceConnection() *discordgo.VoiceConnection { + return conn.Session.VoiceConnections[conn.Guild.ID] +} + +// PlayRandomAudio - play a random sound clip func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate, userID *string) { files, _ := ioutil.ReadDir(config.Config.SoundsPath) if len(files) > 0 { @@ -195,10 +213,13 @@ func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate, userID // if MessageCreate is null play in current channel func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCreate, userID *string) { + voiceConnection := conn.getVoiceConnection() + // summon bot to channel if new message passed in if m != nil { conn.summon(m) - } else if conn.VoiceConnection == nil || !conn.VoiceConnection.Ready { + } else if voiceConnection == nil || !voiceConnection.Ready { + log.Error("[PlayAudio] Voice connection is not ready") return } @@ -217,22 +238,25 @@ func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCre select { case conn.SoundQueue <- soundName: - var newUserID string + var newUserID *string fromWebUI := false // from discord if m != nil && m.Author != nil { - newUserID = m.Author.ID + newUserID = &m.Author.ID } else { fromWebUI = true - newUserID = *userID + newUserID = userID } - // log event when user plays sound clip - err := model.LogSoundPlayedEvent(db.GetConn(), newUserID, soundName, fromWebUI) + // newUserID will be null if bot plays voice join sound + if newUserID != nil { + // log event when user plays sound clip + err := model.LogSoundPlayedEvent(db.GetConn(), *newUserID, soundName, fromWebUI) - if err != nil { - log.Error(err) + if err != nil { + log.Error(err) + } } default: @@ -243,27 +267,26 @@ func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCre if !conn.SoundPlayingLock { conn.playSoundsInQueue() } - } // playSoundsInQueue - play sounds until audio queue is empty func (conn *AudioConnection) playSoundsInQueue() { conn.toggleSoundPlayingLock(true) - + voiceConnection := conn.getVoiceConnection() // Start speaking. - conn.VoiceConnection.Speaking(true) + voiceConnection.Speaking(true) for { select { case newSoundName := <-conn.SoundQueue: - if !conn.VoiceConnection.Ready { + if !voiceConnection.Ready { return } // Send the buffer data. for _, buff := range conn.Sounds[newSoundName].Content { - conn.VoiceConnection.OpusSend <- buff + voiceConnection.OpusSend <- buff } // Sleep for a specificed amount of time before ending. @@ -271,7 +294,7 @@ func (conn *AudioConnection) playSoundsInQueue() { default: // Stop speaking - conn.VoiceConnection.Speaking(false) + voiceConnection.Speaking(false) conn.toggleSoundPlayingLock(false) return } @@ -359,34 +382,65 @@ loop: } // start listening to the voice channel +// endless loop - look into closing if ever set up for multiple servers to prevent memory leak +// should be fine to keep this open for now func (conn *AudioConnection) startAudioListener() { + if conn.AudioListenerLock { + return + } + + log.Info("[startAudioListener] Voice connection listener started") + conn.AudioListenerLock = true if conn.VoiceClipQueue == nil { conn.VoiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize) } - // create new channel to watch for voice connection - // when voice connection is not ready the loop will exit - exitChan := make(chan bool) + localSleep := func() { + time.Sleep(time.Second / 10) + } + // Concurrently check and see if voice connection is not in ready state + // because we need to exit the sound handler. + vcExitChan := make(chan bool, 1) go func() { for { - if !conn.VoiceConnection.Ready { - exitChan <- true - break + voiceConnection := conn.getVoiceConnection() + if voiceConnection != nil { + voiceConnection.RLock() + ready := voiceConnection != nil && voiceConnection.Ready + voiceConnection.RUnlock() + + if !ready { + vcExitChan <- true + } } - time.Sleep(time.Second * 1) + localSleep() } }() -loop: for { + voiceConnection := conn.getVoiceConnection() + if voiceConnection == nil { + localSleep() + continue + } + voiceConnection.RLock() + ready := voiceConnection != nil && voiceConnection.Ready + voiceConnection.RUnlock() + + // if connection lost wait for ready + if !ready { + localSleep() + continue + } select { // grab incoming audio - case opusChannel, ok := <-conn.VoiceConnection.OpusRecv: + case opusChannel, ok := <-voiceConnection.OpusRecv: + if !ok { continue } @@ -398,13 +452,12 @@ loop: // add current packet to channel queue conn.VoiceClipQueue <- opusChannel + break - // check if voice connection fails then break out of audio listener - case <-exitChan: - break loop + // if voice is interrupted continue loop (e.g. disconnects) + case <-vcExitChan: + log.Info("[startAudioListener] exitChan is exiting") + localSleep() } } - - // remove lock upon exit - conn.AudioListenerLock = false } diff --git a/server/webserver/model/user.go b/server/webserver/model/user.go index 5816115..3dc3199 100644 --- a/server/webserver/model/user.go +++ b/server/webserver/model/user.go @@ -9,19 +9,20 @@ import ( // User - type User struct { - ID string `gorm:"primary_key" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt *time.Time `json:"deleted_at"` - Email string `json:"email"` - Username string `json:"username"` - Avatar string `json:"avatar"` - Discriminator string `json:"discriminator"` - Token string `gorm:"-" json:"token"` - Verified bool `json:"verified"` - MFAEnabled bool `json:"mfa_enabled"` - Bot bool `json:"bot"` - Permissions *int `gorm:"default:1;not null" json:"permissions"` + ID string `gorm:"primary_key" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt *time.Time `json:"deleted_at"` + Email string `json:"email"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Discriminator string `json:"discriminator"` + Token string `gorm:"-" json:"token"` + Verified bool `json:"verified"` + MFAEnabled bool `json:"mfa_enabled"` + Bot bool `json:"bot"` + Permissions *int `gorm:"default:1;not null" json:"permissions"` + VoiceJoinSound *string `json:"voice_join_sound"` // sound clip that plays when user joins channel } // UserSave - @@ -33,3 +34,10 @@ func UserSave(conn *gorm.DB, u *User) error { // with the actual object in FirstOrCreate method return conn.Where(&User{ID: u.ID}).Assign(userCopy).FirstOrCreate(u).Error } + +// UserGet - get user by id +func UserGet(conn *gorm.DB, id string) (*User, error) { + user := &User{ID: id} + err := conn.First(user).Error + return user, err +}