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)
}
}