From f35618b1a898c7cf6c64df4ab9ccb8298d31fc43 Mon Sep 17 00:00:00 2001 From: Sean Hinchee Date: Thu, 28 Sep 2017 16:43:10 -0500 Subject: [PATCH] updating discordgo vending --- DiscordGo/LICENSE | 2 +- DiscordGo/README.md | 66 +- DiscordGo/discord.go | 165 +-- DiscordGo/discord_test.go | 112 +- DiscordGo/docs/GettingStarted.md | 142 +++ DiscordGo/docs/img/discordgo.png | Bin 0 -> 34331 bytes DiscordGo/docs/index.md | 33 + DiscordGo/endpoints.go | 191 +-- DiscordGo/event.go | 238 ++++ DiscordGo/eventhandlers.go | 1030 ++++++++++++++++ DiscordGo/events.go | 279 +++-- DiscordGo/examples/README.md | 11 + DiscordGo/examples/airhorn/README.md | 51 + DiscordGo/examples/airhorn/airhorn.dca | Bin 0 -> 16300 bytes DiscordGo/examples/airhorn/main.go | 211 ++++ DiscordGo/examples/appmaker/README.md | 57 + DiscordGo/examples/appmaker/main.go | 103 ++ DiscordGo/examples/avatar/README.md | 49 + DiscordGo/examples/avatar/main.go | 89 ++ DiscordGo/examples/mytoken/README.md | 47 + DiscordGo/examples/mytoken/main.go | 40 + DiscordGo/examples/pingpong/README.md | 42 + DiscordGo/examples/pingpong/main.go | 71 ++ DiscordGo/logging.go | 95 ++ DiscordGo/message.go | 268 ++++- DiscordGo/message_test.go | 41 + DiscordGo/mkdocs.yml | 17 + DiscordGo/oauth2.go | 126 ++ DiscordGo/oauth2_test.go | 57 + DiscordGo/ratelimit.go | 183 +++ DiscordGo/ratelimit_test.go | 112 ++ DiscordGo/restapi.go | 1297 ++++++++++++++++++--- DiscordGo/restapi_test.go | 142 ++- DiscordGo/state.go | 741 +++++++++--- DiscordGo/structs.go | 613 +++++++--- DiscordGo/tools/cmd/eventhandlers/main.go | 124 ++ DiscordGo/types.go | 58 + DiscordGo/types_test.go | 24 + DiscordGo/user.go | 42 + DiscordGo/user_test.go | 16 + DiscordGo/voice.go | 629 +++++++--- DiscordGo/wsapi.go | 758 ++++++++---- 42 files changed, 7117 insertions(+), 1255 deletions(-) create mode 100644 DiscordGo/docs/GettingStarted.md create mode 100644 DiscordGo/docs/img/discordgo.png create mode 100644 DiscordGo/docs/index.md create mode 100644 DiscordGo/event.go create mode 100644 DiscordGo/eventhandlers.go create mode 100644 DiscordGo/examples/README.md create mode 100644 DiscordGo/examples/airhorn/README.md create mode 100644 DiscordGo/examples/airhorn/airhorn.dca create mode 100644 DiscordGo/examples/airhorn/main.go create mode 100644 DiscordGo/examples/appmaker/README.md create mode 100644 DiscordGo/examples/appmaker/main.go create mode 100644 DiscordGo/examples/avatar/README.md create mode 100644 DiscordGo/examples/avatar/main.go create mode 100644 DiscordGo/examples/mytoken/README.md create mode 100644 DiscordGo/examples/mytoken/main.go create mode 100644 DiscordGo/examples/pingpong/README.md create mode 100644 DiscordGo/examples/pingpong/main.go create mode 100644 DiscordGo/logging.go create mode 100644 DiscordGo/message_test.go create mode 100644 DiscordGo/mkdocs.yml create mode 100644 DiscordGo/oauth2.go create mode 100644 DiscordGo/oauth2_test.go create mode 100644 DiscordGo/ratelimit.go create mode 100644 DiscordGo/ratelimit_test.go create mode 100644 DiscordGo/tools/cmd/eventhandlers/main.go create mode 100644 DiscordGo/types.go create mode 100644 DiscordGo/types_test.go create mode 100644 DiscordGo/user.go create mode 100644 DiscordGo/user_test.go diff --git a/DiscordGo/LICENSE b/DiscordGo/LICENSE index a762558413aff2e377b7ecb1b4ffa14db184f4c3..8d062ea57d85f00ce7dc583f833dbe4e93018d6d 100644 --- a/DiscordGo/LICENSE +++ b/DiscordGo/LICENSE @@ -11,7 +11,7 @@ modification, are permitted provided that the following conditions are met: this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. -* Neither the name of GoDiscord nor the names of its +* Neither the name of discordgo nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/DiscordGo/README.md b/DiscordGo/README.md index 3c038aeddbef92e1fb4e5888a9f63e447c972e6e..eb9f14f041cd4900d40335f6c783426e6a68d201 100644 --- a/DiscordGo/README.md +++ b/DiscordGo/README.md @@ -1,31 +1,33 @@ - -Discordgo -==== ->If for some reason you stumbled upon this repo, use the official one: https://github.com/bwmarrin/discordgo -This is merely a master fork for stability reasons of discord-cli. -Since their highly unstable develop branch is their default one this can give problems with continious integration tools or even the dependency errors. When 1.0 arrives, and becomes default, this repo gets removed. +# DiscordGo + +[![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) [![Go report](http://goreportcard.com/badge/bwmarrin/discordgo)](http://goreportcard.com/report/bwmarrin/discordgo) [![Build Status](https://travis-ci.org/bwmarrin/discordgo.svg?branch=master)](https://travis-ci.org/bwmarrin/discordgo) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23discordgo-blue.svg)](https://discord.gg/0f1SbxBZjYoCtNPP) [![Discord API](https://img.shields.io/badge/Discord%20API-%23go_discordgo-blue.svg)](https://discord.gg/0SBTUU1wZTWT6sqd) -[![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) [![Go report](http://goreportcard.com/badge/bwmarrin/discordgo)](http://goreportcard.com/report/bwmarrin/discordgo) [![Build Status](https://travis-ci.org/bwmarrin/discordgo.svg?branch=master)](https://travis-ci.org/bwmarrin/discordgo) + -Discordgo is a [Go](https://golang.org/) package that provides low level -bindings to the [Discord](https://discordapp.com/) chat client API. Discordgo -has nearly complete support for all of the Discord JSON-API endpoints, websocket +DiscordGo is a [Go](https://golang.org/) package that provides low level +bindings to the [Discord](https://discordapp.com/) chat client API. DiscordGo +has nearly complete support for all of the Discord API endpoints, websocket interface, and voice interface. -* See [dgVoice](https://github.com/bwmarrin/dgvoice) package to extend Discordgo -with additional voice helper functions and features. +If you would like to help the DiscordGo package please use +[this link](https://discordapp.com/oauth2/authorize?client_id=173113690092994561&scope=bot) +to add the official DiscordGo test bot **dgo** to your server. This provides +indispensable help to this project. + +* See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of +additional voice helper functions and features for DiscordGo * See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with -Discord (and Discordgo) +Discord (and DiscordGo) -Join [#go_discordgo](https://discord.gg/0SBTUU1wZTWT6sqd) Discord chat channel -for support. +**For help with this package or general Go discussion, please join the [Discord +Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.** ## Getting Started ### master vs develop Branch -* The master branch represents the latest released version of Discordgo. This +* 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 release notes on the github [releases](https://github.com/bwmarrin/discordgo/releases) page. @@ -43,9 +45,6 @@ slightly and have a more stable package with documented releases then use *maste ### Installing -Discordgo has been tested to compile on Debian 8 (Go 1.3.3), -FreeBSD 10 (Go 1.5.1), and Windows 7 (Go 1.5.2). - This assumes you already have a working Go environment, if not please see [this page](https://golang.org/doc/install) first. @@ -62,8 +61,6 @@ cd $GOPATH/src/github.com/bwmarrin/discordgo git checkout develop ``` - - ### Usage Import the package into your project. @@ -76,7 +73,7 @@ Construct a new Discord client which can be used to access the variety of Discord API functions and to set callback functions for Discord events. ```go -discord, err := discordgo.New("username", "password") +discord, err := discordgo.New("authentication token") ``` See Documentation and Examples below for more detailed information. @@ -85,29 +82,30 @@ See Documentation and Examples below for more detailed information. ## Documentation **NOTICE** : This library and the Discord API are unfinished. -Because of that there may be major changes to library functions, constants, -and structures. +Because of that there may be major changes to library in the future. -The Discordgo code is fairly well documented at this point and is currently +The DiscordGo code is fairly well documented at this point and is currently the only documentation available. Both GoDoc and GoWalker (below) present that information in a nice format. - [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) - [![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/bwmarrin/discordgo) -- [Unofficial Discord API Documentation](https://discordapi.readthedocs.org/en/latest/) - Hand crafted documentation coming eventually. ## Examples -Below is a list of examples and other projects using Discordgo. Please submit +Below is a list of examples and other projects using DiscordGo. Please submit an issue if you would like your project added or removed from this list -- [Basic - New](https://github.com/bwmarrin/discordgo/tree/develop/examples/new_basic) A basic example using the easy New() helper function -- [Basic - API](https://github.com/bwmarrin/discordgo/tree/develop/examples/api_basic) A basic example using the low level API functions. -- [Bruxism](https://github.com/iopred/bruxism) A chat bot for YouTube and Discord -- [GoGerard](https://github.com/GoGerard/GoGerard) A modern bot for Discord -- [Digo](https://github.com/sethdmoore/digo) A pluggable bot for your Discord server +- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) A collection of example programs written with DiscordGo +- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) A curated list of high quality projects using DiscordGo + +## Troubleshooting +For help with common problems please reference the +[Troubleshooting](https://github.com/bwmarrin/discordgo/wiki/Troubleshooting) +section of the project wiki. + ## Contributing Contributions are very welcomed, however please follow the below guidelines. @@ -126,3 +124,7 @@ discussing it. See [this chart](https://abal.moe/Discord/Libraries.html) for a feature comparison and list of other Discord API libraries. + +## Special Thanks + +[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs diff --git a/DiscordGo/discord.go b/DiscordGo/discord.go index f3ab96e252b63ad8068deb719bf2386d2e465609..40eabe183fcb9453c364c58e5f2cd6e4332a8c1f 100644 --- a/DiscordGo/discord.go +++ b/DiscordGo/discord.go @@ -14,12 +14,17 @@ package discordgo import ( + "errors" "fmt" - "reflect" + "net/http" + "time" ) -// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) -const VERSION = "0.11.0" +// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) +const VERSION = "0.17.0" + +// ErrMFA will be risen by New when the user has 2FA. +var ErrMFA = errors.New("account has 2FA enabled") // New creates a new Discord session and will automate some startup // tasks if given enough information to do so. Currently you can pass zero @@ -27,23 +32,37 @@ const VERSION = "0.11.0" // There are 3 ways to call New: // With a single auth token - All requests will use the token blindly, // no verification of the token will be done and requests may fail. +// IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT ` +// eg: `"Bot "` // With an email and password - Discord will sign in with the provided // credentials. // With an email, password and auth token - Discord will verify the auth // token, if it is invalid it will sign in with the provided // credentials. This is the Discord recommended way to sign in. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func New(args ...interface{}) (s *Session, err error) { // Create an empty Session interface. s = &Session{ State: NewState(), + ratelimiter: NewRatelimiter(), StateEnabled: true, Compress: true, ShouldReconnectOnError: true, + ShardID: 0, + ShardCount: 1, + MaxRestRetries: 3, + Client: &http.Client{Timeout: (20 * time.Second)}, + sequence: new(int64), + LastHeartbeatAck: time.Now().UTC(), } // If no arguments are passed return the empty Session interface. - // Later I will add default values, if appropriate. if args == nil { return } @@ -58,7 +77,7 @@ func New(args ...interface{}) (s *Session, err error) { case []string: if len(v) > 3 { - err = fmt.Errorf("Too many string parameters provided.") + err = fmt.Errorf("too many string parameters provided") return } @@ -89,15 +108,15 @@ func New(args ...interface{}) (s *Session, err error) { } else if s.Token == "" { s.Token = v } else { - err = fmt.Errorf("Too many string parameters provided.") + err = fmt.Errorf("too many string parameters provided") return } // case Config: - // TODO: Parse configuration + // TODO: Parse configuration struct default: - err = fmt.Errorf("Unsupported parameter type provided.") + err = fmt.Errorf("unsupported parameter type provided") return } } @@ -111,7 +130,11 @@ func New(args ...interface{}) (s *Session, err error) { } else { err = s.Login(auth, pass) if err != nil || s.Token == "" { - err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) + if s.MFA { + err = ErrMFA + } else { + err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) + } return } } @@ -121,127 +144,3 @@ func New(args ...interface{}) (s *Session, err error) { return } - -// validateHandler takes an event handler func, and returns the type of event. -// eg. -// Session.validateHandler(func (s *discordgo.Session, m *discordgo.MessageCreate)) -// will return the reflect.Type of *discordgo.MessageCreate -func (s *Session) validateHandler(handler interface{}) reflect.Type { - handlerType := reflect.TypeOf(handler) - - if handlerType.NumIn() != 2 { - panic("Unable to add event handler, handler must be of the type func(*discordgo.Session, *discordgo.EventType).") - } - - if handlerType.In(0) != reflect.TypeOf(s) { - panic("Unable to add event handler, first argument must be of type *discordgo.Session.") - } - - eventType := handlerType.In(1) - - // Support handlers of type interface{}, this is a special handler, which is triggered on every event. - if eventType.Kind() == reflect.Interface { - eventType = nil - } - - return eventType -} - -// AddHandler allows you to add an event handler that will be fired anytime -// the Discord WSAPI event that matches the interface fires. -// eventToInterface in events.go has a list of all the Discord WSAPI events -// and their respective interface. -// eg: -// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { -// }) -// -// or: -// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { -// }) -// The return value of this method is a function, that when called will remove the -// event handler. -func (s *Session) AddHandler(handler interface{}) func() { - s.initialize() - - eventType := s.validateHandler(handler) - - s.handlersMu.Lock() - defer s.handlersMu.Unlock() - - h := reflect.ValueOf(handler) - - handlers := s.handlers[eventType] - if handlers == nil { - handlers = []reflect.Value{} - } - s.handlers[eventType] = append(handlers, h) - - // This must be done as we need a consistent reference to the - // reflected value, otherwise a RemoveHandler method would have - // been nice. - return func() { - s.handlersMu.Lock() - defer s.handlersMu.Unlock() - - handlers := s.handlers[eventType] - for i, v := range handlers { - if h == v { - s.handlers[eventType] = append(handlers[:i], handlers[i+1:]...) - return - } - } - } -} - -// handle calls any handlers that match the event type and any handlers of -// interface{}. -func (s *Session) handle(event interface{}) { - s.handlersMu.RLock() - defer s.handlersMu.RUnlock() - - if s.handlers == nil { - return - } - - handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)} - - if handlers, ok := s.handlers[reflect.TypeOf(event)]; ok { - for _, handler := range handlers { - handler.Call(handlerParameters) - } - } - - if handlers, ok := s.handlers[nil]; ok { - for _, handler := range handlers { - handler.Call(handlerParameters) - } - } -} - -// initialize adds all internal handlers and state tracking handlers. -func (s *Session) initialize() { - s.handlersMu.Lock() - if s.handlers != nil { - s.handlersMu.Unlock() - return - } - - s.handlers = map[interface{}][]reflect.Value{} - s.handlersMu.Unlock() - - s.AddHandler(s.onEvent) - s.AddHandler(s.onReady) - s.AddHandler(s.onVoiceServerUpdate) - s.AddHandler(s.onVoiceStateUpdate) - s.AddHandler(s.State.onInterface) -} - -// onEvent handles events that are unhandled or errored while unmarshalling -func (s *Session) onEvent(se *Session, e *Event) { - printEvent(e) -} - -// onReady handles the ready event. -func (s *Session) onReady(se *Session, r *Ready) { - go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) -} diff --git a/DiscordGo/discord_test.go b/DiscordGo/discord_test.go index bf706690a8ed60c37d83a645fceafa9eca8f5e1e..ca4472ada61b292277fb08842825e1098a8f1bfd 100644 --- a/DiscordGo/discord_test.go +++ b/DiscordGo/discord_test.go @@ -3,6 +3,7 @@ package discordgo import ( "os" "runtime" + "sync/atomic" "testing" "time" ) @@ -10,18 +11,26 @@ import ( ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////// VARS NEEDED FOR TESTING var ( - dg *Session // Stores global discordgo session + dg *Session // Stores a global discordgo user session + dgBot *Session // Stores a global discordgo bot session - envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating + envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating the user account + envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account envEmail = os.Getenv("DG_EMAIL") // Email to use when authenticating envPassword = os.Getenv("DG_PASSWORD") // Password to use when authenticating - // envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests - envChannel = os.Getenv("DG_CHANNEL") // Channel ID to use for tests + envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests + envChannel = os.Getenv("DG_CHANNEL") // Channel ID to use for tests // envUser = os.Getenv("DG_USER") // User ID to use for tests envAdmin = os.Getenv("DG_ADMIN") // User ID of admin user to use for tests ) func init() { + if envBotToken != "" { + if d, err := New(envBotToken); err == nil { + dgBot = d + } + } + if envEmail == "" || envPassword == "" || envToken == "" { return } @@ -31,46 +40,6 @@ func init() { } } -////////////////////////////////////////////////////////////////////////////// -//////////////////////////////////////////// HELPER FUNCTIONS USED FOR TESTING - -// This waits x time for the check bool to be the want bool -func waitBoolEqual(timeout time.Duration, check *bool, want bool) bool { - - start := time.Now() - for { - if *check == want { - return true - } - - if time.Since(start) > timeout { - return false - } - - runtime.Gosched() - } -} - -// Checks if we're connected to Discord -func isConnected() bool { - - if dg == nil { - return false - } - - if dg.Token == "" { - return false - } - - // Need a way to see if the ws connection is nil - - if !waitBoolEqual(10*time.Second, &dg.DataReady, true) { - return false - } - - return true -} - ////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////// START OF TESTS @@ -203,8 +172,21 @@ func TestOpenClose(t *testing.T) { t.Fatalf("TestClose, d.Open failed: %+v", err) } - if !waitBoolEqual(10*time.Second, &d.DataReady, true) { - t.Fatal("DataReady never became true.") + // We need a better way to know the session is ready for use, + // this is totally gross. + start := time.Now() + for { + d.RLock() + if d.DataReady { + d.RUnlock() + break + } + d.RUnlock() + + if time.Since(start) > 10*time.Second { + t.Fatal("DataReady never became true.yy") + } + runtime.Gosched() } // TODO find a better way @@ -226,19 +208,20 @@ func TestOpenClose(t *testing.T) { } func TestAddHandler(t *testing.T) { - testHandlerCalled := 0 + + testHandlerCalled := int32(0) testHandler := func(s *Session, m *MessageCreate) { - testHandlerCalled++ + atomic.AddInt32(&testHandlerCalled, 1) } - interfaceHandlerCalled := 0 + interfaceHandlerCalled := int32(0) interfaceHandler := func(s *Session, i interface{}) { - interfaceHandlerCalled++ + atomic.AddInt32(&interfaceHandlerCalled, 1) } - bogusHandlerCalled := false + bogusHandlerCalled := int32(0) bogusHandler := func(s *Session, se *Session) { - bogusHandlerCalled = true + atomic.AddInt32(&bogusHandlerCalled, 1) } d := Session{} @@ -248,41 +231,46 @@ func TestAddHandler(t *testing.T) { d.AddHandler(interfaceHandler) d.AddHandler(bogusHandler) - d.handle(&MessageCreate{}) - d.handle(&MessageDelete{}) + d.handleEvent(messageCreateEventType, &MessageCreate{}) + d.handleEvent(messageDeleteEventType, &MessageDelete{}) + + <-time.After(500 * time.Millisecond) // testHandler will be called twice because it was added twice. - if testHandlerCalled != 2 { + if atomic.LoadInt32(&testHandlerCalled) != 2 { t.Fatalf("testHandler was not called twice.") } // interfaceHandler will be called twice, once for each event. - if interfaceHandlerCalled != 2 { + if atomic.LoadInt32(&interfaceHandlerCalled) != 2 { t.Fatalf("interfaceHandler was not called twice.") } - if bogusHandlerCalled { + if atomic.LoadInt32(&bogusHandlerCalled) != 0 { t.Fatalf("bogusHandler was called.") } } func TestRemoveHandler(t *testing.T) { - testHandlerCalled := 0 + + testHandlerCalled := int32(0) testHandler := func(s *Session, m *MessageCreate) { - testHandlerCalled++ + atomic.AddInt32(&testHandlerCalled, 1) } d := Session{} r := d.AddHandler(testHandler) - d.handle(&MessageCreate{}) + d.handleEvent(messageCreateEventType, &MessageCreate{}) r() - d.handle(&MessageCreate{}) + d.handleEvent(messageCreateEventType, &MessageCreate{}) + + <-time.After(500 * time.Millisecond) // testHandler will be called once, as it was removed in between calls. - if testHandlerCalled != 1 { + if atomic.LoadInt32(&testHandlerCalled) != 1 { t.Fatalf("testHandler was not called once.") } } diff --git a/DiscordGo/docs/GettingStarted.md b/DiscordGo/docs/GettingStarted.md new file mode 100644 index 0000000000000000000000000000000000000000..efeb3eaacd01cc5f5e3b200abc2c5a648b470fe1 --- /dev/null +++ b/DiscordGo/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/DiscordGo/docs/img/discordgo.png b/DiscordGo/docs/img/discordgo.png new file mode 100644 index 0000000000000000000000000000000000000000..eb15a3dc0a02cfdce0d3ec413b17841c76c059f4 GIT binary patch literal 34331 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 M%f2cQdHebQ0FDv&rT_o{ literal 0 HcmV?d00001 diff --git a/DiscordGo/docs/index.md b/DiscordGo/docs/index.md new file mode 100644 index 0000000000000000000000000000000000000000..9b433daf8418695ed1845e643c34cc0c041ad3dd --- /dev/null +++ b/DiscordGo/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/DiscordGo/endpoints.go b/DiscordGo/endpoints.go index 262d88f144fa4d45579e01721599283d32308e25..b10f95890b490c4e29b7eea872d9c8ef0e83b6e3 100644 --- a/DiscordGo/endpoints.go +++ b/DiscordGo/endpoints.go @@ -11,78 +11,125 @@ package discordgo +// APIVersion is the Discord API version used for the REST and Websocket API. +var APIVersion = "6" + // Known Discord API Endpoints. var ( - STATUS = "https://status.discordapp.com/api/v2/" - SM = STATUS + "scheduled-maintenances/" - SM_ACTIVE = SM + "active.json" - SM_UPCOMING = SM + "upcoming.json" - - DISCORD = "https://discordapp.com" // TODO consider removing - API = DISCORD + "/api/" - GUILDS = API + "guilds/" - CHANNELS = API + "channels/" - USERS = API + "users/" - GATEWAY = API + "gateway" - - AUTH = API + "auth/" - LOGIN = AUTH + "login" - LOGOUT = AUTH + "logout" - VERIFY = AUTH + "verify" - VERIFY_RESEND = AUTH + "verify/resend" - FORGOT_PASSWORD = AUTH + "forgot" - RESET_PASSWORD = AUTH + "reset" - REGISTER = AUTH + "register" - - VOICE = API + "/voice/" - VOICE_REGIONS = VOICE + "regions" - VOICE_ICE = VOICE + "ice" - - TUTORIAL = API + "tutorial/" - TUTORIAL_INDICATORS = TUTORIAL + "indicators" - - TRACK = API + "track" - SSO = API + "sso" - REPORT = API + "report" - INTEGRATIONS = API + "integrations" - - USER = func(uID string) string { return USERS + uID } - USER_AVATAR = func(uID, aID string) string { return USERS + uID + "/avatars/" + aID + ".jpg" } - USER_SETTINGS = func(uID string) string { return USERS + uID + "/settings" } - USER_GUILDS = func(uID string) string { return USERS + uID + "/guilds" } - USER_GUILD = func(uID, gID string) string { return USERS + uID + "/guilds/" + gID } - USER_CHANNELS = func(uID string) string { return USERS + uID + "/channels" } - USER_DEVICES = func(uID string) string { return USERS + uID + "/devices" } - USER_CONNECTIONS = func(uID string) string { return USERS + uID + "/connections" } - - GUILD = func(gID string) string { return GUILDS + gID } - GUILD_INIVTES = func(gID string) string { return GUILDS + gID + "/invites" } - GUILD_CHANNELS = func(gID string) string { return GUILDS + gID + "/channels" } - GUILD_MEMBERS = func(gID string) string { return GUILDS + gID + "/members" } - GUILD_MEMBER = func(gID, uID string) string { return GUILDS + gID + "/members/" + uID } - GUILD_BANS = func(gID string) string { return GUILDS + gID + "/bans" } - GUILD_BAN = func(gID, uID string) string { return GUILDS + gID + "/bans/" + uID } - GUILD_INTEGRATIONS = func(gID string) string { return GUILDS + gID + "/integrations" } - GUILD_ROLES = func(gID string) string { return GUILDS + gID + "/roles" } - GUILD_ROLE = func(gID, rID string) string { return GUILDS + gID + "/roles/" + rID } - GUILD_INVITES = func(gID string) string { return GUILDS + gID + "/invites" } - GUILD_EMBED = func(gID string) string { return GUILDS + gID + "/embed" } - GUILD_PRUNE = func(gID string) string { return GUILDS + gID + "/prune" } - GUILD_ICON = func(gID, hash string) string { return GUILDS + gID + "/icons/" + hash + ".jpg" } - GUILD_SPLASH = func(gID, hash string) string { return GUILDS + gID + "/splashes/" + hash + ".jpg" } - - CHANNEL = func(cID string) string { return CHANNELS + cID } - CHANNEL_PERMISSIONS = func(cID string) string { return CHANNELS + cID + "/permissions" } - CHANNEL_PERMISSION = func(cID, tID string) string { return CHANNELS + cID + "/permissions/" + tID } - CHANNEL_INVITES = func(cID string) string { return CHANNELS + cID + "/invites" } - CHANNEL_TYPING = func(cID string) string { return CHANNELS + cID + "/typing" } - CHANNEL_MESSAGES = func(cID string) string { return CHANNELS + cID + "/messages" } - CHANNEL_MESSAGE = func(cID, mID string) string { return CHANNELS + cID + "/messages/" + mID } - CHANNEL_MESSAGE_ACK = func(cID, mID string) string { return CHANNELS + cID + "/messages/" + mID + "/ack" } - - INVITE = func(iID string) string { return API + "invite/" + iID } - - INTEGRATIONS_JOIN = func(iID string) string { return API + "integrations/" + iID + "/join" } - - EMOJI = func(eID string) string { return API + "emojis/" + eID + ".png" } + EndpointStatus = "https://status.discordapp.com/api/v2/" + EndpointSm = EndpointStatus + "scheduled-maintenances/" + EndpointSmActive = EndpointSm + "active.json" + EndpointSmUpcoming = EndpointSm + "upcoming.json" + + EndpointDiscord = "https://discordapp.com/" + EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + EndpointGatewayBot = EndpointGateway + "/bot" + EndpointWebhooks = EndpointAPI + "webhooks/" + + EndpointCDN = "https://cdn.discordapp.com/" + EndpointCDNAttachments = EndpointCDN + "attachments/" + EndpointCDNAvatars = EndpointCDN + "avatars/" + EndpointCDNIcons = EndpointCDN + "icons/" + EndpointCDNSplashes = EndpointCDN + "splashes/" + EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" + + EndpointAuth = EndpointAPI + "auth/" + EndpointLogin = EndpointAuth + "login" + EndpointLogout = EndpointAuth + "logout" + EndpointVerify = EndpointAuth + "verify" + EndpointVerifyResend = EndpointAuth + "verify/resend" + EndpointForgotPassword = EndpointAuth + "forgot" + EndpointResetPassword = EndpointAuth + "reset" + EndpointRegister = EndpointAuth + "register" + + EndpointVoice = EndpointAPI + "/voice/" + EndpointVoiceRegions = EndpointVoice + "regions" + EndpointVoiceIce = EndpointVoice + "ice" + + EndpointTutorial = EndpointAPI + "tutorial/" + EndpointTutorialIndicators = EndpointTutorial + "indicators" + + EndpointTrack = EndpointAPI + "track" + EndpointSso = EndpointAPI + "sso" + EndpointReport = EndpointAPI + "report" + EndpointIntegrations = EndpointAPI + "integrations" + + EndpointUser = func(uID string) string { return EndpointUsers + uID } + EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } + EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } + + EndpointGuild = func(gID string) string { return EndpointGuilds + gID } + EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } + EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } + EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } + EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } + EndpointGuildMemberRole = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID } + EndpointGuildBans = func(gID string) string { return EndpointGuilds + gID + "/bans" } + EndpointGuildBan = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID } + EndpointGuildIntegrations = func(gID string) string { return EndpointGuilds + gID + "/integrations" } + EndpointGuildIntegration = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID } + EndpointGuildIntegrationSync = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID + "/sync" } + EndpointGuildRoles = func(gID string) string { return EndpointGuilds + gID + "/roles" } + EndpointGuildRole = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID } + EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } + EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" } + EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } + EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } + EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } + EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } + + EndpointChannel = func(cID string) string { return EndpointChannels + cID } + EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } + EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID } + EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" } + EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" } + EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } + EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } + EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } + EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } + EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } + EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } + + EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } + + EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } + EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } + EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } + + EndpointMessageReactionsAll = func(cID, mID string) string { + return EndpointChannelMessage(cID, mID) + "/reactions" + } + EndpointMessageReactions = func(cID, mID, eID string) string { + return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + } + EndpointMessageReaction = func(cID, mID, eID, uID string) string { + return EndpointMessageReactions(cID, mID, eID) + "/" + uID + } + + EndpointRelationships = func() string { return EndpointUsers + "@me" + "/relationships" } + EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } + EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } + + EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } + + EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } + + EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + + EndpointOauth2 = EndpointAPI + "oauth2/" + EndpointApplications = EndpointOauth2 + "applications" + EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } + EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" } ) diff --git a/DiscordGo/event.go b/DiscordGo/event.go new file mode 100644 index 0000000000000000000000000000000000000000..3a03f46dd6860a31883bd8e9e7a5764e04bbd6d4 --- /dev/null +++ b/DiscordGo/event.go @@ -0,0 +1,238 @@ +package discordgo + +// EventHandler is an interface for Discord events. +type EventHandler interface { + // Type returns the type of event this handler belongs to. + Type() string + + // Handle is called whenever an event of Type() happens. + // It is the recievers responsibility to type assert that the interface + // is the expected struct. + Handle(*Session, interface{}) +} + +// EventInterfaceProvider is an interface for providing empty interfaces for +// Discord events. +type EventInterfaceProvider interface { + // Type is the type of event this handler belongs to. + Type() string + + // New returns a new instance of the struct this event handler handles. + // This is called once per event. + // The struct is provided to all handlers of the same Type(). + New() interface{} +} + +// interfaceEventType is the event handler type for interface{} events. +const interfaceEventType = "__INTERFACE__" + +// interfaceEventHandler is an event handler for interface{} events. +type interfaceEventHandler func(*Session, interface{}) + +// Type returns the event type for interface{} events. +func (eh interfaceEventHandler) Type() string { + return interfaceEventType +} + +// Handle is the handler for an interface{} event. +func (eh interfaceEventHandler) Handle(s *Session, i interface{}) { + eh(s, i) +} + +var registeredInterfaceProviders = map[string]EventInterfaceProvider{} + +// registerInterfaceProvider registers a provider so that DiscordGo can +// access it's New() method. +func registerInterfaceProvider(eh EventInterfaceProvider) { + if _, ok := registeredInterfaceProviders[eh.Type()]; ok { + return + // XXX: + // if we should error here, we need to do something with it. + // fmt.Errorf("event %s already registered", eh.Type()) + } + registeredInterfaceProviders[eh.Type()] = eh + return +} + +// eventHandlerInstance is a wrapper around an event handler, as functions +// cannot be compared directly. +type eventHandlerInstance struct { + eventHandler EventHandler +} + +// addEventHandler adds an event handler that will be fired anytime +// the Discord WSAPI matching eventHandler.Type() fires. +func (s *Session) addEventHandler(eventHandler EventHandler) func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + if s.handlers == nil { + s.handlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + s.handlers[eventHandler.Type()] = append(s.handlers[eventHandler.Type()], ehi) + + return func() { + s.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// addEventHandler adds an event handler that will be fired the next time +// the Discord WSAPI matching eventHandler.Type() fires. +func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + if s.onceHandlers == nil { + s.onceHandlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + s.onceHandlers[eventHandler.Type()] = append(s.onceHandlers[eventHandler.Type()], ehi) + + return func() { + s.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// AddHandler allows you to add an event handler that will be fired anytime +// the Discord WSAPI event that matches the function fires. +// events.go contains all the Discord WSAPI events that can be fired. +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +// The return value of this method is a function, that when called will remove the +// event handler. +func (s *Session) AddHandler(handler interface{}) func() { + eh := handlerForInterface(handler) + + if eh == nil { + s.log(LogError, "Invalid handler type, handler will never be called") + return func() {} + } + + return s.addEventHandler(eh) +} + +// AddHandlerOnce allows you to add an event handler that will be fired the next time +// the Discord WSAPI event that matches the function fires. +// See AddHandler for more details. +func (s *Session) AddHandlerOnce(handler interface{}) func() { + eh := handlerForInterface(handler) + + if eh == nil { + s.log(LogError, "Invalid handler type, handler will never be called") + return func() {} + } + + return s.addEventHandlerOnce(eh) +} + +// removeEventHandler instance removes an event handler instance. +func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance) { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + handlers := s.handlers[t] + for i := range handlers { + if handlers[i] == ehi { + s.handlers[t] = append(handlers[:i], handlers[i+1:]...) + } + } + + onceHandlers := s.onceHandlers[t] + for i := range onceHandlers { + if onceHandlers[i] == ehi { + s.onceHandlers[t] = append(onceHandlers[:i], handlers[i+1:]...) + } + } +} + +// Handles calling permanent and once handlers for an event type. +func (s *Session) handle(t string, i interface{}) { + for _, eh := range s.handlers[t] { + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } + } + + if len(s.onceHandlers[t]) > 0 { + for _, eh := range s.onceHandlers[t] { + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } + } + s.onceHandlers[t] = nil + } +} + +// Handles an event type by calling internal methods, firing handlers and firing the +// interface{} event. +func (s *Session) handleEvent(t string, i interface{}) { + s.handlersMu.RLock() + defer s.handlersMu.RUnlock() + + // All events are dispatched internally first. + s.onInterface(i) + + // Then they are dispatched to anyone handling interface{} events. + s.handle(interfaceEventType, i) + + // Finally they are dispatched to any typed handlers. + s.handle(t, i) +} + +// setGuildIds will set the GuildID on all the members of a guild. +// This is done as event data does not have it set. +func setGuildIds(g *Guild) { + for _, c := range g.Channels { + c.GuildID = g.ID + } + + for _, m := range g.Members { + m.GuildID = g.ID + } + + for _, vs := range g.VoiceStates { + vs.GuildID = g.ID + } +} + +// onInterface handles all internal events and routes them to the appropriate internal handler. +func (s *Session) onInterface(i interface{}) { + switch t := i.(type) { + case *Ready: + for _, g := range t.Guilds { + setGuildIds(g) + } + s.onReady(t) + case *GuildCreate: + setGuildIds(t.Guild) + case *GuildUpdate: + setGuildIds(t.Guild) + case *VoiceServerUpdate: + go s.onVoiceServerUpdate(t) + case *VoiceStateUpdate: + go s.onVoiceStateUpdate(t) + } + err := s.State.OnInterface(s, i) + if err != nil { + s.log(LogDebug, "error dispatching internal event, %s", err) + } +} + +// onReady handles the ready event. +func (s *Session) onReady(r *Ready) { + + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID +} diff --git a/DiscordGo/eventhandlers.go b/DiscordGo/eventhandlers.go new file mode 100644 index 0000000000000000000000000000000000000000..5cc157de2c4a109d917d78ab5b5b806d6580510c --- /dev/null +++ b/DiscordGo/eventhandlers.go @@ -0,0 +1,1030 @@ +// Code generated by \"eventhandlers\"; DO NOT EDIT +// See events.go + +package discordgo + +// Following are all the event types. +// Event type values are used to match the events returned by Discord. +// EventTypes surrounded by __ are synthetic and are internal to DiscordGo. +const ( + channelCreateEventType = "CHANNEL_CREATE" + channelDeleteEventType = "CHANNEL_DELETE" + channelPinsUpdateEventType = "CHANNEL_PINS_UPDATE" + channelUpdateEventType = "CHANNEL_UPDATE" + connectEventType = "__CONNECT__" + disconnectEventType = "__DISCONNECT__" + eventEventType = "__EVENT__" + guildBanAddEventType = "GUILD_BAN_ADD" + guildBanRemoveEventType = "GUILD_BAN_REMOVE" + guildCreateEventType = "GUILD_CREATE" + guildDeleteEventType = "GUILD_DELETE" + guildEmojisUpdateEventType = "GUILD_EMOJIS_UPDATE" + guildIntegrationsUpdateEventType = "GUILD_INTEGRATIONS_UPDATE" + guildMemberAddEventType = "GUILD_MEMBER_ADD" + guildMemberRemoveEventType = "GUILD_MEMBER_REMOVE" + guildMemberUpdateEventType = "GUILD_MEMBER_UPDATE" + guildMembersChunkEventType = "GUILD_MEMBERS_CHUNK" + guildRoleCreateEventType = "GUILD_ROLE_CREATE" + guildRoleDeleteEventType = "GUILD_ROLE_DELETE" + guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" + guildUpdateEventType = "GUILD_UPDATE" + messageAckEventType = "MESSAGE_ACK" + messageCreateEventType = "MESSAGE_CREATE" + messageDeleteEventType = "MESSAGE_DELETE" + messageDeleteBulkEventType = "MESSAGE_DELETE_BULK" + messageReactionAddEventType = "MESSAGE_REACTION_ADD" + messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" + messageReactionRemoveAllEventType = "MESSAGE_REACTION_REMOVE_ALL" + messageUpdateEventType = "MESSAGE_UPDATE" + presenceUpdateEventType = "PRESENCE_UPDATE" + presencesReplaceEventType = "PRESENCES_REPLACE" + rateLimitEventType = "__RATE_LIMIT__" + readyEventType = "READY" + relationshipAddEventType = "RELATIONSHIP_ADD" + relationshipRemoveEventType = "RELATIONSHIP_REMOVE" + resumedEventType = "RESUMED" + typingStartEventType = "TYPING_START" + userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE" + userNoteUpdateEventType = "USER_NOTE_UPDATE" + userSettingsUpdateEventType = "USER_SETTINGS_UPDATE" + userUpdateEventType = "USER_UPDATE" + voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" + voiceStateUpdateEventType = "VOICE_STATE_UPDATE" +) + +// channelCreateEventHandler is an event handler for ChannelCreate events. +type channelCreateEventHandler func(*Session, *ChannelCreate) + +// Type returns the event type for ChannelCreate events. +func (eh channelCreateEventHandler) Type() string { + return channelCreateEventType +} + +// New returns a new instance of ChannelCreate. +func (eh channelCreateEventHandler) New() interface{} { + return &ChannelCreate{} +} + +// Handle is the handler for ChannelCreate events. +func (eh channelCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelCreate); ok { + eh(s, t) + } +} + +// channelDeleteEventHandler is an event handler for ChannelDelete events. +type channelDeleteEventHandler func(*Session, *ChannelDelete) + +// Type returns the event type for ChannelDelete events. +func (eh channelDeleteEventHandler) Type() string { + return channelDeleteEventType +} + +// New returns a new instance of ChannelDelete. +func (eh channelDeleteEventHandler) New() interface{} { + return &ChannelDelete{} +} + +// Handle is the handler for ChannelDelete events. +func (eh channelDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelDelete); ok { + eh(s, t) + } +} + +// channelPinsUpdateEventHandler is an event handler for ChannelPinsUpdate events. +type channelPinsUpdateEventHandler func(*Session, *ChannelPinsUpdate) + +// Type returns the event type for ChannelPinsUpdate events. +func (eh channelPinsUpdateEventHandler) Type() string { + return channelPinsUpdateEventType +} + +// New returns a new instance of ChannelPinsUpdate. +func (eh channelPinsUpdateEventHandler) New() interface{} { + return &ChannelPinsUpdate{} +} + +// Handle is the handler for ChannelPinsUpdate events. +func (eh channelPinsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelPinsUpdate); ok { + eh(s, t) + } +} + +// channelUpdateEventHandler is an event handler for ChannelUpdate events. +type channelUpdateEventHandler func(*Session, *ChannelUpdate) + +// Type returns the event type for ChannelUpdate events. +func (eh channelUpdateEventHandler) Type() string { + return channelUpdateEventType +} + +// New returns a new instance of ChannelUpdate. +func (eh channelUpdateEventHandler) New() interface{} { + return &ChannelUpdate{} +} + +// Handle is the handler for ChannelUpdate events. +func (eh channelUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelUpdate); ok { + eh(s, t) + } +} + +// connectEventHandler is an event handler for Connect events. +type connectEventHandler func(*Session, *Connect) + +// Type returns the event type for Connect events. +func (eh connectEventHandler) Type() string { + return connectEventType +} + +// Handle is the handler for Connect events. +func (eh connectEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Connect); ok { + eh(s, t) + } +} + +// disconnectEventHandler is an event handler for Disconnect events. +type disconnectEventHandler func(*Session, *Disconnect) + +// Type returns the event type for Disconnect events. +func (eh disconnectEventHandler) Type() string { + return disconnectEventType +} + +// Handle is the handler for Disconnect events. +func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Disconnect); ok { + eh(s, t) + } +} + +// eventEventHandler is an event handler for Event events. +type eventEventHandler func(*Session, *Event) + +// Type returns the event type for Event events. +func (eh eventEventHandler) Type() string { + return eventEventType +} + +// Handle is the handler for Event events. +func (eh eventEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Event); ok { + eh(s, t) + } +} + +// guildBanAddEventHandler is an event handler for GuildBanAdd events. +type guildBanAddEventHandler func(*Session, *GuildBanAdd) + +// Type returns the event type for GuildBanAdd events. +func (eh guildBanAddEventHandler) Type() string { + return guildBanAddEventType +} + +// New returns a new instance of GuildBanAdd. +func (eh guildBanAddEventHandler) New() interface{} { + return &GuildBanAdd{} +} + +// Handle is the handler for GuildBanAdd events. +func (eh guildBanAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildBanAdd); ok { + eh(s, t) + } +} + +// guildBanRemoveEventHandler is an event handler for GuildBanRemove events. +type guildBanRemoveEventHandler func(*Session, *GuildBanRemove) + +// Type returns the event type for GuildBanRemove events. +func (eh guildBanRemoveEventHandler) Type() string { + return guildBanRemoveEventType +} + +// New returns a new instance of GuildBanRemove. +func (eh guildBanRemoveEventHandler) New() interface{} { + return &GuildBanRemove{} +} + +// Handle is the handler for GuildBanRemove events. +func (eh guildBanRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildBanRemove); ok { + eh(s, t) + } +} + +// guildCreateEventHandler is an event handler for GuildCreate events. +type guildCreateEventHandler func(*Session, *GuildCreate) + +// Type returns the event type for GuildCreate events. +func (eh guildCreateEventHandler) Type() string { + return guildCreateEventType +} + +// New returns a new instance of GuildCreate. +func (eh guildCreateEventHandler) New() interface{} { + return &GuildCreate{} +} + +// Handle is the handler for GuildCreate events. +func (eh guildCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildCreate); ok { + eh(s, t) + } +} + +// guildDeleteEventHandler is an event handler for GuildDelete events. +type guildDeleteEventHandler func(*Session, *GuildDelete) + +// Type returns the event type for GuildDelete events. +func (eh guildDeleteEventHandler) Type() string { + return guildDeleteEventType +} + +// New returns a new instance of GuildDelete. +func (eh guildDeleteEventHandler) New() interface{} { + return &GuildDelete{} +} + +// Handle is the handler for GuildDelete events. +func (eh guildDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildDelete); ok { + eh(s, t) + } +} + +// guildEmojisUpdateEventHandler is an event handler for GuildEmojisUpdate events. +type guildEmojisUpdateEventHandler func(*Session, *GuildEmojisUpdate) + +// Type returns the event type for GuildEmojisUpdate events. +func (eh guildEmojisUpdateEventHandler) Type() string { + return guildEmojisUpdateEventType +} + +// New returns a new instance of GuildEmojisUpdate. +func (eh guildEmojisUpdateEventHandler) New() interface{} { + return &GuildEmojisUpdate{} +} + +// Handle is the handler for GuildEmojisUpdate events. +func (eh guildEmojisUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildEmojisUpdate); ok { + eh(s, t) + } +} + +// guildIntegrationsUpdateEventHandler is an event handler for GuildIntegrationsUpdate events. +type guildIntegrationsUpdateEventHandler func(*Session, *GuildIntegrationsUpdate) + +// Type returns the event type for GuildIntegrationsUpdate events. +func (eh guildIntegrationsUpdateEventHandler) Type() string { + return guildIntegrationsUpdateEventType +} + +// New returns a new instance of GuildIntegrationsUpdate. +func (eh guildIntegrationsUpdateEventHandler) New() interface{} { + return &GuildIntegrationsUpdate{} +} + +// Handle is the handler for GuildIntegrationsUpdate events. +func (eh guildIntegrationsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildIntegrationsUpdate); ok { + eh(s, t) + } +} + +// guildMemberAddEventHandler is an event handler for GuildMemberAdd events. +type guildMemberAddEventHandler func(*Session, *GuildMemberAdd) + +// Type returns the event type for GuildMemberAdd events. +func (eh guildMemberAddEventHandler) Type() string { + return guildMemberAddEventType +} + +// New returns a new instance of GuildMemberAdd. +func (eh guildMemberAddEventHandler) New() interface{} { + return &GuildMemberAdd{} +} + +// Handle is the handler for GuildMemberAdd events. +func (eh guildMemberAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberAdd); ok { + eh(s, t) + } +} + +// guildMemberRemoveEventHandler is an event handler for GuildMemberRemove events. +type guildMemberRemoveEventHandler func(*Session, *GuildMemberRemove) + +// Type returns the event type for GuildMemberRemove events. +func (eh guildMemberRemoveEventHandler) Type() string { + return guildMemberRemoveEventType +} + +// New returns a new instance of GuildMemberRemove. +func (eh guildMemberRemoveEventHandler) New() interface{} { + return &GuildMemberRemove{} +} + +// Handle is the handler for GuildMemberRemove events. +func (eh guildMemberRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberRemove); ok { + eh(s, t) + } +} + +// guildMemberUpdateEventHandler is an event handler for GuildMemberUpdate events. +type guildMemberUpdateEventHandler func(*Session, *GuildMemberUpdate) + +// Type returns the event type for GuildMemberUpdate events. +func (eh guildMemberUpdateEventHandler) Type() string { + return guildMemberUpdateEventType +} + +// New returns a new instance of GuildMemberUpdate. +func (eh guildMemberUpdateEventHandler) New() interface{} { + return &GuildMemberUpdate{} +} + +// Handle is the handler for GuildMemberUpdate events. +func (eh guildMemberUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberUpdate); ok { + eh(s, t) + } +} + +// guildMembersChunkEventHandler is an event handler for GuildMembersChunk events. +type guildMembersChunkEventHandler func(*Session, *GuildMembersChunk) + +// Type returns the event type for GuildMembersChunk events. +func (eh guildMembersChunkEventHandler) Type() string { + return guildMembersChunkEventType +} + +// New returns a new instance of GuildMembersChunk. +func (eh guildMembersChunkEventHandler) New() interface{} { + return &GuildMembersChunk{} +} + +// Handle is the handler for GuildMembersChunk events. +func (eh guildMembersChunkEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMembersChunk); ok { + eh(s, t) + } +} + +// guildRoleCreateEventHandler is an event handler for GuildRoleCreate events. +type guildRoleCreateEventHandler func(*Session, *GuildRoleCreate) + +// Type returns the event type for GuildRoleCreate events. +func (eh guildRoleCreateEventHandler) Type() string { + return guildRoleCreateEventType +} + +// New returns a new instance of GuildRoleCreate. +func (eh guildRoleCreateEventHandler) New() interface{} { + return &GuildRoleCreate{} +} + +// Handle is the handler for GuildRoleCreate events. +func (eh guildRoleCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleCreate); ok { + eh(s, t) + } +} + +// guildRoleDeleteEventHandler is an event handler for GuildRoleDelete events. +type guildRoleDeleteEventHandler func(*Session, *GuildRoleDelete) + +// Type returns the event type for GuildRoleDelete events. +func (eh guildRoleDeleteEventHandler) Type() string { + return guildRoleDeleteEventType +} + +// New returns a new instance of GuildRoleDelete. +func (eh guildRoleDeleteEventHandler) New() interface{} { + return &GuildRoleDelete{} +} + +// Handle is the handler for GuildRoleDelete events. +func (eh guildRoleDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleDelete); ok { + eh(s, t) + } +} + +// guildRoleUpdateEventHandler is an event handler for GuildRoleUpdate events. +type guildRoleUpdateEventHandler func(*Session, *GuildRoleUpdate) + +// Type returns the event type for GuildRoleUpdate events. +func (eh guildRoleUpdateEventHandler) Type() string { + return guildRoleUpdateEventType +} + +// New returns a new instance of GuildRoleUpdate. +func (eh guildRoleUpdateEventHandler) New() interface{} { + return &GuildRoleUpdate{} +} + +// Handle is the handler for GuildRoleUpdate events. +func (eh guildRoleUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleUpdate); ok { + eh(s, t) + } +} + +// guildUpdateEventHandler is an event handler for GuildUpdate events. +type guildUpdateEventHandler func(*Session, *GuildUpdate) + +// Type returns the event type for GuildUpdate events. +func (eh guildUpdateEventHandler) Type() string { + return guildUpdateEventType +} + +// New returns a new instance of GuildUpdate. +func (eh guildUpdateEventHandler) New() interface{} { + return &GuildUpdate{} +} + +// Handle is the handler for GuildUpdate events. +func (eh guildUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildUpdate); ok { + eh(s, t) + } +} + +// messageAckEventHandler is an event handler for MessageAck events. +type messageAckEventHandler func(*Session, *MessageAck) + +// Type returns the event type for MessageAck events. +func (eh messageAckEventHandler) Type() string { + return messageAckEventType +} + +// New returns a new instance of MessageAck. +func (eh messageAckEventHandler) New() interface{} { + return &MessageAck{} +} + +// Handle is the handler for MessageAck events. +func (eh messageAckEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageAck); ok { + eh(s, t) + } +} + +// messageCreateEventHandler is an event handler for MessageCreate events. +type messageCreateEventHandler func(*Session, *MessageCreate) + +// Type returns the event type for MessageCreate events. +func (eh messageCreateEventHandler) Type() string { + return messageCreateEventType +} + +// New returns a new instance of MessageCreate. +func (eh messageCreateEventHandler) New() interface{} { + return &MessageCreate{} +} + +// Handle is the handler for MessageCreate events. +func (eh messageCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageCreate); ok { + eh(s, t) + } +} + +// messageDeleteEventHandler is an event handler for MessageDelete events. +type messageDeleteEventHandler func(*Session, *MessageDelete) + +// Type returns the event type for MessageDelete events. +func (eh messageDeleteEventHandler) Type() string { + return messageDeleteEventType +} + +// New returns a new instance of MessageDelete. +func (eh messageDeleteEventHandler) New() interface{} { + return &MessageDelete{} +} + +// Handle is the handler for MessageDelete events. +func (eh messageDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageDelete); ok { + eh(s, t) + } +} + +// messageDeleteBulkEventHandler is an event handler for MessageDeleteBulk events. +type messageDeleteBulkEventHandler func(*Session, *MessageDeleteBulk) + +// Type returns the event type for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Type() string { + return messageDeleteBulkEventType +} + +// New returns a new instance of MessageDeleteBulk. +func (eh messageDeleteBulkEventHandler) New() interface{} { + return &MessageDeleteBulk{} +} + +// Handle is the handler for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageDeleteBulk); ok { + eh(s, t) + } +} + +// messageReactionAddEventHandler is an event handler for MessageReactionAdd events. +type messageReactionAddEventHandler func(*Session, *MessageReactionAdd) + +// Type returns the event type for MessageReactionAdd events. +func (eh messageReactionAddEventHandler) Type() string { + return messageReactionAddEventType +} + +// New returns a new instance of MessageReactionAdd. +func (eh messageReactionAddEventHandler) New() interface{} { + return &MessageReactionAdd{} +} + +// Handle is the handler for MessageReactionAdd events. +func (eh messageReactionAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionAdd); ok { + eh(s, t) + } +} + +// messageReactionRemoveEventHandler is an event handler for MessageReactionRemove events. +type messageReactionRemoveEventHandler func(*Session, *MessageReactionRemove) + +// Type returns the event type for MessageReactionRemove events. +func (eh messageReactionRemoveEventHandler) Type() string { + return messageReactionRemoveEventType +} + +// New returns a new instance of MessageReactionRemove. +func (eh messageReactionRemoveEventHandler) New() interface{} { + return &MessageReactionRemove{} +} + +// Handle is the handler for MessageReactionRemove events. +func (eh messageReactionRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionRemove); ok { + eh(s, t) + } +} + +// messageReactionRemoveAllEventHandler is an event handler for MessageReactionRemoveAll events. +type messageReactionRemoveAllEventHandler func(*Session, *MessageReactionRemoveAll) + +// Type returns the event type for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Type() string { + return messageReactionRemoveAllEventType +} + +// New returns a new instance of MessageReactionRemoveAll. +func (eh messageReactionRemoveAllEventHandler) New() interface{} { + return &MessageReactionRemoveAll{} +} + +// Handle is the handler for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionRemoveAll); ok { + eh(s, t) + } +} + +// messageUpdateEventHandler is an event handler for MessageUpdate events. +type messageUpdateEventHandler func(*Session, *MessageUpdate) + +// Type returns the event type for MessageUpdate events. +func (eh messageUpdateEventHandler) Type() string { + return messageUpdateEventType +} + +// New returns a new instance of MessageUpdate. +func (eh messageUpdateEventHandler) New() interface{} { + return &MessageUpdate{} +} + +// Handle is the handler for MessageUpdate events. +func (eh messageUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageUpdate); ok { + eh(s, t) + } +} + +// presenceUpdateEventHandler is an event handler for PresenceUpdate events. +type presenceUpdateEventHandler func(*Session, *PresenceUpdate) + +// Type returns the event type for PresenceUpdate events. +func (eh presenceUpdateEventHandler) Type() string { + return presenceUpdateEventType +} + +// New returns a new instance of PresenceUpdate. +func (eh presenceUpdateEventHandler) New() interface{} { + return &PresenceUpdate{} +} + +// Handle is the handler for PresenceUpdate events. +func (eh presenceUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*PresenceUpdate); ok { + eh(s, t) + } +} + +// presencesReplaceEventHandler is an event handler for PresencesReplace events. +type presencesReplaceEventHandler func(*Session, *PresencesReplace) + +// Type returns the event type for PresencesReplace events. +func (eh presencesReplaceEventHandler) Type() string { + return presencesReplaceEventType +} + +// New returns a new instance of PresencesReplace. +func (eh presencesReplaceEventHandler) New() interface{} { + return &PresencesReplace{} +} + +// Handle is the handler for PresencesReplace events. +func (eh presencesReplaceEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*PresencesReplace); ok { + eh(s, t) + } +} + +// rateLimitEventHandler is an event handler for RateLimit events. +type rateLimitEventHandler func(*Session, *RateLimit) + +// Type returns the event type for RateLimit events. +func (eh rateLimitEventHandler) Type() string { + return rateLimitEventType +} + +// Handle is the handler for RateLimit events. +func (eh rateLimitEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RateLimit); ok { + eh(s, t) + } +} + +// readyEventHandler is an event handler for Ready events. +type readyEventHandler func(*Session, *Ready) + +// Type returns the event type for Ready events. +func (eh readyEventHandler) Type() string { + return readyEventType +} + +// New returns a new instance of Ready. +func (eh readyEventHandler) New() interface{} { + return &Ready{} +} + +// Handle is the handler for Ready events. +func (eh readyEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Ready); ok { + eh(s, t) + } +} + +// relationshipAddEventHandler is an event handler for RelationshipAdd events. +type relationshipAddEventHandler func(*Session, *RelationshipAdd) + +// Type returns the event type for RelationshipAdd events. +func (eh relationshipAddEventHandler) Type() string { + return relationshipAddEventType +} + +// New returns a new instance of RelationshipAdd. +func (eh relationshipAddEventHandler) New() interface{} { + return &RelationshipAdd{} +} + +// Handle is the handler for RelationshipAdd events. +func (eh relationshipAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RelationshipAdd); ok { + eh(s, t) + } +} + +// relationshipRemoveEventHandler is an event handler for RelationshipRemove events. +type relationshipRemoveEventHandler func(*Session, *RelationshipRemove) + +// Type returns the event type for RelationshipRemove events. +func (eh relationshipRemoveEventHandler) Type() string { + return relationshipRemoveEventType +} + +// New returns a new instance of RelationshipRemove. +func (eh relationshipRemoveEventHandler) New() interface{} { + return &RelationshipRemove{} +} + +// Handle is the handler for RelationshipRemove events. +func (eh relationshipRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RelationshipRemove); ok { + eh(s, t) + } +} + +// resumedEventHandler is an event handler for Resumed events. +type resumedEventHandler func(*Session, *Resumed) + +// Type returns the event type for Resumed events. +func (eh resumedEventHandler) Type() string { + return resumedEventType +} + +// New returns a new instance of Resumed. +func (eh resumedEventHandler) New() interface{} { + return &Resumed{} +} + +// Handle is the handler for Resumed events. +func (eh resumedEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Resumed); ok { + eh(s, t) + } +} + +// typingStartEventHandler is an event handler for TypingStart events. +type typingStartEventHandler func(*Session, *TypingStart) + +// Type returns the event type for TypingStart events. +func (eh typingStartEventHandler) Type() string { + return typingStartEventType +} + +// New returns a new instance of TypingStart. +func (eh typingStartEventHandler) New() interface{} { + return &TypingStart{} +} + +// Handle is the handler for TypingStart events. +func (eh typingStartEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*TypingStart); ok { + eh(s, t) + } +} + +// userGuildSettingsUpdateEventHandler is an event handler for UserGuildSettingsUpdate events. +type userGuildSettingsUpdateEventHandler func(*Session, *UserGuildSettingsUpdate) + +// Type returns the event type for UserGuildSettingsUpdate events. +func (eh userGuildSettingsUpdateEventHandler) Type() string { + return userGuildSettingsUpdateEventType +} + +// New returns a new instance of UserGuildSettingsUpdate. +func (eh userGuildSettingsUpdateEventHandler) New() interface{} { + return &UserGuildSettingsUpdate{} +} + +// Handle is the handler for UserGuildSettingsUpdate events. +func (eh userGuildSettingsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserGuildSettingsUpdate); ok { + eh(s, t) + } +} + +// userNoteUpdateEventHandler is an event handler for UserNoteUpdate events. +type userNoteUpdateEventHandler func(*Session, *UserNoteUpdate) + +// Type returns the event type for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Type() string { + return userNoteUpdateEventType +} + +// New returns a new instance of UserNoteUpdate. +func (eh userNoteUpdateEventHandler) New() interface{} { + return &UserNoteUpdate{} +} + +// Handle is the handler for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserNoteUpdate); ok { + eh(s, t) + } +} + +// userSettingsUpdateEventHandler is an event handler for UserSettingsUpdate events. +type userSettingsUpdateEventHandler func(*Session, *UserSettingsUpdate) + +// Type returns the event type for UserSettingsUpdate events. +func (eh userSettingsUpdateEventHandler) Type() string { + return userSettingsUpdateEventType +} + +// New returns a new instance of UserSettingsUpdate. +func (eh userSettingsUpdateEventHandler) New() interface{} { + return &UserSettingsUpdate{} +} + +// Handle is the handler for UserSettingsUpdate events. +func (eh userSettingsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserSettingsUpdate); ok { + eh(s, t) + } +} + +// userUpdateEventHandler is an event handler for UserUpdate events. +type userUpdateEventHandler func(*Session, *UserUpdate) + +// Type returns the event type for UserUpdate events. +func (eh userUpdateEventHandler) Type() string { + return userUpdateEventType +} + +// New returns a new instance of UserUpdate. +func (eh userUpdateEventHandler) New() interface{} { + return &UserUpdate{} +} + +// Handle is the handler for UserUpdate events. +func (eh userUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserUpdate); ok { + eh(s, t) + } +} + +// voiceServerUpdateEventHandler is an event handler for VoiceServerUpdate events. +type voiceServerUpdateEventHandler func(*Session, *VoiceServerUpdate) + +// Type returns the event type for VoiceServerUpdate events. +func (eh voiceServerUpdateEventHandler) Type() string { + return voiceServerUpdateEventType +} + +// New returns a new instance of VoiceServerUpdate. +func (eh voiceServerUpdateEventHandler) New() interface{} { + return &VoiceServerUpdate{} +} + +// Handle is the handler for VoiceServerUpdate events. +func (eh voiceServerUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*VoiceServerUpdate); ok { + eh(s, t) + } +} + +// voiceStateUpdateEventHandler is an event handler for VoiceStateUpdate events. +type voiceStateUpdateEventHandler func(*Session, *VoiceStateUpdate) + +// Type returns the event type for VoiceStateUpdate events. +func (eh voiceStateUpdateEventHandler) Type() string { + return voiceStateUpdateEventType +} + +// New returns a new instance of VoiceStateUpdate. +func (eh voiceStateUpdateEventHandler) New() interface{} { + return &VoiceStateUpdate{} +} + +// Handle is the handler for VoiceStateUpdate events. +func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*VoiceStateUpdate); ok { + eh(s, t) + } +} + +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Session, interface{}): + return interfaceEventHandler(v) + case func(*Session, *ChannelCreate): + return channelCreateEventHandler(v) + case func(*Session, *ChannelDelete): + return channelDeleteEventHandler(v) + case func(*Session, *ChannelPinsUpdate): + return channelPinsUpdateEventHandler(v) + case func(*Session, *ChannelUpdate): + return channelUpdateEventHandler(v) + case func(*Session, *Connect): + return connectEventHandler(v) + case func(*Session, *Disconnect): + return disconnectEventHandler(v) + case func(*Session, *Event): + return eventEventHandler(v) + case func(*Session, *GuildBanAdd): + return guildBanAddEventHandler(v) + case func(*Session, *GuildBanRemove): + return guildBanRemoveEventHandler(v) + case func(*Session, *GuildCreate): + return guildCreateEventHandler(v) + case func(*Session, *GuildDelete): + return guildDeleteEventHandler(v) + case func(*Session, *GuildEmojisUpdate): + return guildEmojisUpdateEventHandler(v) + case func(*Session, *GuildIntegrationsUpdate): + return guildIntegrationsUpdateEventHandler(v) + case func(*Session, *GuildMemberAdd): + return guildMemberAddEventHandler(v) + case func(*Session, *GuildMemberRemove): + return guildMemberRemoveEventHandler(v) + case func(*Session, *GuildMemberUpdate): + return guildMemberUpdateEventHandler(v) + case func(*Session, *GuildMembersChunk): + return guildMembersChunkEventHandler(v) + case func(*Session, *GuildRoleCreate): + return guildRoleCreateEventHandler(v) + case func(*Session, *GuildRoleDelete): + return guildRoleDeleteEventHandler(v) + case func(*Session, *GuildRoleUpdate): + return guildRoleUpdateEventHandler(v) + case func(*Session, *GuildUpdate): + return guildUpdateEventHandler(v) + case func(*Session, *MessageAck): + return messageAckEventHandler(v) + case func(*Session, *MessageCreate): + return messageCreateEventHandler(v) + case func(*Session, *MessageDelete): + return messageDeleteEventHandler(v) + case func(*Session, *MessageDeleteBulk): + return messageDeleteBulkEventHandler(v) + case func(*Session, *MessageReactionAdd): + return messageReactionAddEventHandler(v) + case func(*Session, *MessageReactionRemove): + return messageReactionRemoveEventHandler(v) + case func(*Session, *MessageReactionRemoveAll): + return messageReactionRemoveAllEventHandler(v) + case func(*Session, *MessageUpdate): + return messageUpdateEventHandler(v) + case func(*Session, *PresenceUpdate): + return presenceUpdateEventHandler(v) + case func(*Session, *PresencesReplace): + return presencesReplaceEventHandler(v) + case func(*Session, *RateLimit): + return rateLimitEventHandler(v) + case func(*Session, *Ready): + return readyEventHandler(v) + case func(*Session, *RelationshipAdd): + return relationshipAddEventHandler(v) + case func(*Session, *RelationshipRemove): + return relationshipRemoveEventHandler(v) + case func(*Session, *Resumed): + return resumedEventHandler(v) + case func(*Session, *TypingStart): + return typingStartEventHandler(v) + case func(*Session, *UserGuildSettingsUpdate): + return userGuildSettingsUpdateEventHandler(v) + case func(*Session, *UserNoteUpdate): + return userNoteUpdateEventHandler(v) + case func(*Session, *UserSettingsUpdate): + return userSettingsUpdateEventHandler(v) + case func(*Session, *UserUpdate): + return userUpdateEventHandler(v) + case func(*Session, *VoiceServerUpdate): + return voiceServerUpdateEventHandler(v) + case func(*Session, *VoiceStateUpdate): + return voiceStateUpdateEventHandler(v) + } + + return nil +} + +func init() { + registerInterfaceProvider(channelCreateEventHandler(nil)) + registerInterfaceProvider(channelDeleteEventHandler(nil)) + registerInterfaceProvider(channelPinsUpdateEventHandler(nil)) + registerInterfaceProvider(channelUpdateEventHandler(nil)) + registerInterfaceProvider(guildBanAddEventHandler(nil)) + registerInterfaceProvider(guildBanRemoveEventHandler(nil)) + registerInterfaceProvider(guildCreateEventHandler(nil)) + registerInterfaceProvider(guildDeleteEventHandler(nil)) + registerInterfaceProvider(guildEmojisUpdateEventHandler(nil)) + registerInterfaceProvider(guildIntegrationsUpdateEventHandler(nil)) + registerInterfaceProvider(guildMemberAddEventHandler(nil)) + registerInterfaceProvider(guildMemberRemoveEventHandler(nil)) + registerInterfaceProvider(guildMemberUpdateEventHandler(nil)) + registerInterfaceProvider(guildMembersChunkEventHandler(nil)) + registerInterfaceProvider(guildRoleCreateEventHandler(nil)) + registerInterfaceProvider(guildRoleDeleteEventHandler(nil)) + registerInterfaceProvider(guildRoleUpdateEventHandler(nil)) + registerInterfaceProvider(guildUpdateEventHandler(nil)) + registerInterfaceProvider(messageAckEventHandler(nil)) + registerInterfaceProvider(messageCreateEventHandler(nil)) + registerInterfaceProvider(messageDeleteEventHandler(nil)) + registerInterfaceProvider(messageDeleteBulkEventHandler(nil)) + registerInterfaceProvider(messageReactionAddEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveAllEventHandler(nil)) + registerInterfaceProvider(messageUpdateEventHandler(nil)) + registerInterfaceProvider(presenceUpdateEventHandler(nil)) + registerInterfaceProvider(presencesReplaceEventHandler(nil)) + registerInterfaceProvider(readyEventHandler(nil)) + registerInterfaceProvider(relationshipAddEventHandler(nil)) + registerInterfaceProvider(relationshipRemoveEventHandler(nil)) + registerInterfaceProvider(resumedEventHandler(nil)) + registerInterfaceProvider(typingStartEventHandler(nil)) + registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userNoteUpdateEventHandler(nil)) + registerInterfaceProvider(userSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userUpdateEventHandler(nil)) + registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) + registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) +} diff --git a/DiscordGo/events.go b/DiscordGo/events.go index 8bc83bceb70beabce48173ec72b0ea4f7db3342f..c78fbdd2c18162ff0f5fd02aa03df780e31a2920 100644 --- a/DiscordGo/events.go +++ b/DiscordGo/events.go @@ -1,150 +1,253 @@ package discordgo -// eventToInterface is a mapping of Discord WSAPI events to their -// DiscordGo event container. -// Each Discord WSAPI event maps to a unique interface. -// Use Session.AddHandler with one of these types to handle that -// type of event. -// eg: -// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { -// }) -// -// or: -// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { -// }) -var eventToInterface = map[string]interface{}{ - "CHANNEL_CREATE": ChannelCreate{}, - "CHANNEL_UPDATE": ChannelUpdate{}, - "CHANNEL_DELETE": ChannelDelete{}, - "GUILD_CREATE": GuildCreate{}, - "GUILD_UPDATE": GuildUpdate{}, - "GUILD_DELETE": GuildDelete{}, - "GUILD_BAN_ADD": GuildBanAdd{}, - "GUILD_BAN_REMOVE": GuildBanRemove{}, - "GUILD_MEMBER_ADD": GuildMemberAdd{}, - "GUILD_MEMBER_UPDATE": GuildMemberUpdate{}, - "GUILD_MEMBER_REMOVE": GuildMemberRemove{}, - "GUILD_ROLE_CREATE": GuildRoleCreate{}, - "GUILD_ROLE_UPDATE": GuildRoleUpdate{}, - "GUILD_ROLE_DELETE": GuildRoleDelete{}, - "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{}, - "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{}, - "MESSAGE_ACK": MessageAck{}, - "MESSAGE_CREATE": MessageCreate{}, - "MESSAGE_UPDATE": MessageUpdate{}, - "MESSAGE_DELETE": MessageDelete{}, - "PRESENCE_UPDATE": PresenceUpdate{}, - "READY": Ready{}, - "USER_UPDATE": UserUpdate{}, - "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, - "TYPING_START": TypingStart{}, - "VOICE_SERVER_UPDATE": VoiceServerUpdate{}, - "VOICE_STATE_UPDATE": VoiceStateUpdate{}, - "MESSAGE_REACTION_ADD": ReactionRemove{}, - "MESSAGE_REACTION_REMOVE": ReactionAdd{}, -} - -// ReactionRemove is an empty struct for an event. -type ReactionRemove struct{} - -// ReactionAdd is an empty struct for an event. -type ReactionAdd struct{} - -// Connect is an empty struct for an event. -type Connect struct{} +import ( + "encoding/json" +) -// Disconnect is an empty struct for an event. -type Disconnect struct{} +// This file contains all the possible structs that can be +// handled by AddHandler/EventHandler. +// DO NOT ADD ANYTHING BUT EVENT HANDLER STRUCTS TO THIS FILE. +//go:generate go run tools/cmd/eventhandlers/main.go -// MessageCreate is a wrapper struct for an event. -type MessageCreate struct { - *Message -} - -// MessageUpdate is a wrapper struct for an event. -type MessageUpdate struct { - *Message -} +// Connect is the data for a Connect event. +// This is a sythetic event and is not dispatched by Discord. +type Connect struct{} -// MessageDelete is a wrapper struct for an event. -type MessageDelete struct { - *Message -} +// Disconnect is the data for a Disconnect event. +// This is a sythetic event and is not dispatched by Discord. +type Disconnect struct{} -// ChannelCreate is a wrapper struct for an event. +// RateLimit is the data for a RateLimit event. +// This is a sythetic event and is not dispatched by Discord. +type RateLimit struct { + *TooManyRequests + URL string +} + +// Event provides a basic initial struct for all websocket events. +type Event struct { + Operation int `json:"op"` + Sequence int64 `json:"s"` + Type string `json:"t"` + RawData json.RawMessage `json:"d"` + // Struct contains one of the other types in this file. + Struct interface{} `json:"-"` +} + +// A Ready stores all data for the websocket READY event. +type Ready struct { + Version int `json:"v"` + SessionID string `json:"session_id"` + User *User `json:"user"` + ReadState []*ReadState `json:"read_state"` + PrivateChannels []*Channel `json:"private_channels"` + Guilds []*Guild `json:"guilds"` + + // Undocumented fields + Settings *Settings `json:"user_settings"` + UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` + Relationships []*Relationship `json:"relationships"` + Presences []*Presence `json:"presences"` + Notes map[string]string `json:"notes"` +} + +// ChannelCreate is the data for a ChannelCreate event. type ChannelCreate struct { *Channel } -// ChannelUpdate is a wrapper struct for an event. +// ChannelUpdate is the data for a ChannelUpdate event. type ChannelUpdate struct { *Channel } -// ChannelDelete is a wrapper struct for an event. +// ChannelDelete is the data for a ChannelDelete event. type ChannelDelete struct { *Channel } -// GuildCreate is a wrapper struct for an event. +// ChannelPinsUpdate stores data for a ChannelPinsUpdate event. +type ChannelPinsUpdate struct { + LastPinTimestamp string `json:"last_pin_timestamp"` + ChannelID string `json:"channel_id"` +} + +// GuildCreate is the data for a GuildCreate event. type GuildCreate struct { *Guild } -// GuildUpdate is a wrapper struct for an event. +// GuildUpdate is the data for a GuildUpdate event. type GuildUpdate struct { *Guild } -// GuildDelete is a wrapper struct for an event. +// GuildDelete is the data for a GuildDelete event. type GuildDelete struct { *Guild } -// GuildBanAdd is a wrapper struct for an event. +// GuildBanAdd is the data for a GuildBanAdd event. type GuildBanAdd struct { - *GuildBan + User *User `json:"user"` + GuildID string `json:"guild_id"` } -// GuildBanRemove is a wrapper struct for an event. +// GuildBanRemove is the data for a GuildBanRemove event. type GuildBanRemove struct { - *GuildBan + User *User `json:"user"` + GuildID string `json:"guild_id"` } -// GuildMemberAdd is a wrapper struct for an event. +// GuildMemberAdd is the data for a GuildMemberAdd event. type GuildMemberAdd struct { *Member } -// GuildMemberUpdate is a wrapper struct for an event. +// GuildMemberUpdate is the data for a GuildMemberUpdate event. type GuildMemberUpdate struct { *Member } -// GuildMemberRemove is a wrapper struct for an event. +// GuildMemberRemove is the data for a GuildMemberRemove event. type GuildMemberRemove struct { *Member } -// GuildRoleCreate is a wrapper struct for an event. +// GuildRoleCreate is the data for a GuildRoleCreate event. type GuildRoleCreate struct { *GuildRole } -// GuildRoleUpdate is a wrapper struct for an event. +// GuildRoleUpdate is the data for a GuildRoleUpdate event. type GuildRoleUpdate struct { *GuildRole } -// VoiceStateUpdate is a wrapper struct for an event. -type VoiceStateUpdate struct { - *VoiceState +// A GuildRoleDelete is the data for a GuildRoleDelete event. +type GuildRoleDelete struct { + RoleID string `json:"role_id"` + GuildID string `json:"guild_id"` +} + +// A GuildEmojisUpdate is the data for a guild emoji update event. +type GuildEmojisUpdate struct { + GuildID string `json:"guild_id"` + Emojis []*Emoji `json:"emojis"` +} + +// A GuildMembersChunk is the data for a GuildMembersChunk event. +type GuildMembersChunk struct { + GuildID string `json:"guild_id"` + Members []*Member `json:"members"` +} + +// GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event. +type GuildIntegrationsUpdate struct { + GuildID string `json:"guild_id"` +} + +// MessageAck is the data for a MessageAck event. +type MessageAck struct { + MessageID string `json:"message_id"` + ChannelID string `json:"channel_id"` +} + +// MessageCreate is the data for a MessageCreate event. +type MessageCreate struct { + *Message +} + +// MessageUpdate is the data for a MessageUpdate event. +type MessageUpdate struct { + *Message } -// UserUpdate is a wrapper struct for an event. +// MessageDelete is the data for a MessageDelete event. +type MessageDelete struct { + *Message +} + +// MessageReactionAdd is the data for a MessageReactionAdd event. +type MessageReactionAdd struct { + *MessageReaction +} + +// MessageReactionRemove is the data for a MessageReactionRemove event. +type MessageReactionRemove struct { + *MessageReaction +} + +// MessageReactionRemoveAll is the data for a MessageReactionRemoveAll event. +type MessageReactionRemoveAll struct { + *MessageReaction +} + +// PresencesReplace is the data for a PresencesReplace event. +type PresencesReplace []*Presence + +// PresenceUpdate is the data for a PresenceUpdate event. +type PresenceUpdate struct { + Presence + GuildID string `json:"guild_id"` + Roles []string `json:"roles"` +} + +// Resumed is the data for a Resumed event. +type Resumed struct { + Trace []string `json:"_trace"` +} + +// RelationshipAdd is the data for a RelationshipAdd event. +type RelationshipAdd struct { + *Relationship +} + +// RelationshipRemove is the data for a RelationshipRemove event. +type RelationshipRemove struct { + *Relationship +} + +// TypingStart is the data for a TypingStart event. +type TypingStart struct { + UserID string `json:"user_id"` + ChannelID string `json:"channel_id"` + Timestamp int `json:"timestamp"` +} + +// UserUpdate is the data for a UserUpdate event. type UserUpdate struct { - *UserUpdate + *User } -// UserSettingsUpdate is a map for an event. +// UserSettingsUpdate is the data for a UserSettingsUpdate event. type UserSettingsUpdate map[string]interface{} + +// UserGuildSettingsUpdate is the data for a UserGuildSettingsUpdate event. +type UserGuildSettingsUpdate struct { + *UserGuildSettings +} + +// UserNoteUpdate is the data for a UserNoteUpdate event. +type UserNoteUpdate struct { + ID string `json:"id"` + Note string `json:"note"` +} + +// VoiceServerUpdate is the data for a VoiceServerUpdate event. +type VoiceServerUpdate struct { + Token string `json:"token"` + GuildID string `json:"guild_id"` + Endpoint string `json:"endpoint"` +} + +// VoiceStateUpdate is the data for a VoiceStateUpdate event. +type VoiceStateUpdate struct { + *VoiceState +} + +// MessageDeleteBulk is the data for a MessageDeleteBulk event +type MessageDeleteBulk struct { + Messages []string `json:"ids"` + ChannelID string `json:"channel_id"` +} diff --git a/DiscordGo/examples/README.md b/DiscordGo/examples/README.md new file mode 100644 index 0000000000000000000000000000000000000000..df7975a193eb9891c09a4d45ddf133f8c67f41d3 --- /dev/null +++ b/DiscordGo/examples/README.md @@ -0,0 +1,11 @@ + + +# DiscordGo Examples + +These examples demonstrate how to utilize DiscordGo. + +Please explore the individual folders and give them a try! + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + diff --git a/DiscordGo/examples/airhorn/README.md b/DiscordGo/examples/airhorn/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6d00ae3b05cff982cec335606beaa210e3111e4d --- /dev/null +++ b/DiscordGo/examples/airhorn/README.md @@ -0,0 +1,51 @@ + + +## DiscordGo Airhorn Example + +This example demonstrates how to utilize DiscordGo to listen for an !airhorn +command in a channel and then play a sound to that user's current voice channel. + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + +### Build + +This assumes you already have a working Go environment setup and that +DiscordGo is correctly installed on your system. + +From within the airhorn example folder, run the below command to compile the +example. + +```sh +go build +``` + +### Usage + +``` +Usage of ./airhorn: + -t string + Bot Token +``` + +The below example shows how to start the bot from the airhorn example folder. + +```sh +./airhorn -t YOUR_BOT_TOKEN +``` + +### Creating sounds + +Airhorn bot uses [DCA](https://github.com/bwmarrin/dca) files, which are +pre-computed files that are easy to send to Discord. + +If you would like to create your own DCA files, please use: +* [dca-rs](https://github.com/nstafie/dca-rs) + +See the below example of creating a DCA file from a WAV file. This also works +with MP3, FLAC, and many other file formats. Of course, you will need to +[install](https://github.com/nstafie/dca-rs#installation) dca-rs first :) + +```sh +./dca-rs -i --raw > +``` diff --git a/DiscordGo/examples/airhorn/airhorn.dca b/DiscordGo/examples/airhorn/airhorn.dca new file mode 100644 index 0000000000000000000000000000000000000000..39e71679b6929f719c34c781cc18d4af7b6035a8 GIT binary patch literal 16300 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 V(c0;3Bu+nw=7h&$c{o0m{{w1h!Dave literal 0 HcmV?d00001 diff --git a/DiscordGo/examples/airhorn/main.go b/DiscordGo/examples/airhorn/main.go new file mode 100644 index 0000000000000000000000000000000000000000..21ceb76b66706e936ecdc7ad49bfbca5f95a999a --- /dev/null +++ b/DiscordGo/examples/airhorn/main.go @@ -0,0 +1,211 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/bwmarrin/discordgo" +) + +func init() { + flag.StringVar(&token, "t", "", "Bot Token") + flag.Parse() +} + +var token string +var buffer = make([][]byte, 0) + +func main() { + + if token == "" { + fmt.Println("No token provided. Please run: airhorn -t ") + return + } + + // Load the sound file. + err := loadSound() + if err != nil { + fmt.Println("Error loading sound: ", err) + fmt.Println("Please copy $GOPATH/src/github.com/bwmarrin/examples/airhorn/airhorn.dca to this directory.") + return + } + + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + token) + if err != nil { + fmt.Println("Error creating Discord session: ", err) + return + } + + // Register ready as a callback for the ready events. + dg.AddHandler(ready) + + // Register messageCreate as a callback for the messageCreate events. + dg.AddHandler(messageCreate) + + // Register guildCreate as a callback for the guildCreate events. + dg.AddHandler(guildCreate) + + // Open the websocket and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("Error opening Discord session: ", err) + } + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Airhorn is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() +} + +// This function will be called (due to AddHandler above) when the bot receives +// the "ready" event from Discord. +func ready(s *discordgo.Session, event *discordgo.Ready) { + + // Set the playing status. + s.UpdateStatus(0, "!airhorn") +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + + // check if the message is "!airhorn" + if strings.HasPrefix(m.Content, "!airhorn") { + + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + + // Look for the message sender in that guild's current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + err = playSound(s, g.ID, vs.ChannelID) + if err != nil { + fmt.Println("Error playing sound:", err) + } + + return + } + } + } +} + +// This function will be called (due to AddHandler above) every time a new +// guild is joined. +func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { + + if event.Guild.Unavailable { + return + } + + for _, channel := range event.Guild.Channels { + if channel.ID == event.Guild.ID { + _, _ = s.ChannelMessageSend(channel.ID, "Airhorn is ready! Type !airhorn while in a voice channel to play a sound.") + return + } + } +} + +// loadSound attempts to load an encoded sound file from disk. +func loadSound() error { + + file, err := os.Open("airhorn.dca") + if err != nil { + fmt.Println("Error opening dca file :", err) + return err + } + + var opuslen int16 + + for { + // Read opus frame length from dca file. + err = binary.Read(file, binary.LittleEndian, &opuslen) + + // If this is the end of the file, just return. + if err == io.EOF || err == io.ErrUnexpectedEOF { + err := file.Close() + if err != nil { + return err + } + return nil + } + + if err != nil { + fmt.Println("Error reading from dca file :", err) + return err + } + + // Read encoded pcm from dca file. + InBuf := make([]byte, opuslen) + err = binary.Read(file, binary.LittleEndian, &InBuf) + + // Should not be any end of file errors + if err != nil { + fmt.Println("Error reading from dca file :", err) + return err + } + + // Append encoded pcm data to the buffer. + buffer = append(buffer, InBuf) + } +} + +// playSound plays the current buffer to the provided channel. +func playSound(s *discordgo.Session, guildID, channelID string) (err error) { + + // Join the provided voice channel. + vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) + if err != nil { + return err + } + + // Sleep for a specified amount of time before playing the sound + time.Sleep(250 * time.Millisecond) + + // Start speaking. + vc.Speaking(true) + + // Send the buffer data. + for _, buff := range buffer { + vc.OpusSend <- buff + } + + // Stop speaking + vc.Speaking(false) + + // Sleep for a specificed amount of time before ending. + time.Sleep(250 * time.Millisecond) + + // Disconnect from the provided voice channel. + vc.Disconnect() + + return nil +} diff --git a/DiscordGo/examples/appmaker/README.md b/DiscordGo/examples/appmaker/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e0cc29a66e9d6dde6e6b13212a36201c1ffa9727 --- /dev/null +++ b/DiscordGo/examples/appmaker/README.md @@ -0,0 +1,57 @@ + + +## DiscordGo AppMaker Example + +This example demonstrates how to utilize DiscordGo to create, view, and delete +Bot Applications on your account. + +These tasks are normally accomplished from the +[Discord Developers](https://discordapp.com/developers/applications/me) site. + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + +### Build + +This assumes you already have a working Go environment setup and that +DiscordGo is correctly installed on your system. + +From within the appmaker example folder, run the below command to compile the +example. + +```sh +go build +``` + +### Usage + +This example only uses authentication tokens for authentication. While +user email/password is supported by DiscordGo, it is not recommended. + +``` +./appmaker --help +Usage of ./appmaker: + -d string + Application ID to delete + -l List Applications Only + -n string + Name to give App/Bot + -t string + Owner Account Token +``` + +* Account Token is required. The account will be the "owner" of any bot +applications created. + +* If you provide the **-l** flag than appmaker will only display a list of +applications on the provided account. + +* If you provide a **-d** flag with a valid application ID then that application +will be deleted. + +Below example will create a new Bot Application under the given account. +The Bot will be named **DiscordGoRocks** + +```sh +./appmaker -t YOUR_USER_TOKEN -n DiscordGoRocks +``` diff --git a/DiscordGo/examples/appmaker/main.go b/DiscordGo/examples/appmaker/main.go new file mode 100644 index 0000000000000000000000000000000000000000..286fe1694c0a19700771e4057a2889b52c9ce067 --- /dev/null +++ b/DiscordGo/examples/appmaker/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line options +var ( + Token string + Name string + DeleteID string + ListOnly bool +) + +func init() { + + flag.StringVar(&Token, "t", "", "Owner Account Token") + flag.StringVar(&Name, "n", "", "Name to give App/Bot") + flag.StringVar(&DeleteID, "d", "", "Application ID to delete") + flag.BoolVar(&ListOnly, "l", false, "List Applications Only") + flag.Parse() + + if Token == "" { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + var err error + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // If -l set, only display a list of existing applications + // for the given account. + if ListOnly { + + aps, err := dg.Applications() + if err != nil { + fmt.Println("error fetching applications,", err) + return + } + + for _, v := range aps { + fmt.Println("-----------------------------------------------------") + b, _ := json.MarshalIndent(v, "", " ") + fmt.Println(string(b)) + } + return + } + + // if -d set, delete the given Application + if DeleteID != "" { + err = dg.ApplicationDelete(DeleteID) + if err != nil { + fmt.Println("error deleting application,", err) + } + return + } + + if Name == "" { + flag.Usage() + os.Exit(1) + } + + // Create a new application. + ap := &discordgo.Application{} + ap.Name = Name + ap, err = dg.ApplicationCreate(ap) + if err != nil { + fmt.Println("error creating new applicaiton,", err) + return + } + + fmt.Printf("Application created successfully:\n") + b, _ := json.MarshalIndent(ap, "", " ") + fmt.Println(string(b)) + + // Create the bot account under the application we just created + bot, err := dg.ApplicationBotCreate(ap.ID) + if err != nil { + fmt.Println("error creating bot account,", err) + return + } + + fmt.Printf("Bot account created successfully.\n") + b, _ = json.MarshalIndent(bot, "", " ") + fmt.Println(string(b)) + + fmt.Println("Please save the above posted info in a secure place.") + fmt.Println("You will need that information to login with your bot account.") +} diff --git a/DiscordGo/examples/avatar/README.md b/DiscordGo/examples/avatar/README.md new file mode 100644 index 0000000000000000000000000000000000000000..582a35fe24ec45af2b2f473d5ef078cc640c0923 --- /dev/null +++ b/DiscordGo/examples/avatar/README.md @@ -0,0 +1,49 @@ + + +## DiscordGo Avatar Example + +This example demonstrates how to utilize DiscordGo to change the avatar for +a Discord account. This example works both with a local file or the URL of +an image. + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + +### Build + +This assumes you already have a working Go environment setup and that +DiscordGo is correctly installed on your system. + +From within the avatar example folder, run the below command to compile the +example. + +```sh +go build +``` + +### Usage + +This example uses bot tokens for authentication only. While email/password is +supported by DiscordGo, it is not recommended to use them. + +``` +./avatar --help +Usage of ./avatar: + -f string + Avatar File Name + -t string + Bot Token + -u string + URL to the avatar image +``` + +The below example shows how to set your Avatar from a local file. + +```sh +./avatar -t TOKEN -f avatar.png +``` +The below example shows how to set your Avatar from a URL. + +```sh +./avatar -t TOKEN -u http://bwmarrin.github.io/discordgo/img/discordgo.png +``` diff --git a/DiscordGo/examples/avatar/main.go b/DiscordGo/examples/avatar/main.go new file mode 100644 index 0000000000000000000000000000000000000000..e0a9c8802017f35b2c761d2f84a487f9ca27ea05 --- /dev/null +++ b/DiscordGo/examples/avatar/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Token string + AvatarFile string + AvatarURL string +) + +func init() { + + flag.StringVar(&Token, "t", "", "Bot Token") + flag.StringVar(&AvatarFile, "f", "", "Avatar File Name") + flag.StringVar(&AvatarURL, "u", "", "URL to the avatar image") + flag.Parse() + + if Token == "" || (AvatarFile == "" && AvatarURL == "") { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Declare these here so they can be used in the below two if blocks and + // still carry over to the end of this function. + var base64img string + var contentType string + + // If we're using a URL link for the Avatar + if AvatarURL != "" { + + resp, err := http.Get(AvatarURL) + if err != nil { + fmt.Println("Error retrieving the file, ", err) + return + } + + defer func() { + _ = resp.Body.Close() + }() + + img, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading the response, ", err) + return + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // If we're using a local file for the Avatar + if AvatarFile != "" { + img, err := ioutil.ReadFile(AvatarFile) + if err != nil { + fmt.Println(err) + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // Now lets format our base64 image into the proper format Discord wants + // and then call UserUpdate to set it as our user's Avatar. + avatar := fmt.Sprintf("data:%s;base64,%s", contentType, base64img) + _, err = dg.UserUpdate("", "", "", avatar, "") + if err != nil { + fmt.Println(err) + } +} diff --git a/DiscordGo/examples/mytoken/README.md b/DiscordGo/examples/mytoken/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6db8bfbeea9f57bdce14c55275f4c7aec7afe9c1 --- /dev/null +++ b/DiscordGo/examples/mytoken/README.md @@ -0,0 +1,47 @@ + + +## DiscordGo MyToken Example + +This example demonstrates how to utilize DiscordGo to login with an email and +password then to print out the Authentication Token for that user's account. + +Everytime this application is run a new authentication token is generated +for your account. Logging you in via email and password then creating a new +token is a cpu/mem expensive task for Discord. Because of that, it is highly +recommended to avoid doing this very often. Please only use this once to get a +token for your use and then always just your token. + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + +### Build + +This assumes you already have a working Go environment setup and that +DiscordGo is correctly installed on your system. + +From within the mytoken example folder, run the below command to compile the +example. + +```sh +go build +``` + +### Usage + +You must authenticate using both Email and Password for an account. + +``` +./mytoken --help +Usage of ./mytoken: + -e string + Account Email + -p string + Account Password +``` + +The below example shows how to start the program using an Email and Password for +authentication. + +```sh +./mytoken -e youremail@here.com -p MySecretPassword +``` diff --git a/DiscordGo/examples/mytoken/main.go b/DiscordGo/examples/mytoken/main.go new file mode 100644 index 0000000000000000000000000000000000000000..9375eadc9e12d18aa710788ab27721d6d0c06db9 --- /dev/null +++ b/DiscordGo/examples/mytoken/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.Parse() + + if Email == "" || Password == "" { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(Email, Password) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Print out your token. + fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) +} diff --git a/DiscordGo/examples/pingpong/README.md b/DiscordGo/examples/pingpong/README.md new file mode 100644 index 0000000000000000000000000000000000000000..489d384d1a8886a522c8d87133930177a2de628a --- /dev/null +++ b/DiscordGo/examples/pingpong/README.md @@ -0,0 +1,42 @@ + + +## DiscordGo Ping Pong Example + +This example demonstrates how to utilize DiscordGo to create a Ping Pong Bot. + +This Bot will respond to "ping" with "Pong!" and "pong" with "Ping!". + +**Join [Discord Gophers](https://discord.gg/0f1SbxBZjYoCtNPP) +Discord chat channel for support.** + +### Build + +This assumes you already have a working Go environment setup and that +DiscordGo is correctly installed on your system. + + +From within the pingpong example folder, run the below command to compile the +example. + +```sh +go build +``` + +### Usage + +This example uses bot tokens for authentication only. While user/password is +supported by DiscordGo, it is not recommended for bots. + +``` +./pingpong --help +Usage of ./pingpong: + -t string + Bot Token +``` + +The below example shows how to start the bot + +```sh +./pingpong -t YOUR_BOT_TOKEN +Bot is now running. Press CTRL-C to exit. +``` diff --git a/DiscordGo/examples/pingpong/main.go b/DiscordGo/examples/pingpong/main.go new file mode 100644 index 0000000000000000000000000000000000000000..155e782f1dc6a73adcc825345d663ee132f451ec --- /dev/null +++ b/DiscordGo/examples/pingpong/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Token string +) + +func init() { + + flag.StringVar(&Token, "t", "", "Bot Token") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Register the messageCreate func as a callback for MessageCreate events. + dg.AddHandler(messageCreate) + + // Open a websocket connection to Discord and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + // If the message is "ping" reply with "Pong!" + if m.Content == "ping" { + s.ChannelMessageSend(m.ChannelID, "Pong!") + } + + // If the message is "pong" reply with "Ping!" + if m.Content == "pong" { + s.ChannelMessageSend(m.ChannelID, "Ping!") + } +} diff --git a/DiscordGo/logging.go b/DiscordGo/logging.go new file mode 100644 index 0000000000000000000000000000000000000000..70d78d601c4edbb42e7e90fff741ef673baed447 --- /dev/null +++ b/DiscordGo/logging.go @@ -0,0 +1,95 @@ +// 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 ( + "fmt" + "log" + "runtime" + "strings" +) + +const ( + + // LogError level is used for critical errors that could lead to data loss + // or panic that would not be returned to a calling function. + LogError int = iota + + // LogWarning level is used for very abnormal events and errors that are + // also returend to a calling function. + LogWarning + + // LogInformational level is used for normal non-error activity + LogInformational + + // LogDebug level is for very detailed non-error activity. This is + // 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 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 msgL > v.LogLevel { + return + } + + msglog(msgL, 2, format, a...) +} + +// printJSON is a helper function to display JSON data in a easy to read format. +/* NOT USED ATM +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/DiscordGo/message.go b/DiscordGo/message.go index b2b5d00f82ee1557c5211dcce845b6f83e4ebdb4..19345b9538eca7b6dff63220b06e415c5d6b3309 100644 --- a/DiscordGo/message.go +++ b/DiscordGo/message.go @@ -10,73 +10,243 @@ package discordgo import ( - "fmt" + "io" + "regexp" "strings" ) +// MessageType is the type of Message +type MessageType int + +// Block contains the valid known MessageType values +const ( + MessageTypeDefault MessageType = iota + MessageTypeRecipientAdd + MessageTypeRecipientRemove + MessageTypeCall + MessageTypeChannelNameChange + MessageTypeChannelIconChange + MessageTypeChannelPinnedMessage + MessageTypeGuildMemberJoin +) + // A Message stores all data related to a specific Discord message. type Message struct { - ID string `json:"id"` - ChannelID string `json:"channel_id"` - Content string `json:"content"` - Timestamp string `json:"timestamp"` - EditedTimestamp string `json:"edited_timestamp"` - Tts bool `json:"tts"` - MentionEveryone bool `json:"mention_everyone"` - Author *User `json:"author"` - Attachments []*Attachment `json:"attachments"` - Embeds []*Embed `json:"embeds"` - Mentions []*User `json:"mentions"` -} - -// An Attachment stores data for message attachments. -type Attachment struct { + ID string `json:"id"` + ChannelID string `json:"channel_id"` + Content string `json:"content"` + Timestamp Timestamp `json:"timestamp"` + EditedTimestamp Timestamp `json:"edited_timestamp"` + MentionRoles []string `json:"mention_roles"` + Tts bool `json:"tts"` + MentionEveryone bool `json:"mention_everyone"` + Author *User `json:"author"` + Attachments []*MessageAttachment `json:"attachments"` + Embeds []*MessageEmbed `json:"embeds"` + Mentions []*User `json:"mentions"` + Reactions []*MessageReactions `json:"reactions"` + Type MessageType `json:"type"` +} + +// File stores info about files you e.g. send in messages. +type File struct { + Name string + ContentType string + Reader io.Reader +} + +// MessageSend stores all parameters you can send with ChannelMessageSendComplex. +type MessageSend struct { + Content string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + Tts bool `json:"tts"` + Files []*File `json:"-"` + + // TODO: Remove this when compatibility is not required. + File *File `json:"-"` +} + +// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which +// is also where you should get the instance from. +type MessageEdit struct { + Content *string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + + ID string + Channel string +} + +// NewMessageEdit returns a MessageEdit struct, initialized +// with the Channel and ID. +func NewMessageEdit(channelID string, messageID string) *MessageEdit { + return &MessageEdit{ + Channel: channelID, + ID: messageID, + } +} + +// SetContent is the same as setting the variable Content, +// except it doesn't take a pointer. +func (m *MessageEdit) SetContent(str string) *MessageEdit { + m.Content = &str + return m +} + +// SetEmbed is a convenience function for setting the embed, +// so you can chain commands. +func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit { + m.Embed = embed + return m +} + +// A MessageAttachment stores data for message attachments. +type MessageAttachment struct { ID string `json:"id"` URL string `json:"url"` ProxyURL string `json:"proxy_url"` + Filename string `json:"filename"` Width int `json:"width"` Height int `json:"height"` - Filename string `json:"filename"` Size int `json:"size"` } -// An Embed stores data for message embeds. -type Embed struct { - URL string `json:"url"` - Type string `json:"type"` - Title string `json:"title"` - Description string `json:"description"` - Thumbnail *struct { - URL string `json:"url"` - ProxyURL string `json:"proxy_url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"thumbnail"` - Provider *struct { - URL string `json:"url"` - Name string `json:"name"` - } `json:"provider"` - Author *struct { - URL string `json:"url"` - Name string `json:"name"` - } `json:"author"` - Video *struct { - URL string `json:"url"` - Width int `json:"width"` - Height int `json:"height"` - } `json:"video"` +// MessageEmbedFooter is a part of a MessageEmbed struct. +type MessageEmbedFooter struct { + Text string `json:"text,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedImage is a part of a MessageEmbed struct. +type MessageEmbedImage struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedThumbnail is a part of a MessageEmbed struct. +type MessageEmbedThumbnail struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedVideo is a part of a MessageEmbed struct. +type MessageEmbedVideo struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedProvider is a part of a MessageEmbed struct. +type MessageEmbedProvider struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` +} + +// MessageEmbedAuthor is a part of a MessageEmbed struct. +type MessageEmbedAuthor struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedField is a part of a MessageEmbed struct. +type MessageEmbedField struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + Inline bool `json:"inline,omitempty"` +} + +// An MessageEmbed stores data for message embeds. +type MessageEmbed struct { + URL string `json:"url,omitempty"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Color int `json:"color,omitempty"` + Footer *MessageEmbedFooter `json:"footer,omitempty"` + Image *MessageEmbedImage `json:"image,omitempty"` + Thumbnail *MessageEmbedThumbnail `json:"thumbnail,omitempty"` + Video *MessageEmbedVideo `json:"video,omitempty"` + Provider *MessageEmbedProvider `json:"provider,omitempty"` + Author *MessageEmbedAuthor `json:"author,omitempty"` + Fields []*MessageEmbedField `json:"fields,omitempty"` +} + +// MessageReactions holds a reactions object for a message. +type MessageReactions struct { + Count int `json:"count"` + Me bool `json:"me"` + Emoji *Emoji `json:"emoji"` } // ContentWithMentionsReplaced will replace all @ mentions with the // username of the mention. -func (m *Message) ContentWithMentionsReplaced() string { - if m.Mentions == nil { - return m.Content +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + + for _, user := range m.Mentions { + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+user.Username, + ).Replace(content) } - content := m.Content + return +} + +var patternChannels = regexp.MustCompile("<#[^>]*>") + +// ContentWithMoreMentionsReplaced will replace all @ mentions with the +// username of the mention, but also role IDs and more. +func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) { + content = m.Content + + if !s.StateEnabled { + content = m.ContentWithMentionsReplaced() + return + } + + channel, err := s.State.Channel(m.ChannelID) + if err != nil { + content = m.ContentWithMentionsReplaced() + return + } + for _, user := range m.Mentions { - content = strings.Replace(content, fmt.Sprintf("<@%s>", user.ID), - fmt.Sprintf("@%s", user.Username), -1) + nick := user.Username + + member, err := s.State.Member(channel.GuildID, user.ID) + if err == nil && member.Nick != "" { + nick = member.Nick + } + + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+nick, + ).Replace(content) + } + for _, roleID := range m.MentionRoles { + role, err := s.State.Role(channel.GuildID, roleID) + if err != nil || !role.Mentionable { + continue + } + + content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) } - return content + + content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { + channel, err := s.State.Channel(mention[2 : len(mention)-1]) + if err != nil || channel.Type == ChannelTypeGuildVoice { + return mention + } + + return "#" + channel.Name + }) + return } diff --git a/DiscordGo/message_test.go b/DiscordGo/message_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fd2f133604d397449000bc5459a16240bed3cb18 --- /dev/null +++ b/DiscordGo/message_test.go @@ -0,0 +1,41 @@ +package discordgo + +import ( + "testing" +) + +func TestContentWithMoreMentionsReplaced(t *testing.T) { + s := &Session{StateEnabled: true, State: NewState()} + + user := &User{ + ID: "user", + Username: "User Name", + } + + s.StateEnabled = true + s.State.GuildAdd(&Guild{ID: "guild"}) + s.State.RoleAdd("guild", &Role{ + ID: "role", + Name: "Role Name", + Mentionable: true, + }) + s.State.MemberAdd(&Member{ + User: user, + Nick: "User Nick", + GuildID: "guild", + }) + s.State.ChannelAdd(&Channel{ + Name: "Channel Name", + GuildID: "guild", + ID: "channel", + }) + m := &Message{ + Content: "<&role> <@!user> <@user> <#channel>", + ChannelID: "channel", + MentionRoles: []string{"role"}, + Mentions: []*User{user}, + } + if result, _ := m.ContentWithMoreMentionsReplaced(s); result != "@Role Name @User Nick @User Name #Channel Name" { + t.Error(result) + } +} diff --git a/DiscordGo/mkdocs.yml b/DiscordGo/mkdocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ee8eb3785b79579dada2b31b85b761c1e007dbf --- /dev/null +++ b/DiscordGo/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/DiscordGo/oauth2.go b/DiscordGo/oauth2.go new file mode 100644 index 0000000000000000000000000000000000000000..108b32fe38ebecc862614f1785980ad2c997e701 --- /dev/null +++ b/DiscordGo/oauth2.go @@ -0,0 +1,126 @@ +// 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 functions related to Discord OAuth2 endpoints + +package discordgo + +// ------------------------------------------------------------------------------------------------ +// Code specific to Discord OAuth2 Applications +// ------------------------------------------------------------------------------------------------ + +// An Application struct stores values for a Discord OAuth2 Application +type Application struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Secret string `json:"secret,omitempty"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + BotRequireCodeGrant bool `json:"bot_require_code_grant,omitempty"` + BotPublic bool `json:"bot_public,omitempty"` + RPCApplicationState int `json:"rpc_application_state,omitempty"` + Flags int `json:"flags,omitempty"` + Owner *User `json:"owner"` + Bot *User `json:"bot"` +} + +// Application returns an Application structure of a specific Application +// appID : The ID of an Application +func (s *Session) Application(appID string) (st *Application, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// Applications returns all applications for the authenticated user +func (s *Session) Applications() (st []*Application, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointApplications, nil, EndpointApplications) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationCreate creates a new Application +// name : Name of Application / Bot +// uris : Redirect URIs (Not required) +func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error) { + + data := struct { + Name string `json:"name"` + Description string `json:"description"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + }{ap.Name, ap.Description, ap.RedirectURIs} + + body, err := s.RequestWithBucketID("POST", EndpointApplications, data, EndpointApplications) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationUpdate updates an existing Application +// var : desc +func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) { + + data := struct { + Name string `json:"name"` + Description string `json:"description"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + }{ap.Name, ap.Description, ap.RedirectURIs} + + body, err := s.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationDelete deletes an existing Application +// appID : The ID of an Application +func (s *Session) ApplicationDelete(appID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication("")) + if err != nil { + return + } + + return +} + +// ------------------------------------------------------------------------------------------------ +// Code specific to Discord OAuth2 Application Bots +// ------------------------------------------------------------------------------------------------ + +// ApplicationBotCreate creates an Application Bot Account +// +// appID : The ID of an Application +// +// NOTE: func name may change, if I can think up something better. +func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) { + + body, err := s.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/DiscordGo/oauth2_test.go b/DiscordGo/oauth2_test.go new file mode 100644 index 0000000000000000000000000000000000000000..30526eb20eacf6f856bc86a689fbfca7345d9510 --- /dev/null +++ b/DiscordGo/oauth2_test.go @@ -0,0 +1,57 @@ +package discordgo_test + +import ( + "log" + "os" + + "github.com/bwmarrin/discordgo" +) + +func ExampleApplication() { + + // Authentication Token pulled from environment variable DG_TOKEN + Token := os.Getenv("DG_TOKEN") + if Token == "" { + return + } + + // Create a new Discordgo session + dg, err := discordgo.New(Token) + if err != nil { + log.Println(err) + return + } + + // Create an new Application + ap := &discordgo.Application{} + ap.Name = "TestApp" + ap.Description = "TestDesc" + ap, err = dg.ApplicationCreate(ap) + log.Printf("ApplicationCreate: err: %+v, app: %+v\n", err, ap) + + // Get a specific Application by it's ID + ap, err = dg.Application(ap.ID) + log.Printf("Application: err: %+v, app: %+v\n", err, ap) + + // Update an existing Application with new values + ap.Description = "Whooooa" + ap, err = dg.ApplicationUpdate(ap.ID, ap) + log.Printf("ApplicationUpdate: err: %+v, app: %+v\n", err, ap) + + // create a new bot account for this application + bot, err := dg.ApplicationBotCreate(ap.ID) + log.Printf("BotCreate: err: %+v, bot: %+v\n", err, bot) + + // Get a list of all applications for the authenticated user + apps, err := dg.Applications() + log.Printf("Applications: err: %+v, apps : %+v\n", err, apps) + for k, v := range apps { + log.Printf("Applications: %d : %+v\n", k, v) + } + + // Delete the application we created. + err = dg.ApplicationDelete(ap.ID) + log.Printf("Delete: err: %+v\n", err) + + return +} diff --git a/DiscordGo/ratelimit.go b/DiscordGo/ratelimit.go new file mode 100644 index 0000000000000000000000000000000000000000..223c0d04eaa0e109db562a2bcf18af11ecc98aa9 --- /dev/null +++ b/DiscordGo/ratelimit.go @@ -0,0 +1,183 @@ +package discordgo + +import ( + "net/http" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" +) + +// customRateLimit holds information for defining a custom rate limit +type customRateLimit struct { + suffix string + requests int + reset time.Duration +} + +// RateLimiter holds all ratelimit buckets +type RateLimiter struct { + sync.Mutex + global *int64 + buckets map[string]*Bucket + globalRateLimit time.Duration + customRateLimits []*customRateLimit +} + +// NewRatelimiter returns a new RateLimiter +func NewRatelimiter() *RateLimiter { + + return &RateLimiter{ + buckets: make(map[string]*Bucket), + global: new(int64), + customRateLimits: []*customRateLimit{ + &customRateLimit{ + suffix: "//reactions//", + requests: 1, + reset: 200 * time.Millisecond, + }, + }, + } +} + +// getBucket retrieves or creates a bucket +func (r *RateLimiter) getBucket(key string) *Bucket { + r.Lock() + defer r.Unlock() + + if bucket, ok := r.buckets[key]; ok { + return bucket + } + + b := &Bucket{ + remaining: 1, + Key: key, + global: r.global, + } + + // Check if there is a custom ratelimit set for this bucket ID. + for _, rl := range r.customRateLimits { + if strings.HasSuffix(b.Key, rl.suffix) { + b.customRateLimit = rl + break + } + } + + r.buckets[key] = b + return b +} + +// LockBucket Locks until a request can be made +func (r *RateLimiter) LockBucket(bucketID string) *Bucket { + + b := r.getBucket(bucketID) + + b.Lock() + + // If we ran out of calls and the reset time is still ahead of us + // then we need to take it easy and relax a little + if b.remaining < 1 && b.reset.After(time.Now()) { + time.Sleep(b.reset.Sub(time.Now())) + + } + + // Check for global ratelimits + sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) + if now := time.Now(); now.Before(sleepTo) { + time.Sleep(sleepTo.Sub(now)) + } + + b.remaining-- + return b +} + +// Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) +type Bucket struct { + sync.Mutex + Key string + remaining int + limit int + reset time.Time + global *int64 + + lastReset time.Time + customRateLimit *customRateLimit +} + +// Release unlocks the bucket and reads the headers to update the buckets ratelimit info +// and locks up the whole thing in case if there's a global ratelimit. +func (b *Bucket) Release(headers http.Header) error { + defer b.Unlock() + + // Check if the bucket uses a custom ratelimiter + if rl := b.customRateLimit; rl != nil { + if time.Now().Sub(b.lastReset) >= rl.reset { + b.remaining = rl.requests - 1 + b.lastReset = time.Now() + } + if b.remaining < 1 { + b.reset = time.Now().Add(rl.reset) + } + return nil + } + + if headers == nil { + return nil + } + + remaining := headers.Get("X-RateLimit-Remaining") + reset := headers.Get("X-RateLimit-Reset") + global := headers.Get("X-RateLimit-Global") + retryAfter := headers.Get("Retry-After") + + // Update global and per bucket reset time if the proper headers are available + // If global is set, then it will block all buckets until after Retry-After + // If Retry-After without global is provided it will use that for the new reset + // time since it's more accurate than X-RateLimit-Reset. + // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset + if retryAfter != "" { + parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) + if err != nil { + return err + } + + resetAt := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + + // Lock either this single bucket or all buckets + if global != "" { + atomic.StoreInt64(b.global, resetAt.UnixNano()) + } else { + b.reset = resetAt + } + } else if reset != "" { + // Calculate the reset time by using the date header returned from discord + discordTime, err := http.ParseTime(headers.Get("Date")) + if err != nil { + return err + } + + unix, err := strconv.ParseInt(reset, 10, 64) + if err != nil { + return err + } + + // Calculate the time until reset and add it to the current local time + // some extra time is added because without it i still encountered 429's. + // The added amount is the lowest amount that gave no 429's + // in 1k requests + delta := time.Unix(unix, 0).Sub(discordTime) + time.Millisecond*250 + b.reset = time.Now().Add(delta) + } + + // Udpate remaining if header is present + if remaining != "" { + parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) + if err != nil { + return err + } + b.remaining = int(parsedRemaining) + } + + return nil +} diff --git a/DiscordGo/ratelimit_test.go b/DiscordGo/ratelimit_test.go new file mode 100644 index 0000000000000000000000000000000000000000..db18211a5f390c72ea350ba70f5309ec26fe567b --- /dev/null +++ b/DiscordGo/ratelimit_test.go @@ -0,0 +1,112 @@ +package discordgo + +import ( + "net/http" + "strconv" + "testing" + "time" +) + +// This test takes ~2 seconds to run +func TestRatelimitReset(t *testing.T) { + rl := NewRatelimiter() + + sendReq := func(endpoint string) { + bucket := rl.LockBucket(endpoint) + + headers := http.Header(make(map[string][]string)) + + headers.Set("X-RateLimit-Remaining", "0") + // Reset for approx 2 seconds from now + headers.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Second*2).Unix(), 10)) + headers.Set("Date", time.Now().Format(time.RFC850)) + + err := bucket.Release(headers) + if err != nil { + t.Errorf("Release returned error: %v", err) + } + } + + sent := time.Now() + sendReq("/guilds/99/channels") + sendReq("/guilds/55/channels") + sendReq("/guilds/66/channels") + + sendReq("/guilds/99/channels") + sendReq("/guilds/55/channels") + sendReq("/guilds/66/channels") + + // We hit the same endpoint 2 times, so we should only be ratelimited 2 second + // And always less than 4 seconds (unless you're on a stoneage computer or using swap or something...) + if time.Since(sent) >= time.Second && time.Since(sent) < time.Second*4 { + t.Log("OK", time.Since(sent)) + } else { + t.Error("Did not ratelimit correctly, got:", time.Since(sent)) + } +} + +// This test takes ~1 seconds to run +func TestRatelimitGlobal(t *testing.T) { + rl := NewRatelimiter() + + sendReq := func(endpoint string) { + bucket := rl.LockBucket(endpoint) + + headers := http.Header(make(map[string][]string)) + + headers.Set("X-RateLimit-Global", "1") + // Reset for approx 1 seconds from now + headers.Set("Retry-After", "1000") + + err := bucket.Release(headers) + if err != nil { + t.Errorf("Release returned error: %v", err) + } + } + + sent := time.Now() + + // This should trigger a global ratelimit + sendReq("/guilds/99/channels") + time.Sleep(time.Millisecond * 100) + + // This shouldn't go through in less than 1 second + sendReq("/guilds/55/channels") + + if time.Since(sent) >= time.Second && time.Since(sent) < time.Second*2 { + t.Log("OK", time.Since(sent)) + } else { + t.Error("Did not ratelimit correctly, got:", time.Since(sent)) + } +} + +func BenchmarkRatelimitSingleEndpoint(b *testing.B) { + rl := NewRatelimiter() + for i := 0; i < b.N; i++ { + sendBenchReq("/guilds/99/channels", rl) + } +} + +func BenchmarkRatelimitParallelMultiEndpoints(b *testing.B) { + rl := NewRatelimiter() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + sendBenchReq("/guilds/"+strconv.Itoa(i)+"/channels", rl) + i++ + } + }) +} + +// Does not actually send requests, but locks the bucket and releases it with made-up headers +func sendBenchReq(endpoint string, rl *RateLimiter) { + bucket := rl.LockBucket(endpoint) + + headers := http.Header(make(map[string][]string)) + + headers.Set("X-RateLimit-Remaining", "10") + headers.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Unix(), 10)) + headers.Set("Date", time.Now().Format(time.RFC850)) + + bucket.Release(headers) +} diff --git a/DiscordGo/restapi.go b/DiscordGo/restapi.go index 76b678b832dda754e5cc61b1041570a2951e0434..836e4a41c6d91cd3970a15bc1c2c223b35687d6a 100644 --- a/DiscordGo/restapi.go +++ b/DiscordGo/restapi.go @@ -20,24 +20,33 @@ import ( _ "image/png" // For PNG decoding "io" "io/ioutil" + "log" "mime/multipart" "net/http" + "net/textproto" "net/url" "strconv" + "strings" "time" ) -// ErrJSONUnmarshal is returned for JSON Unmarshall errors. -var ErrJSONUnmarshal = errors.New("json unmarshal") +// All error constants +var ( + ErrJSONUnmarshal = errors.New("json unmarshal") + ErrStatusOffline = errors.New("You can't set your Status to offline") + ErrVerificationLevelBounds = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") + ErrGuildNoIcon = errors.New("guild does not have an icon set") + ErrGuildNoSplash = errors.New("guild does not have a splash set") +) -// Request makes a (GET/POST/...) Requests to Discord REST API with JSON data. -// All the other Discord REST Calls in this file use this function. +// Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) { + return s.RequestWithBucketID(method, urlStr, data, strings.SplitN(urlStr, "?", 2)[0]) +} - if s.Debug { - fmt.Println("API REQUEST PAYLOAD :: [" + fmt.Sprintf("%+v", data) + "]") - } - +// RequestWithBucketID makes a (GET/POST/...) Requests to Discord REST API with JSON data. +func (s *Session) RequestWithBucketID(method, urlStr string, data interface{}, bucketID string) (response []byte, err error) { var body []byte if data != nil { body, err = json.Marshal(data) @@ -46,18 +55,27 @@ func (s *Session) Request(method, urlStr string, data interface{}) (response []b } } - return s.request(method, urlStr, "application/json", body) + return s.request(method, urlStr, "application/json", body, bucketID, 0) } // request makes a (GET/POST/...) Requests to Discord REST API. -func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) { +// Sequence is the sequence number, if it fails with a 502 it will +// retry with sequence+1 until it either succeeds or sequence >= session.MaxRestRetries +func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID string, sequence int) (response []byte, err error) { + if bucketID == "" { + bucketID = strings.SplitN(urlStr, "?", 2)[0] + } + + bucket := s.ratelimiter.LockBucket(bucketID) if s.Debug { - fmt.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)) } req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(b)) if err != nil { + bucket.Release(nil) return } @@ -73,23 +91,27 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons if s.Debug { for k, v := range req.Header { - fmt.Printf("API REQUEST HEADER :: [%s] = %+v\n", k, v) + log.Printf("API REQUEST HEADER :: [%s] = %+v\n", k, v) } } - client := &http.Client{Timeout: (20 * time.Second)} - - resp, err := client.Do(req) + resp, err := s.Client.Do(req) if err != nil { + bucket.Release(nil) return } defer func() { - err := resp.Body.Close() - if err != nil { - fmt.Println("error closing resp body") + err2 := resp.Body.Close() + if err2 != nil { + log.Println("error closing resp body") } }() + err = bucket.Release(resp.Header) + if err != nil { + return + } + response, err = ioutil.ReadAll(resp.Body) if err != nil { return @@ -97,33 +119,49 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons if s.Debug { - fmt.Printf("API RESPONSE STATUS :: %s\n", resp.Status) + log.Printf("API RESPONSE STATUS :: %s\n", resp.Status) for k, v := range resp.Header { - fmt.Printf("API RESPONSE HEADER :: [%s] = %+v\n", k, v) + log.Printf("API RESPONSE HEADER :: [%s] = %+v\n", k, v) } - fmt.Printf("API RESPONSE BODY :: [%s]\n", response) + log.Printf("API RESPONSE BODY :: [%s]\n\n\n", response) } - // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html switch resp.StatusCode { - case 200: // OK - case 204: // No Content + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: // TODO check for 401 response, invalidate token if we get one. + case http.StatusBadGateway: + // Retry sending request if possible + if sequence < s.MaxRestRetries { + + s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) + response, err = s.request(method, urlStr, contentType, b, bucketID, sequence+1) + } else { + err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) + } + 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 } - time.Sleep(rl.RetryAfter) - response, err = s.request(method, urlStr, contentType, b) + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + s.handleEvent(rateLimitEventType, RateLimit{TooManyRequests: &rl, URL: urlStr}) + + time.Sleep(rl.RetryAfter * time.Millisecond) + // we can make the above smarter + // this method can cause longer delays than required + + response, err = s.request(method, urlStr, contentType, b, bucketID, sequence) default: // Error condition - err = fmt.Errorf("HTTP %s, %s", resp.Status, response) + err = newRestError(req, resp, response) } return @@ -143,6 +181,12 @@ func unmarshal(data []byte, v interface{}) error { // ------------------------------------------------------------------------------------------------ // Login asks the Discord server for an authentication token. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func (s *Session) Login(email, password string) (err error) { data := struct { @@ -150,13 +194,14 @@ func (s *Session) Login(email, password string) (err error) { Password string `json:"password"` }{email, password} - response, err := s.Request("POST", LOGIN, data) + response, err := s.RequestWithBucketID("POST", EndpointLogin, data, EndpointLogin) if err != nil { return } temp := struct { Token string `json:"token"` + MFA bool `json:"mfa"` }{} err = unmarshal(response, &temp) @@ -165,6 +210,7 @@ func (s *Session) Login(email, password string) (err error) { } s.Token = temp.Token + s.MFA = temp.MFA return } @@ -177,7 +223,7 @@ func (s *Session) Register(username string) (token string, err error) { Username string `json:"username"` }{username} - response, err := s.Request("POST", REGISTER, data) + response, err := s.RequestWithBucketID("POST", EndpointRegister, data, EndpointRegister) if err != nil { return } @@ -211,7 +257,7 @@ func (s *Session) Logout() (err error) { Token string `json:"token"` }{s.Token} - _, err = s.Request("POST", LOGOUT, data) + _, err = s.RequestWithBucketID("POST", EndpointLogout, data, EndpointLogout) return } @@ -223,7 +269,7 @@ func (s *Session) Logout() (err error) { // userID : A user ID or "@me" which is a shortcut of current user ID func (s *Session) User(userID string) (st *User, err error) { - body, err := s.Request("GET", USER(userID), nil) + body, err := s.RequestWithBucketID("GET", EndpointUser(userID), nil, EndpointUsers) if err != nil { return } @@ -232,15 +278,21 @@ func (s *Session) User(userID string) (st *User, err error) { return } -// UserAvatar returns an image.Image of a users Avatar. +// UserAvatar is deprecated. Please use UserAvatarDecode // userID : A user ID or "@me" which is a shortcut of current user ID func (s *Session) UserAvatar(userID string) (img image.Image, err error) { u, err := s.User(userID) if err != nil { return } + img, err = s.UserAvatarDecode(u) + return +} - body, err := s.Request("GET", USER_AVATAR(userID, u.Avatar), nil) +// UserAvatarDecode returns an image.Image of a user's Avatar +// user : The user which avatar should be retrieved +func (s *Session) UserAvatarDecode(u *User) (img image.Image, err error) { + body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(u.ID, u.Avatar), nil, EndpointUserAvatar("", "")) if err != nil { return } @@ -258,14 +310,14 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri // If left blank, avatar will be set to null/blank data := struct { - Email string `json:"email"` - Password string `json:"password"` - Username string `json:"username"` + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` + Username string `json:"username,omitempty"` Avatar string `json:"avatar,omitempty"` NewPassword string `json:"new_password,omitempty"` }{email, password, username, avatar, newPassword} - body, err := s.Request("PATCH", USER("@me"), data) + body, err := s.RequestWithBucketID("PATCH", EndpointUser("@me"), data, EndpointUsers) if err != nil { return } @@ -277,7 +329,28 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri // UserSettings returns the settings for a given user func (s *Session) UserSettings() (st *Settings, err error) { - body, err := s.Request("GET", USER_SETTINGS("@me"), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserSettings("@me"), nil, EndpointUserSettings("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserUpdateStatus update the user status +// status : The new status (Actual valid status are 'online','idle','dnd','invisible') +func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { + if status == StatusOffline { + err = ErrStatusOffline + return + } + + data := struct { + Status Status `json:"status"` + }{status} + + body, err := s.RequestWithBucketID("PATCH", EndpointUserSettings("@me"), data, EndpointUserSettings("")) if err != nil { return } @@ -290,7 +363,7 @@ func (s *Session) UserSettings() (st *Settings, err error) { // channels. func (s *Session) UserChannels() (st []*Channel, err error) { - body, err := s.Request("GET", USER_CHANNELS("@me"), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserChannels("@me"), nil, EndpointUserChannels("")) if err != nil { return } @@ -307,7 +380,40 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) RecipientID string `json:"recipient_id"` }{recipientID} - body, err := s.Request("POST", USER_CHANNELS("@me"), data) + body, err := s.RequestWithBucketID("POST", EndpointUserChannels("@me"), data, EndpointUserChannels("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserGuilds returns an array of UserGuild structures for all guilds. +// limit : The number guilds that can be returned. (max 100) +// beforeID : If provided all guilds returned will be before given ID. +// afterID : If provided all guilds returned will be after given ID. +func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGuild, err error) { + + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if afterID != "" { + v.Set("after", afterID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + + uri := EndpointUserGuilds("@me") + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) if err != nil { return } @@ -316,10 +422,12 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) return } -// UserGuilds returns an array of Guild structures for all guilds. -func (s *Session) UserGuilds() (st []*Guild, err error) { +// UserGuildSettingsEdit Edits the users notification settings for a guild +// guildID : The ID of the guild to edit the settings on +// settings : The settings to update +func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) { - body, err := s.Request("GET", USER_GUILDS("@me"), nil) + body, err := s.RequestWithBucketID("PATCH", EndpointUserGuildSettings("@me", guildID), settings, EndpointUserGuildSettings("", guildID)) if err != nil { return } @@ -328,6 +436,123 @@ func (s *Session) UserGuilds() (st []*Guild, err error) { return } +// UserChannelPermissions returns the permission of a user in a channel. +// userID : The ID of the user to calculate permissions for. +// channelID : The ID of the channel to calculate permission for. +// +// NOTE: This function is now deprecated and will be removed in the future. +// Please see the same function inside state.go +func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + // Try to just get permissions from state. + apermissions, err = s.State.UserChannelPermissions(userID, channelID) + if err == nil { + return + } + + // Otherwise try get as much data from state as possible, falling back to the network. + channel, err := s.State.Channel(channelID) + if err != nil || channel == nil { + channel, err = s.Channel(channelID) + if err != nil { + return + } + } + + guild, err := s.State.Guild(channel.GuildID) + if err != nil || guild == nil { + guild, err = s.Guild(channel.GuildID) + if err != nil { + return + } + } + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + member, err := s.State.Member(guild.ID, userID) + if err != nil || member == nil { + member, err = s.GuildMember(guild.ID, userID) + if err != nil { + return + } + } + + return memberPermissions(guild, channel, member), nil +} + +// Calculates the permissions for a member. +// https://support.discordapp.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured- +func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) { + userID := member.User.ID + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + for _, role := range guild.Roles { + if role.ID == guild.ID { + apermissions |= role.Permissions + break + } + } + + for _, role := range guild.Roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + apermissions |= role.Permissions + break + } + } + } + + if apermissions&PermissionAdministrator == PermissionAdministrator { + apermissions |= PermissionAll + } + + // Apply @everyone overrides from the channel. + for _, overwrite := range channel.PermissionOverwrites { + if guild.ID == overwrite.ID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } + } + + denies := 0 + allows := 0 + + // Member overwrites can override role overrides, so do two passes + for _, overwrite := range channel.PermissionOverwrites { + for _, roleID := range member.Roles { + if overwrite.Type == "role" && roleID == overwrite.ID { + denies |= overwrite.Deny + allows |= overwrite.Allow + break + } + } + } + + apermissions &= ^denies + apermissions |= allows + + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.Type == "member" && overwrite.ID == userID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } + } + + if apermissions&PermissionAdministrator == PermissionAdministrator { + apermissions |= PermissionAllChannel + } + + return apermissions +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Guilds // ------------------------------------------------------------------------------------------------ @@ -343,7 +568,7 @@ func (s *Session) Guild(guildID string) (st *Guild, err error) { } } - body, err := s.Request("GET", GUILD(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return } @@ -360,7 +585,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) { Name string `json:"name"` }{name} - body, err := s.Request("POST", GUILDS, data) + body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds) if err != nil { return } @@ -371,14 +596,38 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) { // GuildEdit edits a new Guild // guildID : The ID of a Guild -// name : A name for the Guild (2-100 characters) -func (s *Session) GuildEdit(guildID, name string) (st *Guild, err error) { +// g : A GuildParams struct with the values Name, Region and VerificationLevel defined. +func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error) { + + // Bounds checking for VerificationLevel, interval: [0, 3] + if g.VerificationLevel != nil { + val := *g.VerificationLevel + if val < 0 || val > 3 { + err = ErrVerificationLevelBounds + return + } + } - data := struct { - Name string `json:"name"` - }{name} + //Bounds checking for regions + if g.Region != "" { + isValid := false + regions, _ := s.VoiceRegions() + for _, r := range regions { + if g.Region == r.ID { + isValid = true + } + } + if !isValid { + var valid []string + for _, r := range regions { + valid = append(valid, r.ID) + } + err = fmt.Errorf("Region not a valid region (%q)", valid) + return + } + } - body, err := s.Request("POST", GUILD(guildID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), g, EndpointGuild(guildID)) if err != nil { return } @@ -391,7 +640,7 @@ func (s *Session) GuildEdit(guildID, name string) (st *Guild, err error) { // guildID : The ID of a Guild func (s *Session) GuildDelete(guildID string) (st *Guild, err error) { - body, err := s.Request("DELETE", GUILD(guildID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return } @@ -404,16 +653,16 @@ func (s *Session) GuildDelete(guildID string) (st *Guild, err error) { // guildID : The ID of a Guild func (s *Session) GuildLeave(guildID string) (err error) { - _, err = s.Request("DELETE", USER_GUILD("@me", guildID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointUserGuild("@me", guildID), nil, EndpointUserGuild("", guildID)) return } // GuildBans returns an array of User structures for all bans of a // given guild. // guildID : The ID of a Guild. -func (s *Session) GuildBans(guildID string) (st []*User, err error) { +func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { - body, err := s.Request("GET", GUILD_BANS(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildBans(guildID), nil, EndpointGuildBans(guildID)) if err != nil { return } @@ -428,14 +677,31 @@ func (s *Session) GuildBans(guildID string) (st []*User, err error) { // userID : The ID of a User // days : The number of days of previous comments to delete. func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { + return s.GuildBanCreateWithReason(guildID, userID, "", days) +} + +// GuildBanCreateWithReason bans the given user from the given guild also providing a reaso. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for this ban +// days : The number of days of previous comments to delete. +func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days int) (err error) { - uri := GUILD_BAN(guildID, userID) + uri := EndpointGuildBan(guildID, userID) + queryParams := url.Values{} if days > 0 { - uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days) + queryParams.Set("delete-message-days", strconv.Itoa(days)) + } + if reason != "" { + queryParams.Set("reason", reason) + } + + if len(queryParams) > 0 { + uri += "?" + queryParams.Encode() } - _, err = s.Request("PUT", uri, nil) + _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) return } @@ -444,22 +710,22 @@ func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { // userID : The ID of a User func (s *Session) GuildBanDelete(guildID, userID string) (err error) { - _, err = s.Request("DELETE", GUILD_BAN(guildID, userID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, "")) return } // GuildMembers returns a list of members for a guild. // guildID : The ID of a Guild. -// offset : A number of members to skip -// limit : max number of members to return -func (s *Session) GuildMembers(guildID string, offset, limit int) (st []*Member, err error) { +// after : The id of the member to return members after +// limit : max number of members to return (max 1000) +func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*Member, err error) { - uri := GUILD_MEMBERS(guildID) + uri := EndpointGuildMembers(guildID) v := url.Values{} - if offset > 0 { - v.Set("offset", strconv.Itoa(offset)) + if after != "" { + v.Set("after", after) } if limit > 0 { @@ -470,7 +736,7 @@ func (s *Session) GuildMembers(guildID string, offset, limit int) (st []*Member, uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } - body, err := s.Request("GET", uri, nil) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID)) if err != nil { return } @@ -484,7 +750,7 @@ func (s *Session) GuildMembers(guildID string, offset, limit int) (st []*Member, // userID : The ID of a User func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { - body, err := s.Request("GET", GUILD_MEMBER(guildID, userID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -498,7 +764,21 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { // userID : The ID of a User func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { - _, err = s.Request("DELETE", GUILD_MEMBER(guildID, userID), nil) + return s.GuildMemberDeleteWithReason(guildID, userID, "") +} + +// GuildMemberDeleteWithReason removes the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for the kick +func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) { + + uri := EndpointGuildMember(guildID, userID) + if reason != "" { + uri += "?reason=" + url.QueryEscape(reason) + } + + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, "")) return } @@ -512,7 +792,7 @@ func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err e Roles []string `json:"roles"` }{roles} - _, err = s.Request("PATCH", GUILD_MEMBER(guildID, userID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -532,7 +812,7 @@ func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) ChannelID string `json:"channel_id"` }{channelID} - _, err = s.Request("PATCH", GUILD_MEMBER(guildID, userID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -540,12 +820,52 @@ func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) return } +// GuildMemberNickname updates the nickname of a guild member +// guildID : The ID of a guild +// userID : The ID of a user +// userID : The ID of a user or "@me" which is a shortcut of the current user ID +func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) { + + data := struct { + Nick string `json:"nick"` + }{nickname} + + if userID == "@me" { + userID += "/nick" + } + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + return +} + +// GuildMemberRoleAdd adds the specified role to a given member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// roleID : The ID of a Role to be assigned to the user. +func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) { + + _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + + return +} + +// GuildMemberRoleRemove removes the specified role to a given member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// roleID : The ID of a Role to be removed from the user. +func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + + return +} + // GuildChannels returns an array of Channel structures for all channels of a // given guild. // guildID : The ID of a Guild. func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { - body, err := s.Request("GET", GUILD_CHANNELS(guildID), nil) + body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, EndpointGuildChannels(guildID), 0) if err != nil { return } @@ -566,7 +886,7 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, Type string `json:"type"` }{name, ctype} - body, err := s.Request("POST", GUILD_CHANNELS(guildID), data) + body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) if err != nil { return } @@ -575,10 +895,19 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, return } +// GuildChannelsReorder updates the order of channels in a guild +// guildID : The ID of a Guild. +// channels : Updated channels. +func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID)) + return +} + // GuildInvites returns an array of Invite structures for the given guild // guildID : The ID of a Guild. func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { - body, err := s.Request("GET", GUILD_INVITES(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID)) if err != nil { return } @@ -591,7 +920,7 @@ func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { // guildID : The ID of a Guild. func (s *Session) GuildRoles(guildID string) (st []*Role, err error) { - body, err := s.Request("GET", GUILD_ROLES(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { return } @@ -605,7 +934,7 @@ func (s *Session) GuildRoles(guildID string) (st []*Role, err error) { // guildID: The ID of a Guild. func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) { - body, err := s.Request("POST", GUILD_ROLES(guildID), nil) + body, err := s.RequestWithBucketID("POST", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { return } @@ -622,16 +951,23 @@ func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) { // color : The color of the role (decimal, not hex). // hoist : Whether to display the role's users separately. // perm : The permissions for the role. -func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int) (st *Role, err error) { +// mention : Whether this role is mentionable +func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int, mention bool) (st *Role, err error) { + + // Prevent sending a color int that is too big. + if color > 0xFFFFFF { + err = fmt.Errorf("color value cannot be larger than 0xFFFFFF") + } data := struct { - Name string `json:"name"` // The color the role should have (as a decimal, not hex) - Color int `json:"color"` // Whether to display the role's users separately - Hoist bool `json:"hoist"` // The role's name (overwrites existing) + Name string `json:"name"` // The role's name (overwrites existing) + Color int `json:"color"` // The color the role should have (as a decimal, not hex) + Hoist bool `json:"hoist"` // Whether to display the role's users separately Permissions int `json:"permissions"` // The overall permissions number of the role (overwrites existing) - }{name, color, hoist, perm} + Mentionable bool `json:"mentionable"` // Whether this role is mentionable + }{name, color, hoist, perm, mention} - body, err := s.Request("PATCH", GUILD_ROLE(guildID, roleID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRole(guildID, roleID), data, EndpointGuildRole(guildID, "")) if err != nil { return } @@ -646,7 +982,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b // roles : A list of ordered roles. func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, err error) { - body, err := s.Request("PATCH", GUILD_ROLES(guildID), roles) + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRoles(guildID), roles, EndpointGuildRoles(guildID)) if err != nil { return } @@ -661,8 +997,139 @@ func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, e // roleID : The ID of a Role. func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { - _, err = s.Request("DELETE", GUILD_ROLE(guildID, roleID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildRole(guildID, roleID), nil, EndpointGuildRole(guildID, "")) + + return +} + +// GuildPruneCount Returns the number of members that would be removed in a prune operation. +// Requires 'KICK_MEMBER' permission. +// guildID : The ID of a Guild. +// days : The number of days to count prune for (1 or more). +func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, err error) { + count = 0 + + if days <= 0 { + err = ErrPruneDaysBounds + return + } + + p := struct { + Pruned uint32 `json:"pruned"` + }{} + + uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) + + err = unmarshal(body, &p) + if err != nil { + return + } + + count = p.Pruned + + return +} + +// GuildPrune Begin as prune operation. Requires the 'KICK_MEMBERS' permission. +// Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. +// guildID : The ID of a Guild. +// days : The number of days to count prune for (1 or more). +func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err error) { + + count = 0 + + if days <= 0 { + err = ErrPruneDaysBounds + return + } + + data := struct { + days uint32 + }{days} + + p := struct { + Pruned uint32 `json:"pruned"` + }{} + + body, err := s.RequestWithBucketID("POST", EndpointGuildPrune(guildID), data, EndpointGuildPrune(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &p) + if err != nil { + return + } + + count = p.Pruned + + return +} + +// GuildIntegrations returns an array of Integrations for a guild. +// guildID : The ID of a Guild. +func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildIntegrationCreate creates a Guild Integration. +// guildID : The ID of a Guild. +// integrationType : The Integration type. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationCreate(guildID, integrationType, integrationID string) (err error) { + + data := struct { + Type string `json:"type"` + ID string `json:"id"` + }{integrationType, integrationID} + + _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrations(guildID), data, EndpointGuildIntegrations(guildID)) + return +} + +// GuildIntegrationEdit edits a Guild Integration. +// guildID : The ID of a Guild. +// integrationType : The Integration type. +// integrationID : The ID of an integration. +// expireBehavior : The behavior when an integration subscription lapses (see the integration object documentation). +// expireGracePeriod : Period (in seconds) where the integration will ignore lapsed subscriptions. +// enableEmoticons : Whether emoticons should be synced for this integration (twitch only currently). +func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBehavior, expireGracePeriod int, enableEmoticons bool) (err error) { + + data := struct { + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + EnableEmoticons bool `json:"enable_emoticons"` + }{expireBehavior, expireGracePeriod, enableEmoticons} + _, err = s.RequestWithBucketID("PATCH", EndpointGuildIntegration(guildID, integrationID), data, EndpointGuildIntegration(guildID, "")) + return +} + +// GuildIntegrationDelete removes the given integration from the Guild. +// guildID : The ID of a Guild. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildIntegration(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) + return +} + +// GuildIntegrationSync syncs an integration. +// guildID : The ID of a Guild. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationSync(guildID, integrationID string) (err error) { + + _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) return } @@ -675,11 +1142,11 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { } if g.Icon == "" { - err = errors.New("Guild does not have an icon set.") + err = ErrGuildNoIcon return } - body, err := s.Request("GET", GUILD_ICON(guildID, g.Icon), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildIcon(guildID, g.Icon), nil, EndpointGuildIcon(guildID, "")) if err != nil { return } @@ -697,11 +1164,11 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { } if g.Splash == "" { - err = errors.New("Guild does not have a splash set.") + err = ErrGuildNoSplash return } - body, err := s.Request("GET", GUILD_SPLASH(guildID, g.Splash), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildSplash(guildID, g.Splash), nil, EndpointGuildSplash(guildID, "")) if err != nil { return } @@ -710,6 +1177,29 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { return } +// GuildEmbed returns the embed for a Guild. +// guildID : The ID of a Guild. +func (s *Session) GuildEmbed(guildID string) (st *GuildEmbed, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildEmbed(guildID), nil, EndpointGuildEmbed(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildEmbedEdit returns the embed for a Guild. +// guildID : The ID of a Guild. +func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) (err error) { + + data := GuildEmbed{enabled, channelID} + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildEmbed(guildID), data, EndpointGuildEmbed(guildID)) + return +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Channels // ------------------------------------------------------------------------------------------------ @@ -717,7 +1207,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { // Channel returns a Channel strucutre of a specific Channel. // channelID : The ID of the Channel you want returned. func (s *Session) Channel(channelID string) (st *Channel, err error) { - body, err := s.Request("GET", CHANNEL(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { return } @@ -735,7 +1225,7 @@ func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { Name string `json:"name"` }{name} - body, err := s.Request("PATCH", CHANNEL(channelID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) if err != nil { return } @@ -748,7 +1238,7 @@ func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { // channelID : The ID of a Channel func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) { - body, err := s.Request("DELETE", CHANNEL(channelID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { return } @@ -762,19 +1252,20 @@ func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) { // channelID : The ID of a Channel func (s *Session) ChannelTyping(channelID string) (err error) { - _, err = s.Request("POST", CHANNEL_TYPING(channelID), nil) + _, err = s.RequestWithBucketID("POST", EndpointChannelTyping(channelID), nil, EndpointChannelTyping(channelID)) return } // ChannelMessages returns an array of Message structures for messages within // a given channel. // channelID : The ID of a Channel. -// limit : The number messages that can be returned. +// limit : The number messages that can be returned. (max 100) // beforeID : If provided all messages returned will be before given ID. // afterID : If provided all messages returned will be after given ID. -func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID string) (st []*Message, err error) { +// aroundID : If provided all messages returned will be around given ID. +func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID, aroundID string) (st []*Message, err error) { - uri := CHANNEL_MESSAGES(channelID) + uri := EndpointChannelMessages(channelID) v := url.Values{} if limit > 0 { @@ -786,11 +1277,14 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID if beforeID != "" { v.Set("before", beforeID) } + if aroundID != "" { + v.Set("around", aroundID) + } if len(v) > 0 { uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } - body, err := s.Request("GET", uri, nil) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID)) if err != nil { return } @@ -799,64 +1293,165 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID return } -// ChannelMessageAck acknowledges and marks the given message as read +// ChannelMessage gets a single message by ID from a given channel. // channeld : The ID of a Channel // messageID : the ID of a Message -func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { +func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err error) { + + response, err := s.RequestWithBucketID("GET", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) + if err != nil { + return + } - _, err = s.Request("POST", CHANNEL_MESSAGE_ACK(channelID, messageID), nil) + err = unmarshal(response, &st) return } -// channelMessageSend sends a message to the given channel. -// channelID : The ID of a Channel. -// content : The message to send. -// tts : Whether to send the message with TTS. -func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *Message, err error) { - - // TODO: nonce string ? - data := struct { - Content string `json:"content"` - TTS bool `json:"tts"` - }{content, tts} +// ChannelMessageAck acknowledges and marks the given message as read +// channeld : The ID of a Channel +// messageID : the ID of a Message +// lastToken : token returned by last ack +func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) { - // Send the message to the given channel - response, err := s.Request("POST", CHANNEL_MESSAGES(channelID), data) + body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, "")) if err != nil { return } - err = unmarshal(response, &st) + err = unmarshal(body, &st) return } // ChannelMessageSend sends a message to the given channel. // channelID : The ID of a Channel. // content : The message to send. -func (s *Session) ChannelMessageSend(channelID string, content string) (st *Message, err error) { +func (s *Session) ChannelMessageSend(channelID string, content string) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + }) +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +// ChannelMessageSendComplex sends a message to the given channel. +// channelID : The ID of a Channel. +// data : The message struct to send. +func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) (st *Message, err error) { + if data.Embed != nil && data.Embed.Type == "" { + data.Embed.Type = "rich" + } + + endpoint := EndpointChannelMessages(channelID) + + // TODO: Remove this when compatibility is not required. + files := data.Files + if data.File != nil { + if files == nil { + files = []*File{data.File} + } else { + err = fmt.Errorf("cannot specify both File and Files") + return + } + } - return s.channelMessageSend(channelID, content, false) + var response []byte + if len(files) > 0 { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + var payload []byte + payload, err = json.Marshal(data) + if err != nil { + return + } + + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } + + err = bodywriter.Close() + if err != nil { + return + } + + response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + } else { + response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + } + if err != nil { + return + } + + err = unmarshal(response, &st) + return } // ChannelMessageSendTTS sends a message to the given channel with Text to Speech. // channelID : The ID of a Channel. // content : The message to send. -func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *Message, err error) { +func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + Tts: true, + }) +} - return s.channelMessageSend(channelID, content, true) +// ChannelMessageSendEmbed sends a message to the given channel with embedded data. +// channelID : The ID of a Channel. +// embed : The embed data to send. +func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Embed: embed, + }) } // ChannelMessageEdit edits an existing message, replacing it entirely with // the given content. -// channeld : The ID of a Channel -// messageID : the ID of a Message -func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *Message, err error) { +// channelID : The ID of a Channel +// messageID : The ID of a Message +// content : The contents of the message +func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetContent(content)) +} - data := struct { - Content string `json:"content"` - }{content} +// ChannelMessageEditComplex edits an existing message, replacing it entirely with +// the given MessageEdit struct +func (s *Session) ChannelMessageEditComplex(m *MessageEdit) (st *Message, err error) { + if m.Embed != nil && m.Embed.Type == "" { + m.Embed.Type = "rich" + } - response, err := s.Request("PATCH", CHANNEL_MESSAGE(channelID, messageID), data) + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(m.Channel, m.ID), m, EndpointChannelMessage(m.Channel, "")) if err != nil { return } @@ -865,50 +1460,105 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st * return } +// ChannelMessageEditEmbed edits an existing message with embedded data. +// channelID : The ID of a Channel +// messageID : The ID of a Message +// embed : The embed data to send +func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbed(embed)) +} + // ChannelMessageDelete deletes a message from the Channel. func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { - _, err = s.Request("DELETE", CHANNEL_MESSAGE(channelID, messageID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) return } -// ChannelFileSend sends a file to the given channel. -// channelID : The ID of a Channel. -// io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) { - - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) +// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. +// If only one messageID is in the slice call channelMessageDelete funciton. +// If the slice is empty do nothing. +// channelID : The ID of the channel for the messages to delete. +// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. +func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) (err error) { - writer, err := bodywriter.CreateFormFile("file", name) - if err != nil { - return nil, err + if len(messages) == 0 { + return } - _, err = io.Copy(writer, r) - if err != nil { + if len(messages) == 1 { + err = s.ChannelMessageDelete(channelID, messages[0]) return } - err = bodywriter.Close() - if err != nil { - return + if len(messages) > 100 { + messages = messages[:100] } - response, err := s.request("POST", CHANNEL_MESSAGES(channelID), bodywriter.FormDataContentType(), body.Bytes()) + data := struct { + Messages []string `json:"messages"` + }{messages} + + _, err = s.RequestWithBucketID("POST", EndpointChannelMessagesBulkDelete(channelID), data, EndpointChannelMessagesBulkDelete(channelID)) + return +} + +// ChannelMessagePin pins a message within a given channel. +// channelID: The ID of a channel. +// messageID: The ID of a message. +func (s *Session) ChannelMessagePin(channelID, messageID string) (err error) { + + _, err = s.RequestWithBucketID("PUT", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + return +} + +// ChannelMessageUnpin unpins a message within a given channel. +// channelID: The ID of a channel. +// messageID: The ID of a message. +func (s *Session) ChannelMessageUnpin(channelID, messageID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + return +} + +// ChannelMessagesPinned returns an array of Message structures for pinned messages +// within a given channel +// channelID : The ID of a Channel. +func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointChannelMessagesPins(channelID), nil, EndpointChannelMessagesPins(channelID)) + if err != nil { return } - err = unmarshal(response, &st) + err = unmarshal(body, &st) return } +// ChannelFileSend sends a file to the given channel. +// channelID : The ID of a Channel. +// name: The name of the file. +// io.Reader : A reader for the file contents. +func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}}) +} + +// ChannelFileSendWithMessage sends a file to the given channel with an message. +// DEPRECATED. Use ChannelMessageSendComplex instead. +// channelID : The ID of a Channel. +// content: Optional Message content. +// name: The name of the file. +// io.Reader : A reader for the file contents. +func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}, Content: content}) +} + // ChannelInvites returns an array of Invite structures for the given channel // channelID : The ID of a Channel func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { - body, err := s.Request("GET", CHANNEL_INVITES(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannelInvites(channelID), nil, EndpointChannelInvites(channelID)) if err != nil { return } @@ -924,13 +1574,13 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { data := struct { - MaxAge int `json:"max_age"` - MaxUses int `json:"max_uses"` - Temporary bool `json:"temporary"` - XKCDPass bool `json:"xkcdpass"` + MaxAge int `json:"max_age"` + MaxUses int `json:"max_uses"` + Temporary bool `json:"temporary"` + XKCDPass string `json:"xkcdpass"` }{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass} - body, err := s.Request("POST", CHANNEL_INVITES(channelID), data) + body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) if err != nil { return } @@ -951,7 +1601,7 @@ func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, a Deny int `json:"deny"` }{targetID, targetType, allow, deny} - _, err = s.Request("PUT", CHANNEL_PERMISSION(channelID, targetID), data) + _, err = s.RequestWithBucketID("PUT", EndpointChannelPermission(channelID, targetID), data, EndpointChannelPermission(channelID, "")) return } @@ -959,7 +1609,7 @@ func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, a // NOTE: Name of this func may change. func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error) { - _, err = s.Request("DELETE", CHANNEL_PERMISSION(channelID, targetID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelPermission(channelID, targetID), nil, EndpointChannelPermission(channelID, "")) return } @@ -971,7 +1621,7 @@ func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error // inviteID : The invite code (or maybe xkcdpass?) func (s *Session) Invite(inviteID string) (st *Invite, err error) { - body, err := s.Request("GET", INVITE(inviteID), nil) + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -984,7 +1634,7 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) { // inviteID : the code (or maybe xkcdpass?) of an invite func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { - body, err := s.Request("DELETE", INVITE(inviteID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -997,7 +1647,7 @@ func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { // inviteID : The invite code (or maybe xkcdpass?) func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { - body, err := s.Request("POST", INVITE(inviteID), nil) + body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -1013,7 +1663,7 @@ func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { // VoiceRegions returns the voice server regions func (s *Session) VoiceRegions() (st []*VoiceRegion, err error) { - body, err := s.Request("GET", VOICE_REGIONS, nil) + body, err := s.RequestWithBucketID("GET", EndpointVoiceRegions, nil, EndpointVoiceRegions) if err != nil { return } @@ -1025,7 +1675,7 @@ func (s *Session) VoiceRegions() (st []*VoiceRegion, err error) { // VoiceICE returns the voice server ICE information func (s *Session) VoiceICE() (st *VoiceICE, err error) { - body, err := s.Request("GET", VOICE_ICE, nil) + body, err := s.RequestWithBucketID("GET", EndpointVoiceIce, nil, EndpointVoiceIce) if err != nil { return } @@ -1038,10 +1688,10 @@ func (s *Session) VoiceICE() (st *VoiceICE, err error) { // Functions specific to Discord Websockets // ------------------------------------------------------------------------------------------------ -// Gateway returns the a websocket Gateway address +// Gateway returns the websocket Gateway address func (s *Session) Gateway() (gateway string, err error) { - response, err := s.Request("GET", GATEWAY, nil) + response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway) if err != nil { return } @@ -1056,5 +1706,336 @@ func (s *Session) Gateway() (gateway string, err error) { } gateway = temp.URL + + // Ensure the gateway always has a trailing slash. + // MacOS will fail to connect if we add query params without a trailing slash on the base domain. + if !strings.HasSuffix(gateway, "/") { + gateway += "/" + } + + return +} + +// GatewayBot returns the websocket Gateway address and the recommended number of shards +func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) { + + response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot) + if err != nil { + return + } + + err = unmarshal(response, &st) + if err != nil { + return + } + + // Ensure the gateway always has a trailing slash. + // MacOS will fail to connect if we add query params without a trailing slash on the base domain. + if !strings.HasSuffix(st.URL, "/") { + st.URL += "/" + } + + return +} + +// Functions specific to Webhooks + +// WebhookCreate returns a new Webhook. +// channelID: The ID of a Channel. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, err error) { + + data := struct { + Name string `json:"name"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("POST", EndpointChannelWebhooks(channelID), data, EndpointChannelWebhooks(channelID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// ChannelWebhooks returns all webhooks for a given channel. +// channelID: The ID of a channel. +func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointChannelWebhooks(channelID), nil, EndpointChannelWebhooks(channelID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildWebhooks returns all webhooks for a given guild. +// guildID: The ID of a Guild. +func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildWebhooks(guildID), nil, EndpointGuildWebhooks(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// Webhook returns a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointWebhook(webhookID), nil, EndpointWebhooks) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookWithToken returns a webhook for a given ID +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEdit updates an existing Webhook. +// webhookID: The ID of a webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEditWithToken updates an existing Webhook with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookDelete deletes a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) WebhookDelete(webhookID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) + + return +} + +// WebhookDeleteWithToken deletes a webhook for a given ID with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("DELETE", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookExecute executes a webhook. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook +func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { + uri := EndpointWebhookToken(webhookID, token) + + if wait { + uri += "?wait=true" + } + + _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", "")) + + return +} + +// MessageReactionAdd creates an emoji reaction to a message. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error { + + _, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, emojiID, "@me"), nil, EndpointMessageReaction(channelID, "", "", "")) + + return err +} + +// MessageReactionRemove deletes an emoji reaction to a message. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +// userID : @me or ID of the user to delete the reaction for. +func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, emojiID, userID), nil, EndpointMessageReaction(channelID, "", "", "")) + + return err +} + +// MessageReactionsRemoveAll deletes all reactions from a message +// channelID : The channel ID +// messageID : The message ID. +func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactionsAll(channelID, messageID), nil, EndpointMessageReactionsAll(channelID, messageID)) + + return err +} + +// MessageReactions gets all the users reactions for a specific emoji. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +// limit : max number of users to return (max 100) +func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int) (st []*User, err error) { + uri := EndpointMessageReactions(channelID, messageID, emojiID) + + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to user notes +// ------------------------------------------------------------------------------------------------ + +// UserNoteSet sets the note for a specific user. +func (s *Session) UserNoteSet(userID string, message string) (err error) { + data := struct { + Note string `json:"note"` + }{message} + + _, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes("")) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Relationships (Friends list) +// ------------------------------------------------------------------------------------------------ + +// RelationshipsGet returns an array of all the relationships of the user. +func (s *Session) RelationshipsGet() (r []*Relationship, err error) { + body, err := s.RequestWithBucketID("GET", EndpointRelationships(), nil, EndpointRelationships()) + if err != nil { + return + } + + err = unmarshal(body, &r) + return +} + +// relationshipCreate creates a new relationship. (I.e. send or accept a friend request, block a user.) +// relationshipType : 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req +func (s *Session) relationshipCreate(userID string, relationshipType int) (err error) { + data := struct { + Type int `json:"type"` + }{relationshipType} + + _, err = s.RequestWithBucketID("PUT", EndpointRelationship(userID), data, EndpointRelationships()) + return +} + +// RelationshipFriendRequestSend sends a friend request to a user. +// userID: ID of the user. +func (s *Session) RelationshipFriendRequestSend(userID string) (err error) { + err = s.relationshipCreate(userID, 4) + return +} + +// RelationshipFriendRequestAccept accepts a friend request from a user. +// userID: ID of the user. +func (s *Session) RelationshipFriendRequestAccept(userID string) (err error) { + err = s.relationshipCreate(userID, 1) + return +} + +// RelationshipUserBlock blocks a user. +// userID: ID of the user. +func (s *Session) RelationshipUserBlock(userID string) (err error) { + err = s.relationshipCreate(userID, 2) + return +} + +// RelationshipDelete removes the relationship with a user. +// userID: ID of the user. +func (s *Session) RelationshipDelete(userID string) (err error) { + _, err = s.RequestWithBucketID("DELETE", EndpointRelationship(userID), nil, EndpointRelationships()) + return +} + +// RelationshipsMutualGet returns an array of all the users both @me and the given user is friends with. +// userID: ID of the user. +func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) { + body, err := s.RequestWithBucketID("GET", EndpointRelationshipsMutual(userID), nil, EndpointRelationshipsMutual(userID)) + if err != nil { + return + } + + err = unmarshal(body, &mf) return } diff --git a/DiscordGo/restapi_test.go b/DiscordGo/restapi_test.go index 4c2ea50cd49577fb88c10c2c6abde6a6b1afb92a..7aa4e604d6fc57d6c5b1810bdfbb90243941ee7b 100644 --- a/DiscordGo/restapi_test.go +++ b/DiscordGo/restapi_test.go @@ -24,14 +24,22 @@ func TestChannelMessageSend(t *testing.T) { } } +/* +// removed for now, only works on BOT accounts now func TestUserAvatar(t *testing.T) { + if dg == nil { t.Skip("Cannot TestUserAvatar, dg not set.") } - a, err := dg.UserAvatar("@me") + u, err := dg.User("@me") + if err != nil { + t.Error("error fetching @me user,", err) + } + + a, err := dg.UserAvatar(u.ID) if err != nil { - if err.Error() == `HTTP 404 NOT FOUND, {"message": ""}` { + if err.Error() == `HTTP 404 NOT FOUND, {"code": 0, "message": "404: Not Found"}` { t.Skip("Skipped, @me doesn't have an Avatar") } t.Errorf(err.Error()) @@ -41,7 +49,9 @@ func TestUserAvatar(t *testing.T) { t.Errorf("a == nil, should be image.Image") } } +*/ +/* Running this causes an error due to 2/hour rate limit on username changes func TestUserUpdate(t *testing.T) { if dg == nil { t.Skip("Cannot test logout, dg not set.") @@ -67,6 +77,7 @@ func TestUserUpdate(t *testing.T) { t.Error("Username != " + u.Username) } } +*/ //func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) { @@ -103,7 +114,7 @@ func TestUserGuilds(t *testing.T) { t.Skip("Cannot TestUserGuilds, dg not set.") } - _, err := dg.UserGuilds() + _, err := dg.UserGuilds(10, "", "") if err != nil { t.Errorf(err.Error()) } @@ -120,6 +131,17 @@ func TestUserSettings(t *testing.T) { } } +func TestUserUpdateStatus(t *testing.T) { + if dg == nil { + t.Skip("Cannot TestUserSettings, dg not set.") + } + + _, err := dg.UserUpdateStatus(StatusDoNotDisturb) + if err != nil { + t.Errorf(err.Error()) + } +} + // TestLogout tests the Logout() function. This should not return an error. func TestLogout(t *testing.T) { @@ -133,6 +155,84 @@ func TestLogout(t *testing.T) { } } +func TestGateway(t *testing.T) { + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + _, err := dg.Gateway() + if err != nil { + t.Errorf("Gateway() returned error: %+v", err) + } +} + +func TestGatewayBot(t *testing.T) { + + if dgBot == nil { + t.Skip("Skipping, dgBot not set.") + } + _, err := dgBot.GatewayBot() + if err != nil { + t.Errorf("GatewayBot() returned error: %+v", err) + } +} + +func TestVoiceICE(t *testing.T) { + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + _, err := dg.VoiceICE() + if err != nil { + t.Errorf("VoiceICE() returned error: %+v", err) + } +} + +func TestVoiceRegions(t *testing.T) { + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + _, err := dg.VoiceRegions() + if err != nil { + t.Errorf("VoiceRegions() returned error: %+v", err) + } +} +func TestGuildRoles(t *testing.T) { + + if envGuild == "" { + t.Skip("Skipping, DG_GUILD not set.") + } + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + _, err := dg.GuildRoles(envGuild) + if err != nil { + t.Errorf("GuildRoles(envGuild) returned error: %+v", err) + } + +} + +func TestGuildMemberNickname(t *testing.T) { + + if envGuild == "" { + t.Skip("Skipping, DG_GUILD not set.") + } + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + err := dg.GuildMemberNickname(envGuild, "@me/nick", "testnickname") + if err != nil { + t.Errorf("GuildNickname returned error: %+v", err) + } +} + // TestChannelMessageSend2 tests the ChannelMessageSend() function. This should not return an error. func TestChannelMessageSend2(t *testing.T) { @@ -149,3 +249,39 @@ func TestChannelMessageSend2(t *testing.T) { t.Errorf("ChannelMessageSend returned error: %+v", err) } } + +// TestGuildPruneCount tests GuildPruneCount() function. This should not return an error. +func TestGuildPruneCount(t *testing.T) { + + if envGuild == "" { + t.Skip("Skipping, DG_GUILD not set.") + } + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + _, err := dg.GuildPruneCount(envGuild, 1) + if err != nil { + t.Errorf("GuildPruneCount returned error: %+v", err) + } +} + +/* +// TestGuildPrune tests GuildPrune() function. This should not return an error. +func TestGuildPrune(t *testing.T) { + + if envGuild == "" { + t.Skip("Skipping, DG_GUILD not set.") + } + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + + _, err := dg.GuildPrune(envGuild, 1) + if err != nil { + t.Errorf("GuildPrune returned error: %+v", err) + } +} +*/ diff --git a/DiscordGo/state.go b/DiscordGo/state.go index 2bdf35f23fe3e7ee0b1c5f8d3c3d2b36c2a627de..35a8e7578b1b1421b230d2166c460a6437f204a3 100644 --- a/DiscordGo/state.go +++ b/DiscordGo/state.go @@ -14,11 +14,36 @@ package discordgo import ( "errors" - "fmt" + "sort" + "sync" ) // ErrNilState is returned when the state is nil. -var ErrNilState = errors.New("State not instantiated, please use discordgo.New() or assign Session.State.") +var ErrNilState = errors.New("state not instantiated, please use discordgo.New() or assign Session.State") + +// ErrStateNotFound is returned when the state cache +// requested is not found +var ErrStateNotFound = errors.New("state cache not found") + +// A State contains the current known state. +// As discord sends this in a READY blob, it seems reasonable to simply +// use that struct as the data store. +type State struct { + sync.RWMutex + Ready + + MaxMessageCount int + TrackChannels bool + TrackEmojis bool + TrackMembers bool + TrackRoles bool + TrackVoice bool + TrackPresences bool + + guildMap map[string]*Guild + channelMap map[string]*Channel + memberMap map[string]map[string]*Member +} // NewState creates an empty state. func NewState() *State { @@ -27,20 +52,24 @@ func NewState() *State { PrivateChannels: []*Channel{}, Guilds: []*Guild{}, }, + TrackChannels: true, + TrackEmojis: true, + TrackMembers: true, + TrackRoles: true, + TrackVoice: true, + TrackPresences: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + memberMap: make(map[string]map[string]*Member), } } -// OnReady takes a Ready event and updates all internal state. -func (s *State) OnReady(r *Ready) error { - if s == nil { - return ErrNilState +func (s *State) createMemberMap(guild *Guild) { + members := make(map[string]*Member) + for _, m := range guild.Members { + members[m.User.ID] = m } - - s.Lock() - defer s.Unlock() - - s.Ready = *r - return nil + s.memberMap[guild.ID] = members } // GuildAdd adds a guild to the current world state, or @@ -53,20 +82,47 @@ func (s *State) GuildAdd(guild *Guild) error { s.Lock() defer s.Unlock() - // If the guild exists, replace it. - for i, g := range s.Guilds { - if g.ID == guild.ID { - // Don't stomp on properties that don't come in updates. + // Update the channels to point to the right guild, adding them to the channelMap as we go + for _, c := range guild.Channels { + s.channelMap[c.ID] = c + } + + // If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid + if guild.Members != nil { + s.createMemberMap(guild) + } else if _, ok := s.memberMap[guild.ID]; !ok { + // Even if we have no new member slice, we still initialize the member map for this guild if it doesn't exist + s.memberMap[guild.ID] = make(map[string]*Member) + } + + if g, ok := s.guildMap[guild.ID]; ok { + // We are about to replace `g` in the state with `guild`, but first we need to + // make sure we preserve any fields that the `guild` doesn't contain from `g`. + if guild.Roles == nil { + guild.Roles = g.Roles + } + if guild.Emojis == nil { + guild.Emojis = g.Emojis + } + if guild.Members == nil { guild.Members = g.Members + } + if guild.Presences == nil { guild.Presences = g.Presences + } + if guild.Channels == nil { guild.Channels = g.Channels + } + if guild.VoiceStates == nil { guild.VoiceStates = g.VoiceStates - s.Guilds[i] = guild - return nil } + *g = *guild + return nil } s.Guilds = append(s.Guilds, guild) + s.guildMap[guild.ID] = guild + return nil } @@ -76,9 +132,16 @@ func (s *State) GuildRemove(guild *Guild) error { return ErrNilState } + _, err := s.Guild(guild.ID) + if err != nil { + return err + } + s.Lock() defer s.Unlock() + delete(s.guildMap, guild.ID) + for i, g := range s.Guilds { if g.ID == guild.ID { s.Guilds = append(s.Guilds[:i], s.Guilds[i+1:]...) @@ -86,7 +149,7 @@ func (s *State) GuildRemove(guild *Guild) error { } } - return errors.New("Guild not found.") + return nil } // Guild gets a guild by ID. @@ -101,13 +164,112 @@ func (s *State) Guild(guildID string) (*Guild, error) { s.RLock() defer s.RUnlock() - for _, g := range s.Guilds { - if g.ID == guildID { - return g, nil + if g, ok := s.guildMap[guildID]; ok { + return g, nil + } + + return nil, ErrStateNotFound +} + +// PresenceAdd adds a presence to the current world state, or +// updates it if it already exists. +func (s *State) PresenceAdd(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + //guild.Presences[i] = presence + + //Update status + guild.Presences[i].Game = presence.Game + guild.Presences[i].Roles = presence.Roles + if presence.Status != "" { + guild.Presences[i].Status = presence.Status + } + if presence.Nick != "" { + guild.Presences[i].Nick = presence.Nick + } + + //Update the optionally sent user information + //ID Is a mandatory field so you should not need to check if it is empty + guild.Presences[i].User.ID = presence.User.ID + + if presence.User.Avatar != "" { + guild.Presences[i].User.Avatar = presence.User.Avatar + } + if presence.User.Discriminator != "" { + guild.Presences[i].User.Discriminator = presence.User.Discriminator + } + if presence.User.Email != "" { + guild.Presences[i].User.Email = presence.User.Email + } + if presence.User.Token != "" { + guild.Presences[i].User.Token = presence.User.Token + } + if presence.User.Username != "" { + guild.Presences[i].User.Username = presence.User.Username + } + + return nil } } - return nil, errors.New("Guild not found.") + guild.Presences = append(guild.Presences, presence) + return nil +} + +// PresenceRemove removes a presence from the current world state. +func (s *State) PresenceRemove(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + guild.Presences = append(guild.Presences[:i], guild.Presences[i+1:]...) + return nil + } + } + + return ErrStateNotFound +} + +// Presence gets a presence by ID from a guild. +func (s *State) Presence(guildID, userID string) (*Presence, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + for _, p := range guild.Presences { + if p.User.ID == userID { + return p, nil + } + } + + return nil, ErrStateNotFound } // TODO: Consider moving Guild state update methods onto *Guild. @@ -127,14 +289,19 @@ func (s *State) MemberAdd(member *Member) error { s.Lock() defer s.Unlock() - for i, m := range guild.Members { - if m.User.ID == member.User.ID { - guild.Members[i] = member - return nil - } + members, ok := s.memberMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + m, ok := members[member.User.ID] + if !ok { + members[member.User.ID] = member + guild.Members = append(guild.Members, member) + } else { + *m = *member // Update the actual data, which will also update the member pointer in the slice } - guild.Members = append(guild.Members, member) return nil } @@ -152,6 +319,17 @@ func (s *State) MemberRemove(member *Member) error { s.Lock() defer s.Unlock() + members, ok := s.memberMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + _, ok = members[member.User.ID] + if !ok { + return ErrStateNotFound + } + delete(members, member.User.ID) + for i, m := range guild.Members { if m.User.ID == member.User.ID { guild.Members = append(guild.Members[:i], guild.Members[i+1:]...) @@ -159,7 +337,7 @@ func (s *State) MemberRemove(member *Member) error { } } - return errors.New("Member not found.") + return ErrStateNotFound } // Member gets a member by ID from a guild. @@ -168,6 +346,78 @@ func (s *State) Member(guildID, userID string) (*Member, error) { return nil, ErrNilState } + s.RLock() + defer s.RUnlock() + + members, ok := s.memberMap[guildID] + if !ok { + return nil, ErrStateNotFound + } + + m, ok := members[userID] + if ok { + return m, nil + } + + return nil, ErrStateNotFound +} + +// RoleAdd adds a role to the current world state, or +// updates it if it already exists. +func (s *State) RoleAdd(guildID string, role *Role) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, r := range guild.Roles { + if r.ID == role.ID { + guild.Roles[i] = role + return nil + } + } + + guild.Roles = append(guild.Roles, role) + return nil +} + +// RoleRemove removes a role from current world state by ID. +func (s *State) RoleRemove(guildID, roleID string) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, r := range guild.Roles { + if r.ID == roleID { + guild.Roles = append(guild.Roles[:i], guild.Roles[i+1:]...) + return nil + } + } + + return ErrStateNotFound +} + +// Role gets a role by ID from a guild. +func (s *State) Role(guildID, roleID string) (*Role, error) { + if s == nil { + return nil, ErrNilState + } + guild, err := s.Guild(guildID) if err != nil { return nil, err @@ -176,16 +426,16 @@ func (s *State) Member(guildID, userID string) (*Member, error) { s.RLock() defer s.RUnlock() - for _, m := range guild.Members { - if m.User.ID == userID { - return m, nil + for _, r := range guild.Roles { + if r.ID == roleID { + return r, nil } } - return nil, errors.New("Member not found.") + return nil, ErrStateNotFound } -// ChannelAdd adds a guild to the current world state, or +// ChannelAdd adds a channel to the current world state, or // updates it if it already exists. // Channels may exist either as PrivateChannels or inside // a guild. @@ -194,43 +444,35 @@ func (s *State) ChannelAdd(channel *Channel) error { return ErrNilState } - if channel.IsPrivate { - s.Lock() - defer s.Unlock() + s.Lock() + defer s.Unlock() - // If the channel exists, replace it. - for i, c := range s.PrivateChannels { - if c.ID == channel.ID { - // Don't stomp on messages. - channel.Messages = c.Messages - s.PrivateChannels[i] = channel - return nil - } + // If the channel exists, replace it + if c, ok := s.channelMap[channel.ID]; ok { + if channel.Messages == nil { + channel.Messages = c.Messages } - - s.PrivateChannels = append(s.PrivateChannels, channel) - } else { - guild, err := s.Guild(channel.GuildID) - if err != nil { - return err + if channel.PermissionOverwrites == nil { + channel.PermissionOverwrites = c.PermissionOverwrites } - s.Lock() - defer s.Unlock() + *c = *channel + return nil + } - // If the channel exists, replace it. - for i, c := range guild.Channels { - if c.ID == channel.ID { - // Don't stomp on messages. - channel.Messages = c.Messages - guild.Channels[i] = channel - return nil - } + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { + s.PrivateChannels = append(s.PrivateChannels, channel) + } else { + guild, ok := s.guildMap[channel.GuildID] + if !ok { + return ErrStateNotFound } guild.Channels = append(guild.Channels, channel) } + s.channelMap[channel.ID] = channel + return nil } @@ -240,14 +482,19 @@ func (s *State) ChannelRemove(channel *Channel) error { return ErrNilState } - if channel.IsPrivate { + _, err := s.Channel(channel.ID) + if err != nil { + return err + } + + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.Lock() defer s.Unlock() for i, c := range s.PrivateChannels { if c.ID == channel.ID { s.PrivateChannels = append(s.PrivateChannels[:i], s.PrivateChannels[i+1:]...) - return nil + break } } } else { @@ -262,53 +509,26 @@ func (s *State) ChannelRemove(channel *Channel) error { for i, c := range guild.Channels { if c.ID == channel.ID { guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...) - return nil + break } } } - return errors.New("Channel not found.") + delete(s.channelMap, channel.ID) + + return nil } // GuildChannel gets a channel by ID from a guild. +// This method is Deprecated, use Channel(channelID) func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) { - if s == nil { - return nil, ErrNilState - } - - guild, err := s.Guild(guildID) - if err != nil { - return nil, err - } - - s.RLock() - defer s.RUnlock() - - for _, c := range guild.Channels { - if c.ID == channelID { - return c, nil - } - } - - return nil, errors.New("Channel not found.") + return s.Channel(channelID) } // PrivateChannel gets a private channel by ID. +// This method is Deprecated, use Channel(channelID) func (s *State) PrivateChannel(channelID string) (*Channel, error) { - if s == nil { - return nil, ErrNilState - } - - s.RLock() - defer s.RUnlock() - - for _, c := range s.PrivateChannels { - if c.ID == channelID { - return c, nil - } - } - - return nil, errors.New("Channel not found.") + return s.Channel(channelID) } // Channel gets a channel by ID, it will look in all guilds an private channels. @@ -317,19 +537,14 @@ func (s *State) Channel(channelID string) (*Channel, error) { return nil, ErrNilState } - c, err := s.PrivateChannel(channelID) - if err == nil { - return c, nil - } + s.RLock() + defer s.RUnlock() - for _, g := range s.Guilds { - c, err := s.GuildChannel(g.ID, channelID) - if err == nil { - return c, nil - } + if c, ok := s.channelMap[channelID]; ok { + return c, nil } - return nil, errors.New("Channel not found.") + return nil, ErrStateNotFound } // Emoji returns an emoji for a guild and emoji id. @@ -352,7 +567,7 @@ func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) { } } - return nil, errors.New("Emoji not found.") + return nil, ErrStateNotFound } // EmojiAdd adds an emoji to the current world state. @@ -406,10 +621,31 @@ func (s *State) MessageAdd(message *Message) error { s.Lock() defer s.Unlock() - // If the message exists, replace it. - for i, m := range c.Messages { + // If the message exists, merge in the new message contents. + for _, m := range c.Messages { if m.ID == message.ID { - c.Messages[i] = message + if message.Content != "" { + m.Content = message.Content + } + if message.EditedTimestamp != "" { + m.EditedTimestamp = message.EditedTimestamp + } + if message.Mentions != nil { + m.Mentions = message.Mentions + } + if message.Embeds != nil { + m.Embeds = message.Embeds + } + if message.Attachments != nil { + m.Attachments = message.Attachments + } + if message.Timestamp != "" { + m.Timestamp = message.Timestamp + } + if message.Author != nil { + m.Author = message.Author + } + return nil } } @@ -417,14 +653,7 @@ func (s *State) MessageAdd(message *Message) error { c.Messages = append(c.Messages, message) if len(c.Messages) > s.MaxMessageCount { - s.Unlock() - for len(c.Messages) > s.MaxMessageCount { - err := s.MessageRemove(c.Messages[0]) - if err != nil { - fmt.Println("message remove error: ", err) - } - } - s.Lock() + c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] } return nil } @@ -435,7 +664,12 @@ func (s *State) MessageRemove(message *Message) error { return ErrNilState } - c, err := s.Channel(message.ChannelID) + return s.messageRemoveByID(message.ChannelID, message.ID) +} + +// messageRemoveByID removes a message by channelID and messageID from the world state. +func (s *State) messageRemoveByID(channelID, messageID string) error { + c, err := s.Channel(channelID) if err != nil { return err } @@ -444,13 +678,44 @@ func (s *State) MessageRemove(message *Message) error { defer s.Unlock() for i, m := range c.Messages { - if m.ID == message.ID { + if m.ID == messageID { c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) return nil } } - return errors.New("Message not found.") + return ErrStateNotFound +} + +func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { + guild, err := s.Guild(update.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + // Handle Leaving Channel + if update.ChannelID == "" { + for i, state := range guild.VoiceStates { + if state.UserID == update.UserID { + guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...) + return nil + } + } + } else { + for i, state := range guild.VoiceStates { + if state.UserID == update.UserID { + guild.VoiceStates[i] = update.VoiceState + return nil + } + } + + guild.VoiceStates = append(guild.VoiceStates, update.VoiceState) + } + + return nil } // Message gets a message by channel and message ID. @@ -473,21 +738,66 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { } } - return nil, errors.New("Message not found.") + return nil, ErrStateNotFound } -// onInterface handles all events related to states. -func (s *State) onInterface(se *Session, i interface{}) (err error) { +// OnReady takes a Ready event and updates all internal state. +func (s *State) onReady(se *Session, r *Ready) (err error) { if s == nil { return ErrNilState } + + s.Lock() + defer s.Unlock() + + // We must track at least the current user for Voice, even + // if state is disabled, store the bare essentials. + if !se.StateEnabled { + ready := Ready{ + Version: r.Version, + SessionID: r.SessionID, + User: r.User, + } + + s.Ready = ready + + return nil + } + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + s.createMemberMap(g) + + for _, c := range g.Channels { + s.channelMap[c.ID] = c + } + } + + for _, c := range s.PrivateChannels { + s.channelMap[c.ID] = c + } + + return nil +} + +// OnInterface handles all events related to states. +func (s *State) OnInterface(se *Session, i interface{}) (err error) { + if s == nil { + return ErrNilState + } + + r, ok := i.(*Ready) + if ok { + return s.onReady(se, r) + } + if !se.StateEnabled { return nil } switch t := i.(type) { - case *Ready: - err = s.OnReady(t) case *GuildCreate: err = s.GuildAdd(t.Guild) case *GuildUpdate: @@ -495,26 +805,179 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { case *GuildDelete: err = s.GuildRemove(t.Guild) case *GuildMemberAdd: - err = s.MemberAdd(t.Member) + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } case *GuildMemberUpdate: - err = s.MemberAdd(t.Member) + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } case *GuildMemberRemove: - err = s.MemberRemove(t.Member) + if s.TrackMembers { + err = s.MemberRemove(t.Member) + } + case *GuildRoleCreate: + if s.TrackRoles { + err = s.RoleAdd(t.GuildID, t.Role) + } + case *GuildRoleUpdate: + if s.TrackRoles { + err = s.RoleAdd(t.GuildID, t.Role) + } + case *GuildRoleDelete: + if s.TrackRoles { + err = s.RoleRemove(t.GuildID, t.RoleID) + } case *GuildEmojisUpdate: - err = s.EmojisAdd(t.GuildID, t.Emojis) + if s.TrackEmojis { + err = s.EmojisAdd(t.GuildID, t.Emojis) + } case *ChannelCreate: - err = s.ChannelAdd(t.Channel) + if s.TrackChannels { + err = s.ChannelAdd(t.Channel) + } case *ChannelUpdate: - err = s.ChannelAdd(t.Channel) + if s.TrackChannels { + err = s.ChannelAdd(t.Channel) + } case *ChannelDelete: - err = s.ChannelRemove(t.Channel) + if s.TrackChannels { + err = s.ChannelRemove(t.Channel) + } case *MessageCreate: - err = s.MessageAdd(t.Message) + if s.MaxMessageCount != 0 { + err = s.MessageAdd(t.Message) + } case *MessageUpdate: - err = s.MessageAdd(t.Message) + if s.MaxMessageCount != 0 { + err = s.MessageAdd(t.Message) + } case *MessageDelete: - err = s.MessageRemove(t.Message) + if s.MaxMessageCount != 0 { + err = s.MessageRemove(t.Message) + } + case *MessageDeleteBulk: + if s.MaxMessageCount != 0 { + for _, mID := range t.Messages { + s.messageRemoveByID(t.ChannelID, mID) + } + } + case *VoiceStateUpdate: + if s.TrackVoice { + err = s.voiceStateUpdate(t) + } + case *PresenceUpdate: + if s.TrackPresences { + s.PresenceAdd(t.GuildID, &t.Presence) + } + if s.TrackMembers { + if t.Status == StatusOffline { + return + } + + var m *Member + m, err = s.Member(t.GuildID, t.User.ID) + + if err != nil { + // Member not found; this is a user coming online + m = &Member{ + GuildID: t.GuildID, + Nick: t.Nick, + User: t.User, + Roles: t.Roles, + } + + } else { + + if t.Nick != "" { + m.Nick = t.Nick + } + + if t.User.Username != "" { + m.User.Username = t.User.Username + } + + // PresenceUpdates always contain a list of roles, so there's no need to check for an empty list here + m.Roles = t.Roles + + } + + err = s.MemberAdd(m) + } + } return } + +// UserChannelPermissions returns the permission of a user in a channel. +// userID : The ID of the user to calculate permissions for. +// channelID : The ID of the channel to calculate permission for. +func (s *State) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + if s == nil { + return 0, ErrNilState + } + + channel, err := s.Channel(channelID) + if err != nil { + return + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return + } + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return + } + + return memberPermissions(guild, channel, member), nil +} + +// UserColor returns the color of a user in a channel. +// While colors are defined at a Guild level, determining for a channel is more useful in message handlers. +// 0 is returned in cases of error, which is the color of @everyone. +// userID : The ID of the user to calculate the color for. +// channelID : The ID of the channel to calculate the color for. +func (s *State) UserColor(userID, channelID string) int { + if s == nil { + return 0 + } + + channel, err := s.Channel(channelID) + if err != nil { + return 0 + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return 0 + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return 0 + } + + roles := Roles(guild.Roles) + sort.Sort(roles) + + for _, role := range roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + if role.Color != 0 { + return role.Color + } + } + } + } + + return 0 +} diff --git a/DiscordGo/structs.go b/DiscordGo/structs.go index 14883a9ba0c64ca7a9ef350c6b73907b9df97ad2..c3e3956673895b153e02ea252bba38c27b5f8271 100644 --- a/DiscordGo/structs.go +++ b/DiscordGo/structs.go @@ -13,11 +13,12 @@ package discordgo import ( "encoding/json" - "reflect" + "net/http" + "strconv" "sync" "time" - "bitbucket.org/henesy/disco/websocket" + "github.com/gorilla/websocket" ) // A Session represents a connection to the Discord API. @@ -28,9 +29,11 @@ type Session struct { // Authentication token for this session Token string + MFA bool // 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 @@ -38,42 +41,75 @@ type Session struct { // Should the session request compressed websocket data. Compress bool + // Sharding + ShardID int + ShardCount int + // Should state tracking be enabled. // State tracking is the best way for getting the the users // active guilds and the members of the guilds. StateEnabled bool + // Whether or not to call event handlers synchronously. + // e.g false = launch event handlers in their own goroutines. + SyncEvents bool + // Exposed but should not be modified by User. // Whether the Data Websocket is ready - DataReady bool + DataReady bool // NOTE: Maye be deprecated soon + + // Max number of REST API retries + MaxRestRetries int + + // Status stores the currect status of the websocket connection + // this is being tested, may stay, may go away. + status int32 // Whether the Voice Websocket is ready - VoiceReady bool + VoiceReady bool // NOTE: Deprecated. // Whether the UDP Connection is ready - UDPReady bool + UDPReady bool // NOTE: Deprecated - // Stores all details related to voice connections - Voice *Voice + // Stores a mapping of guild id's to VoiceConnections + VoiceConnections map[string]*VoiceConnection // Managed state object, updated internally with events when // StateEnabled is true. State *State - handlersMu sync.RWMutex - // This is a mapping of event struct to a reflected value - // for event handlers. - // We store the reflected value instead of the function - // reference as it is more performant, instead of re-reflecting - // the function each event. - handlers map[interface{}][]reflect.Value + // The http client used for REST requests + Client *http.Client + + // Stores the last HeartbeatAck that was recieved (in UTC) + LastHeartbeatAck time.Time + + // Event handlers + handlersMu sync.RWMutex + handlers map[string][]*eventHandlerInstance + onceHandlers map[string][]*eventHandlerInstance // The websocket connection. wsConn *websocket.Conn // When nil, the session is not listening. listening chan interface{} + + // used to deal with rate limits + ratelimiter *RateLimiter + + // sequence tracks the current gateway api websocket sequence number + sequence *int64 + + // stores sessions current Discord Gateway + gateway string + + // stores session ID of current Gateway connection + sessionID string + + // used to make sure gateway websocket writes do not happen concurrently + wsMutex sync.Mutex } // A VoiceRegion stores data for a specific voice region server. @@ -99,33 +135,45 @@ type ICEServer struct { // A Invite stores all data related to a specific Discord Guild or Channel invite. type Invite struct { - Guild *Guild `json:"guild"` - Channel *Channel `json:"channel"` - Inviter *User `json:"inviter"` - Code string `json:"code"` - CreatedAt string `json:"created_at"` // TODO make timestamp - MaxAge int `json:"max_age"` - Uses int `json:"uses"` - MaxUses int `json:"max_uses"` - XkcdPass bool `json:"xkcdpass"` - Revoked bool `json:"revoked"` - Temporary bool `json:"temporary"` + Guild *Guild `json:"guild"` + Channel *Channel `json:"channel"` + Inviter *User `json:"inviter"` + Code string `json:"code"` + CreatedAt Timestamp `json:"created_at"` + MaxAge int `json:"max_age"` + Uses int `json:"uses"` + MaxUses int `json:"max_uses"` + XkcdPass string `json:"xkcdpass"` + Revoked bool `json:"revoked"` + Temporary bool `json:"temporary"` } +// ChannelType is the type of a Channel +type ChannelType int + +// Block contains known ChannelType values +const ( + ChannelTypeGuildText ChannelType = iota + ChannelTypeDM + ChannelTypeGuildVoice + ChannelTypeGroupDM + ChannelTypeGuildCategory +) + // A Channel holds all data related to an individual Discord channel. type Channel struct { ID string `json:"id"` GuildID string `json:"guild_id"` Name string `json:"name"` Topic string `json:"topic"` + Type ChannelType `json:"type"` + LastMessageID string `json:"last_message_id"` + NSFW bool `json:"nsfw"` Position int `json:"position"` Bitrate int `json:"bitrate"` - Type string `json:"type"` - PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` - IsPrivate bool `json:"is_private"` - LastMessageID string `json:"last_message_id"` - Recipient *User `json:"recipient"` + Recipients []*User `json:"recipient"` Messages []*Message `json:"-"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` } // A PermissionOverwrite holds permission overwrite data for a Channel @@ -145,27 +193,75 @@ type Emoji struct { RequireColons bool `json:"require_colons"` } +// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. +func (e *Emoji) APIName() string { + if e.ID != "" && e.Name != "" { + return e.Name + ":" + e.ID + } + if e.Name != "" { + return e.Name + } + return e.ID +} + +// VerificationLevel type defination +type VerificationLevel int + +// Constants for VerificationLevel levels from 0 to 3 inclusive +const ( + VerificationLevelNone VerificationLevel = iota + VerificationLevelLow + VerificationLevelMedium + VerificationLevelHigh +) + // A Guild holds all data related to a specific Discord Guild. Guilds are also // sometimes referred to as Servers in the Discord client. type Guild struct { - ID string `json:"id"` - Name string `json:"name"` - Icon string `json:"icon"` - Region string `json:"region"` - AfkChannelID string `json:"afk_channel_id"` - EmbedChannelID string `json:"embed_channel_id"` - OwnerID string `json:"owner_id"` - JoinedAt string `json:"joined_at"` // make this a timestamp - Splash string `json:"splash"` - AfkTimeout int `json:"afk_timeout"` - EmbedEnabled bool `json:"embed_enabled"` - Large bool `json:"large"` // ?? - Roles []*Role `json:"roles"` - Emojis []*Emoji `json:"emojis"` - Members []*Member `json:"members"` - Presences []*Presence `json:"presences"` - Channels []*Channel `json:"channels"` - VoiceStates []*VoiceState `json:"voice_states"` + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Region string `json:"region"` + AfkChannelID string `json:"afk_channel_id"` + EmbedChannelID string `json:"embed_channel_id"` + OwnerID string `json:"owner_id"` + JoinedAt Timestamp `json:"joined_at"` + Splash string `json:"splash"` + AfkTimeout int `json:"afk_timeout"` + MemberCount int `json:"member_count"` + VerificationLevel VerificationLevel `json:"verification_level"` + EmbedEnabled bool `json:"embed_enabled"` + Large bool `json:"large"` // ?? + DefaultMessageNotifications int `json:"default_message_notifications"` + Roles []*Role `json:"roles"` + Emojis []*Emoji `json:"emojis"` + Members []*Member `json:"members"` + Presences []*Presence `json:"presences"` + Channels []*Channel `json:"channels"` + VoiceStates []*VoiceState `json:"voice_states"` + Unavailable bool `json:"unavailable"` +} + +// A UserGuild holds a brief version of a Guild +type UserGuild struct { + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Owner bool `json:"owner"` + Permissions int `json:"permissions"` +} + +// A GuildParams stores all the data needed to update discord guild settings +type GuildParams struct { + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` + DefaultMessageNotifications int `json:"default_message_notifications,omitempty"` // TODO: Separate type? + AfkChannelID string `json:"afk_channel_id,omitempty"` + AfkTimeout int `json:"afk_timeout,omitempty"` + Icon string `json:"icon,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + Splash string `json:"splash,omitempty"` } // A Role stores information about Discord guild member roles. @@ -173,17 +269,34 @@ type Role struct { ID string `json:"id"` Name string `json:"name"` Managed bool `json:"managed"` + Mentionable bool `json:"mentionable"` Hoist bool `json:"hoist"` Color int `json:"color"` Position int `json:"position"` Permissions int `json:"permissions"` } +// Roles are a collection of Role +type Roles []*Role + +func (r Roles) Len() int { + return len(r) +} + +func (r Roles) Less(i, j int) bool { + return r[i].Position > r[j].Position +} + +func (r Roles) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + // A VoiceState stores the voice states of Guilds type VoiceState struct { UserID string `json:"user_id"` SessionID string `json:"session_id"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` Suppress bool `json:"suppress"` SelfMute bool `json:"self_mute"` SelfDeaf bool `json:"self_deaf"` @@ -193,80 +306,112 @@ type VoiceState struct { // A Presence stores the online, offline, or idle and game status of Guild members. type Presence struct { - User *User `json:"user"` - Status string `json:"status"` - Game *Game `json:"game"` + User *User `json:"user"` + Status Status `json:"status"` + Game *Game `json:"game"` + Nick string `json:"nick"` + Roles []string `json:"roles"` + Since *int `json:"since"` } // A Game struct holds the name of the "playing .." game for a user type Game struct { Name string `json:"name"` + Type int `json:"type"` + URL string `json:"url,omitempty"` +} + +// UnmarshalJSON unmarshals json to Game struct +func (g *Game) UnmarshalJSON(bytes []byte) error { + temp := &struct { + Name json.Number `json:"name"` + Type json.RawMessage `json:"type"` + URL string `json:"url"` + }{} + err := json.Unmarshal(bytes, temp) + if err != nil { + return err + } + g.URL = temp.URL + g.Name = temp.Name.String() + + if temp.Type != nil { + err = json.Unmarshal(temp.Type, &g.Type) + if err == nil { + return nil + } + + s := "" + err = json.Unmarshal(temp.Type, &s) + if err == nil { + g.Type, err = strconv.Atoi(s) + } + + return err + } + + return nil } // A Member stores user information for Guild members. type Member struct { GuildID string `json:"guild_id"` JoinedAt string `json:"joined_at"` + Nick string `json:"nick"` Deaf bool `json:"deaf"` Mute bool `json:"mute"` User *User `json:"user"` Roles []string `json:"roles"` } -// A User stores all data for an individual Discord user. -type User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - Avatar string `json:"Avatar"` - Verified bool `json:"verified"` - //Discriminator int `json:"discriminator,string"` // TODO: See below -} - -// TODO: Research issue. -// Discriminator sometimes comes as a string -// and sometimes it comes as a int. Weird. -// to avoid errors I've just commented it out -// but it doesn't seem to just kill the whole -// program. Heartbeat is taken on READY even -// with error and the system continues to read -// it just doesn't seem able to handle this one -// field correctly. Need to research this more. - // A Settings stores data for a specific users Discord client settings. type Settings struct { - RenderEmbeds bool `json:"render_embeds"` - InlineEmbedMedia bool `json:"inline_embed_media"` - EnableTtsCommand bool `json:"enable_tts_command"` - MessageDisplayCompact bool `json:"message_display_compact"` - ShowCurrentGame bool `json:"show_current_game"` - Locale string `json:"locale"` - Theme string `json:"theme"` - MutedChannels []string `json:"muted_channels"` -} - -// 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"` - RawData json.RawMessage `json:"d"` -} - -// A Ready stores all data for the websocket READY event. -type Ready struct { - Version int `json:"v"` - SessionID string `json:"session_id"` - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - User *User `json:"user"` - ReadState []*ReadState `json:"read_state"` - PrivateChannels []*Channel `json:"private_channels"` - Guilds []*Guild `json:"guilds"` -} - -// A RateLimit struct holds information related to a specific rate limit. -type RateLimit struct { + RenderEmbeds bool `json:"render_embeds"` + InlineEmbedMedia bool `json:"inline_embed_media"` + InlineAttachmentMedia bool `json:"inline_attachment_media"` + EnableTtsCommand bool `json:"enable_tts_command"` + MessageDisplayCompact bool `json:"message_display_compact"` + ShowCurrentGame bool `json:"show_current_game"` + ConvertEmoticons bool `json:"convert_emoticons"` + Locale string `json:"locale"` + Theme string `json:"theme"` + GuildPositions []string `json:"guild_positions"` + RestrictedGuilds []string `json:"restricted_guilds"` + FriendSourceFlags *FriendSourceFlags `json:"friend_source_flags"` + Status Status `json:"status"` + DetectPlatformAccounts bool `json:"detect_platform_accounts"` + DeveloperMode bool `json:"developer_mode"` +} + +// Status type defination +type Status string + +// Constants for Status with the different current available status +const ( + StatusOnline Status = "online" + StatusIdle Status = "idle" + StatusDoNotDisturb Status = "dnd" + StatusInvisible Status = "invisible" + StatusOffline Status = "offline" +) + +// FriendSourceFlags stores ... TODO :) +type FriendSourceFlags struct { + All bool `json:"all"` + MutualGuilds bool `json:"mutual_guilds"` + MutualFriends bool `json:"mutual_friends"` +} + +// A Relationship between the logged in user and Relationship.User +type Relationship struct { + User *User `json:"user"` + Type int `json:"type"` // 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req + ID string `json:"id"` +} + +// 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"` @@ -274,68 +419,242 @@ type RateLimit struct { // A ReadState stores data on the read state of channels. type ReadState struct { - MentionCount int + MentionCount int `json:"mention_count"` LastMessageID string `json:"last_message_id"` ID string `json:"id"` } -// A TypingStart stores data for the typing start websocket event. -type TypingStart struct { - UserID string `json:"user_id"` - ChannelID string `json:"channel_id"` - Timestamp int `json:"timestamp"` +// An Ack is used to ack messages +type Ack struct { + Token string `json:"token"` } -// A PresenceUpdate stores data for the pressence update websocket event. -type PresenceUpdate struct { - User *User `json:"user"` - Status string `json:"status"` - Roles []string `json:"roles"` - GuildID string `json:"guild_id"` - Game *Game `json:"game"` +// A GuildRole stores data for guild roles. +type GuildRole struct { + Role *Role `json:"role"` + GuildID string `json:"guild_id"` } -// A MessageAck stores data for the message ack websocket event. -type MessageAck struct { - MessageID string `json:"message_id"` +// A GuildBan stores data for a guild ban. +type GuildBan struct { + Reason string `json:"reason"` + User *User `json:"user"` +} + +// A GuildIntegration stores data for a guild integration. +type GuildIntegration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account *GuildIntegrationAccount `json:"account"` + SyncedAt int `json:"synced_at"` +} + +// A GuildIntegrationAccount stores data for a guild integration account. +type GuildIntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// A GuildEmbed stores data for a guild embed. +type GuildEmbed struct { + Enabled bool `json:"enabled"` ChannelID string `json:"channel_id"` } -// A GuildIntegrationsUpdate stores data for the guild integrations update -// websocket event. -type GuildIntegrationsUpdate struct { - GuildID string `json:"guild_id"` +// A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. +type UserGuildSettingsChannelOverride struct { + Muted bool `json:"muted"` + MessageNotifications int `json:"message_notifications"` + ChannelID string `json:"channel_id"` } -// A GuildRole stores data for guild role websocket events. -type GuildRole struct { - Role *Role `json:"role"` - GuildID string `json:"guild_id"` +// A UserGuildSettings stores data for a users guild settings. +type UserGuildSettings struct { + SupressEveryone bool `json:"suppress_everyone"` + Muted bool `json:"muted"` + MobilePush bool `json:"mobile_push"` + MessageNotifications int `json:"message_notifications"` + GuildID string `json:"guild_id"` + ChannelOverrides []*UserGuildSettingsChannelOverride `json:"channel_overrides"` } -// A GuildRoleDelete stores data for the guild role delete websocket event. -type GuildRoleDelete struct { - RoleID string `json:"role_id"` - GuildID string `json:"guild_id"` +// A UserGuildSettingsEdit stores data for editing UserGuildSettings +type UserGuildSettingsEdit struct { + SupressEveryone bool `json:"suppress_everyone"` + Muted bool `json:"muted"` + MobilePush bool `json:"mobile_push"` + MessageNotifications int `json:"message_notifications"` + ChannelOverrides map[string]*UserGuildSettingsChannelOverride `json:"channel_overrides"` } -// A GuildBan stores data for a guild ban. -type GuildBan struct { - User *User `json:"user"` - GuildID string `json:"guild_id"` +// An APIErrorMessage is an api error message returned from discord +type APIErrorMessage struct { + Code int `json:"code"` + Message string `json:"message"` } -// A GuildEmojisUpdate stores data for a guild emoji update event. -type GuildEmojisUpdate struct { - GuildID string `json:"guild_id"` - Emojis []*Emoji `json:"emojis"` +// Webhook stores the data for a webhook. +type Webhook struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + User *User `json:"user"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Token string `json:"token"` } -// A State contains the current known state. -// As discord sends this in a READY blob, it seems reasonable to simply -// use that struct as the data store. -type State struct { - sync.RWMutex - Ready - MaxMessageCount int +// WebhookParams is a struct for webhook params, used in the WebhookExecute command. +type WebhookParams struct { + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + File string `json:"file,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` } + +// MessageReaction stores the data for a message reaction. +type MessageReaction struct { + UserID string `json:"user_id"` + MessageID string `json:"message_id"` + Emoji Emoji `json:"emoji"` + ChannelID string `json:"channel_id"` +} + +// GatewayBotResponse stores the data for the gateway/bot response +type GatewayBotResponse struct { + URL string `json:"url"` + Shards int `json:"shards"` +} + +// Constants for the different bit offsets of text channel permissions +const ( + PermissionReadMessages = 1 << (iota + 10) + PermissionSendMessages + PermissionSendTTSMessages + PermissionManageMessages + PermissionEmbedLinks + PermissionAttachFiles + PermissionReadMessageHistory + PermissionMentionEveryone + PermissionUseExternalEmojis +) + +// Constants for the different bit offsets of voice permissions +const ( + PermissionVoiceConnect = 1 << (iota + 20) + PermissionVoiceSpeak + PermissionVoiceMuteMembers + PermissionVoiceDeafenMembers + PermissionVoiceMoveMembers + PermissionVoiceUseVAD +) + +// Constants for general management. +const ( + PermissionChangeNickname = 1 << (iota + 26) + PermissionManageNicknames + PermissionManageRoles + PermissionManageWebhooks + PermissionManageEmojis +) + +// Constants for the different bit offsets of general permissions +const ( + PermissionCreateInstantInvite = 1 << iota + PermissionKickMembers + PermissionBanMembers + PermissionAdministrator + PermissionManageChannels + PermissionManageServer + PermissionAddReactions + PermissionViewAuditLogs + + PermissionAllText = PermissionReadMessages | + PermissionSendMessages | + PermissionSendTTSMessages | + PermissionManageMessages | + PermissionEmbedLinks | + PermissionAttachFiles | + PermissionReadMessageHistory | + PermissionMentionEveryone + PermissionAllVoice = PermissionVoiceConnect | + PermissionVoiceSpeak | + PermissionVoiceMuteMembers | + PermissionVoiceDeafenMembers | + PermissionVoiceMoveMembers | + PermissionVoiceUseVAD + PermissionAllChannel = PermissionAllText | + PermissionAllVoice | + PermissionCreateInstantInvite | + PermissionManageRoles | + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs + PermissionAll = PermissionAllChannel | + PermissionKickMembers | + PermissionBanMembers | + PermissionManageServer | + PermissionAdministrator +) + +// Block contains Discord JSON Error Response codes +const ( + ErrCodeUnknownAccount = 10001 + ErrCodeUnknownApplication = 10002 + ErrCodeUnknownChannel = 10003 + ErrCodeUnknownGuild = 10004 + ErrCodeUnknownIntegration = 10005 + ErrCodeUnknownInvite = 10006 + ErrCodeUnknownMember = 10007 + ErrCodeUnknownMessage = 10008 + ErrCodeUnknownOverwrite = 10009 + ErrCodeUnknownProvider = 10010 + ErrCodeUnknownRole = 10011 + ErrCodeUnknownToken = 10012 + ErrCodeUnknownUser = 10013 + ErrCodeUnknownEmoji = 10014 + + ErrCodeBotsCannotUseEndpoint = 20001 + ErrCodeOnlyBotsCanUseEndpoint = 20002 + + ErrCodeMaximumGuildsReached = 30001 + ErrCodeMaximumFriendsReached = 30002 + ErrCodeMaximumPinsReached = 30003 + ErrCodeMaximumGuildRolesReached = 30005 + ErrCodeTooManyReactions = 30010 + + ErrCodeUnauthorized = 40001 + + ErrCodeMissingAccess = 50001 + ErrCodeInvalidAccountType = 50002 + ErrCodeCannotExecuteActionOnDMChannel = 50003 + ErrCodeEmbedCisabled = 50004 + ErrCodeCannotEditFromAnotherUser = 50005 + ErrCodeCannotSendEmptyMessage = 50006 + ErrCodeCannotSendMessagesToThisUser = 50007 + ErrCodeCannotSendMessagesInVoiceChannel = 50008 + ErrCodeChannelVerificationLevelTooHigh = 50009 + ErrCodeOAuth2ApplicationDoesNotHaveBot = 50010 + ErrCodeOAuth2ApplicationLimitReached = 50011 + ErrCodeInvalidOAuthState = 50012 + ErrCodeMissingPermissions = 50013 + ErrCodeInvalidAuthenticationToken = 50014 + ErrCodeNoteTooLong = 50015 + ErrCodeTooFewOrTooManyMessagesToDelete = 50016 + ErrCodeCanOnlyPinMessageToOriginatingChannel = 50019 + ErrCodeCannotExecuteActionOnSystemMessage = 50021 + ErrCodeMessageProvidedTooOldForBulkDelete = 50034 + ErrCodeInvalidFormBody = 50035 + ErrCodeInviteAcceptedToGuildApplicationsBotNotIn = 50036 + + ErrCodeReactionBlocked = 90001 +) diff --git a/DiscordGo/tools/cmd/eventhandlers/main.go b/DiscordGo/tools/cmd/eventhandlers/main.go new file mode 100644 index 0000000000000000000000000000000000000000..839f009da2cba9753d6f5d10a357bb912b7de101 --- /dev/null +++ b/DiscordGo/tools/cmd/eventhandlers/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "log" + "path/filepath" + "regexp" + "sort" + "strings" + "text/template" +) + +var eventHandlerTmpl = template.Must(template.New("eventHandler").Funcs(template.FuncMap{ + "constName": constName, + "isDiscordEvent": isDiscordEvent, + "privateName": privateName, +}).Parse(`// Code generated by \"eventhandlers\"; DO NOT EDIT +// See events.go + +package discordgo + +// Following are all the event types. +// Event type values are used to match the events returned by Discord. +// EventTypes surrounded by __ are synthetic and are internal to DiscordGo. +const ({{range .}} + {{privateName .}}EventType = "{{constName .}}"{{end}} +) +{{range .}} +// {{privateName .}}EventHandler is an event handler for {{.}} events. +type {{privateName .}}EventHandler func(*Session, *{{.}}) + +// Type returns the event type for {{.}} events. +func (eh {{privateName .}}EventHandler) Type() string { + return {{privateName .}}EventType +} +{{if isDiscordEvent .}} +// New returns a new instance of {{.}}. +func (eh {{privateName .}}EventHandler) New() interface{} { + return &{{.}}{} +}{{end}} +// Handle is the handler for {{.}} events. +func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*{{.}}); ok { + eh(s, t) + } +} + +{{end}} +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Session, interface{}): + return interfaceEventHandler(v){{range .}} + case func(*Session, *{{.}}): + return {{privateName .}}EventHandler(v){{end}} + } + + return nil +} + +func init() { {{range .}}{{if isDiscordEvent .}} + registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}} +} +`)) + +func main() { + var buf bytes.Buffer + dir := filepath.Dir(".") + + fs := token.NewFileSet() + parsedFile, err := parser.ParseFile(fs, "events.go", nil, 0) + if err != nil { + log.Fatalf("warning: internal error: could not parse events.go: %s", err) + return + } + + names := []string{} + for object := range parsedFile.Scope.Objects { + names = append(names, object) + } + sort.Strings(names) + eventHandlerTmpl.Execute(&buf, names) + + src, err := format.Source(buf.Bytes()) + if err != nil { + log.Println("warning: internal error: invalid Go generated:", err) + src = buf.Bytes() + } + + err = ioutil.WriteFile(filepath.Join(dir, strings.ToLower("eventhandlers.go")), src, 0644) + if err != nil { + log.Fatal(buf, "writing output: %s", err) + } +} + +var constRegexp = regexp.MustCompile("([a-z])([A-Z])") + +func constCase(name string) string { + return strings.ToUpper(constRegexp.ReplaceAllString(name, "${1}_${2}")) +} + +func isDiscordEvent(name string) bool { + switch { + case name == "Connect", name == "Disconnect", name == "Event", name == "RateLimit", name == "Interface": + return false + default: + return true + } +} + +func constName(name string) string { + if !isDiscordEvent(name) { + return "__" + constCase(name) + "__" + } + + return constCase(name) +} + +func privateName(name string) string { + return strings.ToLower(string(name[0])) + name[1:] +} diff --git a/DiscordGo/types.go b/DiscordGo/types.go new file mode 100644 index 0000000000000000000000000000000000000000..780b6bb978e26a34eccb0b8212d511994d1a5e67 --- /dev/null +++ b/DiscordGo/types.go @@ -0,0 +1,58 @@ +// 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 custom types, currently only a timestamp wrapper. + +package discordgo + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Timestamp stores a timestamp, as sent by the Discord API. +type Timestamp string + +// Parse parses a timestamp string into a time.Time object. +// The only time this can fail is if Discord changes their timestamp format. +func (t Timestamp) Parse() (time.Time, error) { + return time.Parse(time.RFC3339, string(t)) +} + +// RESTError stores error information about a request with a bad response code. +// Message is not always present, there are cases where api calls can fail +// without returning a json message. +type RESTError struct { + Request *http.Request + Response *http.Response + ResponseBody []byte + + Message *APIErrorMessage // Message may be nil. +} + +func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTError { + restErr := &RESTError{ + Request: req, + Response: resp, + ResponseBody: body, + } + + // Attempt to decode the error and assume no message was provided if it fails + var msg *APIErrorMessage + err := json.Unmarshal(body, &msg) + if err == nil { + restErr.Message = msg + } + + return restErr +} + +func (r RESTError) Error() string { + return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) +} diff --git a/DiscordGo/types_test.go b/DiscordGo/types_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1d03aa32e9ce872405aa172fad6507b34c802073 --- /dev/null +++ b/DiscordGo/types_test.go @@ -0,0 +1,24 @@ +package discordgo + +import ( + "testing" + "time" +) + +func TestTimestampParse(t *testing.T) { + ts, err := Timestamp("2016-03-24T23:15:59.605000+00:00").Parse() + if err != nil { + t.Fatal(err) + } + if ts.Year() != 2016 || ts.Month() != time.March || ts.Day() != 24 { + t.Error("Incorrect date") + } + if ts.Hour() != 23 || ts.Minute() != 15 || ts.Second() != 59 { + t.Error("Incorrect time") + } + + _, offset := ts.Zone() + if offset != 0 { + t.Error("Incorrect timezone") + } +} diff --git a/DiscordGo/user.go b/DiscordGo/user.go new file mode 100644 index 0000000000000000000000000000000000000000..76abdd1d7b025fe3e536d0181b757388b58561e6 --- /dev/null +++ b/DiscordGo/user.go @@ -0,0 +1,42 @@ +package discordgo + +import ( + "fmt" + "strings" +) + +// A User stores all data for an individual Discord user. +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Discriminator string `json:"discriminator"` + Token string `json:"token"` + Verified bool `json:"verified"` + MFAEnabled bool `json:"mfa_enabled"` + Bot bool `json:"bot"` +} + +// String returns a unique identifier of the form username#discriminator +func (u *User) String() string { + return fmt.Sprintf("%s#%s", u.Username, u.Discriminator) +} + +// Mention return a string which mentions the user +func (u *User) Mention() string { + return fmt.Sprintf("<@%s>", u.ID) +} + +// AvatarURL returns a URL to the user's avatar. +// size: The size of the user's avatar as a power of two +func (u *User) AvatarURL(size string) string { + var URL string + if strings.HasPrefix(u.Avatar, "a_") { + URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) + } else { + URL = EndpointUserAvatar(u.ID, u.Avatar) + } + + return URL + "?size=" + size +} diff --git a/DiscordGo/user_test.go b/DiscordGo/user_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a36f673e358c79410d4b2fa128675ac58dffee95 --- /dev/null +++ b/DiscordGo/user_test.go @@ -0,0 +1,16 @@ +package discordgo + +import "testing" + +func TestUser(t *testing.T) { + t.Parallel() + + user := &User{ + Username: "bob", + Discriminator: "8192", + } + + if user.String() != "bob#8192" { + t.Errorf("user.String() == %v", user.String()) + } +} diff --git a/DiscordGo/voice.go b/DiscordGo/voice.go index 507e4f49a02ee85c549f935f35e37a2ac5d594f7..8f033aa006b518ae5a73729a6f52d792b50c9cb2 100644 --- a/DiscordGo/voice.go +++ b/DiscordGo/voice.go @@ -13,48 +13,230 @@ import ( "encoding/binary" "encoding/json" "fmt" + "log" "net" - "runtime" "strings" "sync" "time" - "bitbucket.org/henesy/disco/websocket" + "github.com/gorilla/websocket" + "golang.org/x/crypto/nacl/secretbox" ) // ------------------------------------------------------------------------------------------------ -// Code related to both Voice Websocket and UDP connections. +// Code related to both VoiceConnection Websocket and UDP connections. // ------------------------------------------------------------------------------------------------ -// A Voice struct holds all data and functions related to Discord Voice support. -type Voice struct { - sync.Mutex // future use - Ready bool // If true, voice is ready to send/receive audio - Debug bool // If true, print extra logging - OP2 *voiceOP2 // exported for dgvoice, may change. - OpusSend chan []byte // Chan for sending opus audio - OpusRecv chan *Packet // Chan for receiving opus audio - // FrameRate int // This can be used to set the FrameRate of Opus data - // FrameSize int // This can be used to set the FrameSize of Opus data +// A VoiceConnection struct holds all the data and functions related to a Discord Voice Connection. +type VoiceConnection struct { + sync.RWMutex + + 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 + ChannelID string + deaf bool + mute bool + speaking bool + reconnecting bool // If true, voice connection is trying to reconnect + + OpusSend chan []byte // Chan for sending opus audio + OpusRecv chan *Packet // Chan for receiving opus audio wsConn *websocket.Conn - UDPConn *net.UDPConn // this will become unexported soon. + wsMutex sync.Mutex + udpConn *net.UDPConn + session *Session sessionID string token string endpoint string - guildID string - channelID string - userID string // Used to send a close signal to goroutines close chan struct{} + + // Used to allow blocking until connected + connected chan bool + + // Used to pass the sessionid from onVoiceStateUpdate + // sessionRecv chan string UNUSED ATM + + op4 voiceOP4 + op2 voiceOP2 + + voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler +} + +// VoiceSpeakingUpdateHandler type provides a function defination for the +// VoiceSpeakingUpdate event +type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate) + +// Speaking sends a speaking notification to Discord over the voice websocket. +// This must be sent as true prior to sending audio and should be set to false +// once finished sending audio. +// b : Send true if speaking, false if not. +func (v *VoiceConnection) Speaking(b bool) (err error) { + + v.log(LogDebug, "called (%t)", b) + + type voiceSpeakingData struct { + Speaking bool `json:"speaking"` + Delay int `json:"delay"` + } + + type voiceSpeakingOp struct { + Op int `json:"op"` // Always 5 + Data voiceSpeakingData `json:"d"` + } + + if v.wsConn == nil { + return fmt.Errorf("no VoiceConnection websocket") + } + + data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} + v.wsMutex.Lock() + err = v.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + + v.Lock() + defer v.Unlock() + if err != nil { + v.speaking = false + log.Println("Speaking() write json error:", err) + return + } + + v.speaking = b + + return +} + +// ChangeChannel sends Discord a request to change channels within a Guild +// !!! NOTE !!! This function may be removed in favour of just using ChannelVoiceJoin +func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err error) { + + v.log(LogInformational, "called") + + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, mute, deaf}} + v.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + if err != nil { + return + } + v.ChannelID = channelID + v.deaf = deaf + v.mute = mute + v.speaking = false + + return +} + +// Disconnect disconnects from this voice channel and closes the websocket +// and udp connections to Discord. +// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave +func (v *VoiceConnection) Disconnect() (err error) { + + // Send a OP4 with a nil channel to disconnect + if v.sessionID != "" { + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} + v.session.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.session.wsMutex.Unlock() + v.sessionID = "" + } + + // Close websocket and udp connections + v.Close() + + v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) + + v.session.Lock() + delete(v.session.VoiceConnections, v.GuildID) + v.session.Unlock() + + return +} + +// Close closes the voice ws and udp connections +func (v *VoiceConnection) Close() { + + v.log(LogInformational, "called") + + v.Lock() + defer v.Unlock() + + v.Ready = false + v.speaking = false + + if v.close != nil { + v.log(LogInformational, "closing v.close") + close(v.close) + v.close = nil + } + + if v.udpConn != nil { + v.log(LogInformational, "closing udp") + err := v.udpConn.Close() + if err != nil { + log.Println("error closing udp connection: ", err) + } + v.udpConn = nil + } + + if v.wsConn != nil { + v.log(LogInformational, "sending close frame") + + // To cleanly close a connection, a client should send a close + // frame and wait for the server to close the connection. + v.wsMutex.Lock() + err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + v.wsMutex.Unlock() + if err != nil { + v.log(LogError, "error closing websocket, %s", err) + } + + // TODO: Wait for Discord to actually close the connection. + time.Sleep(1 * time.Second) + + v.log(LogInformational, "closing websocket") + err = v.wsConn.Close() + if err != nil { + v.log(LogError, "error closing websocket, %s", err) + } + + v.wsConn = nil + } +} + +// AddHandler adds a Handler for VoiceSpeakingUpdate events. +func (v *VoiceConnection) AddHandler(h VoiceSpeakingUpdateHandler) { + v.Lock() + defer v.Unlock() + + v.voiceSpeakingUpdateHandlers = append(v.voiceSpeakingUpdateHandlers, h) +} + +// VoiceSpeakingUpdate is a struct for a VoiceSpeakingUpdate event. +type VoiceSpeakingUpdate struct { + UserID string `json:"user_id"` + SSRC int `json:"ssrc"` + Speaking bool `json:"speaking"` } // ------------------------------------------------------------------------------------------------ -// Code related to the Voice websocket connection +// Unexported Internal Functions Below. // ------------------------------------------------------------------------------------------------ +// A voiceOP4 stores the data for the voice operation 4 websocket event +// which provides us with the NaCl SecretBox encryption key +type voiceOP4 struct { + SecretKey [32]byte `json:"secret_key"` + Mode string `json:"mode"` +} + // A voiceOP2 stores the data for the voice operation 2 websocket event // which is sort of like the voice READY packet type voiceOP2 struct { @@ -64,67 +246,119 @@ type voiceOP2 struct { HeartbeatInterval time.Duration `json:"heartbeat_interval"` } -type voiceHandshakeData struct { - ServerID string `json:"server_id"` - UserID string `json:"user_id"` - SessionID string `json:"session_id"` - Token string `json:"token"` -} +// WaitUntilConnected waits for the Voice Connection to +// become ready, if it does not become ready it retuns an err +func (v *VoiceConnection) waitUntilConnected() error { + + v.log(LogInformational, "called") -type voiceHandshakeOp struct { - Op int `json:"op"` // Always 0 - Data voiceHandshakeData `json:"d"` + i := 0 + for { + v.RLock() + ready := v.Ready + v.RUnlock() + if ready { + return nil + } + + if i > 10 { + return fmt.Errorf("timeout waiting for voice") + } + + time.Sleep(1 * time.Second) + i++ + } } // Open opens a voice connection. This should be called // after VoiceChannelJoin is used and the data VOICE websocket events // are captured. -func (v *Voice) Open() (err error) { +func (v *VoiceConnection) open() (err error) { + + v.log(LogInformational, "called") v.Lock() defer v.Unlock() // Don't open a websocket if one is already open if v.wsConn != nil { + v.log(LogWarning, "refusing to overwrite non-nil websocket") return } - // Connect to Voice Websocket + // TODO temp? loop to wait for the SessionID + i := 0 + for { + if v.sessionID != "" { + break + } + if i > 20 { // only loop for up to 1 second total + return fmt.Errorf("did not receive voice Session ID in time") + } + time.Sleep(50 * time.Millisecond) + i++ + } + + // Connect to VoiceConnection Websocket vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) + v.log(LogInformational, "connecting to voice endpoint %s", vg) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) if err != nil { - fmt.Println("VOICE error opening websocket:", err) + v.log(LogWarning, "error connecting to voice endpoint %s, %s", vg, err) + v.log(LogDebug, "voice struct: %#v\n", v) return } - data := voiceHandshakeOp{0, voiceHandshakeData{v.guildID, v.userID, v.sessionID, v.token}} + type voiceHandshakeData struct { + ServerID string `json:"server_id"` + UserID string `json:"user_id"` + SessionID string `json:"session_id"` + Token string `json:"token"` + } + type voiceHandshakeOp struct { + Op int `json:"op"` // Always 0 + Data voiceHandshakeData `json:"d"` + } + data := voiceHandshakeOp{0, voiceHandshakeData{v.GuildID, v.UserID, v.sessionID, v.token}} err = v.wsConn.WriteJSON(data) if err != nil { - fmt.Println("VOICE error sending init packet:", err) + v.log(LogWarning, "error sending init packet, %s", err) return } - // Start a listening for voice websocket events - // TODO add a check here to make sure Listen worked by monitoring - // a chan or bool? v.close = make(chan struct{}) go v.wsListen(v.wsConn, v.close) + // add loop/check for Ready bool here? + // then return false if not ready? + // but then wsListen will also err. + return } // wsListen listens on the voice websocket for messages and passes them // to the voice event handler. This is automatically called by the Open func -func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { +func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { + + v.log(LogInformational, "called") for { - messageType, message, err := v.wsConn.ReadMessage() + _, message, err := v.wsConn.ReadMessage() if err != nil { - // TODO: add reconnect, matching wsapi.go:listen() - // TODO: Handle this problem better. - // TODO: needs proper logging - fmt.Println("Voice Listen Error:", err) + // Detect if we have been closed manually. If a Close() has already + // happened, the websocket we are listening on will be different to the + // current session. + v.RLock() + sameConnection := v.wsConn == wsConn + v.RUnlock() + if sameConnection { + + v.log(LogError, "voice endpoint %s websocket closed unexpectantly, %s", v.endpoint, err) + + // Start reconnect goroutine then exit. + go v.reconnect() + } return } @@ -133,23 +367,20 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { case <-close: return default: - go v.wsEvent(messageType, message) + go v.onEvent(message) } } } // wsEvent handles any voice websocket events. This is only called by the // wsListen() function. -func (v *Voice) wsEvent(messageType int, message []byte) { +func (v *VoiceConnection) onEvent(message []byte) { - if v.Debug { - fmt.Println("wsEvent received: ", messageType) - printJSON(message) - } + v.log(LogDebug, "received: %s", string(message)) var e Event if err := json.Unmarshal(message, &e); err != nil { - fmt.Println("wsEvent Unmarshall error: ", err) + v.log(LogError, "unmarshall error, %s", err) return } @@ -157,21 +388,19 @@ func (v *Voice) wsEvent(messageType int, message []byte) { case 2: // READY - v.OP2 = &voiceOP2{} - if err := json.Unmarshal(e.RawData, v.OP2); err != nil { - fmt.Println("voiceWS.onEvent OP2 Unmarshall error: ", err) - printJSON(e.RawData) // TODO: Better error logging + if err := json.Unmarshal(e.RawData, &v.op2); err != nil { + v.log(LogError, "OP2 unmarshall error, %s, %s", err, string(e.RawData)) return } // Start the voice websocket heartbeat to keep the connection alive - go v.wsHeartbeat(v.wsConn, v.close, v.OP2.HeartbeatInterval) + go v.wsHeartbeat(v.wsConn, v.close, v.op2.HeartbeatInterval) // TODO monitor a chan/bool to verify this was successful // Start the UDP connection err := v.udpOpen() if err != nil { - fmt.Println("Error opening udp connection: ", err) + v.log(LogError, "error opening udp connection, %s", err) return } @@ -180,35 +409,51 @@ func (v *Voice) wsEvent(messageType int, message []byte) { if v.OpusSend == nil { v.OpusSend = make(chan []byte, 2) } - go v.opusSender(v.UDPConn, v.close, v.OpusSend, 48000, 960) + go v.opusSender(v.udpConn, v.close, v.OpusSend, 48000, 960) // Start the opusReceiver - if v.OpusRecv == nil { - v.OpusRecv = make(chan *Packet, 2) + if !v.deaf { + if v.OpusRecv == nil { + v.OpusRecv = make(chan *Packet, 2) + } + + go v.opusReceiver(v.udpConn, v.close, v.OpusRecv) } - go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) + return case 3: // HEARTBEAT response // add code to use this to track latency? return - case 4: - // TODO + case 4: // udp encryption secret key + v.Lock() + defer v.Unlock() + + v.op4 = voiceOP4{} + if err := json.Unmarshal(e.RawData, &v.op4); err != nil { + v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData)) + return + } + return case 5: - // SPEAKING TRUE/FALSE NOTIFICATION - /* - { - "user_id": "1238921738912", - "ssrc": 2, - "speaking": false - } - */ + if len(v.voiceSpeakingUpdateHandlers) == 0 { + return + } + + voiceSpeakingUpdate := &VoiceSpeakingUpdate{} + if err := json.Unmarshal(e.RawData, voiceSpeakingUpdate); err != nil { + v.log(LogError, "OP5 unmarshall error, %s, %s", err, string(e.RawData)) + return + } + + for _, h := range v.voiceSpeakingUpdateHandlers { + h(v, voiceSpeakingUpdate) + } default: - fmt.Println("UNKNOWN VOICE OP: ", e.Operation) - printJSON(e.RawData) + v.log(LogDebug, "unknown voice operation, %d, %s", e.Operation, string(e.RawData)) } return @@ -225,7 +470,7 @@ type voiceHeartbeatOp struct { // wsHeartbeat sends regular heartbeats to voice Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. -func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { if close == nil || wsConn == nil { return @@ -233,10 +478,14 @@ func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i tim var err error ticker := time.NewTicker(i * time.Millisecond) + defer ticker.Stop() for { + v.log(LogDebug, "sending heartbeat packet") + v.wsMutex.Lock() err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())}) + v.wsMutex.Unlock() if err != nil { - fmt.Println("wsHeartbeat send error: ", err) + v.log(LogError, "error sending heartbeat to voice endpoint %s, %s", v.endpoint, err) return } @@ -249,44 +498,14 @@ func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i tim } } -type voiceSpeakingData struct { - Speaking bool `json:"speaking"` - Delay int `json:"delay"` -} - -type voiceSpeakingOp struct { - Op int `json:"op"` // Always 5 - Data voiceSpeakingData `json:"d"` -} - -// Speaking sends a speaking notification to Discord over the voice websocket. -// This must be sent as true prior to sending audio and should be set to false -// once finished sending audio. -// b : Send true if speaking, false if not. -func (v *Voice) Speaking(b bool) (err error) { - - if v.wsConn == nil { - return fmt.Errorf("No Voice websocket.") - } - - data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} - err = v.wsConn.WriteJSON(data) - if err != nil { - fmt.Println("Speaking() write json error:", err) - return - } - - return -} - // ------------------------------------------------------------------------------------------------ -// Code related to the Voice UDP connection +// Code related to the VoiceConnection UDP connection // ------------------------------------------------------------------------------------------------ type voiceUDPData struct { Address string `json:"address"` // Public IP of machine running this code Port uint16 `json:"port"` // UDP Port of machine running this code - Mode string `json:"mode"` // plain or ? (plain or encrypted) + Mode string `json:"mode"` // always "xsalsa20_poly1305" } type voiceUDPD struct { @@ -303,7 +522,7 @@ type voiceUDPOp struct { // initial required handshake. This connection is left open in the session // and can be used to send or receive audio. This should only be called // from voice.wsEvent OP2 -func (v *Voice) udpOpen() (err error) { +func (v *VoiceConnection) udpOpen() (err error) { v.Lock() defer v.Unlock() @@ -312,7 +531,7 @@ func (v *Voice) udpOpen() (err error) { return fmt.Errorf("nil voice websocket") } - if v.UDPConn != nil { + if v.udpConn != nil { return fmt.Errorf("udp connection already open") } @@ -324,29 +543,27 @@ func (v *Voice) udpOpen() (err error) { return fmt.Errorf("empty endpoint") } - host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.OP2.Port) + host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) addr, err := net.ResolveUDPAddr("udp", host) if err != nil { - fmt.Println("udpOpen resolve addr error: ", err) - // TODO better logging + v.log(LogWarning, "error resolving udp host %s, %s", host, err) return } - v.UDPConn, err = net.DialUDP("udp", nil, addr) + v.log(LogInformational, "connecting to udp addr %s", addr.String()) + v.udpConn, err = net.DialUDP("udp", nil, addr) if err != nil { - fmt.Println("udpOpen dial udp error: ", err) - // TODO better logging + v.log(LogWarning, "error connecting to udp addr %s, %s", addr.String(), err) return } - // Create a 70 byte array and put the SSRC code from the Op 2 Voice event + // Create a 70 byte array and put the SSRC code from the Op 2 VoiceConnection event // into it. Then send that over the UDP connection to Discord sb := make([]byte, 70) - binary.BigEndian.PutUint32(sb, v.OP2.SSRC) - _, err = v.UDPConn.Write(sb) + binary.BigEndian.PutUint32(sb, v.op2.SSRC) + _, err = v.udpConn.Write(sb) if err != nil { - fmt.Println("udpOpen udp write error : ", err) - // TODO better logging + v.log(LogWarning, "udp write error to %s, %s", addr.String(), err) return } @@ -355,17 +572,18 @@ func (v *Voice) udpOpen() (err error) { // of the response. This should be our public IP and PORT as Discord // saw us. rb := make([]byte, 70) - rlen, _, err := v.UDPConn.ReadFromUDP(rb) + rlen, _, err := v.udpConn.ReadFromUDP(rb) if err != nil { - fmt.Println("udpOpen udp read error : ", err) - // TODO better logging + v.log(LogWarning, "udp read error, %s, %s", addr.String(), err) return } + if rlen < 70 { - fmt.Println("Voice RLEN should be 70 but isn't") + v.log(LogWarning, "received udp packet too small") + return fmt.Errorf("received udp packet too small") } - // Loop over position 4 though 20 to grab the IP address + // Loop over position 4 through 20 to grab the IP address // Should never be beyond position 20. var ip string for i := 4; i < 20; i++ { @@ -380,16 +598,18 @@ func (v *Voice) udpOpen() (err error) { // Take the data from above and send it back to Discord to finalize // the UDP connection handshake. - data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "plain"}}} + data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "xsalsa20_poly1305"}}} + v.wsMutex.Lock() err = v.wsConn.WriteJSON(data) + v.wsMutex.Unlock() if err != nil { - fmt.Println("udpOpen write json error:", err) + v.log(LogWarning, "udp write error, %#v, %s", data, err) return } // start udpKeepAlive - go v.udpKeepAlive(v.UDPConn, v.close, 5*time.Second) + go v.udpKeepAlive(v.udpConn, v.close, 5*time.Second) // TODO: find a way to check that it fired off okay return @@ -397,9 +617,9 @@ func (v *Voice) udpOpen() (err error) { // udpKeepAlive sends a udp packet to keep the udp connection open // This is still a bit of a "proof of concept" -func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct{}, i time.Duration) { - if UDPConn == nil || close == nil { + if udpConn == nil || close == nil { return } @@ -409,14 +629,15 @@ func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time packet := make([]byte, 8) ticker := time.NewTicker(i) + defer ticker.Stop() for { binary.LittleEndian.PutUint64(packet, sequence) sequence++ - _, err = UDPConn.Write(packet) + _, err = udpConn.Write(packet) if err != nil { - fmt.Println("udpKeepAlive udp write error : ", err) + v.log(LogError, "write error, %s", err) return } @@ -431,32 +652,38 @@ func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time // opusSender will listen on the given channel and send any // pre-encoded opus audio to Discord. Supposedly. -func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { +func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { - if UDPConn == nil || close == nil { + if udpConn == nil || close == nil { return } - runtime.LockOSThread() - - // Voice is now ready to receive audio packets + // VoiceConnection is now ready to receive audio packets // TODO: this needs reviewed as I think there must be a better way. + v.Lock() v.Ready = true - defer func() { v.Ready = false }() + v.Unlock() + defer func() { + v.Lock() + v.Ready = false + v.Unlock() + }() var sequence uint16 var timestamp uint32 var recvbuf []byte var ok bool udpHeader := make([]byte, 12) + var nonce [24]byte // build the parts that don't change in the udpHeader udpHeader[0] = 0x80 udpHeader[1] = 0x78 - binary.BigEndian.PutUint32(udpHeader[8:], v.OP2.SSRC) + binary.BigEndian.PutUint32(udpHeader[8:], v.op2.SSRC) // start a send loop that loops until buf chan is closed ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000))) + defer ticker.Stop() for { // Get data from chan. If chan is closed, return. @@ -470,12 +697,25 @@ func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-c // else, continue loop } + v.RLock() + speaking := v.speaking + v.RUnlock() + if !speaking { + err := v.Speaking(true) + if err != nil { + v.log(LogError, "error sending speaking packet, %s", err) + } + } + // Add sequence and timestamp to udpPacket binary.BigEndian.PutUint16(udpHeader[2:], sequence) binary.BigEndian.PutUint32(udpHeader[4:], timestamp) - // Combine the UDP Header and the opus data - sendbuf := append(udpHeader, recvbuf...) + // encrypt the opus data + copy(nonce[:], udpHeader) + v.RLock() + sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey) + v.RUnlock() // block here until we're exactly at the right time :) // Then send rtp audio packet to Discord over UDP @@ -485,10 +725,11 @@ func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-c case <-ticker.C: // continue } - _, err := UDPConn.Write(sendbuf) + _, err := udpConn.Write(sendbuf) if err != nil { - fmt.Println("error writing to udp connection: ", err) + v.log(LogError, "udp write error, %s", err) + v.log(LogDebug, "voice struct: %#v\n", v) return } @@ -519,19 +760,31 @@ type Packet struct { // opusReceiver listens on the UDP socket for incoming packets // and sends them across the given channel // NOTE :: This function may change names later. -func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { +func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { - if UDPConn == nil || close == nil { + if udpConn == nil || close == nil { return } - p := Packet{} recvbuf := make([]byte, 1024) + var nonce [24]byte for { - rlen, err := UDPConn.Read(recvbuf) + rlen, err := udpConn.Read(recvbuf) if err != nil { - fmt.Println("opusReceiver UDP Read error:", err) + // Detect if we have been closed manually. If a Close() has already + // happened, the udp connection we are listening on will be different + // to the current session. + v.RLock() + sameConnection := v.udpConn == udpConn + v.RUnlock() + if sameConnection { + + v.log(LogError, "udp read error, %s, %s", v.endpoint, err) + v.log(LogDebug, "voice struct: %#v\n", v) + + go v.reconnect() + } return } @@ -543,48 +796,92 @@ func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan } // For now, skip anything except audio. - if rlen < 12 || recvbuf[0] != 0x80 { + if rlen < 12 || (recvbuf[0] != 0x80 && recvbuf[0] != 0x90) { continue } + // build a audio packet struct + p := Packet{} p.Type = recvbuf[0:2] p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4]) p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8]) p.SSRC = binary.BigEndian.Uint32(recvbuf[8:12]) - p.Opus = recvbuf[12:rlen] + // decrypt opus data + copy(nonce[:], recvbuf[0:12]) + p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) + + if len(p.Opus) > 8 && recvbuf[0] == 0x90 { + // Extension bit is set, first 8 bytes is the extended header + p.Opus = p.Opus[8:] + } if c != nil { - c <- &p + select { + case c <- &p: + case <-close: + return + } } } } -// Close closes the voice ws and udp connections -func (v *Voice) Close() { +// Reconnect will close down a voice connection then immediately try to +// reconnect to that session. +// NOTE : This func is messy and a WIP while I find what works. +// It will be cleaned up once a proven stable option is flushed out. +// aka: this is ugly shit code, please don't judge too harshly. +func (v *VoiceConnection) reconnect() { + + v.log(LogInformational, "called") v.Lock() - defer v.Unlock() + if v.reconnecting { + v.log(LogInformational, "already reconnecting to channel %s, exiting", v.ChannelID) + v.Unlock() + return + } + v.reconnecting = true + v.Unlock() - v.Ready = false + defer func() { v.reconnecting = false }() - if v.close != nil { - close(v.close) - v.close = nil - } + // Close any currently open connections + v.Close() - if v.UDPConn != nil { - err := v.UDPConn.Close() - if err != nil { - fmt.Println("error closing udp connection: ", err) + wait := time.Duration(1) + for { + + <-time.After(wait * time.Second) + wait *= 2 + if wait > 600 { + wait = 600 } - v.UDPConn = nil - } - if v.wsConn != nil { - err := v.wsConn.Close() + if v.session.DataReady == false || v.session.wsConn == nil { + v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID) + continue + } + + v.log(LogInformational, "trying to reconnect to channel %s", v.ChannelID) + + _, err := v.session.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf) + if err == nil { + v.log(LogInformational, "successfully reconnected to channel %s", v.ChannelID) + return + } + + v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) + + // if the reconnect above didn't work lets just send a disconnect + // packet to reset things. + // Send a OP4 with a nil channel to disconnect + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} + v.session.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.session.wsMutex.Unlock() if err != nil { - fmt.Println("error closing websocket connection: ", err) + v.log(LogError, "error sending disconnect packet, %s", err) } - v.wsConn = nil + } } diff --git a/DiscordGo/wsapi.go b/DiscordGo/wsapi.go index 0963e8eea721f8b4c7227b0a99b9da2252403242..df87092e0913311bc1cf6784b614fa3b6ff7bbd1 100644 --- a/DiscordGo/wsapi.go +++ b/DiscordGo/wsapi.go @@ -15,39 +15,41 @@ import ( "compress/zlib" "encoding/json" "errors" - "fmt" "io" "net/http" - "reflect" "runtime" + "sync/atomic" "time" - "bitbucket.org/henesy/disco/websocket" + "github.com/gorilla/websocket" ) -type handshakeProperties struct { - OS string `json:"$os"` - Browser string `json:"$browser"` - Device string `json:"$device"` - Referer string `json:"$referer"` - ReferringDomain string `json:"$referring_domain"` -} +// ErrWSAlreadyOpen is thrown when you attempt to open +// a websocket that already is open. +var ErrWSAlreadyOpen = errors.New("web socket already opened") -type handshakeData struct { - Version int `json:"v"` - Token string `json:"token"` - Properties handshakeProperties `json:"properties"` - LargeThreshold int `json:"large_threshold"` - Compress bool `json:"compress"` -} +// ErrWSNotFound is thrown when you attempt to use a websocket +// that doesn't exist +var ErrWSNotFound = errors.New("no websocket connection exists") + +// ErrWSShardBounds is thrown when you try to use a shard ID that is +// less than the total shard count +var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount") -type handshakeOp struct { - Op int `json:"op"` - Data handshakeData `json:"d"` +type resumePacket struct { + Op int `json:"op"` + Data struct { + Token string `json:"token"` + SessionID string `json:"session_id"` + Sequence int64 `json:"seq"` + } `json:"d"` } // Open opens a websocket connection to Discord. func (s *Session) Open() (err error) { + + s.log(LogInformational, "called") + s.Lock() defer func() { if err != nil { @@ -55,142 +57,188 @@ func (s *Session) Open() (err error) { } }() + // A basic state is a hard requirement for Voice. + if s.State == nil { + state := NewState() + state.TrackChannels = false + state.TrackEmojis = false + state.TrackMembers = false + state.TrackRoles = false + state.TrackVoice = false + s.State = state + } + if s.wsConn != nil { - err = errors.New("Web socket already opened.") + err = ErrWSAlreadyOpen return } + if s.VoiceConnections == nil { + s.log(LogInformational, "creating new VoiceConnections map") + s.VoiceConnections = make(map[string]*VoiceConnection) + } + // 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 = s.gateway + "?v=" + APIVersion + "&encoding=json" } 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 err != nil { - return + sequence := atomic.LoadInt64(s.sequence) + if s.sessionID != "" && sequence > 0 { + + p := resumePacket{} + p.Op = 6 + p.Data.Token = s.Token + p.Data.SessionID = s.sessionID + p.Data.Sequence = sequence + + s.log(LogInformational, "sending resume packet to gateway") + err = s.wsConn.WriteJSON(p) + if err != nil { + s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err) + return + } + + } else { + + err = s.identify() + 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. s.listening = make(chan interface{}) go s.listen(s.wsConn, s.listening) + s.LastHeartbeatAck = time.Now().UTC() s.Unlock() - s.initialize() - s.handle(&Connect{}) + s.log(LogInformational, "emit connect event") + s.handleEvent(connectEventType, &Connect{}) + s.log(LogInformational, "exiting") return } -// Close closes a websocket and stops all listening/heartbeat goroutines. -// TODO: Add support for Voice WS/UDP connections -func (s *Session) Close() (err error) { - s.Lock() - - s.DataReady = false - - if s.listening != nil { - close(s.listening) - s.listening = nil - } - - if s.wsConn != nil { - err = s.wsConn.Close() - s.wsConn = nil - } - - s.Unlock() - - s.handle(&Disconnect{}) +// listen polls the websocket connection for events, it will stop when the +// listening channel is closed, or an error occurs. +func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { - return -} + s.log(LogInformational, "called") -// listen polls the websocket connection for events, it will stop when -// the listening channel is closed, or an error occurs. -func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { for { + messageType, message, err := wsConn.ReadMessage() + if err != nil { + // Detect if we have been closed manually. If a Close() has already - // happened, the websocket we are listening on will be different to the - // current session. + // happened, the websocket we are listening on will be different to + // the current session. s.RLock() sameConnection := s.wsConn == wsConn s.RUnlock() + if sameConnection { - // There has been an error reading, Close() the websocket so that - // OnDisconnect is fired. + + s.log(LogWarning, "error reading from gateway %s websocket, %s", s.gateway, err) + // There has been an error reading, close the websocket so that + // OnDisconnect event is emitted. err := s.Close() if err != nil { - fmt.Println("error closing session connection: ", err) + s.log(LogWarning, "error closing session connection, %s", err) } - // Attempt to reconnect, with expenonential backoff up to 10 minutes. - if s.ShouldReconnectOnError { - wait := time.Duration(1) - for { - if s.Open() == nil { - return - } - <-time.After(wait * time.Second) - wait *= 2 - if wait > 600 { - wait = 600 - } - } - } + s.log(LogInformational, "calling reconnect() now") + s.reconnect() } + return } select { + case <-listening: return + default: - go s.event(messageType, message) + s.onEvent(messageType, message) + } } } type heartbeatOp struct { - Op int `json:"op"` - Data int `json:"d"` + Op int `json:"op"` + Data int64 `json:"d"` } +type helloOp struct { + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + Trace []string `json:"_trace"` +} + +// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. +const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond + // heartbeat sends regular heartbeats to Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. -func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { +func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) { + + s.log(LogInformational, "called") if listening == nil || wsConn == nil { return } - s.Lock() - s.DataReady = true - s.Unlock() - var err error - ticker := time.NewTicker(i * time.Millisecond) + ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) + defer ticker.Stop() + for { - err = wsConn.WriteJSON(heartbeatOp{1, int(time.Now().Unix())}) - if err != nil { - fmt.Println("Error sending heartbeat:", err) + s.RLock() + last := s.LastHeartbeatAck + s.RUnlock() + sequence := atomic.LoadInt64(s.sequence) + s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) + s.wsMutex.Lock() + err = wsConn.WriteJSON(heartbeatOp{1, sequence}) + s.wsMutex.Unlock() + if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { + if err != nil { + s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) + } else { + s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) + } + s.Close() + s.reconnect() return } + s.Lock() + s.DataReady = true + s.Unlock() select { case <-ticker.C: @@ -201,13 +249,11 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} } } -type updateStatusGame struct { - Name string `json:"name"` -} - type updateStatusData struct { - IdleSince *int `json:"idle_since"` - Game *updateStatusGame `json:"game"` + IdleSince *int `json:"since"` + Game *Game `json:"game"` + AFK bool `json:"afk"` + Status string `json:"status"` } type updateStatusOp struct { @@ -215,101 +261,236 @@ type updateStatusOp struct { Data updateStatusData `json:"d"` } -// UpdateStatus is used to update the authenticated user's status. -// If idle>0 then set status to idle. If game>0 then set game. +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// If game!="" and url!="" then set the status type to streaming with the URL set. // if otherwise, set status to active, and no game. -func (s *Session) UpdateStatus(idle int, game string) (err error) { +func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { + + s.log(LogInformational, "called") + s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("No websocket connection exists.") + return ErrWSNotFound + } + + usd := updateStatusData{ + Status: "online", } - var usd updateStatusData if idle > 0 { usd.IdleSince = &idle } + if game != "" { - usd.Game = &updateStatusGame{game} + gameType := 0 + if url != "" { + gameType = 1 + } + usd.Game = &Game{ + Name: game, + Type: gameType, + URL: url, + } } + s.wsMutex.Lock() err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) + s.wsMutex.Unlock() return } -// Front line handler for all Websocket Events. Determines the -// event type and passes the message along to the next handler. +// UpdateStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStatus(idle int, game string) (err error) { + return s.UpdateStreamingStatus(idle, game, "") +} + +type requestGuildMembersData struct { + GuildID string `json:"guild_id"` + Query string `json:"query"` + Limit int `json:"limit"` +} + +type requestGuildMembersOp struct { + Op int `json:"op"` + Data requestGuildMembersData `json:"d"` +} + +// RequestGuildMembers requests guild members from the gateway +// The gateway responds with GuildMembersChunk events +// guildID : The ID of the guild to request members of +// query : String that username starts with, leave empty to return all members +// limit : Max number of items to return, or 0 to request all members matched +func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err error) { + s.log(LogInformational, "called") + + s.RLock() + defer s.RUnlock() + if s.wsConn == nil { + return ErrWSNotFound + } + + data := requestGuildMembersData{ + GuildID: guildID, + Query: query, + Limit: limit, + } + + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data}) + s.wsMutex.Unlock() + + return +} + +// 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 messageType == 2 { - z, err1 := zlib.NewReader(reader) - if err1 != nil { - fmt.Println(err1) + // If this is a compressed message, uncompress it. + if messageType == websocket.BinaryMessage { + + z, err2 := zlib.NewReader(reader) + if err2 != nil { + s.log(LogError, "error uncompressing websocket message, %s", err) return } + defer func() { - err := z.Close() - if err != nil { - fmt.Println("error closing zlib:", err) + err3 := z.Close() + if err3 != nil { + 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 { - fmt.Println(err) + s.log(LogError, "error decoding websocket message, %s", err) + return + } + + s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", 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") + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(heartbeatOp{1, atomic.LoadInt64(s.sequence)}) + s.wsMutex.Unlock() + if err != nil { + s.log(LogError, "error sending heartbeat in response to Op1") + return + } + + return + } + + // Reconnect + // Must immediately disconnect from gateway and reconnect to new gateway. + if e.Operation == 7 { + s.log(LogInformational, "Closing and reconnecting in response to Op7") + s.Close() + s.reconnect() return } - if s.Debug { - printEvent(e) + // Invalid Session + // Must respond with a Identify packet. + if e.Operation == 9 { + + s.log(LogInformational, "sending identify packet to gateway in response to Op9") + + err = s.identify() + if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) + return + } + + return + } + + if e.Operation == 10 { + var h helloOp + if err = json.Unmarshal(e.RawData, &h); err != nil { + s.log(LogError, "error unmarshalling helloOp, %s", err) + } else { + go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval) + } + return + } + + if e.Operation == 11 { + s.Lock() + s.LastHeartbeatAck = time.Now().UTC() + s.Unlock() + s.log(LogInformational, "got heartbeat ACK") + 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 } - i := eventToInterface[e.Type] - if i != nil { - // Create a new instance of the event type. - i = reflect.New(reflect.TypeOf(i)).Interface() + // Store the message sequence + atomic.StoreInt64(s.sequence, e.Sequence) + + // Map event to registered event handlers and pass it along to any registered handlers. + if eh, ok := registeredInterfaceProviders[e.Type]; ok { + e.Struct = eh.New() // Attempt to unmarshal our event. - // If there is an error we should handle the event itself. - if err = unmarshal(e.RawData, i); err != nil { - fmt.Println("Unable to unmarshal event data.") - i = e + if err = json.Unmarshal(e.RawData, e.Struct); err != nil { + s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) } + + // 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.handleEvent(e.Type, e.Struct) } else { - //fmt.Println("Unknown event.") - i = e + s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) } - s.handle(i) - - return + // For legacy reasons, we send the raw event also, this could be useful for handling unknown events. + s.handleEvent(eventEventType, e) } // ------------------------------------------------------------------------------------------------ // Code related to voice connections that initiate over the data websocket // ------------------------------------------------------------------------------------------------ -// A VoiceServerUpdate stores the data received during the Voice Server Update -// data websocket event. This data is used during the initial Voice Channel -// join handshaking. -type VoiceServerUpdate struct { - Token string `json:"token"` - GuildID string `json:"guild_id"` - Endpoint string `json:"endpoint"` -} - type voiceChannelJoinData struct { GuildID *string `json:"guild_id"` ChannelID *string `json:"channel_id"` @@ -322,113 +503,280 @@ type voiceChannelJoinOp struct { Data voiceChannelJoinData `json:"d"` } -// ChannelVoiceJoin joins the session user to a voice channel. After calling -// this func please monitor the Session.Voice.Ready bool to determine when -// it is ready and able to send/receive audio, that should happen quickly. +// ChannelVoiceJoin joins the session user to a voice channel. // -// gID : Guild ID of the channel to join. -// cID : Channel ID of the channel to join. -// mute : If true, you will be set to muted upon joining. -// deaf : If true, you will be set to deafened upon joining. -func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (err error) { +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { + + s.log(LogInformational, "called") - // Create new voice{} struct if one does not exist. - // If you create this prior to calling this func then you can manually - // set some variables if needed, such as to enable debugging. - if s.Voice == nil { - s.Voice = &Voice{} + s.RLock() + voice, _ = s.VoiceConnections[gID] + s.RUnlock() + + if voice == nil { + voice = &VoiceConnection{} + s.Lock() + s.VoiceConnections[gID] = voice + s.Unlock() } + voice.Lock() + voice.GuildID = gID + voice.ChannelID = cID + voice.deaf = deaf + voice.mute = mute + voice.session = s + voice.Unlock() + // Send the request to Discord that we want to join the voice channel data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} + s.wsMutex.Lock() err = s.wsConn.WriteJSON(data) + s.wsMutex.Unlock() if err != nil { return } - // Store gID and cID for later use - s.Voice.guildID = gID - s.Voice.channelID = cID - - return -} - -// ChannelVoiceLeave disconnects from the currently connected -// voice channel. -func (s *Session) ChannelVoiceLeave() (err error) { - - if s.Voice == nil { - return - } - - // Send the request to Discord that we want to leave voice - data := voiceChannelJoinOp{4, voiceChannelJoinData{nil, nil, true, true}} - err = s.wsConn.WriteJSON(data) + // doesn't exactly work perfect yet.. TODO + err = voice.waitUntilConnected() if err != nil { + s.log(LogWarning, "error waiting for voice to connect, %s", err) + voice.Close() return } - // Close voice and nil data struct - s.Voice.Close() - s.Voice = nil - return } -// onVoiceStateUpdate handles Voice State Update events on the data -// websocket. This comes immediately after the call to VoiceChannelJoin -// for the session user. -func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { +// onVoiceStateUpdate handles Voice State Update events on the data websocket. +func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { - // Ignore if Voice is nil - if s.Voice == nil { + // If we don't have a connection for the channel, don't bother + if st.ChannelID == "" { return } - // Need to have this happen at login and store it in the Session - // TODO : This should be done upon connecting to Discord, or - // be moved to a small helper function - self, err := s.User("@me") // TODO: move to Login/New - if err != nil { - fmt.Println(err) + // Check if we have a voice connection to update + s.RLock() + voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() + if !exists { return } - // This event comes for all users, if it's not for the session - // user just ignore it. - // TODO Move this IF to the event() func - if st.UserID != self.ID { + // We only care about events that are about us. + if s.State.User.ID != st.UserID { return } // Store the SessionID for later use. - s.Voice.userID = self.ID // TODO: Review - s.Voice.sessionID = st.SessionID + voice.Lock() + voice.UserID = st.UserID + voice.sessionID = st.SessionID + voice.ChannelID = st.ChannelID + voice.Unlock() } // onVoiceServerUpdate handles the Voice Server Update data websocket event. -// This event tells us the information needed to open a voice websocket -// connection and should happen after the VOICE_STATE event. // // This is also fired if the Guild's voice region changes while connected // to a voice channel. In that case, need to re-establish connection to // the new region endpoint. -func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { +func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) { - // Store values for later use - s.Voice.token = st.Token - s.Voice.endpoint = st.Endpoint - s.Voice.guildID = st.GuildID + s.log(LogInformational, "called") + + s.RLock() + voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() + + // If no VoiceConnection exists, just skip this + if !exists { + return + } // If currently connected to voice ws/udp, then disconnect. // Has no effect if not connected. - s.Voice.Close() + voice.Close() - // We now have enough information to open a voice websocket conenction - // so, that's what the next call does. - err := s.Voice.Open() + // Store values for later use + voice.Lock() + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.GuildID = st.GuildID + voice.Unlock() + + // Open a conenction to the voice server + err := voice.open() if err != nil { - fmt.Println("onVoiceServerUpdate Voice.Open error: ", err) - // TODO better logging + s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) + } +} + +type identifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` + Referer string `json:"$referer"` + ReferringDomain string `json:"$referring_domain"` +} + +type identifyData struct { + Token string `json:"token"` + Properties identifyProperties `json:"properties"` + LargeThreshold int `json:"large_threshold"` + Compress bool `json:"compress"` + Shard *[2]int `json:"shard,omitempty"` +} + +type identifyOp struct { + Op int `json:"op"` + Data identifyData `json:"d"` +} + +// identify sends the identify packet to the gateway +func (s *Session) identify() error { + + properties := identifyProperties{runtime.GOOS, + "Discordgo v" + VERSION, + "", + "", + "", } + + data := identifyData{s.Token, + properties, + 250, + s.Compress, + nil, + } + + if s.ShardCount > 1 { + + if s.ShardID >= s.ShardCount { + return ErrWSShardBounds + } + + data.Shard = &[2]int{s.ShardID, s.ShardCount} + } + + op := identifyOp{2, data} + + s.wsMutex.Lock() + err := s.wsConn.WriteJSON(op) + s.wsMutex.Unlock() + if err != nil { + return err + } + + return nil +} + +func (s *Session) reconnect() { + + s.log(LogInformational, "called") + + var err error + + if s.ShouldReconnectOnError { + + wait := time.Duration(1) + + for { + s.log(LogInformational, "trying to reconnect to gateway") + + err = s.Open() + if err == nil { + s.log(LogInformational, "successfully reconnected to gateway") + + // I'm not sure if this is actually needed. + // if the gw reconnect works properly, voice should stay alive + // However, there seems to be cases where something "weird" + // happens. So we're doing this for now just to improve + // stability in those edge cases. + s.RLock() + defer s.RUnlock() + for _, v := range s.VoiceConnections { + + s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) + go v.reconnect() + + // This is here just to prevent violently spamming the + // voice reconnects + time.Sleep(1 * time.Second) + + } + return + } + + // Certain race conditions can call reconnect() twice. If this happens, we + // just break out of the reconnect loop + if err == ErrWSAlreadyOpen { + s.log(LogInformational, "Websocket already exists, no need to reconnect") + return + } + + s.log(LogError, "error reconnecting to gateway, %s", err) + + <-time.After(wait * time.Second) + wait *= 2 + if wait > 600 { + wait = 600 + } + } + } +} + +// Close closes a websocket and stops all listening/heartbeat goroutines. +// TODO: Add support for Voice WS/UDP connections +func (s *Session) Close() (err error) { + + s.log(LogInformational, "called") + s.Lock() + + s.DataReady = false + + if s.listening != nil { + s.log(LogInformational, "closing listening channel") + close(s.listening) + s.listening = nil + } + + // TODO: Close all active Voice Connections too + // this should force stop any reconnecting voice channels too + + if s.wsConn != nil { + + s.log(LogInformational, "sending close frame") + // To cleanly close a connection, a client should send a close + // frame and wait for the server to close the connection. + s.wsMutex.Lock() + err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + s.wsMutex.Unlock() + if err != nil { + s.log(LogInformational, "error closing websocket, %s", err) + } + + // TODO: Wait for Discord to actually close the connection. + time.Sleep(1 * time.Second) + + s.log(LogInformational, "closing gateway websocket") + err = s.wsConn.Close() + if err != nil { + s.log(LogInformational, "error closing websocket, %s", err) + } + + s.wsConn = nil + } + + s.Unlock() + + s.log(LogInformational, "emit disconnect event") + s.handleEvent(disconnectEventType, &Disconnect{}) + + return }