From 1fc0e2053b3dbb81f042a0e99296480d584d33e2 Mon Sep 17 00:00:00 2001 From: andrei Date: Sat, 5 Mar 2016 01:02:21 -0800 Subject: [PATCH 1/4] Add support for multiple voice connections With the upcoming API changes, Discord will be allowing bot users to be onnected to more than one voice channel at a time. This commit is a first stab at implementing that functionality in discordgo. Voice works pretty good right now, ideally the next step is to cleanup some of the channel-spam and weird blocking-spots. --- structs.go | 4 +-- voice.go | 70 +++++++++++++++++++++++++++-------------- wsapi.go | 91 ++++++++++++++++++++++++++++-------------------------- 3 files changed, 95 insertions(+), 70 deletions(-) diff --git a/structs.go b/structs.go index 2646541..54c8eab 100644 --- a/structs.go +++ b/structs.go @@ -54,8 +54,8 @@ type Session struct { // Whether the UDP Connection is ready UDPReady bool - // Stores all details related to voice connections - Voice *Voice + // Stores a mapping of channel id's to VoiceConnections + VoiceConnections map[string]*VoiceConnection // Managed state object, updated internally with events when // StateEnabled is true. diff --git a/voice.go b/voice.go index fb1fa3a..fa32456 100644 --- a/voice.go +++ b/voice.go @@ -24,14 +24,15 @@ import ( ) // ------------------------------------------------------------------------------------------------ -// Code related to both Voice Websocket and UDP connections. +// Code related to both VoiceConnection Websocket and UDP connections. // ------------------------------------------------------------------------------------------------ -// A Voice struct holds all data and functions related to Discord Voice support. -type Voice struct { +// A VoiceConnectionConnection struct holds all the data and functions related to a Discord Voice Connection. +type VoiceConnection struct { sync.Mutex // future use Ready bool // If true, voice is ready to send/receive audio Debug bool // If true, print extra logging + Receive bool // If false, don't try to receive packets OP2 *voiceOP2 // exported for dgvoice, may change. OpusSend chan []byte // Chan for sending opus audio OpusRecv chan *Packet // Chan for receiving opus audio @@ -40,6 +41,7 @@ type Voice struct { wsConn *websocket.Conn UDPConn *net.UDPConn // this will become unexported soon. + session *Session sessionID string token string @@ -51,10 +53,16 @@ type Voice struct { // Used to send a close signal to goroutines close chan struct{} + + // Used to allow blocking until connected + connected chan bool + + // Used to pass the sessionid from onVoiceStateUpdate + sessionRecv chan string } // ------------------------------------------------------------------------------------------------ -// Code related to the Voice websocket connection +// Code related to the VoiceConnection websocket connection // ------------------------------------------------------------------------------------------------ // A voiceOP4 stores the data for the voice operation 4 websocket event @@ -86,9 +94,9 @@ type voiceHandshakeOp struct { } // Open opens a voice connection. This should be called -// after VoiceChannelJoin is used and the data VOICE websocket events +// after VoiceConnectionChannelJoin is used and the data VOICE websocket events // are captured. -func (v *Voice) Open() (err error) { +func (v *VoiceConnection) Open() (err error) { v.Lock() defer v.Unlock() @@ -98,7 +106,7 @@ func (v *Voice) Open() (err error) { return } - // Connect to Voice Websocket + // Connect to VoiceConnection Websocket vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) if err != nil { @@ -123,9 +131,13 @@ func (v *Voice) Open() (err error) { return } +func (v *VoiceConnection) WaitUntilConnected() { + <-v.connected +} + // wsListen listens on the voice websocket for messages and passes them // to the voice event handler. This is automatically called by the Open func -func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { +func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { for { messageType, message, err := v.wsConn.ReadMessage() @@ -133,7 +145,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { // TODO: add reconnect, matching wsapi.go:listen() // TODO: Handle this problem better. // TODO: needs proper logging - fmt.Println("Voice Listen Error:", err) + fmt.Println("VoiceConnection Listen Error:", err) return } @@ -149,7 +161,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { // wsEvent handles any voice websocket events. This is only called by the // wsListen() function. -func (v *Voice) wsEvent(messageType int, message []byte) { +func (v *VoiceConnection) wsEvent(messageType int, message []byte) { if v.Debug { fmt.Println("wsEvent received: ", messageType) @@ -195,7 +207,13 @@ func (v *Voice) wsEvent(messageType int, message []byte) { if v.OpusRecv == nil { v.OpusRecv = make(chan *Packet, 2) } - go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) + + if v.Receive { + go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) + } + + // Send the ready event + v.connected <- true return case 3: // HEARTBEAT response @@ -240,7 +258,7 @@ type voiceHeartbeatOp struct { // wsHeartbeat sends regular heartbeats to voice Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. -func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { if close == nil || wsConn == nil { return @@ -278,10 +296,10 @@ type voiceSpeakingOp struct { // This must be sent as true prior to sending audio and should be set to false // once finished sending audio. // b : Send true if speaking, false if not. -func (v *Voice) Speaking(b bool) (err error) { +func (v *VoiceConnection) Speaking(b bool) (err error) { if v.wsConn == nil { - return fmt.Errorf("No Voice websocket.") + return fmt.Errorf("No VoiceConnection websocket.") } data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} @@ -295,7 +313,7 @@ func (v *Voice) Speaking(b bool) (err error) { } // ------------------------------------------------------------------------------------------------ -// Code related to the Voice UDP connection +// Code related to the VoiceConnection UDP connection // ------------------------------------------------------------------------------------------------ type voiceUDPData struct { @@ -318,7 +336,7 @@ type voiceUDPOp struct { // initial required handshake. This connection is left open in the session // and can be used to send or receive audio. This should only be called // from voice.wsEvent OP2 -func (v *Voice) udpOpen() (err error) { +func (v *VoiceConnection) udpOpen() (err error) { v.Lock() defer v.Unlock() @@ -354,7 +372,7 @@ func (v *Voice) udpOpen() (err error) { return } - // Create a 70 byte array and put the SSRC code from the Op 2 Voice event + // Create a 70 byte array and put the SSRC code from the Op 2 VoiceConnection event // into it. Then send that over the UDP connection to Discord sb := make([]byte, 70) binary.BigEndian.PutUint32(sb, v.OP2.SSRC) @@ -377,7 +395,7 @@ func (v *Voice) udpOpen() (err error) { return } if rlen < 70 { - fmt.Println("Voice RLEN should be 70 but isn't") + fmt.Println("VoiceConnection RLEN should be 70 but isn't") } // Loop over position 4 though 20 to grab the IP address @@ -412,7 +430,7 @@ func (v *Voice) udpOpen() (err error) { // udpKeepAlive sends a udp packet to keep the udp connection open // This is still a bit of a "proof of concept" -func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) { if UDPConn == nil || close == nil { return @@ -446,7 +464,7 @@ func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time // opusSender will listen on the given channel and send any // pre-encoded opus audio to Discord. Supposedly. -func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { +func (v *VoiceConnection) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { if UDPConn == nil || close == nil { return @@ -454,7 +472,7 @@ func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-c runtime.LockOSThread() - // Voice is now ready to receive audio packets + // VoiceConnection is now ready to receive audio packets // TODO: this needs reviewed as I think there must be a better way. v.Ready = true defer func() { v.Ready = false }() @@ -536,7 +554,7 @@ type Packet struct { // opusReceiver listens on the UDP socket for incoming packets // and sends them across the given channel // NOTE :: This function may change names later. -func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { +func (v *VoiceConnection) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { if UDPConn == nil || close == nil { return @@ -581,11 +599,15 @@ func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan } // Close closes the voice ws and udp connections -func (v *Voice) Close() { - +func (v *VoiceConnection) Close() { v.Lock() defer v.Unlock() + if v.Ready { + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.guildID, nil, true, true}} + v.session.wsConn.WriteJSON(data) + } + v.Ready = false if v.close != nil { diff --git a/wsapi.go b/wsapi.go index 7e4ad19..9e93b20 100644 --- a/wsapi.go +++ b/wsapi.go @@ -55,6 +55,8 @@ func (s *Session) Open() (err error) { } }() + s.VoiceConnections = make(map[string]*VoiceConnection) + if s.wsConn != nil { err = errors.New("Web socket already opened.") return @@ -248,6 +250,7 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { func (s *Session) event(messageType int, message []byte) { var err error var reader io.Reader + reader = bytes.NewBuffer(message) if messageType == 2 { @@ -333,58 +336,49 @@ type voiceChannelJoinOp struct { // cID : Channel ID of the channel to join. // mute : If true, you will be set to muted upon joining. // deaf : If true, you will be set to deafened upon joining. -func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (err error) { - - // Create new voice{} struct if one does not exist. - // If you create this prior to calling this func then you can manually - // set some variables if needed, such as to enable debugging. - if s.Voice == nil { - s.Voice = &Voice{} - } - +func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { // Send the request to Discord that we want to join the voice channel data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} err = s.wsConn.WriteJSON(data) if err != nil { - return + return nil, err } + // Create a new voice session + voice = &VoiceConnection{ + Receive: true, + session: s, + connected: make(chan bool), + sessionRecv: make(chan string), + } + + // Store this in the waiting map so it can get a session/token + s.VoiceConnections[gID] = voice + // Store gID and cID for later use - s.Voice.guildID = gID - s.Voice.channelID = cID + voice.guildID = gID + voice.channelID = cID - return -} - -// ChannelVoiceLeave disconnects from the currently connected -// voice channel. -func (s *Session) ChannelVoiceLeave() (err error) { - - if s.Voice == nil { - return - } - - // Send the request to Discord that we want to leave voice - data := voiceChannelJoinOp{4, voiceChannelJoinData{nil, nil, true, true}} - err = s.wsConn.WriteJSON(data) - if err != nil { - return - } - - // Close voice and nil data struct - s.Voice.Close() - s.Voice = nil - - return + return voice, err } // onVoiceStateUpdate handles Voice State Update events on the data // websocket. This comes immediately after the call to VoiceChannelJoin // for the session user. func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { + // If we don't have a connection for the channel, don't bother + if st.ChannelID == "" { + return + } - // Ignore if Voice is nil - if s.Voice == nil { + channel, err := s.Channel(st.ChannelID) + if err != nil { + fmt.Println(err) + return + } + + voice, exists := s.VoiceConnections[channel.GuildID] + if !exists { return } @@ -405,8 +399,8 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { } // Store the SessionID for later use. - s.Voice.userID = self.ID // TODO: Review - s.Voice.sessionID = st.SessionID + voice.userID = self.ID // TODO: Review + voice.sessionRecv <- st.SessionID } // onVoiceServerUpdate handles the Voice Server Update data websocket event. @@ -417,19 +411,28 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { // to a voice channel. In that case, need to re-establish connection to // the new region endpoint. func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { + voice, exists := s.VoiceConnections[st.GuildID] + + // If no VoiceConnection exists, just skip this + if !exists { + return + } // Store values for later use - s.Voice.token = st.Token - s.Voice.endpoint = st.Endpoint - s.Voice.guildID = st.GuildID + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.guildID = st.GuildID // If currently connected to voice ws/udp, then disconnect. // Has no effect if not connected. - s.Voice.Close() + voice.Close() + + // Wait for the sessionID from onVoiceStateUpdate + voice.sessionID = <-voice.sessionRecv // We now have enough information to open a voice websocket conenction // so, that's what the next call does. - err := s.Voice.Open() + err := voice.Open() if err != nil { fmt.Println("onVoiceServerUpdate Voice.Open error: ", err) // TODO better logging From b48e8c992ed442a5ba05e5cc3165f880979d1331 Mon Sep 17 00:00:00 2001 From: andrei Date: Sat, 5 Mar 2016 03:53:42 -0800 Subject: [PATCH 2/4] Track voice state changes, expose stuff, add channel changing --- state.go | 41 +++++++++++++++++++++++++++++++++++++++++ structs.go | 1 + voice.go | 23 ++++++++++++++++++----- wsapi.go | 21 +++++++-------------- 4 files changed, 67 insertions(+), 19 deletions(-) diff --git a/state.go b/state.go index 2bdf35f..a9dbbdf 100644 --- a/state.go +++ b/state.go @@ -453,6 +453,45 @@ func (s *State) MessageRemove(message *Message) error { return errors.New("Message not found.") } +func (s *State) VoiceStateUpdate(update *VoiceStateUpdate) error { + var exists bool + var guild *Guild + + for _, guild = range s.Guilds { + if guild.ID == update.GuildID { + exists = true + break + } + } + + if !exists { + return nil + } + + // Handle Leaving Channel + if update.ChannelID == "" { + for i, state := range guild.VoiceStates { + if state.UserID == update.UserID { + guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...) + } + } + } else { + exists := false + for _, state := range guild.VoiceStates { + if state.UserID == update.UserID { + state.ChannelID = update.ChannelID + exists = true + } + } + + if !exists { + guild.VoiceStates = append(guild.VoiceStates, update.VoiceState) + } + } + + return nil +} + // Message gets a message by channel and message ID. func (s *State) Message(channelID, messageID string) (*Message, error) { if s == nil { @@ -514,6 +553,8 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { err = s.MessageAdd(t.Message) case *MessageDelete: err = s.MessageRemove(t.Message) + case *VoiceStateUpdate: + err = s.VoiceStateUpdate(t) } return diff --git a/structs.go b/structs.go index 54c8eab..9ffa96e 100644 --- a/structs.go +++ b/structs.go @@ -203,6 +203,7 @@ type VoiceState struct { UserID string `json:"user_id"` SessionID string `json:"session_id"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` Suppress bool `json:"suppress"` SelfMute bool `json:"self_mute"` SelfDeaf bool `json:"self_deaf"` diff --git a/voice.go b/voice.go index fa32456..9d67ed5 100644 --- a/voice.go +++ b/voice.go @@ -36,6 +36,9 @@ type VoiceConnection struct { OP2 *voiceOP2 // exported for dgvoice, may change. OpusSend chan []byte // Chan for sending opus audio OpusRecv chan *Packet // Chan for receiving opus audio + GuildID string + ChannelID string + UserID string // FrameRate int // This can be used to set the FrameRate of Opus data // FrameSize int // This can be used to set the FrameSize of Opus data @@ -46,9 +49,6 @@ type VoiceConnection struct { sessionID string token string endpoint string - guildID string - channelID string - userID string op4 voiceOP4 // Used to send a close signal to goroutines @@ -114,7 +114,7 @@ func (v *VoiceConnection) Open() (err error) { return } - data := voiceHandshakeOp{0, voiceHandshakeData{v.guildID, v.userID, v.sessionID, v.token}} + data := voiceHandshakeOp{0, voiceHandshakeData{v.GuildID, v.UserID, v.sessionID, v.token}} err = v.wsConn.WriteJSON(data) if err != nil { @@ -604,7 +604,7 @@ func (v *VoiceConnection) Close() { defer v.Unlock() if v.Ready { - data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.guildID, nil, true, true}} + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} v.session.wsConn.WriteJSON(data) } @@ -631,3 +631,16 @@ func (v *VoiceConnection) Close() { v.wsConn = nil } } + +// Change channels +func (v *VoiceConnection) ChangeChannel(channelID string) (err error) { + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, true, true}} + + err = v.session.wsConn.WriteJSON(data) + + if err == nil { + v.ChannelID = channelID + } + + return err +} diff --git a/wsapi.go b/wsapi.go index 9e93b20..d177a96 100644 --- a/wsapi.go +++ b/wsapi.go @@ -356,8 +356,8 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi s.VoiceConnections[gID] = voice // Store gID and cID for later use - voice.guildID = gID - voice.channelID = cID + voice.GuildID = gID + voice.ChannelID = cID return voice, err } @@ -371,13 +371,8 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { return } - channel, err := s.Channel(st.ChannelID) - if err != nil { - fmt.Println(err) - return - } - - voice, exists := s.VoiceConnections[channel.GuildID] + // Check if we have a voice connection to update + voice, exists := s.VoiceConnections[st.GuildID] if !exists { return } @@ -391,15 +386,13 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { return } - // This event comes for all users, if it's not for the session - // user just ignore it. - // TODO Move this IF to the event() func + // We only care about events that are about us if st.UserID != self.ID { return } // Store the SessionID for later use. - voice.userID = self.ID // TODO: Review + voice.UserID = self.ID // TODO: Review voice.sessionRecv <- st.SessionID } @@ -421,7 +414,7 @@ func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { // Store values for later use voice.token = st.Token voice.endpoint = st.Endpoint - voice.guildID = st.GuildID + voice.GuildID = st.GuildID // If currently connected to voice ws/udp, then disconnect. // Has no effect if not connected. From 5dc0b9f2a1cf8e98d3d87550974a784d922a0ffb Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 8 Mar 2016 02:36:00 -0800 Subject: [PATCH 3/4] Add connect timeout, fix ChannelVoiceJoin "leaking" connections --- structs.go | 2 +- voice.go | 28 ++++++++++++++++++---------- wsapi.go | 26 +++++++++++++++++++++----- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/structs.go b/structs.go index 9ffa96e..cee6b16 100644 --- a/structs.go +++ b/structs.go @@ -54,7 +54,7 @@ type Session struct { // Whether the UDP Connection is ready UDPReady bool - // Stores a mapping of channel id's to VoiceConnections + // Stores a mapping of guild id's to VoiceConnections VoiceConnections map[string]*VoiceConnection // Managed state object, updated internally with events when diff --git a/voice.go b/voice.go index 9d67ed5..49484a4 100644 --- a/voice.go +++ b/voice.go @@ -12,6 +12,7 @@ package discordgo import ( "encoding/binary" "encoding/json" + "errors" "fmt" "net" "runtime" @@ -97,7 +98,6 @@ type voiceHandshakeOp struct { // after VoiceConnectionChannelJoin is used and the data VOICE websocket events // are captured. func (v *VoiceConnection) Open() (err error) { - v.Lock() defer v.Unlock() @@ -131,8 +131,19 @@ func (v *VoiceConnection) Open() (err error) { return } -func (v *VoiceConnection) WaitUntilConnected() { - <-v.connected +func (v *VoiceConnection) WaitUntilConnected() error { + if v.Ready { + return nil + } + + value, ok := <-v.connected + + if (!value && !v.Ready) || !ok { + delete(v.session.VoiceConnections, v.GuildID) + return errors.New("Timed out connecting to voice") + } + + return nil } // wsListen listens on the voice websocket for messages and passes them @@ -603,9 +614,11 @@ func (v *VoiceConnection) Close() { v.Lock() defer v.Unlock() - if v.Ready { + // Send a OP4 with a nil channel to disconnect + if v.sessionID != "" { data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} v.session.wsConn.WriteJSON(data) + v.sessionID = "" } v.Ready = false @@ -632,15 +645,10 @@ func (v *VoiceConnection) Close() { } } -// Change channels +// Request to change channels func (v *VoiceConnection) ChangeChannel(channelID string) (err error) { data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, true, true}} - err = v.session.wsConn.WriteJSON(data) - if err == nil { - v.ChannelID = channelID - } - return err } diff --git a/wsapi.go b/wsapi.go index d177a96..7bfbc39 100644 --- a/wsapi.go +++ b/wsapi.go @@ -332,11 +332,17 @@ type voiceChannelJoinOp struct { // this func please monitor the Session.Voice.Ready bool to determine when // it is ready and able to send/receive audio, that should happen quickly. // -// gID : Guild ID of the channel to join. -// cID : Channel ID of the channel to join. -// mute : If true, you will be set to muted upon joining. -// deaf : If true, you will be set to deafened upon joining. -func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +// timeout : If greater than zero, the timeout in milliseconds after which connecting will fail +func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool, timeout int) (voice *VoiceConnection, err error) { + // If a voice connection for the guild exists, return that + if _, exists := s.VoiceConnections[gID]; exists { + return s.VoiceConnections[gID], err + } + // Send the request to Discord that we want to join the voice channel data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} err = s.wsConn.WriteJSON(data) @@ -359,6 +365,16 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi voice.GuildID = gID voice.ChannelID = cID + // Queue the timeout in case we fail to connect + if timeout > 0 { + go func() { + time.Sleep(time.Millisecond * time.Duration(timeout)) + if !voice.Ready { + voice.connected <- false + } + }() + } + return voice, err } From cf222be2f8ec54f1108da2b20a7615a3eb8578d1 Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 8 Mar 2016 10:47:41 -0800 Subject: [PATCH 4/4] Cleanup voice connection on close --- voice.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/voice.go b/voice.go index 49484a4..c080813 100644 --- a/voice.go +++ b/voice.go @@ -643,6 +643,8 @@ func (v *VoiceConnection) Close() { } v.wsConn = nil } + + delete(v.session.VoiceConnections, v.GuildID) } // Request to change channels