Selaa lähdekoodia

Merge pull request #141 from b1naryth1ef/feature/multiple-voice-connection

Add the ability to have multiple cross-guild voice connections open.
Bruce 8 vuotta sitten
vanhempi
commit
0dac7777c1
4 muutettua tiedostoa jossa 177 lisäystä ja 78 poistoa
  1. 41 0
      state.go
  2. 3 2
      structs.go
  3. 74 29
      voice.go
  4. 59 47
      wsapi.go

+ 41 - 0
state.go

@@ -460,6 +460,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 {
@@ -521,6 +560,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

+ 3 - 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 guild id's to VoiceConnections
+	VoiceConnections map[string]*VoiceConnection
 
 	// Managed state object, updated internally with events when
 	// StateEnabled is true.
@@ -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"`

+ 74 - 29
voice.go

@@ -12,6 +12,7 @@ package discordgo
 import (
 	"encoding/binary"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"net"
 	"runtime"
@@ -24,37 +25,45 @@ 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
+	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
 
 	wsConn  *websocket.Conn
 	UDPConn *net.UDPConn // this will become unexported soon.
+	session *Session
 
 	sessionID string
 	token     string
 	endpoint  string
-	guildID   string
-	channelID string
-	userID    string
 	op4       voiceOP4
 
 	// 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,10 +95,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 {
@@ -106,7 +114,7 @@ func (v *Voice) 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 {
@@ -123,9 +131,24 @@ func (v *Voice) Open() (err error) {
 	return
 }
 
+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
 // 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 +156,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 +172,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 +218,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 +269,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 +307,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 +324,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 +347,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 +383,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 +406,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 +441,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 +475,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 +483,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 +565,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 +610,17 @@ 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()
 
+	// 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
 
 	if v.close != nil {
@@ -608,4 +643,14 @@ func (v *Voice) Close() {
 		}
 		v.wsConn = nil
 	}
+
+	delete(v.session.VoiceConnections, v.GuildID)
+}
+
+// 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)
+
+	return err
 }

+ 59 - 47
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 {
@@ -329,62 +332,64 @@ 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) (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{}
+//    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)
 	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
+
+	// 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
+	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 {
+	// Check if we have a voice connection to update
+	voice, exists := s.VoiceConnections[st.GuildID]
+	if !exists {
 		return
 	}
 
@@ -397,16 +402,14 @@ 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.
-	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 +420,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