|
@@ -11,6 +11,7 @@
|
|
|
package discordgo
|
|
|
|
|
|
import (
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
"runtime"
|
|
|
"time"
|
|
@@ -18,21 +19,6 @@ import (
|
|
|
"github.com/gorilla/websocket"
|
|
|
)
|
|
|
|
|
|
-// Open opens a websocket connection to Discord.
|
|
|
-func (s *Session) Open() (err error) {
|
|
|
-
|
|
|
- // Get the gateway to use for the Websocket connection
|
|
|
- g, err := s.Gateway()
|
|
|
- if err != nil {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // TODO: See if there's a use for the http response.
|
|
|
- // conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil)
|
|
|
- s.wsConn, _, err = websocket.DefaultDialer.Dial(g, nil)
|
|
|
- return
|
|
|
-}
|
|
|
-
|
|
|
type handshakeProperties struct {
|
|
|
OS string `json:"$os"`
|
|
|
Browser string `json:"$browser"`
|
|
@@ -52,12 +38,98 @@ type handshakeOp struct {
|
|
|
Data handshakeData `json:"d"`
|
|
|
}
|
|
|
|
|
|
-// Handshake sends the client data to Discord during websocket initial connection.
|
|
|
-func (s *Session) Handshake() (err error) {
|
|
|
- // maybe this is SendOrigin? not sure the right name here
|
|
|
+// Open opens a websocket connection to Discord.
|
|
|
+func (s *Session) Open() (err error) {
|
|
|
+ s.Lock()
|
|
|
+ defer func() {
|
|
|
+ if err != nil {
|
|
|
+ s.Unlock()
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ if s.wsConn != nil {
|
|
|
+ err = errors.New("Web socket already opened.")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get the gateway to use for the Websocket connection
|
|
|
+ g, err := s.Gateway()
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: See if there's a use for the http response.
|
|
|
+ // conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil)
|
|
|
+ s.wsConn, _, err = websocket.DefaultDialer.Dial(g, nil)
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{3, s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}}})
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Create listening outside of listen, as it needs to happen inside the mutex
|
|
|
+ // lock.
|
|
|
+ s.listening = make(chan interface{})
|
|
|
+ go s.listen(s.wsConn, s.listening)
|
|
|
+
|
|
|
+ s.Unlock()
|
|
|
+
|
|
|
+ if s.OnConnect != nil {
|
|
|
+ s.OnConnect(s)
|
|
|
+ }
|
|
|
+
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// Close closes a websocket and stops all listening/heartbeat goroutines.
|
|
|
+// TODO: Add support for Voice WS/UDP connections
|
|
|
+func (s *Session) Close() (err error) {
|
|
|
+ s.Lock()
|
|
|
+
|
|
|
+ s.DataReady = false
|
|
|
+
|
|
|
+ if s.listening != nil {
|
|
|
+ close(s.listening)
|
|
|
+ s.listening = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if s.wsConn != nil {
|
|
|
+ err = s.wsConn.Close()
|
|
|
+ s.wsConn = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ s.Unlock()
|
|
|
+
|
|
|
+ if s.OnDisconnect != nil {
|
|
|
+ s.OnDisconnect(s)
|
|
|
+ }
|
|
|
+
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+// listen polls the websocket connection for events, it will stop when
|
|
|
+// the listening channel is closed, or an error occurs.
|
|
|
+func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
|
|
|
+ for {
|
|
|
+ messageType, message, err := wsConn.ReadMessage()
|
|
|
+ if err != nil {
|
|
|
+ // There has been an error reading, Close() the websocket so that
|
|
|
+ // OnDisconnect is fired.
|
|
|
+ s.Close()
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ select {
|
|
|
+ case <-listening:
|
|
|
+ return
|
|
|
+ default:
|
|
|
+ go s.event(messageType, message)
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- data := handshakeOp{2, handshakeData{3, s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}}}
|
|
|
- err = s.wsConn.WriteJSON(data)
|
|
|
return
|
|
|
}
|
|
|
|
|
@@ -79,6 +151,11 @@ type updateStatusOp struct {
|
|
|
// If idle>0 then set status to idle. If game>0 then set game.
|
|
|
// if otherwise, set status to active, and no game.
|
|
|
func (s *Session) UpdateStatus(idle int, game string) (err error) {
|
|
|
+ s.RLock()
|
|
|
+ defer s.RUnlock()
|
|
|
+ if s.wsConn == nil {
|
|
|
+ return errors.New("No websocket connection exists.")
|
|
|
+ }
|
|
|
|
|
|
var usd updateStatusData
|
|
|
if idle > 0 {
|
|
@@ -88,75 +165,7 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) {
|
|
|
usd.Game = &updateStatusGame{game}
|
|
|
}
|
|
|
|
|
|
- data := updateStatusOp{3, usd}
|
|
|
- err = s.wsConn.WriteJSON(data)
|
|
|
-
|
|
|
- return
|
|
|
-}
|
|
|
-
|
|
|
-// Listen starts listening to the websocket connection for events.
|
|
|
-func (s *Session) Listen() (err error) {
|
|
|
- // TODO: need a channel or something to communicate
|
|
|
- // to this so I can tell it to stop listening
|
|
|
-
|
|
|
- if s.wsConn == nil {
|
|
|
- fmt.Println("No websocket connection exists.")
|
|
|
- return // TODO need to return an error.
|
|
|
- }
|
|
|
-
|
|
|
- // Make sure Listen is not already running
|
|
|
- s.listenLock.Lock()
|
|
|
- if s.listenChan != nil {
|
|
|
- s.listenLock.Unlock()
|
|
|
- return
|
|
|
- }
|
|
|
- s.listenChan = make(chan struct{})
|
|
|
- s.listenLock.Unlock()
|
|
|
-
|
|
|
- // this is ugly.
|
|
|
- defer func() {
|
|
|
- if s.listenChan == nil {
|
|
|
- return
|
|
|
- }
|
|
|
- select {
|
|
|
- case <-s.listenChan:
|
|
|
- break
|
|
|
- default:
|
|
|
- close(s.listenChan)
|
|
|
- }
|
|
|
- s.listenChan = nil
|
|
|
- }()
|
|
|
-
|
|
|
- // this is ugly.
|
|
|
- defer func() {
|
|
|
- if s.heartbeatChan == nil {
|
|
|
- return
|
|
|
- }
|
|
|
- select {
|
|
|
- case <-s.heartbeatChan:
|
|
|
- break
|
|
|
- default:
|
|
|
- close(s.heartbeatChan)
|
|
|
- }
|
|
|
- s.listenChan = nil
|
|
|
- }()
|
|
|
-
|
|
|
- for {
|
|
|
- messageType, message, err := s.wsConn.ReadMessage()
|
|
|
- if err != nil {
|
|
|
- fmt.Println("Websocket Listen Error", err)
|
|
|
- // TODO Log error
|
|
|
- break
|
|
|
- }
|
|
|
- go s.event(messageType, message)
|
|
|
-
|
|
|
- // If our chan gets closed, exit out of this loop.
|
|
|
- // TODO: Can we make this smarter, using select
|
|
|
- // and some other trickery? http://www.goinggo.net/2013/10/my-channel-select-bug.html
|
|
|
- if s.listenChan == nil {
|
|
|
- return nil
|
|
|
- }
|
|
|
- }
|
|
|
+ err = s.wsConn.WriteJSON(updateStatusOp{3, usd})
|
|
|
|
|
|
return
|
|
|
}
|
|
@@ -192,17 +201,16 @@ func (s *Session) event(messageType int, message []byte) (err error) {
|
|
|
}
|
|
|
|
|
|
switch e.Type {
|
|
|
-
|
|
|
case "READY":
|
|
|
var st *Ready
|
|
|
if err = unmarshalEvent(e, &st); err == nil {
|
|
|
+ go s.heartbeat(s.wsConn, s.listening, st.HeartbeatInterval)
|
|
|
if s.StateEnabled {
|
|
|
s.State.OnReady(st)
|
|
|
}
|
|
|
if s.OnReady != nil {
|
|
|
s.OnReady(s, st)
|
|
|
}
|
|
|
- go s.Heartbeat(st.HeartbeatInterval)
|
|
|
}
|
|
|
if s.OnReady != nil {
|
|
|
return
|
|
@@ -541,58 +549,45 @@ func (s *Session) event(messageType int, message []byte) (err error) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
-// Heartbeat sends regular heartbeats to Discord so it knows the client
|
|
|
+type heartbeatOp struct {
|
|
|
+ Op int `json:"op"`
|
|
|
+ Data int `json:"d"`
|
|
|
+}
|
|
|
+
|
|
|
+func (s *Session) sendHeartbeat(wsConn *websocket.Conn) error {
|
|
|
+ return wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())})
|
|
|
+}
|
|
|
+
|
|
|
+// heartbeat sends regular heartbeats to 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) Heartbeat(i time.Duration) {
|
|
|
-
|
|
|
- if s.wsConn == nil {
|
|
|
- fmt.Println("No websocket connection exists.")
|
|
|
- return // TODO need to return/log an error.
|
|
|
+func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) {
|
|
|
+ if listening == nil || wsConn == nil {
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- // Make sure Heartbeat is not already running
|
|
|
- s.heartbeatLock.Lock()
|
|
|
- if s.heartbeatChan != nil {
|
|
|
- s.heartbeatLock.Unlock()
|
|
|
+ s.Lock()
|
|
|
+ s.DataReady = true
|
|
|
+ s.Unlock()
|
|
|
+
|
|
|
+ // Send first heartbeat immediately because lag could put the
|
|
|
+ // first heartbeat outside the required heartbeat interval window.
|
|
|
+ err := s.sendHeartbeat(wsConn)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Println("Error sending initial heartbeat:", err)
|
|
|
return
|
|
|
}
|
|
|
- s.heartbeatChan = make(chan struct{})
|
|
|
- s.heartbeatLock.Unlock()
|
|
|
-
|
|
|
- // this is ugly.
|
|
|
- defer func() {
|
|
|
- if s.heartbeatChan == nil {
|
|
|
- return
|
|
|
- }
|
|
|
- select {
|
|
|
- case <-s.heartbeatChan:
|
|
|
- break
|
|
|
- default:
|
|
|
- close(s.heartbeatChan)
|
|
|
- }
|
|
|
- s.listenChan = nil
|
|
|
- }()
|
|
|
|
|
|
- // send first heartbeat immediately because lag could put the
|
|
|
- // first heartbeat outside the required heartbeat interval window
|
|
|
ticker := time.NewTicker(i * time.Millisecond)
|
|
|
for {
|
|
|
-
|
|
|
- err := s.wsConn.WriteJSON(map[string]int{
|
|
|
- "op": 1,
|
|
|
- "d": int(time.Now().Unix()),
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- fmt.Println("error sending data heartbeat:", err)
|
|
|
- s.DataReady = false
|
|
|
- return // TODO log error?
|
|
|
- }
|
|
|
- s.DataReady = true
|
|
|
-
|
|
|
select {
|
|
|
case <-ticker.C:
|
|
|
- case <-s.heartbeatChan:
|
|
|
+ err := s.sendHeartbeat(wsConn)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Println("Error sending heartbeat:", err)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ case <-listening:
|
|
|
return
|
|
|
}
|
|
|
}
|