1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-09 16:42:48 +00:00

fix voice listener - add voice state handler

This commit is contained in:
2021-02-11 20:17:31 -06:00
parent e573d6da44
commit 4aeeb3ef14
5 changed files with 135 additions and 73 deletions

6
server/Gopkg.lock generated
View File

@@ -18,12 +18,12 @@
version = "v1.1.0" version = "v1.1.0"
[[projects]] [[projects]]
digest = "1:99c79dc08249968d203bf4210220c01bd8250301600f5daf0a81f4c5f184f8d9" digest = "1:edf119770bbfa2586be839aa1a4dd3ab75a678e5921ee10e6914353af0966acc"
name = "github.com/bwmarrin/discordgo" name = "github.com/bwmarrin/discordgo"
packages = ["."] packages = ["."]
pruneopts = "UT" pruneopts = "UT"
revision = "befbea878e0f1d0bb21be6723b6cf6db2689f6a8" revision = "c27ad65527ecbc264c674cd3d0e85bb09de942e3"
version = "v0.22.0" version = "v0.23.2"
[[projects]] [[projects]]
digest = "1:3ee1d175a75b911a659fbd860060874c4f503e793c5870d13e5a0ede529a63cf" digest = "1:3ee1d175a75b911a659fbd860060874c4f503e793c5870d13e5a0ede529a63cf"

View File

@@ -26,7 +26,7 @@
[[constraint]] [[constraint]]
name = "github.com/bwmarrin/discordgo" name = "github.com/bwmarrin/discordgo"
version = "0.22.0" version = "0.23.1"
[[constraint]] [[constraint]]
name = "github.com/gin-gonic/gin" name = "github.com/gin-gonic/gin"

View File

@@ -39,6 +39,7 @@ func Start(token string) *discordgo.Session {
// add bot handlers // add bot handlers
_session.AddHandler(bothandlers.SoundsHandler) _session.AddHandler(bothandlers.SoundsHandler)
_session.AddHandler(bothandlers.VoiceStateHandler)
_session.AddHandler(bothandlers.LoggerHandler) _session.AddHandler(bothandlers.LoggerHandler)
_session.AddHandler(func(_s *discordgo.Session, m *discordgo.MessageCreate) { _session.AddHandler(func(_s *discordgo.Session, m *discordgo.MessageCreate) {
if m.Content == config.Config.BotPrefix+"restart" { if m.Content == config.Config.BotPrefix+"restart" {

View File

@@ -33,16 +33,15 @@ var ActiveConnections = make(map[string]*AudioConnection)
// AudioConnection - // AudioConnection -
type AudioConnection struct { type AudioConnection struct {
Guild *discordgo.Guild `json:"guild"` Guild *discordgo.Guild `json:"guild"`
Session *discordgo.Session `json:"-"` Session *discordgo.Session `json:"-"`
VoiceConnection *discordgo.VoiceConnection `json:"-"` CurrentChannel *discordgo.Channel `json:"current_channel"`
CurrentChannel *discordgo.Channel `json:"current_channel"` Sounds map[string]*AudioClip `json:"-"`
Sounds map[string]*AudioClip `json:"-"` SoundQueue chan string `json:"-"`
SoundQueue chan string `json:"-"` VoiceClipQueue chan *discordgo.Packet `json:"-"`
VoiceClipQueue chan *discordgo.Packet `json:"-"` SoundPlayingLock bool `json:"-"`
SoundPlayingLock bool `json:"-"` AudioListenerLock bool `json:"-"`
AudioListenerLock bool `json:"-"` Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
Mutex *sync.Mutex `json:"-"` // mutex for single audio connection
} }
// AudioClip - // AudioClip -
@@ -52,9 +51,31 @@ type AudioClip struct {
Content [][]byte Content [][]byte
} }
// SoundsHandler - // VoiceStateHandler - when users enter voice channels
func SoundsHandler(s *discordgo.Session, m *discordgo.MessageCreate) { 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 // get channel state to get guild id
c, err := s.State.Channel(m.ChannelID) c, err := s.State.Channel(m.ChannelID)
if err != nil { 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() { func (conn *AudioConnection) dismiss() {
if conn.VoiceConnection != nil && !conn.SoundPlayingLock && len(conn.SoundQueue) == 0 { voiceConnection := conn.getVoiceConnection()
conn.VoiceConnection.Disconnect() if voiceConnection != nil && !conn.SoundPlayingLock && len(conn.SoundQueue) == 0 {
voiceConnection.Disconnect()
} }
} }
// summon bot to channel that user is currently in // summon bot to channel that user is currently in
func (conn *AudioConnection) summon(m *discordgo.MessageCreate) { func (conn *AudioConnection) summon(m *discordgo.MessageCreate) {
voiceConnection := conn.getVoiceConnection()
// Join the channel the user issued the command from if not in it // 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 var err error
@@ -159,27 +182,22 @@ func (conn *AudioConnection) summon(m *discordgo.MessageCreate) {
log.Error(err) log.Error(err)
return 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 // set the current channel
conn.CurrentChannel = c conn.CurrentChannel = c
// start listening to audio if not locked go conn.startAudioListener()
if !conn.AudioListenerLock {
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) { func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate, userID *string) {
files, _ := ioutil.ReadDir(config.Config.SoundsPath) files, _ := ioutil.ReadDir(config.Config.SoundsPath)
if len(files) > 0 { if len(files) > 0 {
@@ -195,10 +213,13 @@ func (conn *AudioConnection) PlayRandomAudio(m *discordgo.MessageCreate, userID
// if MessageCreate is null play in current channel // if MessageCreate is null play in current channel
func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCreate, userID *string) { func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCreate, userID *string) {
voiceConnection := conn.getVoiceConnection()
// summon bot to channel if new message passed in // summon bot to channel if new message passed in
if m != nil { if m != nil {
conn.summon(m) 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 return
} }
@@ -217,22 +238,25 @@ func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCre
select { select {
case conn.SoundQueue <- soundName: case conn.SoundQueue <- soundName:
var newUserID string var newUserID *string
fromWebUI := false fromWebUI := false
// from discord // from discord
if m != nil && m.Author != nil { if m != nil && m.Author != nil {
newUserID = m.Author.ID newUserID = &m.Author.ID
} else { } else {
fromWebUI = true fromWebUI = true
newUserID = *userID newUserID = userID
} }
// log event when user plays sound clip // newUserID will be null if bot plays voice join sound
err := model.LogSoundPlayedEvent(db.GetConn(), newUserID, soundName, fromWebUI) if newUserID != nil {
// log event when user plays sound clip
err := model.LogSoundPlayedEvent(db.GetConn(), *newUserID, soundName, fromWebUI)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
}
} }
default: default:
@@ -243,27 +267,26 @@ func (conn *AudioConnection) PlayAudio(soundName string, m *discordgo.MessageCre
if !conn.SoundPlayingLock { if !conn.SoundPlayingLock {
conn.playSoundsInQueue() conn.playSoundsInQueue()
} }
} }
// playSoundsInQueue - play sounds until audio queue is empty // playSoundsInQueue - play sounds until audio queue is empty
func (conn *AudioConnection) playSoundsInQueue() { func (conn *AudioConnection) playSoundsInQueue() {
conn.toggleSoundPlayingLock(true) conn.toggleSoundPlayingLock(true)
voiceConnection := conn.getVoiceConnection()
// Start speaking. // Start speaking.
conn.VoiceConnection.Speaking(true) voiceConnection.Speaking(true)
for { for {
select { select {
case newSoundName := <-conn.SoundQueue: case newSoundName := <-conn.SoundQueue:
if !conn.VoiceConnection.Ready { if !voiceConnection.Ready {
return return
} }
// Send the buffer data. // Send the buffer data.
for _, buff := range conn.Sounds[newSoundName].Content { for _, buff := range conn.Sounds[newSoundName].Content {
conn.VoiceConnection.OpusSend <- buff voiceConnection.OpusSend <- buff
} }
// Sleep for a specificed amount of time before ending. // Sleep for a specificed amount of time before ending.
@@ -271,7 +294,7 @@ func (conn *AudioConnection) playSoundsInQueue() {
default: default:
// Stop speaking // Stop speaking
conn.VoiceConnection.Speaking(false) voiceConnection.Speaking(false)
conn.toggleSoundPlayingLock(false) conn.toggleSoundPlayingLock(false)
return return
} }
@@ -359,34 +382,65 @@ loop:
} }
// start listening to the voice channel // 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() { func (conn *AudioConnection) startAudioListener() {
if conn.AudioListenerLock {
return
}
log.Info("[startAudioListener] Voice connection listener started")
conn.AudioListenerLock = true conn.AudioListenerLock = true
if conn.VoiceClipQueue == nil { if conn.VoiceClipQueue == nil {
conn.VoiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize) conn.VoiceClipQueue = make(chan *discordgo.Packet, voiceClipQueuePacketSize)
} }
// create new channel to watch for voice connection localSleep := func() {
// when voice connection is not ready the loop will exit time.Sleep(time.Second / 10)
exitChan := make(chan bool) }
// 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() { go func() {
for { for {
if !conn.VoiceConnection.Ready { voiceConnection := conn.getVoiceConnection()
exitChan <- true if voiceConnection != nil {
break voiceConnection.RLock()
ready := voiceConnection != nil && voiceConnection.Ready
voiceConnection.RUnlock()
if !ready {
vcExitChan <- true
}
} }
time.Sleep(time.Second * 1) localSleep()
} }
}() }()
loop:
for { 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 { select {
// grab incoming audio // grab incoming audio
case opusChannel, ok := <-conn.VoiceConnection.OpusRecv: case opusChannel, ok := <-voiceConnection.OpusRecv:
if !ok { if !ok {
continue continue
} }
@@ -398,13 +452,12 @@ loop:
// add current packet to channel queue // add current packet to channel queue
conn.VoiceClipQueue <- opusChannel conn.VoiceClipQueue <- opusChannel
break
// check if voice connection fails then break out of audio listener // if voice is interrupted continue loop (e.g. disconnects)
case <-exitChan: case <-vcExitChan:
break loop log.Info("[startAudioListener] exitChan is exiting")
localSleep()
} }
} }
// remove lock upon exit
conn.AudioListenerLock = false
} }

View File

@@ -9,19 +9,20 @@ import (
// User - // User -
type User struct { type User struct {
ID string `gorm:"primary_key" json:"id"` ID string `gorm:"primary_key" json:"id"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"` DeletedAt *time.Time `json:"deleted_at"`
Email string `json:"email"` Email string `json:"email"`
Username string `json:"username"` Username string `json:"username"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Discriminator string `json:"discriminator"` Discriminator string `json:"discriminator"`
Token string `gorm:"-" json:"token"` Token string `gorm:"-" json:"token"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
MFAEnabled bool `json:"mfa_enabled"` MFAEnabled bool `json:"mfa_enabled"`
Bot bool `json:"bot"` Bot bool `json:"bot"`
Permissions *int `gorm:"default:1;not null" json:"permissions"` Permissions *int `gorm:"default:1;not null" json:"permissions"`
VoiceJoinSound *string `json:"voice_join_sound"` // sound clip that plays when user joins channel
} }
// UserSave - // UserSave -
@@ -33,3 +34,10 @@ func UserSave(conn *gorm.DB, u *User) error {
// with the actual object in FirstOrCreate method // with the actual object in FirstOrCreate method
return conn.Where(&User{ID: u.ID}).Assign(userCopy).FirstOrCreate(u).Error 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
}