Sfoglia il codice sorgente

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.
andrei 8 anni fa
parent
commit
1fc0e2053b
3 ha cambiato i file con 93 aggiunte e 68 eliminazioni
  1. 2 2
      structs.go
  2. 46 24
      voice.go
  3. 45 42
      wsapi.go

+ 2 - 2
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.

+ 46 - 24
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 {

+ 45 - 42
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
 	}
 
-	// Store gID and cID for later use
-	s.Voice.guildID = gID
-	s.Voice.channelID = cID
-
-	return
-}
-
-// ChannelVoiceLeave disconnects from the currently connected
-// voice channel.
-func (s *Session) ChannelVoiceLeave() (err error) {
-
-	if s.Voice == nil {
-		return
+	// Create a new voice session
+	voice = &VoiceConnection{
+		Receive:     true,
+		session:     s,
+		connected:   make(chan bool),
+		sessionRecv: make(chan string),
 	}
 
-	// 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
-	}
+	// Store this in the waiting map so it can get a session/token
+	s.VoiceConnections[gID] = voice
 
-	// Close voice and nil data struct
-	s.Voice.Close()
-	s.Voice = nil
+	// Store gID and cID for later use
+	voice.guildID = gID
+	voice.channelID = cID
 
-	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