1
0
mirror of https://github.com/mgerb/go-discord-bot synced 2026-01-09 08:32: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"
[[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"

View File

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

View File

@@ -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" {

View File

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

View File

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