diff --git a/discord.go b/discord.go index 99a343f..72c848f 100644 --- a/discord.go +++ b/discord.go @@ -237,5 +237,9 @@ func (s *Session) initialize() { // onReady handles the ready event. func (s *Session) onReady(se *Session, r *Ready) { + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID + + // Start the heartbeat to keep the connection alive. go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) } diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md new file mode 100644 index 0000000..efeb3ea --- /dev/null +++ b/docs/GettingStarted.md @@ -0,0 +1,142 @@ +# Getting Started + +This page is dedicated to helping you get started on your way to making the +next great Discord bot or client with DiscordGo. Once you've done that please +don't forget to submit it to the +[Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) list :). + + +**First, lets cover a few topics so you can make the best choices on how to +move forward from here.** + + +### Master vs Develop +**When installing DiscordGo you will need to decide if you want to use the current +master branch or the bleeding edge development branch.** + +* The **master** branch represents the latest released version of DiscordGo. This +branch will always have a stable and tested version of the library. Each +release is tagged and you can easily download a specific release and view the +release notes on the github [releases](https://github.com/bwmarrin/discordgo/releases) +page. + +* The **develop** branch is where all development happens and almost always has +new features over the master branch. However breaking changes are frequently +added the develop branch and sometimes bugs are introduced. Bugs get fixed +and the breaking changes get documented before pushing to master. + +*So, what should you use?* + +Due to the how frequently the Discord API is changing there is a high chance +that the *master* branch may be lacking important features. Because of that, if +you can accept the constant changing nature of the *develop* branch and the +chance that it may occasionally contain bugs then it is the recommended branch +to use. Otherwise, if you want to tail behind development slightly and have a +more stable package with documented releases then please use the *master* +branch instead. + + +### Client vs Bot + +You probably already know the answer to this but now is a good time to decide +if your goal is to write a client application or a bot. DiscordGo aims to fully +support both client applications and bots but there are some differences +between the two that you should understand. + +#### Client Application +A client application is a program that is intended to be used by a normal user +as a replacement for the official clients that Discord provides. An example of +this would be a terminal client used to read and send messages with your normal +user account or possibly a new desktop client that provides a different set of +features than the official desktop client that Discord already provides. + +Client applications work with normal user accounts and you can login with an +email address and password or a special authentication token. However, normal +user accounts are not allowed to perform any type of automation and doing so can +cause the account to be banned from Discord. Also normal user accounts do not +support multi-server voice connections and some other features that are +exclusive to Bot accounts only. + +To create a new user account (if you have not done so already) visit the +[Discord](https://discordapp.com/) website and click on the +**Try Discord Now, It's Free** button then follow the steps to setup your +new account. + + +#### Bot Application +A bot application is a special program that interacts with the Discord servers +to perform some form of automation or provide some type of service. Examples +are things like number trivia games, music streaming, channel moderation, +sending reminders, playing loud airhorn sounds, comic generators, YouTube +integration, Twitch integration.. You're *almost* only limited by your imagination. + +Bot applications require the use of a special Bot account. These accounts are +tied to your personal user account. Bot accounts cannot login with the normal +user clients and they cannot join servers the same way a user does. They do not +have access to some user client specific features however they gain access to +many Bot specific features. + +To create a new bot account first create yourself a normal user account on +Discord then visit the [My Applications](https://discordapp.com/developers/applications/me) +page and click on the **New Application** box. Follow the prompts from there +to finish creating your account. + + +**More information about Bots vs Client accounts can be found [here](https://discordapp.com/developers/docs/topics/oauth2#bot-vs-user-accounts)** + +# Requirements + +DiscordGo requires Go version 1.4 or higher. It has been tested to compile and +run successfully on Debian Linux 8, FreeBSD 10, and Windows 7. It is expected +that it should work anywhere Go 1.4 or higher works. If you run into problems +please let us know :) + +You must already have a working Go environment setup to use DiscordGo. If you +are new to Go and have not yet installed and tested it on your computer then +please visit [this page](https://golang.org/doc/install) first then I highly +recommend you walk though [A Tour of Go](https://tour.golang.org/welcome/1) to +help get your familiar with the Go language. Also checkout the relevent Go plugin +for your editor - they are hugely helpful when developing Go code. + +* Vim - [vim-go](https://github.com/fatih/vim-go) +* Sublime - [GoSublime](https://github.com/DisposaBoy/GoSublime) +* Atom - [go-plus](https://atom.io/packages/go-plus) +* Visual Studio - [vscode-go](https://github.com/Microsoft/vscode-go) + + +# Install DiscordGo + +Like any other Go package the fist step is to `go get` the package. This will +always pull the latest released version from the master branch. Then run +`go install` to compile and install the libraries on your system. + +#### Linux/BSD + +Run go get to download the package to your GOPATH/src folder. + +```sh +go get github.com/bwmarrin/discordgo +``` + +If you want to use the develop branch, follow these steps next. + +```sh +cd $GOPATH/src/github.com/bwmarrin/discordgo +git checkout develop +``` + +Finally, compile and install the package into the GOPATH/pkg folder. This isn't +absolutely required but doing this will allow the Go plugin for your editor to +provide autocomplete for all DiscordGo functions. + +```sh +cd $GOPATH/src/github.com/bwmarrin/discordgo +go install +``` + +#### Windows +Placeholder. + + +# Next... +More coming soon. diff --git a/docs/img/discordgo.png b/docs/img/discordgo.png new file mode 100644 index 0000000..eb15a3d Binary files /dev/null and b/docs/img/discordgo.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..9b433da --- /dev/null +++ b/docs/index.md @@ -0,0 +1,33 @@ +## DiscordGo +
+ + +[Go](https://golang.org/) (golang) interface for the [Discord](https://discordapp.com/) +chat service. Provides both low-level direct bindings to the +Discord API and helper functions that allow you to make custom clients and chat +bot applications easily. + +[Discord](https://discordapp.com/) is an all-in-one voice and text chat for +gamers that's free, secure, and works on both your desktop and phone. + +### Why DiscordGo? +* High Performance +* Minimal Memory & CPU Load +* Low-level bindings to Discord REST API Endpoints +* Support for the data websocket interface +* Multi-Server voice connections (send and receive) +* State tracking and caching + +### Learn More +* Check out the [Getting Started](GettingStarted) section +* Read the reference docs on [Godoc](https://godoc.org/github.com/bwmarrin/discordgo) or [GoWalker](https://gowalker.org/github.com/bwmarrin/discordgo) +* Try the [examples](https://github.com/bwmarrin/discordgo/tree/master/examples) +* Explore [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) + +### Join Us! +Both of the below links take you to chat channels where you can get more +information and support for DiscordGo. There's also a chance to make some +friends :) + +* Join the [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) chat server dedicated to Go programming. +* Join the [Discord API](https://discord.gg/0SBTUU1wZTWT6sqd) chat server dedicated to the Discord API. diff --git a/events.go b/events.go index ce1f815..23fa9be 100644 --- a/events.go +++ b/events.go @@ -50,6 +50,12 @@ type Connect struct{} // Disconnect is an empty struct for an event. type Disconnect struct{} +// RateLimit is a struct for the RateLimited event +type RateLimit struct { + *TooManyRequests + URL string +} + // MessageCreate is a wrapper struct for an event. type MessageCreate struct { *Message diff --git a/logging.go b/logging.go new file mode 100644 index 0000000..7c487ed --- /dev/null +++ b/logging.go @@ -0,0 +1,105 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to discordgo package logging + +package discordgo + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "runtime" + "strings" +) + +const ( + + // Critical Errors that could lead to data loss or panic + // Only errors that would not be returned to a calling function + LogError int = iota + + // Very abnormal events. + // Errors that are also returend to a calling function. + LogWarning + + // Normal non-error activity + // Generally, not overly spammy events + LogInformational + + // Very detailed non-error activity + // All HTTP/Websocket packets. + // Very spammy and will impact performance + LogDebug +) + +// msglog provides package wide logging consistancy for discordgo +// the format, a... portion this command follows that of fmt.Printf +// msgL : LogLevel of the message +// caller : 1 + the number of callers away from the message source +// format : Printf style message format +// a ... : comma seperated list of values to pass +func msglog(msgL, caller int, format string, a ...interface{}) { + + pc, file, line, _ := runtime.Caller(caller) + + files := strings.Split(file, "/") + file = files[len(files)-1] + + name := runtime.FuncForPC(pc).Name() + fns := strings.Split(name, ".") + name = fns[len(fns)-1] + + msg := fmt.Sprintf(format, a...) + + log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) +} + +// helper function that wraps msglog for the Session struct +// This adds a check to insure the message is only logged +// if the session log level is equal or higher than the +// message log level +func (s *Session) log(msgL int, format string, a ...interface{}) { + + if s.Debug { // Deprecated + s.LogLevel = LogDebug + } + + if msgL > s.LogLevel { + return + } + + msglog(msgL, 2, format, a...) +} + +// helper function that wraps msglog for the VoiceConnection struct +// This adds a check to insure the message is only logged +// if the voice connection log level is equal or higher than the +// message log level +func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { + + if v.Debug { // Deprecated + v.LogLevel = LogDebug + } + + if msgL > v.LogLevel { + return + } + + msglog(msgL, 2, format, a...) +} + +// printJSON is a helper function to display JSON data in a easy to read format. +func printJSON(body []byte) { + var prettyJSON bytes.Buffer + error := json.Indent(&prettyJSON, body, "", "\t") + if error != nil { + log.Print("JSON parse error: ", error) + } + log.Println(string(prettyJSON.Bytes())) +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..3ee8eb3 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,17 @@ +site_name: DiscordGo +site_author: Bruce Marriner +site_url: http://bwmarrin.github.io/discordgo/ +repo_url: https://github.com/bwmarrin/discordgo + +dev_addr: 0.0.0.0:8000 +theme: yeti + +markdown_extensions: + - smarty + - toc: + permalink: True + - sane_lists + +pages: + - 'Home': 'index.md' + - 'Getting Started': 'GettingStarted.md' diff --git a/restapi.go b/restapi.go index f740b42..60d10a2 100644 --- a/restapi.go +++ b/restapi.go @@ -25,6 +25,8 @@ import ( "net/http" "net/url" "strconv" + "strings" + "sync" "time" ) @@ -49,6 +51,26 @@ func (s *Session) Request(method, urlStr string, data interface{}) (response []b // request makes a (GET/POST/...) Requests to Discord REST API. func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) { + // rate limit mutex for this url + // TODO: review for performance improvements + // ideally we just ignore endpoints that we've never + // received a 429 on. But this simple method works and + // is a lot less complex :) It also might even be more + // performat due to less checks and maps. + var mu *sync.Mutex + s.rateLimit.Lock() + if s.rateLimit.url == nil { + s.rateLimit.url = make(map[string]*sync.Mutex) + } + + bu := strings.Split(urlStr, "?") + mu, _ = s.rateLimit.url[bu[0]] + if mu == nil { + mu = new(sync.Mutex) + s.rateLimit.url[urlStr] = mu + } + s.rateLimit.Unlock() + if s.Debug { log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) @@ -77,7 +99,9 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons client := &http.Client{Timeout: (20 * time.Second)} + mu.Lock() resp, err := client.Do(req) + mu.Unlock() if err != nil { return } @@ -111,13 +135,22 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons // TODO check for 401 response, invalidate token if we get one. case 429: // TOO MANY REQUESTS - Rate limiting - rl := RateLimit{} + + rl := TooManyRequests{} err = json.Unmarshal(response, &rl) if err != nil { - err = fmt.Errorf("Request unmarshal rate limit error : %+v", err) + s.log(LogError, "rate limit unmarshal error, %s", err) return } + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + s.handle(RateLimit{TooManyRequests: &rl, URL: urlStr}) + + mu.Lock() time.Sleep(rl.RetryAfter) + mu.Unlock() + // we can make the above smarter + // this method can cause longer delays then required + response, err = s.request(method, urlStr, contentType, b) default: // Error condition @@ -990,7 +1023,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID // messageID : the ID of a Message func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { - _, err = s.Request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), nil) + _, err = s.request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), "", nil) return } diff --git a/restapi_test.go b/restapi_test.go index c35c457..e72a108 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -31,7 +31,7 @@ func TestUserAvatar(t *testing.T) { a, err := dg.UserAvatar("@me") if err != nil { - if err.Error() == `HTTP 404 NOT FOUND, {"message": ""}` { + if err.Error() == `HTTP 404 NOT FOUND, {"message": "404: Not Found"}` { t.Skip("Skipped, @me doesn't have an Avatar") } t.Errorf(err.Error()) diff --git a/state.go b/state.go index 7993151..e3c8908 100644 --- a/state.go +++ b/state.go @@ -324,6 +324,9 @@ func (s *State) Channel(channelID string) (*Channel, error) { if s == nil { return nil, ErrNilState } + + s.RLock() + defer s.RUnlock() if c, ok := s.channelMap[channelID]; ok { return c, nil diff --git a/structs.go b/structs.go index 5e46068..dd7a1fc 100644 --- a/structs.go +++ b/structs.go @@ -30,7 +30,8 @@ type Session struct { Token string // Debug for printing JSON request/responses - Debug bool + Debug bool // Deprecated, will be removed. + LogLevel int // Should the session reconnect the websocket on errors. ShouldReconnectOnError bool @@ -74,6 +75,26 @@ type Session struct { // When nil, the session is not listening. listening chan interface{} + + // used to deal with rate limits + // may switch to slices later + // TODO: performance test map vs slices + rateLimit rateLimitMutex + + // sequence tracks the current gateway api websocket sequence number + sequence int + + // stores sessions current Discord Gateway + gateway string + + // stores session ID of current Gateway connection + sessionID string +} + +type rateLimitMutex struct { + sync.Mutex + url map[string]*sync.Mutex + bucket map[string]*sync.Mutex // TODO :) } // A VoiceRegion stores data for a specific voice region server. @@ -272,10 +293,9 @@ type FriendSourceFlags struct { // An Event provides a basic initial struct for all websocket event. type Event struct { - Type string `json:"t"` - State int `json:"s"` Operation int `json:"op"` - Direction int `json:"dir"` + Sequence int `json:"s"` + Type string `json:"t"` RawData json.RawMessage `json:"d"` Struct interface{} `json:"-"` } @@ -304,8 +324,9 @@ type Relationship struct { ID string `json:"id"` } -// A RateLimit struct holds information related to a specific rate limit. -type RateLimit struct { +// A TooManyRequests struct holds information received from Discord +// when receiving a HTTP 429 response. +type TooManyRequests struct { Bucket string `json:"bucket"` Message string `json:"message"` RetryAfter time.Duration `json:"retry_after"` diff --git a/util.go b/util.go deleted file mode 100644 index 935103b..0000000 --- a/util.go +++ /dev/null @@ -1,35 +0,0 @@ -// Discordgo - Discord bindings for Go -// Available at https://github.com/bwmarrin/discordgo - -// Copyright 2015-2016 Bruce Marriner . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file contains utility functions for the discordgo package. These -// functions are not exported and are likely to change substantially in -// the future to match specific needs of the discordgo package itself. - -package discordgo - -import ( - "bytes" - "encoding/json" - "fmt" - "log" -) - -// printEvent prints out a WSAPI event. -func printEvent(e *Event) { - log.Println(fmt.Sprintf("Event. Type: %s, State: %d Operation: %d Direction: %d", e.Type, e.State, e.Operation, e.Direction)) - printJSON(e.RawData) -} - -// printJSON is a helper function to display JSON data in a easy to read format. -func printJSON(body []byte) { - var prettyJSON bytes.Buffer - error := json.Indent(&prettyJSON, body, "", "\t") - if error != nil { - log.Print("JSON parse error: ", error) - } - log.Println(string(prettyJSON.Bytes())) -} diff --git a/voice.go b/voice.go index c0971d2..44b3666 100644 --- a/voice.go +++ b/voice.go @@ -32,7 +32,8 @@ import ( type VoiceConnection struct { sync.RWMutex - Debug bool // If true, print extra logging + Debug bool // If true, print extra logging -- DEPRECATED + LogLevel int Ready bool // If true, voice is ready to send/receive audio UserID string GuildID string @@ -125,6 +126,7 @@ func (v *VoiceConnection) Disconnect() (err error) { // Close websocket and udp connections v.Close() + v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) delete(v.session.VoiceConnections, v.GuildID) return @@ -427,6 +429,7 @@ func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struc var err error ticker := time.NewTicker(i * time.Millisecond) for { + v.log(LogDebug, "Sending heartbeat packet") err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())}) if err != nil { log.Println("wsHeartbeat send error: ", err) diff --git a/wsapi.go b/wsapi.go index 4fddeb9..d09f07c 100644 --- a/wsapi.go +++ b/wsapi.go @@ -26,6 +26,8 @@ import ( "github.com/gorilla/websocket" ) +var GATEWAY_VERSION int = 4 + type handshakeProperties struct { OS string `json:"$os"` Browser string `json:"$browser"` @@ -35,7 +37,6 @@ type handshakeProperties struct { } type handshakeData struct { - Version int `json:"v"` Token string `json:"token"` Properties handshakeProperties `json:"properties"` LargeThreshold int `json:"large_threshold"` @@ -49,6 +50,9 @@ type handshakeOp struct { // Open opens a websocket connection to Discord. func (s *Session) Open() (err error) { + + s.log(LogInformational, "called") + s.Lock() defer func() { if err != nil { @@ -56,7 +60,10 @@ func (s *Session) Open() (err error) { } }() - s.VoiceConnections = make(map[string]*VoiceConnection) + if s.VoiceConnections == nil { + s.log(LogInformational, "creating new VoiceConnections map") + s.VoiceConnections = make(map[string]*VoiceConnection) + } if s.wsConn != nil { err = errors.New("Web socket already opened.") @@ -64,25 +71,42 @@ func (s *Session) Open() (err error) { } // Get the gateway to use for the Websocket connection - g, err := s.Gateway() - if err != nil { - return + if s.gateway == "" { + s.gateway, err = s.Gateway() + if err != nil { + return + } + + // Add the version and encoding to the URL + s.gateway = fmt.Sprintf("%s?v=%v&encoding=json", s.gateway, GATEWAY_VERSION) } header := http.Header{} header.Add("accept-encoding", "zlib") - // 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, header) + s.log(LogInformational, "connecting to gateway %s", s.gateway) + s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header) if err != nil { + s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err) + s.gateway = "" // clear cached gateway + // TODO: should we add a retry block here? return } - err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{3, s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) + if s.sessionID != "" && s.sequence > 0 { + + s.log(LogInformational, "sending resume packet to gateway") + // TODO: RESUME + } + //else { + + s.log(LogInformational, "sending identify packet to gateway") + err = s.wsConn.WriteJSON(handshakeOp{2, handshakeData{s.Token, handshakeProperties{runtime.GOOS, "Discordgo v" + VERSION, "", "", ""}, 250, s.Compress}}) if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) return } + //} // Create listening outside of listen, as it needs to happen inside the mutex // lock. @@ -163,7 +187,10 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { case <-listening: return default: - go s.event(messageType, message) + // TODO make s.event a variable that points to a function + // this way it will be possible for an end-user to write + // a completely custom event handler if needed. + go s.onEvent(messageType, message) } } } @@ -189,7 +216,7 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} var err error ticker := time.NewTicker(i * time.Millisecond) for { - err = wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())}) + err = wsConn.WriteJSON(heartbeatOp{1, s.sequence}) if err != nil { log.Println("Error sending heartbeat:", err) return @@ -241,73 +268,104 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { return } -// Front line handler for all Websocket Events. Determines the -// event type and passes the message along to the next handler. +// onEvent is the "event handler" for all messages received on the +// Discord Gateway API websocket connection. +// +// If you use the AddHandler() function to register a handler for a +// specific event this function will pass the event along to that handler. +// +// If you use the AddHandler() function to register a handler for the +// "OnEvent" event then all events will be passed to that handler. +// +// TODO: You may also register a custom event handler entirely using... +func (s *Session) onEvent(messageType int, message []byte) { -// event is the front line handler for all events. This needs to be -// broken up into smaller functions to be more idiomatic Go. -// Events will be handled by any implemented handler in Session. -// All unhandled events will then be handled by OnEvent. -func (s *Session) event(messageType int, message []byte) { var err error var reader io.Reader - reader = bytes.NewBuffer(message) + // If this is a compressed message, uncompress it. if messageType == 2 { - z, err1 := zlib.NewReader(reader) - if err1 != nil { - log.Println(fmt.Sprintf("Error uncompressing message type %d: %s", messageType, err1)) + + z, err := zlib.NewReader(reader) + if err != nil { + s.log(LogError, "error uncompressing websocket message, %s", err) return } + defer func() { err := z.Close() if err != nil { - log.Println("error closing zlib:", err) + s.log(LogWarning, "error closing zlib, %s", err) } }() + reader = z } + // Decode the event into an Event struct. var e *Event decoder := json.NewDecoder(reader) if err = decoder.Decode(&e); err != nil { - log.Println(fmt.Sprintf("Error decoding message type %d: %s", messageType, err)) + s.log(LogError, "error decoding websocket message, %s", err) return } if s.Debug { - printEvent(e) + s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) } + // Ping request. + // Must respond with a heartbeat packet within 5 seconds + if e.Operation == 1 { + s.log(LogInformational, "sending heartbeat in response to Op1") + err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + if err != nil { + s.log(LogError, "error sending heartbeat in response to Op1") + return + } + } + + // Do not try to Dispatch a non-Dispatch Message + if e.Operation != 0 { + // But we probably should be doing something with them. + // TEMP + s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) + return + } + + // Store the message sequence + s.sequence = e.Sequence + + // Map event to registered event handlers and pass it along + // to any registered functions i := eventToInterface[e.Type] if i != nil { + // Create a new instance of the event type. i = reflect.New(reflect.TypeOf(i)).Interface() // Attempt to unmarshal our event. - // If there is an error we should handle the event itself. if err = json.Unmarshal(e.RawData, i); err != nil { - log.Printf("error unmarshalling %s event, %s\n", e.Type, err) - // Ready events must fire, even if they are empty. - if e.Type != "READY" { - i = nil - } - + s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) } - } else { - log.Println("Unknown event.") - i = nil - } - if i != nil { + // Send event to any registered event handlers for it's type. + // Because the above doesn't cancel this, in case of an error + // the struct could be partially populated or at default values. + // However, most errors are due to a single field and I feel + // it's better to pass along what we received than nothing at all. + // TODO: Think about that decision :) + // Either way, READY events must fire, even with errors. s.handle(i) + + } else { + s.log(LogWarning, "unknown event, %#v", e) } + // Emit event to the OnEvent handler e.Struct = i s.handle(e) - - return } // ------------------------------------------------------------------------------------------------ @@ -359,13 +417,11 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi // Create a new voice session // TODO review what all these things are for.... voice = &VoiceConnection{ - GuildID: gID, - ChannelID: cID, - deaf: deaf, - mute: mute, - session: s, - connected: make(chan bool), - sessionRecv: make(chan string), + GuildID: gID, + ChannelID: cID, + deaf: deaf, + mute: mute, + session: s, } // Store voice in VoiceConnections map for this GuildID @@ -375,6 +431,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} err = s.wsConn.WriteJSON(data) if err != nil { + s.log(LogInformational, "Deleting VoiceConnection %s", gID) delete(s.VoiceConnections, gID) return } @@ -383,6 +440,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi err = voice.waitUntilConnected() if err != nil { voice.Close() + s.log(LogInformational, "Deleting VoiceConnection %s", gID) delete(s.VoiceConnections, gID) return } @@ -421,9 +479,6 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { // Store the SessionID for later use. voice.UserID = self.ID // TODO: Review voice.sessionID = st.SessionID - - // TODO: Consider this... - // voice.sessionRecv <- st.SessionID } // onVoiceServerUpdate handles the Voice Server Update data websocket event. @@ -440,29 +495,18 @@ func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { return } + // If currently connected to voice ws/udp, then disconnect. + // Has no effect if not connected. + voice.Close() + // Store values for later use 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. - // voice.Close() - - // Wait for the sessionID from onVoiceStateUpdate - // voice.sessionID = <-voice.sessionRecv - // TODO review above - // wouldn't this cause a huge problem, if it's just a guild server - // update.. ? - // I could add a timeout loop of some sort and also check if the - // sessionID doesn't or does exist already... - // something.. a bit smarter. - - // We now have enough information to open a voice websocket conenction - // so, that's what the next call does. + // Open a conenction to the voice server err := voice.open() if err != nil { - log.Println("onVoiceServerUpdate Voice.Open error: ", err) - // TODO better logging + s.log(LogError, "onVoiceServerUpdate voice.open, ", err) } }