Browse Source

Merge branch 'develop' of https://github.com/iopred/discordgo into develop

Chris Rhodes 8 years ago
parent
commit
aff0ab3fd1
14 changed files with 488 additions and 112 deletions
  1. 4 0
      discord.go
  2. 142 0
      docs/GettingStarted.md
  3. BIN
      docs/img/discordgo.png
  4. 33 0
      docs/index.md
  5. 6 0
      events.go
  6. 105 0
      logging.go
  7. 17 0
      mkdocs.yml
  8. 36 3
      restapi.go
  9. 1 1
      restapi_test.go
  10. 3 0
      state.go
  11. 27 6
      structs.go
  12. 0 35
      util.go
  13. 4 1
      voice.go
  14. 110 66
      wsapi.go

+ 4 - 0
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)
 }

+ 142 - 0
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.

BIN
docs/img/discordgo.png


+ 33 - 0
docs/index.md

@@ -0,0 +1,33 @@
+## DiscordGo
+<hr>
+<img align="right" src="http://bwmarrin.github.io/discordgo/img/discordgo.png">
+
+[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.

+ 6 - 0
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

+ 105 - 0
logging.go

@@ -0,0 +1,105 @@
+// Discordgo - Discord bindings for Go
+// Available at https://github.com/bwmarrin/discordgo
+
+// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>.  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()))
+}

+ 17 - 0
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'

+ 36 - 3
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
 }
 

+ 1 - 1
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())

+ 3 - 0
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

+ 27 - 6
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"`

+ 0 - 35
util.go

@@ -1,35 +0,0 @@
-// Discordgo - Discord bindings for Go
-// Available at https://github.com/bwmarrin/discordgo
-
-// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>.  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()))
-}

+ 4 - 1
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)

+ 110 - 66
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)
 	}
 }