Merge branch 'develop' of https://github.com/iopred/discordgo into develop
This commit is contained in:
commit
aff0ab3fd1
14 changed files with 488 additions and 112 deletions
|
@ -237,5 +237,9 @@ func (s *Session) initialize() {
|
||||||
// onReady handles the ready event.
|
// onReady handles the ready event.
|
||||||
func (s *Session) onReady(se *Session, r *Ready) {
|
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)
|
go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval)
|
||||||
}
|
}
|
||||||
|
|
142
docs/GettingStarted.md
Normal file
142
docs/GettingStarted.md
Normal file
|
@ -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
Normal file
BIN
docs/img/discordgo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
33
docs/index.md
Normal file
33
docs/index.md
Normal file
|
@ -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.
|
|
@ -50,6 +50,12 @@ type Connect struct{}
|
||||||
// Disconnect is an empty struct for an event.
|
// Disconnect is an empty struct for an event.
|
||||||
type Disconnect struct{}
|
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.
|
// MessageCreate is a wrapper struct for an event.
|
||||||
type MessageCreate struct {
|
type MessageCreate struct {
|
||||||
*Message
|
*Message
|
||||||
|
|
105
logging.go
Normal file
105
logging.go
Normal file
|
@ -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
mkdocs.yml
Normal file
17
mkdocs.yml
Normal file
|
@ -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'
|
39
restapi.go
39
restapi.go
|
@ -25,6 +25,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"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.
|
// request makes a (GET/POST/...) Requests to Discord REST API.
|
||||||
func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) {
|
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 {
|
if s.Debug {
|
||||||
log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
|
log.Printf("API REQUEST %8s :: %s\n", method, urlStr)
|
||||||
log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b))
|
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)}
|
client := &http.Client{Timeout: (20 * time.Second)}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
mu.Unlock()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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.
|
// TODO check for 401 response, invalidate token if we get one.
|
||||||
|
|
||||||
case 429: // TOO MANY REQUESTS - Rate limiting
|
case 429: // TOO MANY REQUESTS - Rate limiting
|
||||||
rl := RateLimit{}
|
|
||||||
|
rl := TooManyRequests{}
|
||||||
err = json.Unmarshal(response, &rl)
|
err = json.Unmarshal(response, &rl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("Request unmarshal rate limit error : %+v", err)
|
s.log(LogError, "rate limit unmarshal error, %s", err)
|
||||||
return
|
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)
|
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)
|
response, err = s.request(method, urlStr, contentType, b)
|
||||||
|
|
||||||
default: // Error condition
|
default: // Error condition
|
||||||
|
@ -990,7 +1023,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID
|
||||||
// messageID : the ID of a Message
|
// messageID : the ID of a Message
|
||||||
func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ func TestUserAvatar(t *testing.T) {
|
||||||
|
|
||||||
a, err := dg.UserAvatar("@me")
|
a, err := dg.UserAvatar("@me")
|
||||||
if err != nil {
|
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.Skip("Skipped, @me doesn't have an Avatar")
|
||||||
}
|
}
|
||||||
t.Errorf(err.Error())
|
t.Errorf(err.Error())
|
||||||
|
|
3
state.go
3
state.go
|
@ -324,6 +324,9 @@ func (s *State) Channel(channelID string) (*Channel, error) {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
return nil, ErrNilState
|
return nil, ErrNilState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
if c, ok := s.channelMap[channelID]; ok {
|
if c, ok := s.channelMap[channelID]; ok {
|
||||||
return c, nil
|
return c, nil
|
||||||
|
|
33
structs.go
33
structs.go
|
@ -30,7 +30,8 @@ type Session struct {
|
||||||
Token string
|
Token string
|
||||||
|
|
||||||
// Debug for printing JSON request/responses
|
// Debug for printing JSON request/responses
|
||||||
Debug bool
|
Debug bool // Deprecated, will be removed.
|
||||||
|
LogLevel int
|
||||||
|
|
||||||
// Should the session reconnect the websocket on errors.
|
// Should the session reconnect the websocket on errors.
|
||||||
ShouldReconnectOnError bool
|
ShouldReconnectOnError bool
|
||||||
|
@ -74,6 +75,26 @@ type Session struct {
|
||||||
|
|
||||||
// When nil, the session is not listening.
|
// When nil, the session is not listening.
|
||||||
listening chan interface{}
|
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.
|
// 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.
|
// An Event provides a basic initial struct for all websocket event.
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type string `json:"t"`
|
|
||||||
State int `json:"s"`
|
|
||||||
Operation int `json:"op"`
|
Operation int `json:"op"`
|
||||||
Direction int `json:"dir"`
|
Sequence int `json:"s"`
|
||||||
|
Type string `json:"t"`
|
||||||
RawData json.RawMessage `json:"d"`
|
RawData json.RawMessage `json:"d"`
|
||||||
Struct interface{} `json:"-"`
|
Struct interface{} `json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -304,8 +324,9 @@ type Relationship struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// A RateLimit struct holds information related to a specific rate limit.
|
// A TooManyRequests struct holds information received from Discord
|
||||||
type RateLimit struct {
|
// when receiving a HTTP 429 response.
|
||||||
|
type TooManyRequests struct {
|
||||||
Bucket string `json:"bucket"`
|
Bucket string `json:"bucket"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
RetryAfter time.Duration `json:"retry_after"`
|
RetryAfter time.Duration `json:"retry_after"`
|
||||||
|
|
35
util.go
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()))
|
|
||||||
}
|
|
5
voice.go
5
voice.go
|
@ -32,7 +32,8 @@ import (
|
||||||
type VoiceConnection struct {
|
type VoiceConnection struct {
|
||||||
sync.RWMutex
|
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
|
Ready bool // If true, voice is ready to send/receive audio
|
||||||
UserID string
|
UserID string
|
||||||
GuildID string
|
GuildID string
|
||||||
|
@ -125,6 +126,7 @@ func (v *VoiceConnection) Disconnect() (err error) {
|
||||||
// Close websocket and udp connections
|
// Close websocket and udp connections
|
||||||
v.Close()
|
v.Close()
|
||||||
|
|
||||||
|
v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID)
|
||||||
delete(v.session.VoiceConnections, v.GuildID)
|
delete(v.session.VoiceConnections, v.GuildID)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -427,6 +429,7 @@ func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struc
|
||||||
var err error
|
var err error
|
||||||
ticker := time.NewTicker(i * time.Millisecond)
|
ticker := time.NewTicker(i * time.Millisecond)
|
||||||
for {
|
for {
|
||||||
|
v.log(LogDebug, "Sending heartbeat packet")
|
||||||
err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())})
|
err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("wsHeartbeat send error: ", err)
|
log.Println("wsHeartbeat send error: ", err)
|
||||||
|
|
176
wsapi.go
176
wsapi.go
|
@ -26,6 +26,8 @@ import (
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var GATEWAY_VERSION int = 4
|
||||||
|
|
||||||
type handshakeProperties struct {
|
type handshakeProperties struct {
|
||||||
OS string `json:"$os"`
|
OS string `json:"$os"`
|
||||||
Browser string `json:"$browser"`
|
Browser string `json:"$browser"`
|
||||||
|
@ -35,7 +37,6 @@ type handshakeProperties struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type handshakeData struct {
|
type handshakeData struct {
|
||||||
Version int `json:"v"`
|
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Properties handshakeProperties `json:"properties"`
|
Properties handshakeProperties `json:"properties"`
|
||||||
LargeThreshold int `json:"large_threshold"`
|
LargeThreshold int `json:"large_threshold"`
|
||||||
|
@ -49,6 +50,9 @@ type handshakeOp struct {
|
||||||
|
|
||||||
// Open opens a websocket connection to Discord.
|
// Open opens a websocket connection to Discord.
|
||||||
func (s *Session) Open() (err error) {
|
func (s *Session) Open() (err error) {
|
||||||
|
|
||||||
|
s.log(LogInformational, "called")
|
||||||
|
|
||||||
s.Lock()
|
s.Lock()
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
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 {
|
if s.wsConn != nil {
|
||||||
err = errors.New("Web socket already opened.")
|
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
|
// Get the gateway to use for the Websocket connection
|
||||||
g, err := s.Gateway()
|
if s.gateway == "" {
|
||||||
if err != nil {
|
s.gateway, err = s.Gateway()
|
||||||
return
|
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 := http.Header{}
|
||||||
header.Add("accept-encoding", "zlib")
|
header.Add("accept-encoding", "zlib")
|
||||||
|
|
||||||
// TODO: See if there's a use for the http response.
|
s.log(LogInformational, "connecting to gateway %s", s.gateway)
|
||||||
// conn, response, err := websocket.DefaultDialer.Dial(session.Gateway, nil)
|
s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header)
|
||||||
s.wsConn, _, err = websocket.DefaultDialer.Dial(g, header)
|
|
||||||
if err != nil {
|
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
|
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 {
|
if err != nil {
|
||||||
|
s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
//}
|
||||||
|
|
||||||
// Create listening outside of listen, as it needs to happen inside the mutex
|
// Create listening outside of listen, as it needs to happen inside the mutex
|
||||||
// lock.
|
// lock.
|
||||||
|
@ -163,7 +187,10 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
|
||||||
case <-listening:
|
case <-listening:
|
||||||
return
|
return
|
||||||
default:
|
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
|
var err error
|
||||||
ticker := time.NewTicker(i * time.Millisecond)
|
ticker := time.NewTicker(i * time.Millisecond)
|
||||||
for {
|
for {
|
||||||
err = wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())})
|
err = wsConn.WriteJSON(heartbeatOp{1, s.sequence})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error sending heartbeat:", err)
|
log.Println("Error sending heartbeat:", err)
|
||||||
return
|
return
|
||||||
|
@ -241,73 +268,104 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Front line handler for all Websocket Events. Determines the
|
// onEvent is the "event handler" for all messages received on the
|
||||||
// event type and passes the message along to the next handler.
|
// 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 err error
|
||||||
var reader io.Reader
|
var reader io.Reader
|
||||||
|
|
||||||
reader = bytes.NewBuffer(message)
|
reader = bytes.NewBuffer(message)
|
||||||
|
|
||||||
|
// If this is a compressed message, uncompress it.
|
||||||
if messageType == 2 {
|
if messageType == 2 {
|
||||||
z, err1 := zlib.NewReader(reader)
|
|
||||||
if err1 != nil {
|
z, err := zlib.NewReader(reader)
|
||||||
log.Println(fmt.Sprintf("Error uncompressing message type %d: %s", messageType, err1))
|
if err != nil {
|
||||||
|
s.log(LogError, "error uncompressing websocket message, %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := z.Close()
|
err := z.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error closing zlib:", err)
|
s.log(LogWarning, "error closing zlib, %s", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reader = z
|
reader = z
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decode the event into an Event struct.
|
||||||
var e *Event
|
var e *Event
|
||||||
decoder := json.NewDecoder(reader)
|
decoder := json.NewDecoder(reader)
|
||||||
if err = decoder.Decode(&e); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Debug {
|
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]
|
i := eventToInterface[e.Type]
|
||||||
if i != nil {
|
if i != nil {
|
||||||
|
|
||||||
// Create a new instance of the event type.
|
// Create a new instance of the event type.
|
||||||
i = reflect.New(reflect.TypeOf(i)).Interface()
|
i = reflect.New(reflect.TypeOf(i)).Interface()
|
||||||
|
|
||||||
// Attempt to unmarshal our event.
|
// 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 {
|
if err = json.Unmarshal(e.RawData, i); err != nil {
|
||||||
log.Printf("error unmarshalling %s event, %s\n", e.Type, err)
|
s.log(LogError, "error unmarshalling %s event, %s", e.Type, err)
|
||||||
// Ready events must fire, even if they are empty.
|
|
||||||
if e.Type != "READY" {
|
|
||||||
i = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} 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)
|
s.handle(i)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
s.log(LogWarning, "unknown event, %#v", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit event to the OnEvent handler
|
||||||
e.Struct = i
|
e.Struct = i
|
||||||
s.handle(e)
|
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
|
// Create a new voice session
|
||||||
// TODO review what all these things are for....
|
// TODO review what all these things are for....
|
||||||
voice = &VoiceConnection{
|
voice = &VoiceConnection{
|
||||||
GuildID: gID,
|
GuildID: gID,
|
||||||
ChannelID: cID,
|
ChannelID: cID,
|
||||||
deaf: deaf,
|
deaf: deaf,
|
||||||
mute: mute,
|
mute: mute,
|
||||||
session: s,
|
session: s,
|
||||||
connected: make(chan bool),
|
|
||||||
sessionRecv: make(chan string),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store voice in VoiceConnections map for this GuildID
|
// 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}}
|
data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
|
||||||
err = s.wsConn.WriteJSON(data)
|
err = s.wsConn.WriteJSON(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.log(LogInformational, "Deleting VoiceConnection %s", gID)
|
||||||
delete(s.VoiceConnections, gID)
|
delete(s.VoiceConnections, gID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -383,6 +440,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi
|
||||||
err = voice.waitUntilConnected()
|
err = voice.waitUntilConnected()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
voice.Close()
|
voice.Close()
|
||||||
|
s.log(LogInformational, "Deleting VoiceConnection %s", gID)
|
||||||
delete(s.VoiceConnections, gID)
|
delete(s.VoiceConnections, gID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -421,9 +479,6 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
|
||||||
// Store the SessionID for later use.
|
// Store the SessionID for later use.
|
||||||
voice.UserID = self.ID // TODO: Review
|
voice.UserID = self.ID // TODO: Review
|
||||||
voice.sessionID = st.SessionID
|
voice.sessionID = st.SessionID
|
||||||
|
|
||||||
// TODO: Consider this...
|
|
||||||
// voice.sessionRecv <- st.SessionID
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onVoiceServerUpdate handles the Voice Server Update data websocket event.
|
// onVoiceServerUpdate handles the Voice Server Update data websocket event.
|
||||||
|
@ -440,29 +495,18 @@ func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If currently connected to voice ws/udp, then disconnect.
|
||||||
|
// Has no effect if not connected.
|
||||||
|
voice.Close()
|
||||||
|
|
||||||
// Store values for later use
|
// Store values for later use
|
||||||
voice.token = st.Token
|
voice.token = st.Token
|
||||||
voice.endpoint = st.Endpoint
|
voice.endpoint = st.Endpoint
|
||||||
voice.GuildID = st.GuildID
|
voice.GuildID = st.GuildID
|
||||||
|
|
||||||
// If currently connected to voice ws/udp, then disconnect.
|
// Open a conenction to the voice server
|
||||||
// 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.
|
|
||||||
err := voice.open()
|
err := voice.open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("onVoiceServerUpdate Voice.Open error: ", err)
|
s.log(LogError, "onVoiceServerUpdate voice.open, ", err)
|
||||||
// TODO better logging
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue