From 3cf4816e18601b1d7027c82d7ddcdb0dde1a8206 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 19:51:08 -0700 Subject: [PATCH 01/32] Remove 1.5 from travis as golint seems to have lost support. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 45b0582..9eb2c71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.5 - 1.6 - tip install: From f31b65315b6156aa262ccd71d3907facf7241b2b Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 20:01:07 -0700 Subject: [PATCH 02/32] Bring back 1.5, make lint non fatal. --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9eb2c71..b511e94 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go go: + - 1.5 - 1.6 - tip install: @@ -9,5 +10,5 @@ install: script: - diff <(gofmt -d .) <(echo -n) - go vet -x ./... - - golint -set_exit_status ./... + - golint ./... - go test -v -race ./... From 7b7b45cfba2c2e53057bc46ab9d6a837fd1d3602 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 20:01:50 -0700 Subject: [PATCH 03/32] Fix lint warnings. --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 337a7f8..f4ffaf1 100644 --- a/restapi.go +++ b/restapi.go @@ -376,9 +376,9 @@ func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSetti return } +// UserChannelPermissions returns the permission of a user in a channel. // NOTE: This function is now deprecated and will be removed in the future. // Please see the same function inside state.go -// 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 *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { From 4b9d2e31ccf04b3ade007603d216ac7e46e423f2 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 20:07:32 -0700 Subject: [PATCH 04/32] Remove 1.5, lint is broken :( :killme: --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index b511e94..0febcbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: go go: - - 1.5 - 1.6 - tip install: From c5abbfa42ecd88bf8718d50329e41a6a04a364a7 Mon Sep 17 00:00:00 2001 From: Austin Davis Date: Tue, 4 Oct 2016 15:45:36 -0600 Subject: [PATCH 05/32] Updates docs to include the newly required "Bot" infront of bot user tokens. (#266) --- examples/airhorn/README.md | 2 +- examples/avatar/localfile/README.md | 4 ++-- examples/avatar/url/README.md | 4 ++-- examples/mytoken/README.md | 10 ++++++++-- examples/new_basic/README.md | 12 +++++++++--- examples/pingpong/README.md | 10 ++++++++-- 6 files changed, 30 insertions(+), 12 deletions(-) diff --git a/examples/airhorn/README.md b/examples/airhorn/README.md index 562e2c9..44f4d52 100644 --- a/examples/airhorn/README.md +++ b/examples/airhorn/README.md @@ -27,7 +27,7 @@ Usage of ./airhorn: The below example shows how to start the bot. ```sh -./airhorn -t +./airhorn -t "Bot YOUR_BOT_TOKEN" ``` ### Creating sounds diff --git a/examples/avatar/localfile/README.md b/examples/avatar/localfile/README.md index 9a39e0a..cf15472 100644 --- a/examples/avatar/localfile/README.md +++ b/examples/avatar/localfile/README.md @@ -37,5 +37,5 @@ Usage of ./ocalfile: For example to start application with Token and a non-default avatar: ```sh -./localfile -t "YOUR_BOT_TOKEN" -f "./pathtoavatar.jpg" -``` \ No newline at end of file +./localfile -t "Bot YOUR_BOT_TOKEN" -f "./pathtoavatar.jpg" +``` diff --git a/examples/avatar/url/README.md b/examples/avatar/url/README.md index 6247f00..340fd68 100644 --- a/examples/avatar/url/README.md +++ b/examples/avatar/url/README.md @@ -37,5 +37,5 @@ Usage of ./url: For example to start application with Token and a non-default avatar: ```sh -./url -t "YOUR_BOT_TOKEN" -l "http://bwmarrin.github.io/discordgo/img/discordgo.png" -``` \ No newline at end of file +./url -t "Bot YOUR_BOT_TOKEN" -l "http://bwmarrin.github.io/discordgo/img/discordgo.png" +``` diff --git a/examples/mytoken/README.md b/examples/mytoken/README.md index 4450873..a870615 100644 --- a/examples/mytoken/README.md +++ b/examples/mytoken/README.md @@ -2,12 +2,12 @@ MyToken Example ==== -This example demonstrates how to utilize DiscordGo to print out the +This example demonstrates how to utilize DiscordGo to print out the Authentication Token for a given user account. ### Build -This assumes you already have a working Go environment setup and that +This assumes you already have a working Go environment setup and that DiscordGo is correctly installed on your system. ```sh @@ -33,3 +33,9 @@ authentication. ```sh ./mytoken -e EmailHere -p PasswordHere ``` + +The below example shows how to start the bot using the bot user's token + +```sh +./pingpong -t "Bot YOUR_BOT_TOKEN" +``` diff --git a/examples/new_basic/README.md b/examples/new_basic/README.md index d45523e..c5557ff 100644 --- a/examples/new_basic/README.md +++ b/examples/new_basic/README.md @@ -2,14 +2,14 @@ Basic New Example ==== -This example demonstrates how to utilize DiscordGo to connect to Discord +This example demonstrates how to utilize DiscordGo to connect to Discord and print out all received chat messages. This example uses the high level New() helper function to connect to Discord. ### Build -This assumes you already have a working Go environment setup and that +This assumes you already have a working Go environment setup and that DiscordGo is correctly installed on your system. ```sh @@ -18,7 +18,7 @@ go build ### Usage -You must authenticate using either an Authentication Token or both Email and +You must authenticate using either an Authentication Token or both Email and Password for an account. Keep in mind official Bot accounts only support authenticating via Token. @@ -39,3 +39,9 @@ authentication. ```sh ./new_basic -e EmailHere -p PasswordHere ``` + +The below example shows how to start the bot using the bot user's token + +```sh +./new_basic -t "Bot YOUR_BOT_TOKEN" +``` diff --git a/examples/pingpong/README.md b/examples/pingpong/README.md index 267d4ad..0eb43eb 100644 --- a/examples/pingpong/README.md +++ b/examples/pingpong/README.md @@ -8,7 +8,7 @@ This Bot will respond to "ping" with "Pong!" and "pong" with "Ping!". ### Build -This assumes you already have a working Go environment setup and that +This assumes you already have a working Go environment setup and that DiscordGo is correctly installed on your system. ```sh @@ -17,7 +17,7 @@ go build ### Usage -You must authenticate using either an Authentication Token or both Email and +You must authenticate using either an Authentication Token or both Email and Password for an account. Keep in mind official Bot accounts only support authenticating via Token. @@ -38,3 +38,9 @@ authentication. ```sh ./pingpong -e EmailHere -p PasswordHere ``` + +The below example shows how to start the bot using the bot user's token + +```sh +./pingpong -t "BOT YOUR_BOT_TOKEN" +``` From f878362d73b01a051091faaa012deeb7a85204af Mon Sep 17 00:00:00 2001 From: Austin Davis Date: Thu, 13 Oct 2016 20:14:03 -0600 Subject: [PATCH 06/32] casing matters for some reason tried BOT and it returns a 401. (#268) Also got the name of the executable wrong in mytoken (damn copy paste...) --- examples/mytoken/README.md | 2 +- examples/pingpong/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/mytoken/README.md b/examples/mytoken/README.md index a870615..fa77bab 100644 --- a/examples/mytoken/README.md +++ b/examples/mytoken/README.md @@ -37,5 +37,5 @@ authentication. The below example shows how to start the bot using the bot user's token ```sh -./pingpong -t "Bot YOUR_BOT_TOKEN" +./mytoken -t "Bot YOUR_BOT_TOKEN" ``` diff --git a/examples/pingpong/README.md b/examples/pingpong/README.md index 0eb43eb..454594b 100644 --- a/examples/pingpong/README.md +++ b/examples/pingpong/README.md @@ -42,5 +42,5 @@ authentication. The below example shows how to start the bot using the bot user's token ```sh -./pingpong -t "BOT YOUR_BOT_TOKEN" +./pingpong -t "Bot YOUR_BOT_TOKEN" ``` From 4c32e412881afdeded92870b73f5727c2433b09e Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 29 Oct 2016 16:30:07 -0700 Subject: [PATCH 07/32] Update examples to use Bot tokens. --- examples/airhorn/README.md | 4 ++-- examples/airhorn/main.go | 6 +++--- examples/avatar/localfile/README.md | 2 +- examples/avatar/url/README.md | 2 +- examples/new_basic/README.md | 24 ++++++------------------ examples/new_basic/main.go | 13 ++++--------- examples/pingpong/README.md | 22 +++++----------------- examples/pingpong/main.go | 14 +++++--------- 8 files changed, 27 insertions(+), 60 deletions(-) diff --git a/examples/airhorn/README.md b/examples/airhorn/README.md index 44f4d52..984fd6b 100644 --- a/examples/airhorn/README.md +++ b/examples/airhorn/README.md @@ -21,13 +21,13 @@ cp ../src/github.com/bwmarrin/discordgo/examples/airhorn/airhorn.dca . ``` Usage of ./airhorn: -t string - Account Token + Bot Token ``` The below example shows how to start the bot. ```sh -./airhorn -t "Bot YOUR_BOT_TOKEN" +./airhorn -t YOUR_BOT_TOKEN ``` ### Creating sounds diff --git a/examples/airhorn/main.go b/examples/airhorn/main.go index cc61301..3afe047 100644 --- a/examples/airhorn/main.go +++ b/examples/airhorn/main.go @@ -13,7 +13,7 @@ import ( ) func init() { - flag.StringVar(&token, "t", "", "Account Token") + flag.StringVar(&token, "t", "", "Bot Token") flag.Parse() } @@ -34,8 +34,8 @@ func main() { return } - // Create a new Discord session using the provided token. - dg, err := discordgo.New(token) + // 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 diff --git a/examples/avatar/localfile/README.md b/examples/avatar/localfile/README.md index cf15472..1e2bf29 100644 --- a/examples/avatar/localfile/README.md +++ b/examples/avatar/localfile/README.md @@ -34,7 +34,7 @@ Usage of ./ocalfile: Avatar File Name. ``` -For example to start application with Token and a non-default avatar: +For example to start application with a bot token and a non-default avatar: ```sh ./localfile -t "Bot YOUR_BOT_TOKEN" -f "./pathtoavatar.jpg" diff --git a/examples/avatar/url/README.md b/examples/avatar/url/README.md index 340fd68..e11e0a8 100644 --- a/examples/avatar/url/README.md +++ b/examples/avatar/url/README.md @@ -34,7 +34,7 @@ Usage of ./url: Link to the avatar image. ``` -For example to start application with Token and a non-default avatar: +For example to start application with a bot token and a non-default avatar: ```sh ./url -t "Bot YOUR_BOT_TOKEN" -l "http://bwmarrin.github.io/discordgo/img/discordgo.png" diff --git a/examples/new_basic/README.md b/examples/new_basic/README.md index c5557ff..b51a494 100644 --- a/examples/new_basic/README.md +++ b/examples/new_basic/README.md @@ -3,7 +3,7 @@ Basic New Example ==== This example demonstrates how to utilize DiscordGo to connect to Discord -and print out all received chat messages. +and print out all received chat messages. This example uses the high level New() helper function to connect to Discord. @@ -18,30 +18,18 @@ go build ### Usage -You must authenticate using either an Authentication Token or both Email and -Password for an account. Keep in mind official Bot accounts only support -authenticating via Token. +This example uses bot tokens for authentication only. +While user/password is supported by DiscordGo, it is not recommended. ``` ./new_basic --help Usage of ./new_basic: - -e string - Account Email - -p string - Account Password -t string - Account Token + Bot Token ``` -The below example shows how to start the bot using an Email and Password for -authentication. +The below example shows how to start the bot ```sh -./new_basic -e EmailHere -p PasswordHere -``` - -The below example shows how to start the bot using the bot user's token - -```sh -./new_basic -t "Bot YOUR_BOT_TOKEN" +./new_basic -t YOUR_BOT_TOKEN ``` diff --git a/examples/new_basic/main.go b/examples/new_basic/main.go index c3861ac..0191bb0 100644 --- a/examples/new_basic/main.go +++ b/examples/new_basic/main.go @@ -10,24 +10,19 @@ import ( // Variables used for command line parameters var ( - Email string - Password string - Token string + Token string ) func init() { - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") - flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&Token, "t", "", "Bot Token") flag.Parse() } func main() { - // Create a new Discord session using the provided login information. - // Use discordgo.New(Token) to just use a token for login. - dg, err := discordgo.New(Email, Password, Token) + // 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 diff --git a/examples/pingpong/README.md b/examples/pingpong/README.md index 454594b..d2ad61f 100644 --- a/examples/pingpong/README.md +++ b/examples/pingpong/README.md @@ -17,30 +17,18 @@ go build ### Usage -You must authenticate using either an Authentication Token or both Email and -Password for an account. Keep in mind official Bot accounts only support -authenticating via Token. +This example uses bot tokens for authentication only. +While user/password is supported by DiscordGo, it is not recommended. ``` ./pingpong --help Usage of ./pingpong: - -e string - Account Email - -p string - Account Password -t string - Account Token + Bot Token ``` -The below example shows how to start the bot using an Email and Password for -authentication. +The below example shows how to start the bot ```sh -./pingpong -e EmailHere -p PasswordHere -``` - -The below example shows how to start the bot using the bot user's token - -```sh -./pingpong -t "Bot YOUR_BOT_TOKEN" +./pingpong -t YOUR_BOT_TOKEN ``` diff --git a/examples/pingpong/main.go b/examples/pingpong/main.go index e6893ca..2edd957 100644 --- a/examples/pingpong/main.go +++ b/examples/pingpong/main.go @@ -9,24 +9,20 @@ import ( // Variables used for command line parameters var ( - Email string - Password string - Token string - BotID string + Token string + BotID string ) func init() { - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") - flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&Token, "t", "", "Bot Token") flag.Parse() } func main() { - // Create a new Discord session using the provided login information. - dg, err := discordgo.New(Email, Password, Token) + // 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 From 4edcbf8e531587f17036e722180c9cdcf5309d82 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 5 Nov 2016 00:15:16 -0700 Subject: [PATCH 08/32] Reduce severity of wsConn close error logging. --- wsapi.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wsapi.go b/wsapi.go index a19c384..2ed8208 100644 --- a/wsapi.go +++ b/wsapi.go @@ -655,7 +655,7 @@ func (s *Session) Close() (err error) { // frame and wait for the server to close the connection. err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) if err != nil { - s.log(LogError, "error closing websocket, %s", err) + s.log(LogInformational, "error closing websocket, %s", err) } // TODO: Wait for Discord to actually close the connection. @@ -664,7 +664,7 @@ func (s *Session) Close() (err error) { s.log(LogInformational, "closing gateway websocket") err = s.wsConn.Close() if err != nil { - s.log(LogError, "error closing websocket, %s", err) + s.log(LogInformational, "error closing websocket, %s", err) } s.wsConn = nil From ddfeefaf75c317e057bc870a2aaa8c98d9d51431 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 23 Nov 2016 17:18:05 -0800 Subject: [PATCH 09/32] Update discord.go --- discord.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord.go b/discord.go index d1cfddf..cc0ebd5 100644 --- a/discord.go +++ b/discord.go @@ -27,6 +27,8 @@ const VERSION = "0.13.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 From b5698e658d8bf72f46d5b334d8ea39642f3b1049 Mon Sep 17 00:00:00 2001 From: Matthew Gerstman Date: Sun, 27 Nov 2016 23:48:33 -0500 Subject: [PATCH 10/32] Fix issue with trailing slashes in MacOS (#292) * Fix issue with trailing slashes in MacOS --- restapi.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/restapi.go b/restapi.go index f4ffaf1..a54dfe8 100644 --- a/restapi.go +++ b/restapi.go @@ -1399,5 +1399,12 @@ 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 } From 5835676872d7fa65ee37b32672079c4ede2c1855 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 10 Dec 2016 06:47:14 -0800 Subject: [PATCH 11/32] Bump version to 0.15.0 --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index cda52f3..4081c65 100644 --- a/discord.go +++ b/discord.go @@ -16,7 +16,7 @@ package discordgo import "fmt" // VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) -const VERSION = "0.14.0-dev" +const VERSION = "0.15.0" // New creates a new Discord session and will automate some startup // tasks if given enough information to do so. Currently you can pass zero From 837a3dc5294a9af91660a3d5a2b1749a5d2ee39d Mon Sep 17 00:00:00 2001 From: John Date: Tue, 28 Mar 2017 08:55:53 -0500 Subject: [PATCH 12/32] Fix README format (#344) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e5cbdea..f4466b1 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ 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) From d8f81e54bb594a17281e9b52d7d58d2ada9f8443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=B6ppl?= Date: Sat, 29 Apr 2017 23:21:10 +0200 Subject: [PATCH 13/32] Fix typo It is called 'Semantic Versioning' --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index 4081c65..c8b7f0c 100644 --- a/discord.go +++ b/discord.go @@ -15,7 +15,7 @@ package discordgo import "fmt" -// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) +// VERSION of Discordgo, follows Semantic Versioning. (http://semver.org/) const VERSION = "0.15.0" // New creates a new Discord session and will automate some startup From 25c8012990ae5722b3f53078a42caf821e047754 Mon Sep 17 00:00:00 2001 From: legolord208 Date: Mon, 8 May 2017 15:43:50 +0200 Subject: [PATCH 14/32] Added missing permission constants (#378) --- structs.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/structs.go b/structs.go index 3a6ec05..17a3633 100644 --- a/structs.go +++ b/structs.go @@ -549,6 +549,8 @@ const ( PermissionAdministrator PermissionManageChannels PermissionManageServer + PermissionAddReactions + PermissionViewAuditLogs PermissionAllText = PermissionReadMessages | PermissionSendMessages | @@ -568,7 +570,9 @@ const ( PermissionAllVoice | PermissionCreateInstantInvite | PermissionManageRoles | - PermissionManageChannels + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs PermissionAll = PermissionAllChannel | PermissionKickMembers | PermissionBanMembers | From aa3973f95643bc2dfd6e1fc4eb2a4b8f02dcd9fe Mon Sep 17 00:00:00 2001 From: legolord208 Date: Mon, 8 May 2017 15:48:19 +0200 Subject: [PATCH 15/32] Made error constants (Fixed #315) (#377) --- restapi.go | 23 +++++++++++++++-------- state.go | 28 ++++++++++++++++------------ wsapi.go | 20 ++++++++++++++++---- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/restapi.go b/restapi.go index 7c9fd81..cb482e6 100644 --- a/restapi.go +++ b/restapi.go @@ -29,8 +29,15 @@ import ( "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 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) { @@ -334,7 +341,7 @@ func (s *Session) UserSettings() (st *Settings, err error) { // 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 = errors.New("You can't set your Status to offline") + err = ErrStatusOffline return } @@ -595,7 +602,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error if g.VerificationLevel != nil { val := *g.VerificationLevel if val < 0 || val > 3 { - err = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + err = ErrVerificationLevelBounds return } } @@ -988,7 +995,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er count = 0 if days <= 0 { - err = errors.New("the number of days should be more than or equal to 1") + err = ErrPruneDaysBounds return } @@ -1018,7 +1025,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err count = 0 if days <= 0 { - err = errors.New("the number of days should be more than or equal to 1") + err = ErrPruneDaysBounds return } @@ -1120,7 +1127,7 @@ 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 } @@ -1142,7 +1149,7 @@ 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 } diff --git a/state.go b/state.go index c2c5519..7400ef6 100644 --- a/state.go +++ b/state.go @@ -21,6 +21,10 @@ import ( // ErrNilState is returned when the state is nil. 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. @@ -146,7 +150,7 @@ func (s *State) Guild(guildID string) (*Guild, error) { return g, nil } - return nil, errors.New("guild not found") + return nil, ErrStateNotFound } // PresenceAdd adds a presence to the current world state, or @@ -227,7 +231,7 @@ func (s *State) PresenceRemove(guildID string, presence *Presence) error { } } - return errors.New("presence not found") + return ErrStateNotFound } // Presence gets a presence by ID from a guild. @@ -247,7 +251,7 @@ func (s *State) Presence(guildID, userID string) (*Presence, error) { } } - return nil, errors.New("presence not found") + return nil, ErrStateNotFound } // TODO: Consider moving Guild state update methods onto *Guild. @@ -299,7 +303,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. @@ -322,7 +326,7 @@ func (s *State) Member(guildID, userID string) (*Member, error) { } } - return nil, errors.New("member not found") + return nil, ErrStateNotFound } // RoleAdd adds a role to the current world state, or @@ -372,7 +376,7 @@ func (s *State) RoleRemove(guildID, roleID string) error { } } - return errors.New("role not found") + return ErrStateNotFound } // Role gets a role by ID from a guild. @@ -395,7 +399,7 @@ func (s *State) Role(guildID, roleID string) (*Role, error) { } } - return nil, errors.New("role not found") + return nil, ErrStateNotFound } // ChannelAdd adds a channel to the current world state, or @@ -428,7 +432,7 @@ func (s *State) ChannelAdd(channel *Channel) error { } else { guild, ok := s.guildMap[channel.GuildID] if !ok { - return errors.New("guild for channel not found") + return ErrStateNotFound } guild.Channels = append(guild.Channels, channel) @@ -507,7 +511,7 @@ func (s *State) Channel(channelID string) (*Channel, error) { return c, nil } - return nil, errors.New("channel not found") + return nil, ErrStateNotFound } // Emoji returns an emoji for a guild and emoji id. @@ -530,7 +534,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. @@ -647,7 +651,7 @@ func (s *State) messageRemoveByID(channelID, messageID string) error { } } - return errors.New("message not found") + return ErrStateNotFound } func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { @@ -701,7 +705,7 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { } } - return nil, errors.New("message not found") + return nil, ErrStateNotFound } // OnReady takes a Ready event and updates all internal state. diff --git a/wsapi.go b/wsapi.go index adab402..0912850 100644 --- a/wsapi.go +++ b/wsapi.go @@ -25,6 +25,18 @@ import ( "github.com/gorilla/websocket" ) +// ErrWSAlreadyOpen is thrown when you attempt to open +// a websocket that already is open. +var ErrWSAlreadyOpen = errors.New("web socket already opened") + +// 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 resumePacket struct { Op int `json:"op"` Data struct { @@ -58,7 +70,7 @@ func (s *Session) Open() (err error) { } if s.wsConn != nil { - err = errors.New("web socket already opened") + err = ErrWSAlreadyOpen return } @@ -250,7 +262,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } var usd updateStatusData @@ -307,7 +319,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } data := requestGuildMembersData{ @@ -621,7 +633,7 @@ func (s *Session) identify() error { if s.ShardCount > 1 { if s.ShardID >= s.ShardCount { - return errors.New("ShardID must be less than ShardCount") + return ErrWSShardBounds } data.Shard = &[2]int{s.ShardID, s.ShardCount} From eed1d20f1a7d9a31f897ea86bcd309f7208b51ad Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAA Date: Mon, 8 May 2017 17:32:00 +0200 Subject: [PATCH 16/32] Fix presence unmarshal with game names as numbers (#381) * Game name is also not validated on the server * Use json.Number instead --- structs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structs.go b/structs.go index 17a3633..32f435c 100644 --- a/structs.go +++ b/structs.go @@ -304,7 +304,7 @@ type Game struct { // UnmarshalJSON unmarshals json to Game struct func (g *Game) UnmarshalJSON(bytes []byte) error { temp := &struct { - Name string `json:"name"` + Name json.Number `json:"name"` Type json.RawMessage `json:"type"` URL string `json:"url"` }{} @@ -312,8 +312,8 @@ func (g *Game) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - g.Name = temp.Name g.URL = temp.URL + g.Name = temp.Name.String() if temp.Type != nil { err = json.Unmarshal(temp.Type, &g.Type) From 0993a94b4e1c3291bed2047f583f34792269355c Mon Sep 17 00:00:00 2001 From: Bruce Date: Mon, 15 May 2017 15:02:49 +0000 Subject: [PATCH 17/32] Bump version to 0.16.0 --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index bb3a402..2f0b6fd 100644 --- a/discord.go +++ b/discord.go @@ -21,7 +21,7 @@ import ( ) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) -const VERSION = "0.16.0-dev" +const VERSION = "0.16.0" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled") From f2cad25dc928189d06fe4027d1f13b6e7c1c6e50 Mon Sep 17 00:00:00 2001 From: Bruce Date: Mon, 15 May 2017 16:12:45 +0000 Subject: [PATCH 18/32] Bump version to v0.17.0-dev --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index 2f0b6fd..67ca944 100644 --- a/discord.go +++ b/discord.go @@ -21,7 +21,7 @@ import ( ) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) -const VERSION = "0.16.0" +const VERSION = "0.17.0-dev" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled") From 6aec04d69d3f7f85caaf850c25ffc0c03e016702 Mon Sep 17 00:00:00 2001 From: legolord208 Date: Sat, 20 May 2017 19:56:45 +0200 Subject: [PATCH 19/32] omitempty (#383) --- restapi.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index cb482e6..797cc61 100644 --- a/restapi.go +++ b/restapi.go @@ -309,8 +309,8 @@ 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"` + 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"` From a71f5e3e61f1776d694c8b94be52676c76818550 Mon Sep 17 00:00:00 2001 From: Rinzen Necroforger Date: Mon, 29 May 2017 23:03:39 -0400 Subject: [PATCH 20/32] Support animated avatar URLs (#388) * Methods to obtain a user's avatar URL * Created EndpointUserAvatarAnimated and updated AvatarURL * Fixed the size parameter --- endpoints.go | 21 +++++++++++---------- user.go | 18 +++++++++++++++++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/endpoints.go b/endpoints.go index 96bcf28..6c33ef0 100644 --- a/endpoints.go +++ b/endpoints.go @@ -54,16 +54,17 @@ var ( 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" } - 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 } + 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" } diff --git a/user.go b/user.go index b3a7e4b..76abdd1 100644 --- a/user.go +++ b/user.go @@ -1,6 +1,9 @@ package discordgo -import "fmt" +import ( + "fmt" + "strings" +) // A User stores all data for an individual Discord user. type User struct { @@ -24,3 +27,16 @@ func (u *User) String() string { 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 +} From 874325a50477bc47739eeedcbe566c939912fc21 Mon Sep 17 00:00:00 2001 From: rfw Date: Sat, 10 Jun 2017 13:13:28 -0700 Subject: [PATCH 21/32] Add and fix support for multiple file uploads via ChannelMessageSendComplex via the new field MessageSend.Files. (#391) For compatibility with existing library consumers, the File field is retained but will behave as if Files contained that single file. If both are specified, ChannelMessageSendComplex will return an error. The message JSON payload is moved to a form-data field called `payload_json`, instead of set in multipart form data. This is supported and the recommended way, as per the API docs. Apparently, you can attach multiple files if you just name the parts names differently in the multipart request. The parts are named here using the order the files were specified, as `file%d`. This is not documented in the API docs, but definitely works. This also removes serialization of the File field via json.Marshal, as it will never be directly serialized in the JSON. The new field, Files, is similarly not marshaled. This additionally adds a ContentType field in File, which can be used to specify the content type of the attached file. The ContentType field will default to setting the header to `application/octet-stream` if empty. Discord currently doesn't do much with the Content-Type header, but we should pass this information along anyway in accordance to the MIME standard. --- message.go | 10 ++++--- restapi.go | 77 +++++++++++++++++++++++++++++++++--------------------- 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/message.go b/message.go index 13c2da0..34303b7 100644 --- a/message.go +++ b/message.go @@ -34,8 +34,9 @@ type Message struct { // File stores info about files you e.g. send in messages. type File struct { - Name string - Reader io.Reader + Name string + ContentType string + Reader io.Reader } // MessageSend stores all parameters you can send with ChannelMessageSendComplex. @@ -43,7 +44,10 @@ type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` Tts bool `json:"tts"` - File *File `json:"file"` + Files []*File `json:"-"` + + // TODO: Remove this when compatibility is not required. + File *File `json:"-"` } // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which diff --git a/restapi.go b/restapi.go index 797cc61..0cc4f1c 100644 --- a/restapi.go +++ b/restapi.go @@ -23,6 +23,7 @@ import ( "log" "mime/multipart" "net/http" + "net/textproto" "net/url" "strconv" "strings" @@ -1316,6 +1317,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message }) } +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + // ChannelMessageSendComplex sends a message to the given channel. // channelID : The ID of a Channel. // data : The message struct to send. @@ -1326,48 +1329,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) endpoint := EndpointChannelMessages(channelID) - var response []byte + // 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 + } + } + + var response []byte + if len(files) > 0 { body := &bytes.Buffer{} bodywriter := multipart.NewWriter(body) - // What's a better way of doing this? Reflect? Generator? I'm open to suggestions - - if data.Content != "" { - if err = bodywriter.WriteField("content", data.Content); err != nil { - return - } - } - - if data.Embed != nil { - var embed []byte - embed, err = json.Marshal(data.Embed) - if err != nil { - return - } - err = bodywriter.WriteField("embed", string(embed)) - if err != nil { - return - } - } - - if data.Tts { - if err = bodywriter.WriteField("tts", "true"); err != nil { - return - } - } - - var writer io.Writer - writer, err = bodywriter.CreateFormFile("file", data.File.Name) + var payload []byte + payload, err = json.Marshal(data) if err != nil { return } - _, err = io.Copy(writer, data.File.Reader) + 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 From 0983790428deecb066c4ee44e757dcb42bb55c47 Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Sat, 10 Jun 2017 15:39:27 -0500 Subject: [PATCH 22/32] Fix goroutine leak in opusReceiver (#393) --- voice.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/voice.go b/voice.go index 8b566f4..5bbd0ad 100644 --- a/voice.go +++ b/voice.go @@ -814,7 +814,11 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) if c != nil { - c <- &p + select { + case c <- &p: + case <-close: + return + } } } } From 5a02430c027c455827b4b376bb29d2c7e11abc82 Mon Sep 17 00:00:00 2001 From: legolord208 Date: Tue, 13 Jun 2017 17:49:41 +0200 Subject: [PATCH 23/32] ContentWithMoreMentionsReplaced for roles and nicks (Fixes #208) (#375) * ContentWithMentionReplaced on roles and nicks (Fixes #208) * Compatibility * Like this? :thinking: * More efficient regexp * Tweaked a little * Tweaked a little more * Tweaked even more * Removed unessecary crap condition that is useless * Disallow voice channel * Moved regexp declaration --- message.go | 68 +++++++++++++++++++++++++++++++++++++++++++------ message_test.go | 41 +++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 message_test.go diff --git a/message.go b/message.go index 34303b7..d46f3f3 100644 --- a/message.go +++ b/message.go @@ -10,9 +10,9 @@ package discordgo import ( - "fmt" "io" "regexp" + "strings" ) // A Message stores all data related to a specific Discord message. @@ -172,13 +172,65 @@ type MessageReactions struct { // ContentWithMentionsReplaced will replace all @ mentions with the // username of the mention. -func (m *Message) ContentWithMentionsReplaced() string { - if m.Mentions == nil { - return m.Content - } - content := m.Content +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + for _, user := range m.Mentions { - content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username) + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+user.Username, + ).Replace(content) } - return 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 { + 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) + } + + content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { + channel, err := s.State.Channel(mention[2 : len(mention)-1]) + if err != nil || channel.Type == "voice" { + return mention + } + + return "#" + channel.Name + }) + return } diff --git a/message_test.go b/message_test.go new file mode 100644 index 0000000..fd2f133 --- /dev/null +++ b/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) + } +} From bb4b96e26d2111b37d79b1842111751b04eb56a2 Mon Sep 17 00:00:00 2001 From: Erik McClure Date: Tue, 27 Jun 2017 20:52:59 -0700 Subject: [PATCH 24/32] Add heartbeat ACK response and error handling (#396) * Add heartbeat ACK response and error handling - Error when sending a heartbeat now triggers a reconnection - Op7 now triggers a reconnection - Session now tracks the last heartbeat ACK that was recieved. If the last ACK is more than FailedHeartbeatAcks*heartbeatinterval in the past, this is treated as a dead connection and a reconnection is forced. * Address @iopred comments --- discord.go | 1 + structs.go | 3 +++ wsapi.go | 37 +++++++++++++++++++++++++++++-------- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/discord.go b/discord.go index 67ca944..04d4719 100644 --- a/discord.go +++ b/discord.go @@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) { 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. diff --git a/structs.go b/structs.go index 32f435c..ad081be 100644 --- a/structs.go +++ b/structs.go @@ -78,6 +78,9 @@ type Session struct { // 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 diff --git a/wsapi.go b/wsapi.go index 0912850..3206060 100644 --- a/wsapi.go +++ b/wsapi.go @@ -131,6 +131,7 @@ func (s *Session) Open() (err error) { // lock. s.listening = make(chan interface{}) go s.listen(s.wsConn, s.listening) + s.LastHeartbeatAck = time.Now().UTC() s.Unlock() @@ -199,10 +200,13 @@ type helloOp struct { Trace []string `json:"_trace"` } +// 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") @@ -211,20 +215,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} } var err error - ticker := time.NewTicker(i * time.Millisecond) + ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) defer ticker.Stop() for { + 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 { - s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) - s.Lock() - s.DataReady = false - s.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() @@ -398,7 +408,10 @@ func (s *Session) onEvent(messageType int, message []byte) { // Reconnect // Must immediately disconnect from gateway and reconnect to new gateway. if e.Operation == 7 { - // TODO + s.log(LogInformational, "Closing and reconnecting in response to Op7") + s.Close() + s.reconnect() + return } // Invalid Session @@ -426,6 +439,14 @@ func (s *Session) onEvent(messageType int, message []byte) { 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. From ef520cb26d2f2789e0f4717dd142b76dfc5f7734 Mon Sep 17 00:00:00 2001 From: AAAAAAAAAAA Date: Mon, 3 Jul 2017 02:42:05 +0200 Subject: [PATCH 25/32] Add GuildMemberDeleteWithReason (#399) --- restapi.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 0cc4f1c..fe10222 100644 --- a/restapi.go +++ b/restapi.go @@ -764,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.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) + return s.GuildMemberDeleteWithReason(guildID, userID, "") +} + +// GuildMemberDelete 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 } From fc8d981e5e31f6f368741e8e1897b344e59c376e Mon Sep 17 00:00:00 2001 From: Bruce Date: Tue, 4 Jul 2017 17:25:42 +0000 Subject: [PATCH 26/32] Removed link to old unofficial docs fixes #387 --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ac9dbcd..eb9f14f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ 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. From 389ba8760d4ddb4be175c0b8826a520957853921 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sat, 8 Jul 2017 15:33:50 +0200 Subject: [PATCH 27/32] Add bot account to testing --- discord_test.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/discord_test.go b/discord_test.go index afac0bc..ca4472a 100644 --- a/discord_test.go +++ b/discord_test.go @@ -11,9 +11,11 @@ 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 @@ -23,6 +25,12 @@ var ( ) func init() { + if envBotToken != "" { + if d, err := New(envBotToken); err == nil { + dgBot = d + } + } + if envEmail == "" || envPassword == "" || envToken == "" { return } From 9d3cd03314c73d4659c30022cce40600f5c2fb5f Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sun, 4 Jun 2017 23:00:01 +0200 Subject: [PATCH 28/32] Add GatewayBot --- endpoints.go | 15 ++++++++------- restapi.go | 22 ++++++++++++++++++++++ restapi_test.go | 11 +++++++++++ structs.go | 6 ++++++ 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/endpoints.go b/endpoints.go index 6c33ef0..0ecdf0b 100644 --- a/endpoints.go +++ b/endpoints.go @@ -18,13 +18,14 @@ var ( EndpointSmActive = EndpointSm + "active.json" EndpointSmUpcoming = EndpointSm + "upcoming.json" - EndpointDiscord = "https://discordapp.com/" - EndpointAPI = EndpointDiscord + "api/" - EndpointGuilds = EndpointAPI + "guilds/" - EndpointChannels = EndpointAPI + "channels/" - EndpointUsers = EndpointAPI + "users/" - EndpointGateway = EndpointAPI + "gateway" - EndpointWebhooks = EndpointAPI + "webhooks/" + EndpointDiscord = "https://discordapp.com/" + EndpointAPI = EndpointDiscord + "api/" + 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/" diff --git a/restapi.go b/restapi.go index fe10222..46844d3 100644 --- a/restapi.go +++ b/restapi.go @@ -1716,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) { return } +// Gateway returns the websocket Gateway address and the reccomended 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. diff --git a/restapi_test.go b/restapi_test.go index a5d326b..cf64612 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -166,6 +166,17 @@ func TestGateway(t *testing.T) { } } +func TestGatewayBot(t *testing.T) { + + if dg == nil { + t.Skip("Skipping, dg not set.") + } + _, err := dg.GatewayBot() + if err != nil { + t.Errorf("GatewayBot() returned error: %+v", err) + } +} + func TestVoiceICE(t *testing.T) { if dg == nil { diff --git a/structs.go b/structs.go index ad081be..9697fa5 100644 --- a/structs.go +++ b/structs.go @@ -512,6 +512,12 @@ type MessageReaction struct { 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) From 83e18aad7d58a055074a29e64f0f5619bc2a8371 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 9 Jun 2017 23:54:09 +0200 Subject: [PATCH 29/32] Fix comment for GatewayBot --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 46844d3..bd944f2 100644 --- a/restapi.go +++ b/restapi.go @@ -1716,7 +1716,7 @@ func (s *Session) Gateway() (gateway string, err error) { return } -// Gateway returns the websocket Gateway address and the reccomended number of shards +// 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) From 31075bc1489843202ebd039a5e041c5b014b00ed Mon Sep 17 00:00:00 2001 From: jonas747 Date: Mon, 10 Jul 2017 18:36:19 +0200 Subject: [PATCH 30/32] Update the GatewayBot test to use the bot session --- restapi_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/restapi_test.go b/restapi_test.go index cf64612..7aa4e60 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -168,10 +168,10 @@ func TestGateway(t *testing.T) { func TestGatewayBot(t *testing.T) { - if dg == nil { - t.Skip("Skipping, dg not set.") + if dgBot == nil { + t.Skip("Skipping, dgBot not set.") } - _, err := dg.GatewayBot() + _, err := dgBot.GatewayBot() if err != nil { t.Errorf("GatewayBot() returned error: %+v", err) } From 71ede90b568d31590b9aca0b56db3938b55ab791 Mon Sep 17 00:00:00 2001 From: Erik McClure Date: Tue, 18 Jul 2017 18:21:45 -0700 Subject: [PATCH 31/32] Fix #406: reconnect() can be called while still connected (#407) --- wsapi.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wsapi.go b/wsapi.go index 3206060..213ea72 100644 --- a/wsapi.go +++ b/wsapi.go @@ -709,6 +709,13 @@ func (s *Session) reconnect() { 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) From 7bb0965a6ff435b2587c2a4c50e3a0fedbf9325b Mon Sep 17 00:00:00 2001 From: legolord208 Date: Sat, 22 Jul 2017 15:17:39 +0200 Subject: [PATCH 32/32] Updated to v6 (fixes #408) (#410) * Updated to v6 * Unified websocket and REST version --- endpoints.go | 5 ++++- message.go | 16 +++++++++++++++- state.go | 4 ++-- structs.go | 16 +++++++++++++--- wsapi.go | 3 +-- 5 files changed, 35 insertions(+), 9 deletions(-) diff --git a/endpoints.go b/endpoints.go index 0ecdf0b..06e3e9e 100644 --- a/endpoints.go +++ b/endpoints.go @@ -11,6 +11,9 @@ package discordgo +// The Discord API version used for the REST and Websocket API. +var ApiVersion = "6" + // Known Discord API Endpoints. var ( EndpointStatus = "https://status.discordapp.com/api/v2/" @@ -19,7 +22,7 @@ var ( EndpointSmUpcoming = EndpointSm + "upcoming.json" EndpointDiscord = "https://discordapp.com/" - EndpointAPI = EndpointDiscord + "api/" + EndpointAPI = EndpointDiscord + "api/v" + ApiVersion + "/" EndpointGuilds = EndpointAPI + "guilds/" EndpointChannels = EndpointAPI + "channels/" EndpointUsers = EndpointAPI + "users/" diff --git a/message.go b/message.go index d46f3f3..5232e83 100644 --- a/message.go +++ b/message.go @@ -15,6 +15,19 @@ import ( "strings" ) +type MessageType int + +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"` @@ -30,6 +43,7 @@ type Message struct { 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. @@ -226,7 +240,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { channel, err := s.State.Channel(mention[2 : len(mention)-1]) - if err != nil || channel.Type == "voice" { + if err != nil || channel.Type == ChannelTypeGuildVoice { return mention } diff --git a/state.go b/state.go index 7400ef6..4ebfb1e 100644 --- a/state.go +++ b/state.go @@ -427,7 +427,7 @@ func (s *State) ChannelAdd(channel *Channel) error { return nil } - if channel.IsPrivate { + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.PrivateChannels = append(s.PrivateChannels, channel) } else { guild, ok := s.guildMap[channel.GuildID] @@ -454,7 +454,7 @@ func (s *State) ChannelRemove(channel *Channel) error { return err } - if channel.IsPrivate { + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { s.Lock() defer s.Unlock() diff --git a/structs.go b/structs.go index 9697fa5..d9f9699 100644 --- a/structs.go +++ b/structs.go @@ -144,18 +144,27 @@ type Invite struct { Temporary bool `json:"temporary"` } +type ChannelType int + +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 string `json:"type"` + Type ChannelType `json:"type"` LastMessageID string `json:"last_message_id"` Position int `json:"position"` Bitrate int `json:"bitrate"` - IsPrivate bool `json:"is_private"` - Recipient *User `json:"recipient"` + Recipients []*User `json:"recipient"` Messages []*Message `json:"-"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` } @@ -295,6 +304,7 @@ type Presence struct { 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 diff --git a/wsapi.go b/wsapi.go index 213ea72..d050ffd 100644 --- a/wsapi.go +++ b/wsapi.go @@ -15,7 +15,6 @@ import ( "compress/zlib" "encoding/json" "errors" - "fmt" "io" "net/http" "runtime" @@ -87,7 +86,7 @@ func (s *Session) Open() (err error) { } // Add the version and encoding to the URL - s.gateway = fmt.Sprintf("%s?v=5&encoding=json", s.gateway) + s.gateway = s.gateway + "?v=" + ApiVersion + "&encoding=json" } header := http.Header{}