Преглед на файлове

Experimental voice code added :)

Bruce Marriner преди 9 години
родител
ревизия
d9aeb4926d
променени са 3 файла, в които са добавени 345 реда и са изтрити 20 реда
  1. 21 9
      session.go
  2. 230 0
      voice.go
  3. 94 11
      wsapi.go

+ 21 - 9
session.go

@@ -12,15 +12,20 @@
 
 package discordgo
 
-import "github.com/gorilla/websocket"
+import (
+	"net"
+
+	"github.com/gorilla/websocket"
+)
 
 // A Session represents a connection to the Discord REST API.
 // token : The authentication token returned from Discord
 // Debug : If set to ture debug logging will be displayed.
 type Session struct {
-	Token string // Authentication token for this session
-	Debug bool   // Debug for printing JSON request/responses
-	Cache int    // number in X to cache some responses
+	Token     string // Authentication token for this session
+	Debug     bool   // Debug for printing JSON request/responses
+	Cache     int    // number in X to cache some responses
+	SessionID string // from websocket READY packet
 
 	// Settable Callback functions for Websocket Events
 	OnEvent                   func(*Session, Event) // should Event be *Event?
@@ -47,15 +52,22 @@ type Session struct {
 	OnGuildRoleDelete         func(*Session, GuildRoleDelete)
 	OnGuildIntegrationsUpdate func(*Session, GuildIntegrationsUpdate)
 
-	// OnMessageCreate func(Session, Event, Message)
-	// ^^ Any value to passing session, event, message?
-	// probably just the Message is all one would need.
-	// but having the sessin could be handy?
-
 	wsConn *websocket.Conn
 	//TODO, add bools for like.
 	// are we connnected to websocket?
 	// have we authenticated to login?
 	// lets put all the general session
 	// tracking and infos here.. clearly
+
+	// Everything below here is used for Voice testing.
+	// This stuff is almost guarenteed to change a lot
+	// and is even a bit hackish right now.
+	VwsConn    *websocket.Conn // new for voice
+	VSessionID string
+	VToken     string
+	VEndpoint  string
+	VGuildID   string
+	VChannelID string
+	Vop2       VoiceOP2
+	UDPConn    *net.UDPConn
 }

+ 230 - 0
voice.go

@@ -0,0 +1,230 @@
+// EVERYTHING in this file is very experimental and will change.
+// these structs and functions setup a voice websocket and
+// create the voice UDP connection.
+
+package discordgo
+
+import (
+	"encoding/binary"
+	"encoding/json"
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/gorilla/websocket"
+)
+
+// A VEvent is the inital structure for voice websocket events.  I think
+// I can reuse the data websocket structure here.
+type VEvent struct {
+	Type      string          `json:"t"`
+	State     int             `json:"s"`
+	Operation int             `json:"op"`
+	RawData   json.RawMessage `json:"d"`
+}
+
+// A VoiceOP2 stores the data for voice operation 2 websocket events
+// which is sort of like the voice READY packet
+type VoiceOP2 struct {
+	SSRC              uint32        `json:"ssrc"`
+	Port              int           `json:"port"`
+	Modes             []string      `json:"modes"`
+	HeartbeatInterval time.Duration `json:"heartbeat_interval"`
+}
+
+// VoiceOpenWS opens a voice websocket connection.  This should be called
+// after VoiceChannelJoin is used and the data VOICE websocket events
+// are captured.
+func (s *Session) VoiceOpenWS() {
+
+	var self User
+	var err error
+
+	self, err = s.User("@me") // AGAIN, Move to @ login and store in session
+
+	// Connect to Voice Websocket
+	vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(s.VEndpoint, ":80"))
+	s.VwsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
+	if err != nil {
+		fmt.Println("VOICE cannot open websocket:", err)
+	}
+
+	// Send initial handshake data to voice websocket.  This is required.
+	json := map[string]interface{}{
+		"op": 0,
+		"d": map[string]interface{}{
+			"server_id":  s.VGuildID,
+			"user_id":    self.ID,
+			"session_id": s.VSessionID,
+			"token":      s.VToken,
+		},
+	}
+
+	err = s.VwsConn.WriteJSON(json)
+	if err != nil {
+		fmt.Println("VOICE ERROR sending init packet:", err)
+	}
+
+	// Start a listening for voice websocket events
+	go s.VListen()
+}
+
+// Close closes the connection to the voice websocket.
+func (s *Session) VoiceCloseWS() {
+	s.VwsConn.Close()
+}
+
+// VListen listens on the voice websocket for messages and passes them
+// to the voice event handler.
+func (s *Session) VListen() (err error) {
+
+	for {
+		messageType, message, err := s.VwsConn.ReadMessage()
+		if err != nil {
+			fmt.Println("Voice Listen Error:", err)
+			break
+		}
+
+		// Pass received message to voice event handler
+		go s.VoiceEvent(messageType, message)
+	}
+
+	return
+}
+
+// VoiceEvent handles any messages received on the voice websocket
+func (s *Session) VoiceEvent(messageType int, message []byte) (err error) {
+
+	if s.Debug {
+		fmt.Println("VOICE EVENT:", messageType)
+		printJSON(message)
+	}
+
+	var e VEvent
+	if err := json.Unmarshal(message, &e); err != nil {
+		return err
+	}
+
+	switch e.Operation {
+
+	case 2: // READY packet
+		var st VoiceOP2
+		if err := json.Unmarshal(e.RawData, &st); err != nil {
+			fmt.Println(e.Type, err)
+			printJSON(e.RawData) // TODO: Better error logginEventg
+			return err
+		}
+
+		// Start the voice websocket heartbeat to keep the connection alive
+		go s.VoiceHeartbeat(st.HeartbeatInterval)
+
+		// Store all event data into the session
+		s.Vop2 = st
+
+		// We now have enough data to start the UDP connection
+		s.VoiceOpenUDP()
+
+		return
+	case 3: // HEARTBEAT response
+		// add code to use this to track latency?
+		return
+	default:
+		fmt.Println("UNKNOWN VOICE OP: ", e.Operation)
+		printJSON(e.RawData)
+	}
+
+	return
+}
+
+// VoiceOpenUDP opens a UDP connect to the voice server and completes the
+// initial required handshake.  This connect is left open in the session
+// and can be used to send or receive audio.
+func (s *Session) VoiceOpenUDP() {
+
+	// TODO: add code to convert hostname into an IP address to avoid problems
+	// with frequent DNS lookups.
+
+	udpHost := fmt.Sprintf("%s:%d", strings.TrimSuffix(s.VEndpoint, ":80"), s.Vop2.Port)
+	serverAddr, err := net.ResolveUDPAddr("udp", udpHost)
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	s.UDPConn, err = net.DialUDP("udp", nil, serverAddr)
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	// Create a 70 byte array and put the SSRC code from the Op 2 Voice event
+	// into it.  Then send that over the UDP connection to Discord
+	sb := make([]byte, 70)
+	binary.BigEndian.PutUint32(sb, s.Vop2.SSRC)
+	s.UDPConn.Write(sb)
+
+	// Create a 70 byte array and listen for the initial handshake response
+	// from Discord.  Once we get it parse the IP and PORT information out
+	// of the response.  This should be our public IP and PORT as Discord
+	// saw us.
+	rb := make([]byte, 70)
+	rlen, _, err := s.UDPConn.ReadFromUDP(rb)
+	if rlen < 70 {
+		fmt.Println("Voice RLEN should be 70 but isn't")
+	}
+
+	ip := string(rb[4:16]) // must be a better way.  TODO: NEEDS TESTING
+	port := make([]byte, 2)
+	port[0] = rb[68]
+	port[1] = rb[69]
+	p := binary.LittleEndian.Uint16(port)
+
+	// Take the parsed data from above and send it back to Discord
+	// to finalize the UDP handshake.
+	json := fmt.Sprintf(`{"op":1,"d":{"protocol":"udp","data":{"address":"%s","port":"%d","mode":"plain"}}}`, ip, p)
+	jsonb := []byte(json)
+
+	err = s.VwsConn.WriteMessage(websocket.TextMessage, jsonb)
+	if err != nil {
+		fmt.Println("error:", err)
+		return
+	}
+
+	// continue to listen for future packets
+	go s.VoiceListenUDP()
+}
+
+// VoiceCloseUDP closes the voice UDP connection.
+func (s *Session) VoiceCloseUDP() {
+	s.UDPConn.Close()
+}
+
+// VoiceListenUDP is test code to listen for UDP packets
+func (s *Session) VoiceListenUDP() {
+
+	for {
+		fmt.Println("READ FROM UDP LOOP:")
+		b := make([]byte, 1024)
+		s.UDPConn.ReadFromUDP(b)
+		fmt.Println("READ FROM UDP: ", b)
+	}
+
+}
+
+// VoiceHeartbeat 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 (s *Session) VoiceHeartbeat(i time.Duration) {
+
+	ticker := time.NewTicker(i * time.Millisecond)
+	for range ticker.C {
+		timestamp := int(time.Now().Unix())
+		err := s.VwsConn.WriteJSON(map[string]int{
+			"op": 3,
+			"d":  timestamp,
+		})
+		if err != nil {
+			fmt.Println(err)
+			return // log error?
+		}
+	}
+}

+ 94 - 11
wsapi.go

@@ -154,10 +154,10 @@ func (s *Session) Listen() (err error) {
 		return // need to return an error.
 	}
 
-	for { // s.wsConn != nil { // need a cleaner way to exit?  this doesn't acheive anything.
+	for {
 		messageType, message, err := s.wsConn.ReadMessage()
 		if err != nil {
-			fmt.Println(err)
+			fmt.Println("Websocket Listen Error", err)
 			break
 		}
 		go s.event(messageType, message)
@@ -203,17 +203,25 @@ func (s *Session) event(messageType int, message []byte) (err error) {
 			s.OnReady(s, st)
 			return
 		}
+	case "VOICE_SERVER_UPDATE":
+		// TEMP CODE FOR TESTING VOICE
+		var st VoiceServerUpdate
+		if err := json.Unmarshal(e.RawData, &st); err != nil {
+			fmt.Println(e.Type, err)
+			printJSON(e.RawData) // TODO: Better error logging
+			return err
+		}
+		s.onVoiceServerUpdate(st)
+		return
 	case "VOICE_STATE_UPDATE":
-		if s.OnVoiceStateUpdate != nil {
-			var st VoiceState
-			if err := json.Unmarshal(e.RawData, &st); err != nil {
-				fmt.Println(e.Type, err)
-				printJSON(e.RawData) // TODO: Better error logging
-				return err
-			}
-			s.OnVoiceStateUpdate(s, st)
-			return
+		// TEMP CODE FOR TESTING VOICE
+		var st VoiceState
+		if err := json.Unmarshal(e.RawData, &st); err != nil {
+			fmt.Println(e.Type, err)
+			printJSON(e.RawData) // TODO: Better error logging
+			return err
 		}
+		s.onVoiceStateUpdate(st)
 	case "PRESENCE_UPDATE":
 		if s.OnPresenceUpdate != nil {
 			var st PresenceUpdate
@@ -468,3 +476,78 @@ func (s *Session) Heartbeat(i time.Duration) {
 		}
 	}
 }
+
+// Everything below is experimental Voice support code
+// all of it will get changed and moved around.
+
+// A VoiceServerUpdate stores the data received during the Voice Server Update
+// data websocket event. This data is used during the initial Voice Channel
+// join handshaking.
+type VoiceServerUpdate struct {
+	Token    string `json:"token"`
+	GuildID  string `json:"guild_id"`
+	Endpoint string `json:"endpoint"`
+}
+
+// VoiceChannelJoin joins the authenticated session user to
+// a voice channel.  All the voice magic starts with this.
+func (s *Session) VoiceChannelJoin(guildID, channelID string) {
+
+	if s.wsConn == nil {
+		fmt.Println("error: no websocket connection exists.")
+		return
+	}
+
+	// Odd, but.. it works.  map interface caused odd unknown opcode error
+	// Later I'll test with a struct
+	json := []byte(fmt.Sprintf(`{"op":4,"d":{"guild_id":"%s","channel_id":"%s","self_mute":false,"self_deaf":false}}`,
+		guildID, channelID))
+
+	err := s.wsConn.WriteMessage(websocket.TextMessage, json)
+	if err != nil {
+		fmt.Println("error:", err)
+		return
+	}
+
+	// Probably will be removed later.
+	s.VGuildID = guildID
+	s.VChannelID = channelID
+}
+
+// onVoiceStateUpdate handles Voice State Update events on the data
+// websocket.  This comes immediately after the call to VoiceChannelJoin
+// for the authenticated session user.  This block is experimental
+// code and will be chaned in the future.
+func (s *Session) onVoiceStateUpdate(st VoiceState) {
+
+	// Need to have this happen at login and store it in the Session
+	self, err := s.User("@me")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+
+	// This event comes for all users, if it's not for the session
+	// user just ignore it.
+	if st.UserID != self.ID {
+		return
+	}
+
+	// Store the SessionID. Used later.
+	s.VSessionID = st.SessionID
+}
+
+// onVoiceServerUpdate handles the Voice Server Update data websocket event.
+// This will later be exposed but is only for experimental use now.
+func (s *Session) onVoiceServerUpdate(st VoiceServerUpdate) {
+
+	// Store all the values.  They are used later.
+	// GuildID is probably not needed and may be dropped.
+	s.VToken = st.Token
+	s.VEndpoint = st.Endpoint
+	s.VGuildID = st.GuildID
+
+	// We now have enough information to open a voice websocket conenction
+	// so, that's what the next call does.
+	s.VoiceOpenWS()
+}