From 092ed4b585669191c7919e4bb5244fc812268b7a Mon Sep 17 00:00:00 2001 From: jonas747 Date: Mon, 11 Jul 2016 22:30:11 +0200 Subject: [PATCH 01/70] Retry on 502's --- restapi.go | 26 +++++++++++++++++++------- structs.go | 3 +++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/restapi.go b/restapi.go index 85a4858..c5a4462 100644 --- a/restapi.go +++ b/restapi.go @@ -45,11 +45,13 @@ 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, 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, sequence int) (response []byte, err error) { // rate limit mutex for this url // TODO: review for performance improvements @@ -135,6 +137,16 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons // 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, sequence+1) + } else { + err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) + } + case 429: // TOO MANY REQUESTS - Rate limiting mu.Lock() // lock URL ratelimit mutex @@ -151,10 +163,10 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons time.Sleep(rl.RetryAfter) // we can make the above smarter - // this method can cause longer delays then required + // this method can cause longer delays than required mu.Unlock() // we have to unlock here - response, err = s.request(method, urlStr, contentType, b) + response, err = s.request(method, urlStr, contentType, b, sequence) default: // Error condition err = fmt.Errorf("HTTP %s, %s", resp.Status, response) @@ -708,7 +720,7 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err // guildID : The ID of a Guild. func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { - body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil) + body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, 0) if err != nil { return } @@ -1084,7 +1096,7 @@ func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err // messageID : the ID of a Message func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { - _, err = s.request("POST", EndpointChannelMessageAck(channelID, messageID), "", nil) + _, err = s.request("POST", EndpointChannelMessageAck(channelID, messageID), "", nil, 0) return } @@ -1236,7 +1248,7 @@ func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Mess return } - response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes()) + response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes(), 0) if err != nil { return } diff --git a/structs.go b/structs.go index 19a291f..168895a 100644 --- a/structs.go +++ b/structs.go @@ -53,6 +53,9 @@ type Session struct { // Whether the Data Websocket is ready 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 From a504a0adb932732e02df0dc533f80e6d515215ce Mon Sep 17 00:00:00 2001 From: jonas747 Date: Tue, 19 Jul 2016 14:18:49 +0200 Subject: [PATCH 02/70] Fix ratelimit mutex with url parameters --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 337a7f8..2dc5834 100644 --- a/restapi.go +++ b/restapi.go @@ -68,7 +68,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte) (respons mu, _ = s.rateLimit.url[bu[0]] if mu == nil { mu = new(sync.Mutex) - s.rateLimit.url[urlStr] = mu + s.rateLimit.url[bu[0]] = mu } s.rateLimit.Unlock() From 43778dc54b1b96582a5cfd17389f1a0ad1ba755e Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 19 Jul 2016 07:44:38 -0500 Subject: [PATCH 03/70] Bump version to 0.14.0-dev --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index d1cfddf..1404a83 100644 --- a/discord.go +++ b/discord.go @@ -19,7 +19,7 @@ import ( ) // VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) -const VERSION = "0.13.0" +const VERSION = "0.14.0-dev" // 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 d28aed7326d27b4dba867c1cfbd2d43433cfd847 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Tue, 19 Jul 2016 07:45:00 -0500 Subject: [PATCH 04/70] Move deprecation comment so lint test passes --- restapi.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index 337a7f8..a738227 100644 --- a/restapi.go +++ b/restapi.go @@ -376,11 +376,12 @@ func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSetti return } -// 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. +// +// 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) { channel, err := s.State.Channel(channelID) if err != nil || channel == nil { From f4b8e2ecc266ac7b277455a7380a90bb09458b6e Mon Sep 17 00:00:00 2001 From: daniel portella Date: Tue, 26 Jul 2016 22:18:20 +0100 Subject: [PATCH 05/70] added ChannelSendFileWithMessage kept it backwards compatible so previous apps dont crash with missing field. --- restapi.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/restapi.go b/restapi.go index 278b3f7..bf5065b 100644 --- a/restapi.go +++ b/restapi.go @@ -1218,12 +1218,28 @@ func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err er // 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) (st *Message, err error) { + return s.ChannelFileSendWithMessage(channelID, "", name, r) +} + +// ChannelFileSendWithMessage sends a file to the given channel with an message. +// 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) (st *Message, err error) { body := &bytes.Buffer{} bodywriter := multipart.NewWriter(body) + if len(content) != 0 { + if err := bodywriter.WriteField("content", content); err != nil { + return nil, err + } + } + writer, err := bodywriter.CreateFormFile("file", name) if err != nil { return nil, err From 0c11cae782ce3dc83eb1628b545bb83063120858 Mon Sep 17 00:00:00 2001 From: uppfinnarn Date: Thu, 28 Jul 2016 21:14:59 +0200 Subject: [PATCH 06/70] Timestamp type; a string with a Parse() function Closes #204 --- message.go | 4 ++-- structs.go | 24 ++++++++++++------------ types.go | 20 ++++++++++++++++++++ types_test.go | 24 ++++++++++++++++++++++++ 4 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 types.go create mode 100644 types_test.go diff --git a/message.go b/message.go index 8966c16..9e33dd2 100644 --- a/message.go +++ b/message.go @@ -19,8 +19,8 @@ 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"` + Timestamp Timestamp `json:"timestamp"` + EditedTimestamp Timestamp `json:"edited_timestamp"` MentionRoles []string `json:"mention_roles"` Tts bool `json:"tts"` MentionEveryone bool `json:"mention_everyone"` diff --git a/structs.go b/structs.go index 19a291f..2dc7336 100644 --- a/structs.go +++ b/structs.go @@ -137,17 +137,17 @@ 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 string `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"` } // A Channel holds all data related to an individual Discord channel. @@ -204,7 +204,7 @@ type Guild struct { 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 + JoinedAt Timestamp `json:"joined_at"` Splash string `json:"splash"` AfkTimeout int `json:"afk_timeout"` VerificationLevel VerificationLevel `json:"verification_level"` diff --git a/types.go b/types.go new file mode 100644 index 0000000..b9541bb --- /dev/null +++ b/types.go @@ -0,0 +1,20 @@ +// 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 ( + "time" +) + +type Timestamp string + +func (t Timestamp) Parse() (time.Time, error) { + return time.Parse("2006-01-02T15:04:05.000000-07:00", string(t)) +} diff --git a/types_test.go b/types_test.go new file mode 100644 index 0000000..1d03aa3 --- /dev/null +++ b/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") + } +} From a20bdc7fd8fc9e94f0dd1059a8ba217438710872 Mon Sep 17 00:00:00 2001 From: uppfinnarn Date: Thu, 28 Jul 2016 22:36:59 +0200 Subject: [PATCH 07/70] Comments --- types.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/types.go b/types.go index b9541bb..c7c6dd3 100644 --- a/types.go +++ b/types.go @@ -13,8 +13,11 @@ import ( "time" ) +// A timestamp, in the format YYYY-MM-DDTHH:MM:SS.MSMSMS-TZ:TZ. type Timestamp string +// Parse parses a timestamp string into a time.Time object. +// The only time this can fail is if you're parsing an invalid timestamp. func (t Timestamp) Parse() (time.Time, error) { return time.Parse("2006-01-02T15:04:05.000000-07:00", string(t)) } From a223abd357aeeb0e76beb3a3a1cc25adc55a6efa Mon Sep 17 00:00:00 2001 From: uppfinnarn Date: Thu, 28 Jul 2016 22:49:51 +0200 Subject: [PATCH 08/70] Changed the comments for go vet --- types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types.go b/types.go index c7c6dd3..6b140e3 100644 --- a/types.go +++ b/types.go @@ -13,11 +13,11 @@ import ( "time" ) -// A timestamp, in the format YYYY-MM-DDTHH:MM:SS.MSMSMS-TZ:TZ. +// 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 you're parsing an invalid timestamp. +// The only time this can fail is if Discord changes their timestamp format. func (t Timestamp) Parse() (time.Time, error) { return time.Parse("2006-01-02T15:04:05.000000-07:00", string(t)) } From 3fd53413deb184c9636bd70bcd3dbe34c291f95d Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 29 Jul 2016 23:13:55 +0200 Subject: [PATCH 09/70] Added userguild struct, and UserGuilds() now returns a userguild --- restapi.go | 2 +- structs.go | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index bf5065b..b2faac1 100644 --- a/restapi.go +++ b/restapi.go @@ -351,7 +351,7 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) } // UserGuilds returns an array of Guild structures for all guilds. -func (s *Session) UserGuilds() (st []*Guild, err error) { +func (s *Session) UserGuilds() (st []*UserGuild, err error) { body, err := s.Request("GET", EndpointUserGuilds("@me"), nil) if err != nil { diff --git a/structs.go b/structs.go index 19a291f..c9d54db 100644 --- a/structs.go +++ b/structs.go @@ -220,6 +220,15 @@ type Guild struct { 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"` From 2a1f0ff893f61c8e717cfe0b1dd7d2910b440b54 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 29 Jul 2016 23:40:56 +0200 Subject: [PATCH 10/70] Added the CHANNEL_PINS_UPDATE event --- events.go | 1 + structs.go | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/events.go b/events.go index 72aabf6..b1a9b09 100644 --- a/events.go +++ b/events.go @@ -16,6 +16,7 @@ var eventToInterface = map[string]interface{}{ "CHANNEL_CREATE": ChannelCreate{}, "CHANNEL_UPDATE": ChannelUpdate{}, "CHANNEL_DELETE": ChannelDelete{}, + "CHANNEL_PINS_UPDATE": ChannelPinsUpdate{}, "GUILD_CREATE": GuildCreate{}, "GUILD_UPDATE": GuildUpdate{}, "GUILD_DELETE": GuildDelete{}, diff --git a/structs.go b/structs.go index 19a291f..b782432 100644 --- a/structs.go +++ b/structs.go @@ -464,6 +464,12 @@ type UserGuildSettingsEdit struct { ChannelOverrides map[string]*UserGuildSettingsChannelOverride `json:"channel_overrides"` } +// ChannelPinsUpdate stores data for the channel pins update event +type ChannelPinsUpdate struct { + LastPinTimestamp string `json:"last_pin_timestamp"` + ChannelID string `json:"channel_id"` +} + // Constants for the different bit offsets of text channel permissions const ( PermissionReadMessages = 1 << (iota + 10) From 9344321923bb546cd30dd12edf3ba418f5e34acf Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sun, 31 Jul 2016 17:20:12 +0200 Subject: [PATCH 11/70] Update GuildMembers to new format, #218 --- restapi.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/restapi.go b/restapi.go index bf5065b..5df9c32 100644 --- a/restapi.go +++ b/restapi.go @@ -603,16 +603,16 @@ func (s *Session) GuildBanDelete(guildID, userID string) (err error) { // GuildMembers returns a list of members for a guild. // guildID : The ID of a Guild. -// offset : A number of members to skip +// 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, offset, limit int) (st []*Member, err error) { +func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*Member, err error) { uri := EndpointGuildMembers(guildID) v := url.Values{} - if offset > 0 { - v.Set("offset", strconv.Itoa(offset)) + if after != "" { + v.Set("after", after) } if limit > 0 { From b961ee3b6f8753ca14e7f57ac6756d0331db263e Mon Sep 17 00:00:00 2001 From: robbix1206 Date: Wed, 31 Aug 2016 15:59:25 +0200 Subject: [PATCH 12/70] #231 Support getting Bot account / owner --- oauth2.go | 1 + 1 file changed, 1 insertion(+) diff --git a/oauth2.go b/oauth2.go index de2848d..11058e1 100644 --- a/oauth2.go +++ b/oauth2.go @@ -21,6 +21,7 @@ type Application struct { Icon string `json:"icon,omitempty"` Secret string `json:"secret,omitempty"` RedirectURIs *[]string `json:"redirect_uris,omitempty"` + Owner *User `json:"owner"` } // Application returns an Application structure of a specific Application From 95fdc0a97182ee8f2466401d88346310f3ff3ed9 Mon Sep 17 00:00:00 2001 From: robbix1206 Date: Wed, 31 Aug 2016 16:40:45 +0200 Subject: [PATCH 13/70] Close the file after read Close the file when it has been full readed --- examples/airhorn/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/airhorn/main.go b/examples/airhorn/main.go index cc61301..160ca85 100644 --- a/examples/airhorn/main.go +++ b/examples/airhorn/main.go @@ -131,6 +131,10 @@ func loadSound() error { // If this is the end of the file, just return. if err == io.EOF || err == io.ErrUnexpectedEOF { + file.Close() + if err != nil { + return err + } return nil } From a2e6e1da453ec48e74c3dbf8bbb7e25694210654 Mon Sep 17 00:00:00 2001 From: robbix1206 Date: Sat, 3 Sep 2016 23:38:34 +0200 Subject: [PATCH 14/70] Fix the pattern The Pattern was not the correct one fixing it by the pattern of RFC3339 --- types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types.go b/types.go index 6b140e3..24f52c4 100644 --- a/types.go +++ b/types.go @@ -19,5 +19,5 @@ 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("2006-01-02T15:04:05.000000-07:00", string(t)) + return time.Parse(time.RFC3339, string(t)) } From 678756c9a93bf3ff231ac527623ec540d8ce4a4d Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 8 Sep 2016 19:39:29 -0700 Subject: [PATCH 15/70] Simplify Guild.Unavailable and correctly merge GUILD_UPDATE events. --- state.go | 36 +++++++++++++++++++++++++++--------- structs.go | 2 +- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/state.go b/state.go index e9eb4d6..45202ac 100644 --- a/state.go +++ b/state.go @@ -98,17 +98,35 @@ func (s *State) GuildAdd(guild *Guild) error { s.channelMap[c.ID] = c } - // If the guild exists, replace it. if g, ok := s.guildMap[guild.ID]; ok { - // If this guild already exists with data, don't stomp on props. - if g.Unavailable != nil && !*g.Unavailable { - guild.Members = g.Members - guild.Presences = g.Presences - guild.Channels = g.Channels - guild.VoiceStates = g.VoiceStates + // The guild already exists in state. + if guild.Unavailable { + // If the new guild is unavailable, just update the `unavailable` property on the + // current guild. + g.Unavailable = guild.Unavailable + } else { + // 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 + } + *g = *guild } - - *g = *guild return nil } diff --git a/structs.go b/structs.go index b782432..270427a 100644 --- a/structs.go +++ b/structs.go @@ -217,7 +217,7 @@ type Guild struct { Presences []*Presence `json:"presences"` Channels []*Channel `json:"channels"` VoiceStates []*VoiceState `json:"voice_states"` - Unavailable *bool `json:"unavailable"` + Unavailable bool `json:"unavailable"` } // A GuildParams stores all the data needed to update discord guild settings From 42cef0c0d0ea2156e2ff455c2dab8a408380ea5e Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 8 Sep 2016 20:06:24 -0700 Subject: [PATCH 16/70] Remove unavailable goop, b1nzy says it'd never happen. --- state.go | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/state.go b/state.go index 45202ac..89dfdd4 100644 --- a/state.go +++ b/state.go @@ -99,34 +99,27 @@ func (s *State) GuildAdd(guild *Guild) error { } if g, ok := s.guildMap[guild.ID]; ok { - // The guild already exists in state. - if guild.Unavailable { - // If the new guild is unavailable, just update the `unavailable` property on the - // current guild. - g.Unavailable = guild.Unavailable - } else { - // 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 - } - *g = *guild + // 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 + } + *g = *guild return nil } From d28edb024e66c547b4b0e51ea474fe0add83f254 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 8 Sep 2016 20:07:43 -0700 Subject: [PATCH 17/70] Fix airhorn example. --- examples/airhorn/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/airhorn/main.go b/examples/airhorn/main.go index cc61301..2fbaea0 100644 --- a/examples/airhorn/main.go +++ b/examples/airhorn/main.go @@ -102,7 +102,7 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { // 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 != nil { + if event.Guild.Unavailable { return } From ef3cd76e3dc4583b06f5f2b8e896643c44280c51 Mon Sep 17 00:00:00 2001 From: soft as HELL Date: Sun, 11 Sep 2016 14:32:41 +0300 Subject: [PATCH 18/70] Add mentionable to Role struct --- structs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/structs.go b/structs.go index b782432..7fcf44b 100644 --- a/structs.go +++ b/structs.go @@ -232,6 +232,7 @@ 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"` From 1ab71f4b12c933980fc1287ac292ef04922e1a5e Mon Sep 17 00:00:00 2001 From: soft as HELL Date: Sun, 11 Sep 2016 14:32:58 +0300 Subject: [PATCH 19/70] Update GuildRoleEdit --- restapi.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/restapi.go b/restapi.go index 5df9c32..1f61cbb 100644 --- a/restapi.go +++ b/restapi.go @@ -797,7 +797,8 @@ 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 { @@ -805,11 +806,12 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b } 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", EndpointGuildRole(guildID, roleID), data) if err != nil { From 003454345bcc4e6289ca197042eb9b96d59a53c6 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 15 Sep 2016 18:51:09 -0700 Subject: [PATCH 20/70] Use the everyone role to initialise the permission flags. --- state.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/state.go b/state.go index e9eb4d6..fa77b20 100644 --- a/state.go +++ b/state.go @@ -706,6 +706,13 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i 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 { From cd6971839a35e6531bd0358b1d28a39e0bf5f4f7 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 15 Sep 2016 18:51:57 -0700 Subject: [PATCH 21/70] Use the everyone role to initialise the permission flags. --- restapi.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/restapi.go b/restapi.go index 1f61cbb..d4c612c 100644 --- a/restapi.go +++ b/restapi.go @@ -412,6 +412,13 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } + 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 { From 374c41e75335c153e59399321203637b4fe9cf3d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Tue, 20 Sep 2016 15:33:08 +0200 Subject: [PATCH 22/70] fix typo --- voice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice.go b/voice.go index 094aa59..1de8987 100644 --- a/voice.go +++ b/voice.go @@ -570,7 +570,7 @@ func (v *VoiceConnection) udpOpen() (err error) { 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++ { From f01c5a334443645c0e8e7831ba3ed5845cc53817 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sun, 25 Sep 2016 19:47:18 +0200 Subject: [PATCH 23/70] Apply more fields to message in message update In case update was handled before create --- state.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/state.go b/state.go index fa77b20..a438ba7 100644 --- a/state.go +++ b/state.go @@ -511,6 +511,12 @@ func (s *State) MessageAdd(message *Message) error { 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 } From 9c7c9d3fd2650aa0ff95fb11dc1816e2b551c69a Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sun, 25 Sep 2016 21:29:59 +0200 Subject: [PATCH 24/70] Added RequestGuildMembers to request guild members from the gateway --- events.go | 1 + structs.go | 6 ++++++ wsapi.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/events.go b/events.go index b1a9b09..49f12ad 100644 --- a/events.go +++ b/events.go @@ -30,6 +30,7 @@ var eventToInterface = map[string]interface{}{ "GUILD_ROLE_DELETE": GuildRoleDelete{}, "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{}, "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{}, + "GUILD_MEMBERS_CHUNK": GuildMembersChunk{}, "MESSAGE_ACK": MessageAck{}, "MESSAGE_CREATE": MessageCreate{}, "MESSAGE_UPDATE": MessageUpdate{}, diff --git a/structs.go b/structs.go index 7fcf44b..dcf6fa0 100644 --- a/structs.go +++ b/structs.go @@ -412,6 +412,12 @@ type GuildEmojisUpdate struct { Emojis []*Emoji `json:"emojis"` } +// A GuildMembersChunk stores data for the Guild Members Chunk websocket event. +type GuildMembersChunk struct { + GuildID string `json:"guild_id"` + Members []*Member `json:"members"` +} + // A GuildIntegration stores data for a guild integration. type GuildIntegration struct { ID string `json:"id"` diff --git a/wsapi.go b/wsapi.go index a19c384..3bf2c93 100644 --- a/wsapi.go +++ b/wsapi.go @@ -269,6 +269,44 @@ 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 hat username sarts with, leave empty to return all members +// limit : max number of items to return, or 0 o reques 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 errors.New("no websocket connection exists") + } + + 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. // From ebb910f3ace036f10957af7d5cf5fecdb35a3ec7 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Tue, 27 Sep 2016 18:25:40 +0200 Subject: [PATCH 25/70] Set default MaxRestRetries to 3 --- discord.go | 1 + 1 file changed, 1 insertion(+) diff --git a/discord.go b/discord.go index 29d9bbd..0d809e4 100644 --- a/discord.go +++ b/discord.go @@ -42,6 +42,7 @@ func New(args ...interface{}) (s *Session, err error) { ShouldReconnectOnError: true, ShardID: 0, ShardCount: 1, + MaxRestRetries: 3, } // If no arguments are passed return the empty Session interface. From 1edd3b6484c4ed71326d104916c62fd0b4e402cf Mon Sep 17 00:00:00 2001 From: andrei Date: Tue, 27 Sep 2016 17:39:14 -0700 Subject: [PATCH 26/70] Remove API call in onVoiceStateUpdate --- wsapi.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/wsapi.go b/wsapi.go index a19c384..3a70c83 100644 --- a/wsapi.go +++ b/wsapi.go @@ -17,7 +17,6 @@ import ( "errors" "fmt" "io" - "log" "net/http" "reflect" "runtime" @@ -474,22 +473,13 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { 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 { - log.Println(err) - return - } - // We only care about events that are about us - if st.UserID != self.ID { + if se.State.Ready.User.ID != st.UserID { return } // Store the SessionID for later use. - voice.UserID = self.ID // TODO: Review + voice.UserID = st.UserID // TODO: Review voice.sessionID = st.SessionID } From 1ecb7458e94c47d177888df0a2769fc2bce1e8f1 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Wed, 28 Sep 2016 06:31:39 +0200 Subject: [PATCH 27/70] Fix typos --- wsapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsapi.go b/wsapi.go index 3bf2c93..835c293 100644 --- a/wsapi.go +++ b/wsapi.go @@ -282,9 +282,9 @@ type requestGuildMembersOp struct { // 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 hat username sarts with, leave empty to return all members -// limit : max number of items to return, or 0 o reques all members matched +// guildID : The ID of the guild to request members of +// query : String that username sarts 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") From c9d0ac84637661dbf2b21bec69a5f611d77f93fe Mon Sep 17 00:00:00 2001 From: jonas747 Date: Wed, 28 Sep 2016 06:43:37 +0200 Subject: [PATCH 28/70] Update comment to UserGuild --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index b2faac1..3dbc55b 100644 --- a/restapi.go +++ b/restapi.go @@ -350,7 +350,7 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) return } -// UserGuilds returns an array of Guild structures for all guilds. +// UserGuilds returns an array of UserGuild structures for all guilds. func (s *Session) UserGuilds() (st []*UserGuild, err error) { body, err := s.Request("GET", EndpointUserGuilds("@me"), nil) From 8164119cac63d0c0a0134ddb2c5eb974a692bceb Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 21:44:16 -0700 Subject: [PATCH 29/70] Support a very light state in all cases to support b1nzy's upcoming PR --- discord.go | 1 + state.go | 85 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 29 deletions(-) diff --git a/discord.go b/discord.go index 776f143..77bde70 100644 --- a/discord.go +++ b/discord.go @@ -237,6 +237,7 @@ func (s *Session) initialize() { s.AddHandler(s.onResumed) s.AddHandler(s.onVoiceServerUpdate) s.AddHandler(s.onVoiceStateUpdate) + s.AddHandler(s.State.onReady) s.AddHandler(s.State.onInterface) } diff --git a/state.go b/state.go index d2d90f3..798c18c 100644 --- a/state.go +++ b/state.go @@ -55,33 +55,6 @@ func NewState() *State { } } -// OnReady takes a Ready event and updates all internal state. -func (s *State) OnReady(r *Ready) error { - if s == nil { - return ErrNilState - } - - s.Lock() - defer s.Unlock() - - s.Ready = *r - - for _, g := range s.Guilds { - s.guildMap[g.ID] = g - - for _, c := range g.Channels { - c.GuildID = g.ID - s.channelMap[c.ID] = c - } - } - - for _, c := range s.PrivateChannels { - s.channelMap[c.ID] = c - } - - return nil -} - // GuildAdd adds a guild to the current world state, or // updates it if it already exists. func (s *State) GuildAdd(guild *Guild) error { @@ -619,6 +592,59 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { return nil, errors.New("Message not found.") } +// OnReady takes a Ready event and updates all internal state. +func (s *State) onReady(se *Session, r *Ready) (err error) { + // We must track at least the current user for Voice, if state is + // either nil or disabled, we will set a very simple state item. + if s == nil || !se.StateEnabled { + ready := Ready{ + Version: r.Version, + SessionID: r.SessionID, + HeartbeatInterval: r.HeartbeatInterval, + User: r.User, + } + + if s == nil { + *s = State{ + TrackChannels: false, + TrackEmojis: false, + TrackMembers: false, + TrackRoles: false, + TrackVoice: false, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + } + } + + s.Lock() + defer s.Unlock() + + s.Ready = ready + + return nil + } + + s.Lock() + defer s.Unlock() + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + + for _, c := range g.Channels { + c.GuildID = g.ID + 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 { @@ -629,8 +655,6 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { } switch t := i.(type) { - case *Ready: - err = s.OnReady(t) case *GuildCreate: err = s.GuildAdd(t.Guild) case *GuildUpdate: @@ -702,6 +726,9 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { // 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 { From 1dcdf130fdbedeef947605cfe8be5c8f640e9558 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Wed, 28 Sep 2016 07:00:11 +0200 Subject: [PATCH 30/70] Fix Another typo --- wsapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsapi.go b/wsapi.go index 835c293..6e69d95 100644 --- a/wsapi.go +++ b/wsapi.go @@ -283,7 +283,7 @@ type requestGuildMembersOp struct { // 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 sarts with, leave empty to return all members +// 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") From 6c53613186964a2c3fdecafea9bf6102b7b76d5c Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:02:28 -0700 Subject: [PATCH 31/70] Thanks govet. --- state.go | 31 ++++++++++--------------------- wsapi.go | 11 +++++++++++ 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/state.go b/state.go index 798c18c..aa7997e 100644 --- a/state.go +++ b/state.go @@ -594,9 +594,16 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { // OnReady takes a Ready event and updates all internal state. func (s *State) onReady(se *Session, r *Ready) (err error) { - // We must track at least the current user for Voice, if state is - // either nil or disabled, we will set a very simple state item. - if s == nil || !se.StateEnabled { + 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, @@ -604,29 +611,11 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { User: r.User, } - if s == nil { - *s = State{ - TrackChannels: false, - TrackEmojis: false, - TrackMembers: false, - TrackRoles: false, - TrackVoice: false, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), - } - } - - s.Lock() - defer s.Unlock() - s.Ready = ready return nil } - s.Lock() - defer s.Unlock() - s.Ready = *r for _, g := range s.Guilds { diff --git a/wsapi.go b/wsapi.go index a19c384..4776fdd 100644 --- a/wsapi.go +++ b/wsapi.go @@ -47,6 +47,17 @@ 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.") return From 2e2e02fc1199061e83dbce28ef2e650f5ebf75e4 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:09:44 -0700 Subject: [PATCH 32/70] Support a very light state in all cases to support b1nzy's upcoming PR (#260) Support a very light state in all cases to support b1nzy's upcoming PR --- discord.go | 1 + state.go | 74 +++++++++++++++++++++++++++++++++--------------------- wsapi.go | 11 ++++++++ 3 files changed, 57 insertions(+), 29 deletions(-) diff --git a/discord.go b/discord.go index 776f143..77bde70 100644 --- a/discord.go +++ b/discord.go @@ -237,6 +237,7 @@ func (s *Session) initialize() { s.AddHandler(s.onResumed) s.AddHandler(s.onVoiceServerUpdate) s.AddHandler(s.onVoiceStateUpdate) + s.AddHandler(s.State.onReady) s.AddHandler(s.State.onInterface) } diff --git a/state.go b/state.go index d2d90f3..aa7997e 100644 --- a/state.go +++ b/state.go @@ -55,33 +55,6 @@ func NewState() *State { } } -// OnReady takes a Ready event and updates all internal state. -func (s *State) OnReady(r *Ready) error { - if s == nil { - return ErrNilState - } - - s.Lock() - defer s.Unlock() - - s.Ready = *r - - for _, g := range s.Guilds { - s.guildMap[g.ID] = g - - for _, c := range g.Channels { - c.GuildID = g.ID - s.channelMap[c.ID] = c - } - } - - for _, c := range s.PrivateChannels { - s.channelMap[c.ID] = c - } - - return nil -} - // GuildAdd adds a guild to the current world state, or // updates it if it already exists. func (s *State) GuildAdd(guild *Guild) error { @@ -619,6 +592,48 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { return nil, errors.New("Message not found.") } +// 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, + HeartbeatInterval: r.HeartbeatInterval, + User: r.User, + } + + s.Ready = ready + + return nil + } + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + + for _, c := range g.Channels { + c.GuildID = g.ID + 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 { @@ -629,8 +644,6 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { } switch t := i.(type) { - case *Ready: - err = s.OnReady(t) case *GuildCreate: err = s.GuildAdd(t.Guild) case *GuildUpdate: @@ -702,6 +715,9 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { // 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 { diff --git a/wsapi.go b/wsapi.go index 6e69d95..362509f 100644 --- a/wsapi.go +++ b/wsapi.go @@ -47,6 +47,17 @@ 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.") return From 0115c9c335929c11d6964dd1072e3c1142fdd2ae Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:36:38 -0700 Subject: [PATCH 33/70] Clean up state access. --- wsapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsapi.go b/wsapi.go index 719cd8f..54a420e 100644 --- a/wsapi.go +++ b/wsapi.go @@ -522,13 +522,13 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { return } - // We only care about events that are about us - if se.State.Ready.User.ID != st.UserID { + // We only care about events that are about us. + if se.State.User.ID != st.UserID { return } // Store the SessionID for later use. - voice.UserID = st.UserID // TODO: Review + voice.UserID = st.UserID voice.sessionID = st.SessionID } From c6ee0d2dd59d9ff0c3afaa7d909f0ec9d2d63abb Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 27 Sep 2016 22:45:12 -0700 Subject: [PATCH 34/70] Clean up state access. (#261) Clean up state access. --- wsapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsapi.go b/wsapi.go index 719cd8f..54a420e 100644 --- a/wsapi.go +++ b/wsapi.go @@ -522,13 +522,13 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { return } - // We only care about events that are about us - if se.State.Ready.User.ID != st.UserID { + // We only care about events that are about us. + if se.State.User.ID != st.UserID { return } // Store the SessionID for later use. - voice.UserID = st.UserID // TODO: Review + voice.UserID = st.UserID voice.sessionID = st.SessionID } From af3fe4842a69fef773ac284fc6d1497a58fff76e Mon Sep 17 00:00:00 2001 From: robbix1206 Date: Wed, 28 Sep 2016 19:04:37 +0200 Subject: [PATCH 35/70] Add Support of changing user status (#258) Add Support of changing user status --- restapi.go | 21 +++++++++++++++++++++ restapi_test.go | 11 +++++++++++ structs.go | 42 ++++++++++++++++++++++++++++-------------- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/restapi.go b/restapi.go index 5ecb48f..3214556 100644 --- a/restapi.go +++ b/restapi.go @@ -332,6 +332,27 @@ func (s *Session) UserSettings() (st *Settings, err error) { 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 = errors.New("You can't set your Status to offline") + return + } + + data := struct { + Status Status `json:"status"` + }{status} + + body, err := s.Request("PATCH", EndpointUserSettings("@me"), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + // UserChannels returns an array of Channel structures for all private // channels. func (s *Session) UserChannels() (st []*Channel, err error) { diff --git a/restapi_test.go b/restapi_test.go index bfe60fc..0d68d6e 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -131,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) { diff --git a/structs.go b/structs.go index dcfb447..88352d9 100644 --- a/structs.go +++ b/structs.go @@ -267,7 +267,7 @@ 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"` + Status Status `json:"status"` Game *Game `json:"game"` } @@ -304,21 +304,35 @@ type User struct { // 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"` - InlineAttachmentMedia bool `json:"inline_attachment_media"` - EnableTtsCommand bool `json:"enable_tts_command"` - MessageDisplayCompact bool `json:"message_display_compact"` - ShowCurrentGame bool `json:"show_current_game"` - AllowEmailFriendRequest bool `json:"allow_email_friend_request"` - 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"` + 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"` From 627f9a3ca683260d649305af0552f94a32aefbc0 Mon Sep 17 00:00:00 2001 From: Austin Davis Date: Tue, 4 Oct 2016 15:45:36 -0600 Subject: [PATCH 36/70] 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 9ac041f85a763f3abfa18b8220235b635f9c93e8 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 19:51:08 -0700 Subject: [PATCH 37/70] 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 c4c27915f79065894fa66778c717341edb647af1 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 20:01:07 -0700 Subject: [PATCH 38/70] 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 9b41864a68fb99721b44038b48d210b248102970 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 4 Oct 2016 20:07:32 -0700 Subject: [PATCH 39/70] 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 9f24531220f48720555bea63cdd0d00c25c3e7e6 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Thu, 13 Oct 2016 21:42:05 +0200 Subject: [PATCH 40/70] Update ChannelMessageAck because of api changes (#270) --- restapi.go | 10 ++++++++-- structs.go | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index 3214556..2e2adf2 100644 --- a/restapi.go +++ b/restapi.go @@ -1127,9 +1127,15 @@ func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err // ChannelMessageAck acknowledges and marks the given message as read // channeld : The ID of a Channel // messageID : the ID of a Message -func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { +// lastToken : token returned by last ack +func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) { - _, err = s.request("POST", EndpointChannelMessageAck(channelID, messageID), "", nil, 0) + body, err := s.Request("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}) + if err != nil { + return + } + + err = unmarshal(body, &st) return } diff --git a/structs.go b/structs.go index 88352d9..3e0c375 100644 --- a/structs.go +++ b/structs.go @@ -408,6 +408,11 @@ type MessageAck struct { ChannelID string `json:"channel_id"` } +// An Ack is used to ack messages +type Ack struct { + Token string `json:"token"` +} + // A GuildIntegrationsUpdate stores data for the guild integrations update // websocket event. type GuildIntegrationsUpdate struct { From 91879640024ad2015ccb5c3e26ce4f3a88aa93cf Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 14 Oct 2016 04:13:42 +0200 Subject: [PATCH 41/70] Added custom rest error type (#271) --- restapi.go | 2 +- structs.go | 6 ++++++ types.go | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 2e2adf2..d51fcbe 100644 --- a/restapi.go +++ b/restapi.go @@ -169,7 +169,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence response, err = s.request(method, urlStr, contentType, b, sequence) default: // Error condition - err = fmt.Errorf("HTTP %s, %s", resp.Status, response) + err = newRestError(req, resp, response) } return diff --git a/structs.go b/structs.go index 3e0c375..47623f4 100644 --- a/structs.go +++ b/structs.go @@ -502,6 +502,12 @@ type UserGuildSettingsEdit struct { ChannelOverrides map[string]*UserGuildSettingsChannelOverride `json:"channel_overrides"` } +// An APIErrorMessage is an api error message returned from discord +type APIErrorMessage struct { + Code int `json:"code"` + Message string `json:"message"` +} + // ChannelPinsUpdate stores data for the channel pins update event type ChannelPinsUpdate struct { LastPinTimestamp string `json:"last_pin_timestamp"` diff --git a/types.go b/types.go index 24f52c4..780b6bb 100644 --- a/types.go +++ b/types.go @@ -10,6 +10,9 @@ package discordgo import ( + "encoding/json" + "fmt" + "net/http" "time" ) @@ -21,3 +24,35 @@ type Timestamp string 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) +} From 212fc66a25fa5e1eb7038d48aa2c54b5dd625b26 Mon Sep 17 00:00:00 2001 From: Austin Davis Date: Thu, 13 Oct 2016 20:14:03 -0600 Subject: [PATCH 42/70] 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 e37343d4d43d457a778c96b2e050162f4ee8ae8f Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 29 Oct 2016 16:30:07 -0700 Subject: [PATCH 43/70] 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 28211e9..ff5e521 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 ee3e50074964950907fba2fcd9c655ff8c943f61 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 27 Oct 2016 23:43:24 -0700 Subject: [PATCH 44/70] Add webhook support. --- endpoints.go | 6 ++ message.go | 88 +++++++++++++++++++------- restapi.go | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++- state.go | 2 +- structs.go | 33 +++++++++- 5 files changed, 274 insertions(+), 26 deletions(-) diff --git a/endpoints.go b/endpoints.go index 682433d..c79cfe6 100644 --- a/endpoints.go +++ b/endpoints.go @@ -24,6 +24,7 @@ var ( EndpointChannels = EndpointAPI + "channels/" EndpointUsers = EndpointAPI + "users/" EndpointGateway = EndpointAPI + "gateway" + EndpointWebhooks = EndpointAPI + "webhooks/" EndpointAuth = EndpointAPI + "auth/" EndpointLogin = EndpointAuth + "login" @@ -73,6 +74,7 @@ var ( EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" } EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" } + 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" } @@ -85,6 +87,10 @@ var ( 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 } + 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 } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } diff --git a/message.go b/message.go index 9e33dd2..7b85973 100644 --- a/message.go +++ b/message.go @@ -41,31 +41,73 @@ type MessageAttachment struct { Size int `json:"size"` } +// 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"` - 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"` + 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"` } // ContentWithMentionsReplaced will replace all @ mentions with the diff --git a/restapi.go b/restapi.go index 5ecb48f..f561b8a 100644 --- a/restapi.go +++ b/restapi.go @@ -440,7 +440,7 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAll } @@ -1439,3 +1439,172 @@ func (s *Session) Gateway() (gateway string, err error) { gateway = temp.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.Request("POST", EndpointChannelWebhooks(channelID), data) + 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.Request("GET", EndpointChannelWebhooks(channelID), nil) + 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.Request("GET", EndpointGuildWebhooks(guildID), nil) + 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.Request("GET", EndpointWebhook(webhookID), nil) + 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.Request("GET", EndpointWebhookToken(webhookID, token), nil) + 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.Request("PATCH", EndpointWebhook(webhookID), data) + 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.Request("PATCH", EndpointWebhookToken(webhookID, token), data) + 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) (st *Webhook, err error) { + + body, err := s.Request("DELETE", EndpointWebhook(webhookID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + 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.Request("DELETE", EndpointWebhookToken(webhookID, token), nil) + 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 bebhook +func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { + uri := EndpointWebhookToken(webhookID, token) + + if wait { + uri += "?wait=true" + } + + fmt.Println(uri) + + _, err = s.Request("POST", uri, data) + + return +} diff --git a/state.go b/state.go index aa7997e..5b6ed07 100644 --- a/state.go +++ b/state.go @@ -755,7 +755,7 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAll } diff --git a/structs.go b/structs.go index dcfb447..62c5aec 100644 --- a/structs.go +++ b/structs.go @@ -489,6 +489,27 @@ type ChannelPinsUpdate struct { ChannelID string `json:"channel_id"` } +// 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"` +} + +// 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"` +} + // Constants for the different bit offsets of text channel permissions const ( PermissionReadMessages = 1 << (iota + 10) @@ -499,6 +520,7 @@ const ( PermissionAttachFiles PermissionReadMessageHistory PermissionMentionEveryone + PermissionUseExternalEmojis ) // Constants for the different bit offsets of voice permissions @@ -511,12 +533,21 @@ const ( 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 - PermissionManageRoles + PermissionAdministrator PermissionManageChannels PermissionManageServer From c96162c425f8635787b5b6de126f837b7790a1ba Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 3 Nov 2016 21:15:10 -0700 Subject: [PATCH 45/70] Add support for Message Reactions. --- endpoints.go | 4 ++++ events.go | 12 ++++++++++++ message.go | 8 ++++++++ restapi.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ structs.go | 19 +++++++++++++++++++ 5 files changed, 92 insertions(+) diff --git a/endpoints.go b/endpoints.go index c79cfe6..25c6141 100644 --- a/endpoints.go +++ b/endpoints.go @@ -92,6 +92,10 @@ var ( EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } + EndpointMessageReactions = func(cID, mID, eID string) string { + return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + "/@me" + } + EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } diff --git a/events.go b/events.go index 49f12ad..c5404ec 100644 --- a/events.go +++ b/events.go @@ -35,6 +35,8 @@ var eventToInterface = map[string]interface{}{ "MESSAGE_CREATE": MessageCreate{}, "MESSAGE_UPDATE": MessageUpdate{}, "MESSAGE_DELETE": MessageDelete{}, + "MESSAGE_REACTION_ADD": MessageReactionAdd{}, + "MESSAGE_REACTION_REMOVE": MessageReactionRemove{}, "PRESENCE_UPDATE": PresenceUpdate{}, "PRESENCES_REPLACE": PresencesReplace{}, "READY": Ready{}, @@ -74,6 +76,16 @@ type MessageDelete struct { *Message } +// MessageReactionAdd is a wrapper struct for an event. +type MessageReactionAdd struct { + *MessageReaction +} + +// MessageReactionRemove is a wrapper struct for an event. +type MessageReactionRemove struct { + *MessageReaction +} + // ChannelCreate is a wrapper struct for an event. type ChannelCreate struct { *Channel diff --git a/message.go b/message.go index 7b85973..d7abda6 100644 --- a/message.go +++ b/message.go @@ -28,6 +28,7 @@ type Message struct { Attachments []*MessageAttachment `json:"attachments"` Embeds []*MessageEmbed `json:"embeds"` Mentions []*User `json:"mentions"` + Reactions []*MessageReactions `json:"reactions"` } // A MessageAttachment stores data for message attachments. @@ -110,6 +111,13 @@ type MessageEmbed struct { 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 { diff --git a/restapi.go b/restapi.go index fae8d61..1d5515c 100644 --- a/restapi.go +++ b/restapi.go @@ -1635,3 +1635,52 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho 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.Request("PUT", EndpointMessageReactions(channelID, messageID, emojiID), nil) + + 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. +func (s *Session) MessageReactionRemove(channelID, messageID, emojiID string) error { + + _, err := s.Request("DELETE", EndpointMessageReactions(channelID, messageID, emojiID), nil) + + 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.Request("GET", uri, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/structs.go b/structs.go index ce4add3..707b2b7 100644 --- a/structs.go +++ b/structs.go @@ -186,6 +186,17 @@ 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 @@ -535,6 +546,14 @@ type WebhookParams struct { 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"` +} + // Constants for the different bit offsets of text channel permissions const ( PermissionReadMessages = 1 << (iota + 10) From 5ef835fc80f38d9a51e404ac017b51ef3a63cf16 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 3 Nov 2016 22:39:55 -0700 Subject: [PATCH 46/70] Fix GuildBans. Fixes #263 --- events.go | 6 ++++-- restapi.go | 2 +- structs.go | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/events.go b/events.go index c5404ec..1e62505 100644 --- a/events.go +++ b/events.go @@ -118,12 +118,14 @@ type GuildDelete struct { // GuildBanAdd is a wrapper struct for an event. type GuildBanAdd struct { - *GuildBan + *User + GuildID string `json:"guild_id"` } // GuildBanRemove is a wrapper struct for an event. type GuildBanRemove struct { - *GuildBan + *User + GuildID string `json:"guild_id"` } // GuildMemberAdd is a wrapper struct for an event. diff --git a/restapi.go b/restapi.go index 1d5515c..e570627 100644 --- a/restapi.go +++ b/restapi.go @@ -604,7 +604,7 @@ func (s *Session) GuildLeave(guildID string) (err error) { // 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", EndpointGuildBans(guildID), nil) if err != nil { diff --git a/structs.go b/structs.go index 707b2b7..a1b0d5e 100644 --- a/structs.go +++ b/structs.go @@ -444,8 +444,8 @@ type GuildRoleDelete struct { // A GuildBan stores data for a guild ban. type GuildBan struct { - User *User `json:"user"` - GuildID string `json:"guild_id"` + Reason string `json:"reason"` + User *User `json:"user"` } // A GuildEmojisUpdate stores data for a guild emoji update event. From 56b19073d38312a4a7570e21120cd1a0cd158d79 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Fri, 4 Nov 2016 16:07:22 +0100 Subject: [PATCH 47/70] Ratelimits (#273) * Added ratelimiter Handles the new ratelimit headers - X-RateLimit-Remaining - X-RateLimit-Reset - X-RateLimit-Global * Pad only reset time with a second * Moved ratelimiter out of internal package * Change for loop, move global ratelimit check inside sleep check * Moved ratelimiter locking to getBucket * Added global bucket * Changed how bucket id's are done Now each request function will need to specify the bucket id if the endpoint contains minor variables * Allow empty bucketID in request * Remove some uneeded Endpoint* function calls * Added test for global ratelimits * Fixed a silly little mistake causing incorrect ratelimits * Update test comments, Fixed treating a endpoint as 2 in ratelimiting * Use date header from discord instead of relying on sys time sync * Update all REST functions to use RequestWithBucketID * Embed mutex into bucket * Added webhook and reaction buckets --- discord.go | 1 + oauth2.go | 12 +-- ratelimit.go | 157 +++++++++++++++++++++++++++++++++ ratelimit_test.go | 112 ++++++++++++++++++++++++ restapi.go | 216 ++++++++++++++++++++++------------------------ structs.go | 4 +- 6 files changed, 378 insertions(+), 124 deletions(-) create mode 100644 ratelimit.go create mode 100644 ratelimit_test.go diff --git a/discord.go b/discord.go index 77bde70..4f7d93c 100644 --- a/discord.go +++ b/discord.go @@ -37,6 +37,7 @@ 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, diff --git a/oauth2.go b/oauth2.go index 11058e1..14ba6bb 100644 --- a/oauth2.go +++ b/oauth2.go @@ -28,7 +28,7 @@ type Application struct { // appID : The ID of an Application func (s *Session) Application(appID string) (st *Application, err error) { - body, err := s.Request("GET", EndpointApplication(appID), nil) + body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication("")) if err != nil { return } @@ -40,7 +40,7 @@ func (s *Session) Application(appID string) (st *Application, err error) { // Applications returns all applications for the authenticated user func (s *Session) Applications() (st []*Application, err error) { - body, err := s.Request("GET", EndpointApplications, nil) + body, err := s.RequestWithBucketID("GET", EndpointApplications, nil, EndpointApplications) if err != nil { return } @@ -60,7 +60,7 @@ func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error RedirectURIs *[]string `json:"redirect_uris,omitempty"` }{ap.Name, ap.Description, ap.RedirectURIs} - body, err := s.Request("POST", EndpointApplications, data) + body, err := s.RequestWithBucketID("POST", EndpointApplications, data, EndpointApplications) if err != nil { return } @@ -79,7 +79,7 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat RedirectURIs *[]string `json:"redirect_uris,omitempty"` }{ap.Name, ap.Description, ap.RedirectURIs} - body, err := s.Request("PUT", EndpointApplication(appID), data) + body, err := s.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication("")) if err != nil { return } @@ -92,7 +92,7 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat // appID : The ID of an Application func (s *Session) ApplicationDelete(appID string) (err error) { - _, err = s.Request("DELETE", EndpointApplication(appID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication("")) if err != nil { return } @@ -111,7 +111,7 @@ func (s *Session) ApplicationDelete(appID string) (err error) { // 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.Request("POST", EndpointApplicationsBot(appID), nil) + body, err := s.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot("")) if err != nil { return } diff --git a/ratelimit.go b/ratelimit.go new file mode 100644 index 0000000..a4674bf --- /dev/null +++ b/ratelimit.go @@ -0,0 +1,157 @@ +package discordgo + +import ( + "net/http" + "strconv" + "sync" + "time" +) + +// Ratelimiter holds all ratelimit buckets +type RateLimiter struct { + sync.Mutex + global *Bucket + buckets map[string]*Bucket + globalRateLimit time.Duration +} + +// New returns a new RateLimiter +func NewRatelimiter() *RateLimiter { + + return &RateLimiter{ + buckets: make(map[string]*Bucket), + global: &Bucket{Key: "global"}, + } +} + +// 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, + } + + 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 + r.global.Lock() + r.global.Unlock() + + 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 *Bucket +} + +// 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() + 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") + + // If it's global just keep the main ratelimit mutex locked + if global != "" { + parsedAfter, err := strconv.Atoi(retryAfter) + if err != nil { + return err + } + + // Lock it in a new goroutine so that this isn't a blocking call + go func() { + // Make sure if several requests were waiting we don't sleep for n * retry-after + // where n is the amount of requests that were going on + sleepTo := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + + b.global.Lock() + + sleepDuration := sleepTo.Sub(time.Now()) + if sleepDuration > 0 { + time.Sleep(sleepDuration) + } + + b.global.Unlock() + }() + + return nil + } + + // Update reset time if either retry after or reset headers are present + // Prefer retryafter because it's more accurate with time sync and whatnot + if retryAfter != "" { + parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) + if err != nil { + return err + } + b.reset = time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + + } 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/ratelimit_test.go b/ratelimit_test.go new file mode 100644 index 0000000..db18211 --- /dev/null +++ b/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/restapi.go b/restapi.go index e570627..c3271f4 100644 --- a/restapi.go +++ b/restapi.go @@ -26,17 +26,19 @@ import ( "net/url" "strconv" "strings" - "sync" "time" ) // ErrJSONUnmarshal is returned for JSON Unmarshall errors. var ErrJSONUnmarshal = errors.New("json unmarshal") -// 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]) +} +// 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) @@ -45,36 +47,19 @@ func (s *Session) Request(method, urlStr string, data interface{}) (response []b } } - return s.request(method, urlStr, "application/json", body, 0) + return s.request(method, urlStr, "application/json", body, bucketID, 0) } // request makes a (GET/POST/...) Requests to Discord REST API. // 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, sequence int) (response []byte, err error) { - - // rate limit mutex for this url - // TODO: review for performance improvements - // ideally we just ignore endpoints that we've never - // received a 429 on. But this simple method works and - // is a lot less complex :) It also might even be more - // performat due to less checks and maps. - var mu *sync.Mutex - - s.rateLimit.Lock() - if s.rateLimit.url == nil { - s.rateLimit.url = make(map[string]*sync.Mutex) +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] } - bu := strings.Split(urlStr, "?") - mu, _ = s.rateLimit.url[bu[0]] - if mu == nil { - mu = new(sync.Mutex) - s.rateLimit.url[bu[0]] = mu - } - s.rateLimit.Unlock() + bucket := s.ratelimiter.LockBucket(bucketID) - mu.Lock() // lock this URL for ratelimiting if s.Debug { log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) @@ -82,6 +67,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(b)) if err != nil { + bucket.Release(nil) return } @@ -104,8 +90,8 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence client := &http.Client{Timeout: (20 * time.Second)} resp, err := client.Do(req) - mu.Unlock() // unlock ratelimit mutex if err != nil { + bucket.Release(nil) return } defer func() { @@ -115,6 +101,11 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence } }() + err = bucket.Release(resp.Header) + if err != nil { + return + } + response, err = ioutil.ReadAll(resp.Body) if err != nil { return @@ -142,20 +133,16 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence if sequence < s.MaxRestRetries { s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) - response, err = s.request(method, urlStr, contentType, b, sequence+1) + 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 - - mu.Lock() // lock URL ratelimit mutex - rl := TooManyRequests{} err = json.Unmarshal(response, &rl) if err != nil { s.log(LogError, "rate limit unmarshal error, %s", err) - mu.Unlock() return } s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) @@ -165,8 +152,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, sequence // we can make the above smarter // this method can cause longer delays than required - mu.Unlock() // we have to unlock here - response, err = s.request(method, urlStr, contentType, b, sequence) + response, err = s.request(method, urlStr, contentType, b, bucketID, sequence) default: // Error condition err = newRestError(req, resp, response) @@ -196,7 +182,7 @@ func (s *Session) Login(email, password string) (err error) { Password string `json:"password"` }{email, password} - response, err := s.Request("POST", EndpointLogin, data) + response, err := s.RequestWithBucketID("POST", EndpointLogin, data, EndpointLogin) if err != nil { return } @@ -223,7 +209,7 @@ func (s *Session) Register(username string) (token string, err error) { Username string `json:"username"` }{username} - response, err := s.Request("POST", EndpointRegister, data) + response, err := s.RequestWithBucketID("POST", EndpointRegister, data, EndpointRegister) if err != nil { return } @@ -257,7 +243,7 @@ func (s *Session) Logout() (err error) { Token string `json:"token"` }{s.Token} - _, err = s.Request("POST", EndpointLogout, data) + _, err = s.RequestWithBucketID("POST", EndpointLogout, data, EndpointLogout) return } @@ -269,7 +255,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", EndpointUser(userID), nil) + body, err := s.RequestWithBucketID("GET", EndpointUser(userID), nil, EndpointUsers) if err != nil { return } @@ -286,7 +272,7 @@ func (s *Session) UserAvatar(userID string) (img image.Image, err error) { return } - body, err := s.Request("GET", EndpointUserAvatar(userID, u.Avatar), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(userID, u.Avatar), nil, EndpointUserAvatar("", "")) if err != nil { return } @@ -311,7 +297,7 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri NewPassword string `json:"new_password,omitempty"` }{email, password, username, avatar, newPassword} - body, err := s.Request("PATCH", EndpointUser("@me"), data) + body, err := s.RequestWithBucketID("PATCH", EndpointUser("@me"), data, EndpointUsers) if err != nil { return } @@ -323,7 +309,7 @@ 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", EndpointUserSettings("@me"), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserSettings("@me"), nil, EndpointUserSettings("")) if err != nil { return } @@ -344,7 +330,7 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { Status Status `json:"status"` }{status} - body, err := s.Request("PATCH", EndpointUserSettings("@me"), data) + body, err := s.RequestWithBucketID("PATCH", EndpointUserSettings("@me"), data, EndpointUserSettings("")) if err != nil { return } @@ -357,7 +343,7 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { // channels. func (s *Session) UserChannels() (st []*Channel, err error) { - body, err := s.Request("GET", EndpointUserChannels("@me"), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserChannels("@me"), nil, EndpointUserChannels("")) if err != nil { return } @@ -374,7 +360,7 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) RecipientID string `json:"recipient_id"` }{recipientID} - body, err := s.Request("POST", EndpointUserChannels("@me"), data) + body, err := s.RequestWithBucketID("POST", EndpointUserChannels("@me"), data, EndpointUserChannels("")) if err != nil { return } @@ -386,7 +372,7 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) // UserGuilds returns an array of UserGuild structures for all guilds. func (s *Session) UserGuilds() (st []*UserGuild, err error) { - body, err := s.Request("GET", EndpointUserGuilds("@me"), nil) + body, err := s.RequestWithBucketID("GET", EndpointUserGuilds("@me"), nil, EndpointUserGuilds("")) if err != nil { return } @@ -400,7 +386,7 @@ func (s *Session) UserGuilds() (st []*UserGuild, err error) { // settings : The settings to update func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) { - body, err := s.Request("PATCH", EndpointUserGuildSettings("@me", guildID), settings) + body, err := s.RequestWithBucketID("PATCH", EndpointUserGuildSettings("@me", guildID), settings, EndpointUserGuildSettings("", guildID)) if err != nil { return } @@ -506,7 +492,7 @@ func (s *Session) Guild(guildID string) (st *Guild, err error) { } } - body, err := s.Request("GET", EndpointGuild(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return } @@ -523,7 +509,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) { Name string `json:"name"` }{name} - body, err := s.Request("POST", EndpointGuilds, data) + body, err := s.RequestWithBucketID("POST", EndpointGuilds, data, EndpointGuilds) if err != nil { return } @@ -571,7 +557,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` }{g.Name, g.Region, g.VerificationLevel} - body, err := s.Request("PATCH", EndpointGuild(guildID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), data, EndpointGuild(guildID)) if err != nil { return } @@ -584,7 +570,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (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", EndpointGuild(guildID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return } @@ -597,7 +583,7 @@ 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", EndpointUserGuild("@me", guildID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointUserGuild("@me", guildID), nil, EndpointUserGuild("", guildID)) return } @@ -606,7 +592,7 @@ func (s *Session) GuildLeave(guildID string) (err error) { // guildID : The ID of a Guild. func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { - body, err := s.Request("GET", EndpointGuildBans(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildBans(guildID), nil, EndpointGuildBans(guildID)) if err != nil { return } @@ -628,7 +614,7 @@ func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days) } - _, err = s.Request("PUT", uri, nil) + _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) return } @@ -637,7 +623,7 @@ 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", EndpointGuildBan(guildID, userID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, "")) return } @@ -663,7 +649,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M 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 } @@ -677,7 +663,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M // userID : The ID of a User func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { - body, err := s.Request("GET", EndpointGuildMember(guildID, userID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -691,7 +677,7 @@ 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", EndpointGuildMember(guildID, userID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) return } @@ -705,7 +691,7 @@ func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err e Roles []string `json:"roles"` }{roles} - _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -725,7 +711,7 @@ func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) ChannelID string `json:"channel_id"` }{channelID} - _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) if err != nil { return } @@ -742,7 +728,7 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err Nick string `json:"nick"` }{nickname} - _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) return } @@ -751,7 +737,7 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err // guildID : The ID of a Guild. func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { - body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, 0) + body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, EndpointGuildChannels(guildID), 0) if err != nil { return } @@ -772,7 +758,7 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, Type string `json:"type"` }{name, ctype} - body, err := s.Request("POST", EndpointGuildChannels(guildID), data) + body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) if err != nil { return } @@ -786,14 +772,14 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, // channels : Updated channels. func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { - _, err = s.Request("PATCH", EndpointGuildChannels(guildID), channels) + _, 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", EndpointGuildInvites(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInivtes(guildID)) if err != nil { return } @@ -806,7 +792,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", EndpointGuildRoles(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { return } @@ -820,7 +806,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", EndpointGuildRoles(guildID), nil) + body, err := s.RequestWithBucketID("POST", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { return } @@ -853,7 +839,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b Mentionable bool `json:"mentionable"` // Whether this role is mentionable }{name, color, hoist, perm, mention} - body, err := s.Request("PATCH", EndpointGuildRole(guildID, roleID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRole(guildID, roleID), data, EndpointGuildRole(guildID, "")) if err != nil { return } @@ -868,7 +854,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", EndpointGuildRoles(guildID), roles) + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRoles(guildID), roles, EndpointGuildRoles(guildID)) if err != nil { return } @@ -883,7 +869,7 @@ 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", EndpointGuildRole(guildID, roleID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildRole(guildID, roleID), nil, EndpointGuildRole(guildID, "")) return } @@ -892,7 +878,7 @@ func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { // guildID : The ID of a Guild. func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) { - body, err := s.Request("GET", EndpointGuildIntegrations(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) if err != nil { return } @@ -913,7 +899,7 @@ func (s *Session) GuildIntegrationCreate(guildID, integrationType, integrationID ID string `json:"id"` }{integrationType, integrationID} - _, err = s.Request("POST", EndpointGuildIntegrations(guildID), data) + _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrations(guildID), data, EndpointGuildIntegrations(guildID)) return } @@ -932,7 +918,7 @@ func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBeha EnableEmoticons bool `json:"enable_emoticons"` }{expireBehavior, expireGracePeriod, enableEmoticons} - _, err = s.Request("PATCH", EndpointGuildIntegration(guildID, integrationID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildIntegration(guildID, integrationID), data, EndpointGuildIntegration(guildID, "")) return } @@ -941,7 +927,7 @@ func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBeha // integrationID : The ID of an integration. func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err error) { - _, err = s.Request("DELETE", EndpointGuildIntegration(guildID, integrationID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildIntegration(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) return } @@ -950,7 +936,7 @@ func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err err // integrationID : The ID of an integration. func (s *Session) GuildIntegrationSync(guildID, integrationID string) (err error) { - _, err = s.Request("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil) + _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) return } @@ -967,7 +953,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { return } - body, err := s.Request("GET", EndpointGuildIcon(guildID, g.Icon), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildIcon(guildID, g.Icon), nil, EndpointGuildIcon(guildID, "")) if err != nil { return } @@ -989,7 +975,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { return } - body, err := s.Request("GET", EndpointGuildSplash(guildID, g.Splash), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildSplash(guildID, g.Splash), nil, EndpointGuildSplash(guildID, "")) if err != nil { return } @@ -1002,7 +988,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { // guildID : The ID of a Guild. func (s *Session) GuildEmbed(guildID string) (st *GuildEmbed, err error) { - body, err := s.Request("GET", EndpointGuildEmbed(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildEmbed(guildID), nil, EndpointGuildEmbed(guildID)) if err != nil { return } @@ -1017,7 +1003,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) data := GuildEmbed{enabled, channelID} - _, err = s.Request("PATCH", EndpointGuildEmbed(guildID), data) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildEmbed(guildID), data, EndpointGuildEmbed(guildID)) return } @@ -1028,7 +1014,7 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) // 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", EndpointChannel(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { return } @@ -1046,7 +1032,7 @@ func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { Name string `json:"name"` }{name} - body, err := s.Request("PATCH", EndpointChannel(channelID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) if err != nil { return } @@ -1059,7 +1045,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", EndpointChannel(channelID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { return } @@ -1073,7 +1059,7 @@ 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", EndpointChannelTyping(channelID), nil) + _, err = s.RequestWithBucketID("POST", EndpointChannelTyping(channelID), nil, EndpointChannelTyping(channelID)) return } @@ -1101,7 +1087,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID 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 } @@ -1115,7 +1101,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID // messageID : the ID of a Message func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err error) { - response, err := s.Request("GET", EndpointChannelMessage(channelID, messageID), nil) + response, err := s.RequestWithBucketID("GET", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) if err != nil { return } @@ -1130,7 +1116,7 @@ func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err // lastToken : token returned by last ack func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) { - body, err := s.Request("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}) + body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, "")) if err != nil { return } @@ -1152,7 +1138,7 @@ func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *M }{content, tts} // Send the message to the given channel - response, err := s.Request("POST", EndpointChannelMessages(channelID), data) + response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID)) if err != nil { return } @@ -1187,7 +1173,7 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st * Content string `json:"content"` }{content} - response, err := s.Request("PATCH", EndpointChannelMessage(channelID, messageID), data) + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(channelID, messageID), data, EndpointChannelMessage(channelID, "")) if err != nil { return } @@ -1199,7 +1185,7 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st * // ChannelMessageDelete deletes a message from the Channel. func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { - _, err = s.Request("DELETE", EndpointChannelMessage(channelID, messageID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) return } @@ -1227,7 +1213,7 @@ func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) Messages []string `json:"messages"` }{messages} - _, err = s.Request("POST", EndpointChannelMessagesBulkDelete(channelID), data) + _, err = s.RequestWithBucketID("POST", EndpointChannelMessagesBulkDelete(channelID), data, EndpointChannelMessagesBulkDelete(channelID)) return } @@ -1236,7 +1222,7 @@ func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) // messageID: The ID of a message. func (s *Session) ChannelMessagePin(channelID, messageID string) (err error) { - _, err = s.Request("PUT", EndpointChannelMessagePin(channelID, messageID), nil) + _, err = s.RequestWithBucketID("PUT", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) return } @@ -1245,7 +1231,7 @@ func (s *Session) ChannelMessagePin(channelID, messageID string) (err error) { // messageID: The ID of a message. func (s *Session) ChannelMessageUnpin(channelID, messageID string) (err error) { - _, err = s.Request("DELETE", EndpointChannelMessagePin(channelID, messageID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) return } @@ -1254,7 +1240,7 @@ func (s *Session) ChannelMessageUnpin(channelID, messageID string) (err error) { // channelID : The ID of a Channel. func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err error) { - body, err := s.Request("GET", EndpointChannelMessagesPins(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannelMessagesPins(channelID), nil, EndpointChannelMessagesPins(channelID)) if err != nil { return @@ -1303,7 +1289,7 @@ func (s *Session) ChannelFileSendWithMessage(channelID, content string, name str return } - response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes(), 0) + response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes(), EndpointChannelMessages(channelID), 0) if err != nil { return } @@ -1316,7 +1302,7 @@ func (s *Session) ChannelFileSendWithMessage(channelID, content string, name str // channelID : The ID of a Channel func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { - body, err := s.Request("GET", EndpointChannelInvites(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannelInvites(channelID), nil, EndpointChannelInvites(channelID)) if err != nil { return } @@ -1338,7 +1324,7 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e XKCDPass string `json:"xkcdpass"` }{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass} - body, err := s.Request("POST", EndpointChannelInvites(channelID), data) + body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) if err != nil { return } @@ -1359,7 +1345,7 @@ func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, a Deny int `json:"deny"` }{targetID, targetType, allow, deny} - _, err = s.Request("PUT", EndpointChannelPermission(channelID, targetID), data) + _, err = s.RequestWithBucketID("PUT", EndpointChannelPermission(channelID, targetID), data, EndpointChannelPermission(channelID, "")) return } @@ -1367,7 +1353,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", EndpointChannelPermission(channelID, targetID), nil) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelPermission(channelID, targetID), nil, EndpointChannelPermission(channelID, "")) return } @@ -1379,7 +1365,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", EndpointInvite(inviteID), nil) + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -1392,7 +1378,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", EndpointInvite(inviteID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -1405,7 +1391,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", EndpointInvite(inviteID), nil) + body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) if err != nil { return } @@ -1421,7 +1407,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", EndpointVoiceRegions, nil) + body, err := s.RequestWithBucketID("GET", EndpointVoiceRegions, nil, EndpointVoiceRegions) if err != nil { return } @@ -1433,7 +1419,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", EndpointVoiceIce, nil) + body, err := s.RequestWithBucketID("GET", EndpointVoiceIce, nil, EndpointVoiceIce) if err != nil { return } @@ -1449,7 +1435,7 @@ func (s *Session) VoiceICE() (st *VoiceICE, err error) { // Gateway returns the a websocket Gateway address func (s *Session) Gateway() (gateway string, err error) { - response, err := s.Request("GET", EndpointGateway, nil) + response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway) if err != nil { return } @@ -1480,7 +1466,7 @@ func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, er Avatar string `json:"avatar,omitempty"` }{name, avatar} - body, err := s.Request("POST", EndpointChannelWebhooks(channelID), data) + body, err := s.RequestWithBucketID("POST", EndpointChannelWebhooks(channelID), data, EndpointChannelWebhooks(channelID)) if err != nil { return } @@ -1494,7 +1480,7 @@ func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, er // channelID: The ID of a channel. func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { - body, err := s.Request("GET", EndpointChannelWebhooks(channelID), nil) + body, err := s.RequestWithBucketID("GET", EndpointChannelWebhooks(channelID), nil, EndpointChannelWebhooks(channelID)) if err != nil { return } @@ -1508,7 +1494,7 @@ func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { // guildID: The ID of a Guild. func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { - body, err := s.Request("GET", EndpointGuildWebhooks(guildID), nil) + body, err := s.RequestWithBucketID("GET", EndpointGuildWebhooks(guildID), nil, EndpointGuildWebhooks(guildID)) if err != nil { return } @@ -1522,7 +1508,7 @@ func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { // webhookID: The ID of a webhook. func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { - body, err := s.Request("GET", EndpointWebhook(webhookID), nil) + body, err := s.RequestWithBucketID("GET", EndpointWebhook(webhookID), nil, EndpointWebhooks) if err != nil { return } @@ -1537,7 +1523,7 @@ func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { // token : The auth token for the webhook. func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err error) { - body, err := s.Request("GET", EndpointWebhookToken(webhookID, token), nil) + body, err := s.RequestWithBucketID("GET", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) if err != nil { return } @@ -1558,7 +1544,7 @@ func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err err Avatar string `json:"avatar,omitempty"` }{name, avatar} - body, err := s.Request("PATCH", EndpointWebhook(webhookID), data) + body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) if err != nil { return } @@ -1580,7 +1566,7 @@ func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (s Avatar string `json:"avatar,omitempty"` }{name, avatar} - body, err := s.Request("PATCH", EndpointWebhookToken(webhookID, token), data) + body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", "")) if err != nil { return } @@ -1594,7 +1580,7 @@ func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (s // webhookID: The ID of a webhook. func (s *Session) WebhookDelete(webhookID string) (st *Webhook, err error) { - body, err := s.Request("DELETE", EndpointWebhook(webhookID), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) if err != nil { return } @@ -1609,7 +1595,7 @@ func (s *Session) WebhookDelete(webhookID string) (st *Webhook, err error) { // token : The auth token for the webhook. func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, err error) { - body, err := s.Request("DELETE", EndpointWebhookToken(webhookID, token), nil) + body, err := s.RequestWithBucketID("DELETE", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) if err != nil { return } @@ -1631,7 +1617,7 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho fmt.Println(uri) - _, err = s.Request("POST", uri, data) + _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", "")) return } @@ -1642,7 +1628,7 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho // emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error { - _, err := s.Request("PUT", EndpointMessageReactions(channelID, messageID, emojiID), nil) + _, err := s.RequestWithBucketID("PUT", EndpointMessageReactions(channelID, messageID, emojiID), nil, EndpointMessageReactions(channelID, "", "")) return err } @@ -1653,7 +1639,7 @@ func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error // emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. func (s *Session) MessageReactionRemove(channelID, messageID, emojiID string) error { - _, err := s.Request("DELETE", EndpointMessageReactions(channelID, messageID, emojiID), nil) + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactions(channelID, messageID, emojiID), nil, EndpointMessageReactions(channelID, "", "")) return err } @@ -1676,7 +1662,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } - body, err := s.Request("GET", uri, nil) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReactions(channelID, "", "")) if err != nil { return } diff --git a/structs.go b/structs.go index a1b0d5e..62ebc84 100644 --- a/structs.go +++ b/structs.go @@ -88,9 +88,7 @@ type Session struct { listening chan interface{} // used to deal with rate limits - // may switch to slices later - // TODO: performance test map vs slices - rateLimit rateLimitMutex + ratelimiter *RateLimiter // sequence tracks the current gateway api websocket sequence number sequence int From 3e7c0435bc2bc87a6a0b57953e4083bf05bfc252 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 5 Nov 2016 00:15:16 -0700 Subject: [PATCH 48/70] 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 54a420e..ab4f3ad 100644 --- a/wsapi.go +++ b/wsapi.go @@ -694,7 +694,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. @@ -703,7 +703,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 f0cb6780226e147a0559f51fab6616ce07ec1ce3 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Sun, 6 Nov 2016 10:18:25 +0100 Subject: [PATCH 49/70] Add member_count to guild --- structs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/structs.go b/structs.go index 62ebc84..ed389c1 100644 --- a/structs.go +++ b/structs.go @@ -219,6 +219,7 @@ type Guild struct { 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"` // ?? From 5d1dd7ddac453fc704dd0bb6b33bb576fa4a9a93 Mon Sep 17 00:00:00 2001 From: jonas747 Date: Mon, 7 Nov 2016 09:57:31 +0100 Subject: [PATCH 50/70] Add nick and roles to Presence --- structs.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/structs.go b/structs.go index ed389c1..6b73e5f 100644 --- a/structs.go +++ b/structs.go @@ -276,9 +276,11 @@ 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 Status `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"` } // A Game struct holds the name of the "playing .." game for a user From 982cd7d7c3e63178dd5405838cf812af89f69016 Mon Sep 17 00:00:00 2001 From: AI Date: Tue, 8 Nov 2016 05:06:08 +0500 Subject: [PATCH 51/70] Add support for the prune endpoint (#282) * Add support for the prune endpoint Adds functions to get the amount of members that could be pruned and to prune members using the prune endpoint. May close: bwmarrin/discordgo#147 * Deal with the go vet error Removed the json tags from the unexported struct. Should pass the tests now. * Make the PR consistent with the rest of the file. Removes url building in favour of string concatenation. * Fix the previous commit Adds back the result struct. Converts the uint32 to string. * Deal with golint comments * Remove the failing test Cleans up the uri concatenation. Removes the failing test due to incorrect permissions. --- ratelimit.go | 4 +-- restapi.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ restapi_test.go | 36 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/ratelimit.go b/ratelimit.go index a4674bf..bc320f0 100644 --- a/ratelimit.go +++ b/ratelimit.go @@ -7,7 +7,7 @@ import ( "time" ) -// Ratelimiter holds all ratelimit buckets +// RateLimiter holds all ratelimit buckets type RateLimiter struct { sync.Mutex global *Bucket @@ -15,7 +15,7 @@ type RateLimiter struct { globalRateLimit time.Duration } -// New returns a new RateLimiter +// NewRatelimiter returns a new RateLimiter func NewRatelimiter() *RateLimiter { return &RateLimiter{ diff --git a/restapi.go b/restapi.go index c3271f4..d3125f1 100644 --- a/restapi.go +++ b/restapi.go @@ -874,6 +874,71 @@ func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { 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 = errors.New("The number of days should be more than or equal to 1.") + 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 = errors.New("The number of days should be more than or equal to 1.") + 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) { diff --git a/restapi_test.go b/restapi_test.go index 0d68d6e..e4d111b 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -238,3 +238,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) + } +} +*/ From 602885488b90e5167d9b4cc898f3fed93947258a Mon Sep 17 00:00:00 2001 From: AI Date: Wed, 9 Nov 2016 00:49:30 +0500 Subject: [PATCH 52/70] Fix supprt for reaction endpoint (#283) - Adds support for deleting the reaction for a given user. - Requires MANAGE_MESSAGES permission. - Fixes the Sesssion.MessageReactions(...) function. --- endpoints.go | 5 ++++- restapi.go | 11 ++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/endpoints.go b/endpoints.go index 25c6141..0453c95 100644 --- a/endpoints.go +++ b/endpoints.go @@ -93,7 +93,10 @@ var ( EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } EndpointMessageReactions = func(cID, mID, eID string) string { - return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + "/@me" + return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + } + EndpointMessageReaction = func(cID, mID, eID, uID string) string { + return EndpointMessageReactions(cID, mID, eID) + "/" + uID } EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } diff --git a/restapi.go b/restapi.go index d3125f1..587042d 100644 --- a/restapi.go +++ b/restapi.go @@ -1672,7 +1672,7 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, // WebhookExecute executes a webhook. // webhookID: The ID of a webhook. -// token : The auth token for the bebhook +// token : The auth token for the webhook func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { uri := EndpointWebhookToken(webhookID, token) @@ -1693,7 +1693,7 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho // 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", EndpointMessageReactions(channelID, messageID, emojiID), nil, EndpointMessageReactions(channelID, "", "")) + _, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, emojiID, "@me"), nil, EndpointMessageReaction(channelID, "", "", "")) return err } @@ -1702,9 +1702,10 @@ func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error // channelID : The channel ID. // messageID : The message ID. // emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. -func (s *Session) MessageReactionRemove(channelID, messageID, emojiID string) error { +// 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", EndpointMessageReactions(channelID, messageID, emojiID), nil, EndpointMessageReactions(channelID, "", "")) + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, emojiID, userID), nil, EndpointMessageReaction(channelID, "", "", "")) return err } @@ -1727,7 +1728,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } - body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReactions(channelID, "", "")) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) if err != nil { return } From ed7a451a31191b432f7456869e26c78975d9d381 Mon Sep 17 00:00:00 2001 From: AI Date: Wed, 9 Nov 2016 06:07:04 +0500 Subject: [PATCH 53/70] Add support for relationships (#284) * Add support for relationships Adds Support for: - Sending friend request. - Accepting friend request. - Getting all the relationships. - Getting all the mutual friends with another user. - Blocking a user. **Note:** - Bot accounts are not allowed to access the endpoint. - May close bwmarrin/discordgo#150 * Implement requested changes Changed the uint8 declarations to int. * Change the missed unint8 declaration to int Missed one instance of unint8 during previous push. --- endpoints.go | 10 +++++--- events.go | 12 ++++++++++ restapi.go | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/endpoints.go b/endpoints.go index 0453c95..4b81983 100644 --- a/endpoints.go +++ b/endpoints.go @@ -87,10 +87,10 @@ var ( 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 } - 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 } + 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 } EndpointMessageReactions = func(cID, mID, eID string) string { return EndpointChannelMessage(cID, mID) + "/reactions/" + eID @@ -99,6 +99,10 @@ var ( 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" } diff --git a/events.go b/events.go index 1e62505..76d46c6 100644 --- a/events.go +++ b/events.go @@ -40,6 +40,8 @@ var eventToInterface = map[string]interface{}{ "PRESENCE_UPDATE": PresenceUpdate{}, "PRESENCES_REPLACE": PresencesReplace{}, "READY": Ready{}, + "RELATIONSHIP_ADD": RelationshipAdd{}, + "RELATIONSHIP_REMOVE": RelationshipRemove{}, "USER_UPDATE": UserUpdate{}, "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, "USER_GUILD_SETTINGS_UPDATE": UserGuildSettingsUpdate{}, @@ -156,6 +158,16 @@ type GuildRoleUpdate struct { // PresencesReplace is an array of Presences for an event. type PresencesReplace []*Presence +// RelationshipAdd is a wrapper struct for an event. +type RelationshipAdd struct { + *Relationship +} + +// RelationshipRemove is a wrapper struct for an event. +type RelationshipRemove struct { + *Relationship +} + // VoiceStateUpdate is a wrapper struct for an event. type VoiceStateUpdate struct { *VoiceState diff --git a/restapi.go b/restapi.go index 587042d..1423fa9 100644 --- a/restapi.go +++ b/restapi.go @@ -1736,3 +1736,71 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i err = unmarshal(body, &st) 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} + + fmt.Println("Data: " + fmt.Sprintf("%v", data)) + + _, 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 +} From 8f6d44cceeda9c3bc3579539eefc7f5e6bbaee5a Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 9 Nov 2016 22:27:12 -0800 Subject: [PATCH 54/70] Fix GuildBans. --- events.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/events.go b/events.go index 76d46c6..fc1c979 100644 --- a/events.go +++ b/events.go @@ -120,13 +120,13 @@ type GuildDelete struct { // GuildBanAdd is a wrapper struct for an event. type GuildBanAdd struct { - *User + User *User `json:"user"` GuildID string `json:"guild_id"` } // GuildBanRemove is a wrapper struct for an event. type GuildBanRemove struct { - *User + User *User `json:"user"` GuildID string `json:"guild_id"` } From c5a94de19cebe9153c9ccb1e4b6e22d466835c5a Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Fri, 11 Nov 2016 08:20:00 -0800 Subject: [PATCH 55/70] Silence missing voice op codes. --- voice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice.go b/voice.go index 1de8987..43de329 100644 --- a/voice.go +++ b/voice.go @@ -441,7 +441,7 @@ func (v *VoiceConnection) onEvent(message []byte) { } default: - v.log(LogError, "unknown voice operation, %d, %s", e.Operation, string(e.RawData)) + v.log(LogDebug, "unknown voice operation, %d, %s", e.Operation, string(e.RawData)) } return From c352d7016c19a015305f3f6129dbc6c1ff113b89 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 12 Nov 2016 11:50:06 -0800 Subject: [PATCH 56/70] Clean up ordering of internal handlers. (#285) --- discord.go | 30 ++++++++++++++++++++---------- state.go | 6 ++++++ wsapi.go | 6 +++--- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/discord.go b/discord.go index 4f7d93c..b3415c5 100644 --- a/discord.go +++ b/discord.go @@ -207,6 +207,8 @@ func (s *Session) handle(event interface{}) { handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)} + s.onInterface(event) + if handlers, ok := s.handlers[nil]; ok { for _, handler := range handlers { go handler.Call(handlerParameters) @@ -226,24 +228,32 @@ func (s *Session) initialize() { s.log(LogInformational, "called") s.handlersMu.Lock() + defer s.handlersMu.Unlock() + if s.handlers != nil { - s.handlersMu.Unlock() return } s.handlers = map[interface{}][]reflect.Value{} - s.handlersMu.Unlock() +} - s.AddHandler(s.onReady) - s.AddHandler(s.onResumed) - s.AddHandler(s.onVoiceServerUpdate) - s.AddHandler(s.onVoiceStateUpdate) - s.AddHandler(s.State.onReady) - s.AddHandler(s.State.onInterface) +// 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: + s.onReady(t) + case *Resumed: + s.onResumed(t) + case *VoiceServerUpdate: + go s.onVoiceServerUpdate(t) + case *VoiceStateUpdate: + go s.onVoiceStateUpdate(t) + } + s.State.onInterface(s, i) } // onReady handles the ready event. -func (s *Session) onReady(se *Session, r *Ready) { +func (s *Session) onReady(r *Ready) { // Store the SessionID within the Session struct. s.sessionID = r.SessionID @@ -253,7 +263,7 @@ func (s *Session) onReady(se *Session, r *Ready) { } // onResumed handles the resumed event. -func (s *Session) onResumed(se *Session, r *Resumed) { +func (s *Session) onResumed(r *Resumed) { // Start the heartbeat to keep the connection alive. go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) diff --git a/state.go b/state.go index 5b6ed07..fa5989b 100644 --- a/state.go +++ b/state.go @@ -639,6 +639,12 @@ 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 } diff --git a/wsapi.go b/wsapi.go index ab4f3ad..449531c 100644 --- a/wsapi.go +++ b/wsapi.go @@ -509,7 +509,7 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi } // onVoiceStateUpdate handles Voice State Update events on the data websocket. -func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { +func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { // If we don't have a connection for the channel, don't bother if st.ChannelID == "" { @@ -523,7 +523,7 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { } // We only care about events that are about us. - if se.State.User.ID != st.UserID { + if s.State.User.ID != st.UserID { return } @@ -537,7 +537,7 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { // 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) { s.log(LogInformational, "called") From b7c7e60fd581a21a92642bfb59eb7f5ac576cbe0 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 12 Nov 2016 11:59:51 -0800 Subject: [PATCH 57/70] Fix presence unmarshalling as the values are inconsistent from Discord. (#287) --- structs.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/structs.go b/structs.go index 6b73e5f..b2d8aad 100644 --- a/structs.go +++ b/structs.go @@ -14,6 +14,7 @@ package discordgo import ( "encoding/json" "reflect" + "strconv" "sync" "time" @@ -290,6 +291,37 @@ type Game struct { URL string `json:"url"` } +func (g *Game) UnmarshalJSON(bytes []byte) error { + temp := &struct { + Name string `json:"name"` + Type json.RawMessage `json:"type"` + URL string `json:"url"` + }{} + err := json.Unmarshal(bytes, temp) + if err != nil { + return err + } + g.Name = temp.Name + g.URL = temp.URL + + 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"` From 717c8f2538e1855ca6499a48d8de043aa1ace584 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sun, 13 Nov 2016 21:51:57 -0800 Subject: [PATCH 58/70] Support embed messages. --- restapi.go | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/restapi.go b/restapi.go index 1423fa9..fd4f8cd 100644 --- a/restapi.go +++ b/restapi.go @@ -1228,6 +1228,28 @@ func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *M return s.channelMessageSend(channelID, content, true) } +// ChannelMessageSendEmbed sends a message to the given channel with embedded data (bot only). +// channelID : The ID of a Channel. +// embed : The embed data to send. +func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (st *Message, err error) { + if embed != nil && embed.Type == "" { + embed.Type = "rich" + } + + data := struct { + Embed *MessageEmbed `json:"embed"` + }{embed} + + // Send the message to the given channel + response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID)) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + // ChannelMessageEdit edits an existing message, replacing it entirely with // the given content. // channeld : The ID of a Channel @@ -1680,8 +1702,6 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho uri += "?wait=true" } - fmt.Println(uri) - _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", "")) return @@ -1759,8 +1779,6 @@ func (s *Session) relationshipCreate(userID string, relationshipType int) (err e Type int `json:"type"` }{relationshipType} - fmt.Println("Data: " + fmt.Sprintf("%v", data)) - _, err = s.RequestWithBucketID("PUT", EndpointRelationship(userID), data, EndpointRelationships()) return } From 3f6a127baab537bc15968f647d07c0acb1323260 Mon Sep 17 00:00:00 2001 From: rytone Date: Sat, 19 Nov 2016 22:28:52 -0600 Subject: [PATCH 59/70] Support for editing messages with embed data (#290) --- restapi.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/restapi.go b/restapi.go index fd4f8cd..a9b08ee 100644 --- a/restapi.go +++ b/restapi.go @@ -1269,6 +1269,28 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st * return } +// ChannelMessageEditEmbed edits an existing message with embedded data (bot only). +// 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) (st *Message, err error) { + if embed != nil && embed.Type == "" { + embed.Type = "rich" + } + + data := struct { + Embed *MessageEmbed `json:"embed"` + }{embed} + + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(channelID, messageID), data, EndpointChannelMessage(channelID, "")) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + // ChannelMessageDelete deletes a message from the Channel. func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { From 03343b41fd0c96cc081597a647364caba5f7e3cb Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 23 Nov 2016 17:18:05 -0800 Subject: [PATCH 60/70] Update discord.go --- discord.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord.go b/discord.go index b3415c5..a42c77c 100644 --- a/discord.go +++ b/discord.go @@ -27,6 +27,8 @@ const VERSION = "0.14.0-dev" // 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 1bc3fb9cd7a0749bfd22db00d6dbe8e5125d6661 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 29 Nov 2016 19:05:09 -0800 Subject: [PATCH 61/70] Set the GuildID for Members and VoiceStates on ready. --- state.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/state.go b/state.go index fa5989b..b016f4b 100644 --- a/state.go +++ b/state.go @@ -625,6 +625,14 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { c.GuildID = g.ID s.channelMap[c.ID] = c } + + for _, m := range g.Members { + m.GuildID = g.ID + } + + for _, vs := range g.VoiceStates { + vs.GuildID = g.ID + } } for _, c := range s.PrivateChannels { From 54e0bd6087d5c616727c0322af200ce454167e19 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 29 Nov 2016 19:09:01 -0800 Subject: [PATCH 62/70] Fix Admin permission calculations. --- restapi.go | 2 +- state.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index a9b08ee..c108369 100644 --- a/restapi.go +++ b/restapi.go @@ -470,7 +470,7 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAllChannel } diff --git a/state.go b/state.go index b016f4b..d9e6b5d 100644 --- a/state.go +++ b/state.go @@ -792,7 +792,7 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i } } - if apermissions&PermissionManageRoles > 0 { + if apermissions&PermissionAdministrator > 0 { apermissions |= PermissionAllChannel } From 20d230cd04f054ff95f4e853d8fd17c01c75252b Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 29 Nov 2016 19:17:03 -0800 Subject: [PATCH 63/70] Update GuildID's on GuildAdd --- state.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/state.go b/state.go index d9e6b5d..201820e 100644 --- a/state.go +++ b/state.go @@ -81,15 +81,24 @@ func (s *State) GuildAdd(guild *Guild) error { guild.Emojis = g.Emojis } if guild.Members == nil { + for _, m := range g.Members { + m.GuildID = guild.ID + } guild.Members = g.Members } if guild.Presences == nil { guild.Presences = g.Presences } if guild.Channels == nil { + for _, c := range g.Channels { + c.GuildID = guild.ID + } guild.Channels = g.Channels } if guild.VoiceStates == nil { + for _, g := range g.VoiceStates { + g.GuildID = guild.ID + } guild.VoiceStates = g.VoiceStates } *g = *guild From d726f933750462824e63b8b24bac267ff0cfb2fe Mon Sep 17 00:00:00 2001 From: Matthew Gerstman Date: Sun, 27 Nov 2016 23:48:33 -0500 Subject: [PATCH 64/70] 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 c108369..9eead00 100644 --- a/restapi.go +++ b/restapi.go @@ -1559,6 +1559,13 @@ 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 315ae4523181e6d03eadc84f6f13018efdf142de Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Tue, 29 Nov 2016 20:23:10 -0800 Subject: [PATCH 65/70] Set GuildID on all members in discordgo land not in state. --- discord.go | 23 +++++++++++++++++++++++ state.go | 19 ------------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/discord.go b/discord.go index a42c77c..4730376 100644 --- a/discord.go +++ b/discord.go @@ -239,11 +239,34 @@ func (s *Session) initialize() { s.handlers = map[interface{}][]reflect.Value{} } +// 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 *Resumed: s.onResumed(t) case *VoiceServerUpdate: diff --git a/state.go b/state.go index 201820e..ee82f13 100644 --- a/state.go +++ b/state.go @@ -67,7 +67,6 @@ func (s *State) GuildAdd(guild *Guild) error { // Update the channels to point to the right guild, adding them to the channelMap as we go for _, c := range guild.Channels { - c.GuildID = guild.ID s.channelMap[c.ID] = c } @@ -81,24 +80,15 @@ func (s *State) GuildAdd(guild *Guild) error { guild.Emojis = g.Emojis } if guild.Members == nil { - for _, m := range g.Members { - m.GuildID = guild.ID - } guild.Members = g.Members } if guild.Presences == nil { guild.Presences = g.Presences } if guild.Channels == nil { - for _, c := range g.Channels { - c.GuildID = guild.ID - } guild.Channels = g.Channels } if guild.VoiceStates == nil { - for _, g := range g.VoiceStates { - g.GuildID = guild.ID - } guild.VoiceStates = g.VoiceStates } *g = *guild @@ -631,17 +621,8 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { s.guildMap[g.ID] = g for _, c := range g.Channels { - c.GuildID = g.ID s.channelMap[c.ID] = c } - - for _, m := range g.Members { - m.GuildID = g.ID - } - - for _, vs := range g.VoiceStates { - vs.GuildID = g.ID - } } for _, c := range s.PrivateChannels { From 2092185ec576ee54d03ec92a0bee7f9ac0cbd803 Mon Sep 17 00:00:00 2001 From: AI Date: Wed, 30 Nov 2016 21:57:22 +0500 Subject: [PATCH 66/70] Implement support for new member role endpoint (#289) Implements support for the new member role add and delete endpoint hammerandchisel/discord-api-docs#179 --- endpoints.go | 1 + restapi.go | 22 ++++++++++++++++++++++ structs.go | 1 + 3 files changed, 24 insertions(+) diff --git a/endpoints.go b/endpoints.go index 4b81983..f63240f 100644 --- a/endpoints.go +++ b/endpoints.go @@ -62,6 +62,7 @@ var ( 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" } diff --git a/restapi.go b/restapi.go index 9eead00..3704886 100644 --- a/restapi.go +++ b/restapi.go @@ -732,6 +732,28 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err 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, userID, roleID)) + + 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, userID, roleID)) + + return +} + // GuildChannels returns an array of Channel structures for all channels of a // given guild. // guildID : The ID of a Guild. diff --git a/structs.go b/structs.go index b2d8aad..f2d0a2e 100644 --- a/structs.go +++ b/structs.go @@ -291,6 +291,7 @@ type Game struct { URL string `json:"url"` } +// UnmarshalJSON unmarshals json to Game struct func (g *Game) UnmarshalJSON(bytes []byte) error { temp := &struct { Name string `json:"name"` From 9f7a7c9352f8d6b230fc7069990bfde10d900fe0 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 30 Nov 2016 17:59:17 -0800 Subject: [PATCH 67/70] Don't stomp on messages or permission overrides on ChannelUpdate. --- state.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/state.go b/state.go index ee82f13..71d9476 100644 --- a/state.go +++ b/state.go @@ -308,8 +308,12 @@ func (s *State) ChannelAdd(channel *Channel) error { // If the channel exists, replace it if c, ok := s.channelMap[channel.ID]; ok { - channel.Messages = c.Messages - channel.PermissionOverwrites = c.PermissionOverwrites + if c.Messages != nil { + channel.Messages = c.Messages + } + if c.PermissionOverwrites != nil { + channel.PermissionOverwrites = c.PermissionOverwrites + } *c = *channel return nil From 24a51f654fd1dec56c6d46e1c51f3d1bc786301e Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Wed, 30 Nov 2016 18:02:13 -0800 Subject: [PATCH 68/70] :ok_hand: --- state.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/state.go b/state.go index 71d9476..25dd3d1 100644 --- a/state.go +++ b/state.go @@ -308,10 +308,10 @@ func (s *State) ChannelAdd(channel *Channel) error { // If the channel exists, replace it if c, ok := s.channelMap[channel.ID]; ok { - if c.Messages != nil { + if channel.Messages == nil { channel.Messages = c.Messages } - if c.PermissionOverwrites != nil { + if channel.PermissionOverwrites == nil { channel.PermissionOverwrites = c.PermissionOverwrites } From 36601253a4774c5fd6ec1949d7b815c1584639b7 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 3 Dec 2016 17:30:07 -0800 Subject: [PATCH 69/70] Remove use of reflect. This introduces gogenerate'ed EventHandlers from the files in events.go This also adds support for AddHandlerOnce. --- discord.go | 172 +----- discord_test.go | 8 +- event.go | 238 ++++++++ eventhandlers.go | 977 ++++++++++++++++++++++++++++++++ events.go | 243 ++++---- restapi.go | 2 +- structs.go | 96 +--- tools/cmd/eventhandlers/main.go | 123 ++++ wsapi.go | 35 +- 9 files changed, 1505 insertions(+), 389 deletions(-) create mode 100644 event.go create mode 100644 eventhandlers.go create mode 100644 tools/cmd/eventhandlers/main.go diff --git a/discord.go b/discord.go index 4730376..cda52f3 100644 --- a/discord.go +++ b/discord.go @@ -13,10 +13,7 @@ // Package discordgo provides Discord binding for Go package discordgo -import ( - "fmt" - "reflect" -) +import "fmt" // VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) const VERSION = "0.14.0-dev" @@ -126,170 +123,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) - - s.handlers[eventType] = append(s.handlers[eventType], 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)} - - s.onInterface(event) - - if handlers, ok := s.handlers[nil]; ok { - for _, handler := range handlers { - go handler.Call(handlerParameters) - } - } - - if handlers, ok := s.handlers[reflect.TypeOf(event)]; ok { - for _, handler := range handlers { - go handler.Call(handlerParameters) - } - } -} - -// initialize adds all internal handlers and state tracking handlers. -func (s *Session) initialize() { - - s.log(LogInformational, "called") - - s.handlersMu.Lock() - defer s.handlersMu.Unlock() - - if s.handlers != nil { - return - } - - s.handlers = map[interface{}][]reflect.Value{} -} - -// 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 *Resumed: - s.onResumed(t) - case *VoiceServerUpdate: - go s.onVoiceServerUpdate(t) - case *VoiceStateUpdate: - go s.onVoiceStateUpdate(t) - } - s.State.onInterface(s, i) -} - -// onReady handles the ready event. -func (s *Session) onReady(r *Ready) { - - // Store the SessionID within the Session struct. - s.sessionID = r.SessionID - - // Start the heartbeat to keep the connection alive. - go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) -} - -// onResumed handles the resumed event. -func (s *Session) onResumed(r *Resumed) { - - // Start the heartbeat to keep the connection alive. - go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) -} diff --git a/discord_test.go b/discord_test.go index cd892a9..afac0bc 100644 --- a/discord_test.go +++ b/discord_test.go @@ -223,8 +223,8 @@ 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) @@ -253,11 +253,11 @@ func TestRemoveHandler(t *testing.T) { 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) diff --git a/event.go b/event.go new file mode 100644 index 0000000..245f0c1 --- /dev/null +++ b/event.go @@ -0,0 +1,238 @@ +package discordgo + +import "fmt" + +// 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) error { + if _, ok := registeredInterfaceProviders[eh.Type()]; ok { + return fmt.Errorf("event %s already registered", eh.Type()) + } + registeredInterfaceProviders[eh.Type()] = eh + return nil +} + +// 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] { + go eh.eventHandler.Handle(s, i) + } + + if len(s.onceHandlers[t]) > 0 { + for _, eh := range s.onceHandlers[t] { + 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 *Resumed: + s.onResumed(t) + case *VoiceServerUpdate: + go s.onVoiceServerUpdate(t) + case *VoiceStateUpdate: + go s.onVoiceStateUpdate(t) + } + s.State.onInterface(s, i) +} + +// onReady handles the ready event. +func (s *Session) onReady(r *Ready) { + + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID + + // Start the heartbeat to keep the connection alive. + go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) +} + +// onResumed handles the resumed event. +func (s *Session) onResumed(r *Resumed) { + + // Start the heartbeat to keep the connection alive. + go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) +} diff --git a/eventhandlers.go b/eventhandlers.go new file mode 100644 index 0000000..6d78bac --- /dev/null +++ b/eventhandlers.go @@ -0,0 +1,977 @@ +// 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" + messageReactionAddEventType = "MESSAGE_REACTION_ADD" + messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" + 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" + 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 +} + +// New returns a new instance of Connect. +func (eh connectEventHandler) New() interface{} { + return &Connect{} +} + +// 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 +} + +// New returns a new instance of Disconnect. +func (eh disconnectEventHandler) New() interface{} { + return &Disconnect{} +} + +// 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 +} + +// New returns a new instance of Event. +func (eh eventEventHandler) New() interface{} { + return &Event{} +} + +// 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) + } +} + +// 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) + } +} + +// 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 +} + +// New returns a new instance of RateLimit. +func (eh rateLimitEventHandler) New() interface{} { + return &RateLimit{} +} + +// 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) + } +} + +// 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, *MessageReactionAdd): + return messageReactionAddEventHandler(v) + case func(*Session, *MessageReactionRemove): + return messageReactionRemoveEventHandler(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, *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(messageReactionAddEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveEventHandler(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(userSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userUpdateEventHandler(nil)) + registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) + registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) +} diff --git a/events.go b/events.go index fc1c979..19c11bd 100644 --- a/events.go +++ b/events.go @@ -1,187 +1,238 @@ 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{}, - "CHANNEL_PINS_UPDATE": ChannelPinsUpdate{}, - "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{}, - "GUILD_MEMBERS_CHUNK": GuildMembersChunk{}, - "MESSAGE_ACK": MessageAck{}, - "MESSAGE_CREATE": MessageCreate{}, - "MESSAGE_UPDATE": MessageUpdate{}, - "MESSAGE_DELETE": MessageDelete{}, - "MESSAGE_REACTION_ADD": MessageReactionAdd{}, - "MESSAGE_REACTION_REMOVE": MessageReactionRemove{}, - "PRESENCE_UPDATE": PresenceUpdate{}, - "PRESENCES_REPLACE": PresencesReplace{}, - "READY": Ready{}, - "RELATIONSHIP_ADD": RelationshipAdd{}, - "RELATIONSHIP_REMOVE": RelationshipRemove{}, - "USER_UPDATE": UserUpdate{}, - "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, - "USER_GUILD_SETTINGS_UPDATE": UserGuildSettingsUpdate{}, - "TYPING_START": TypingStart{}, - "VOICE_SERVER_UPDATE": VoiceServerUpdate{}, - "VOICE_STATE_UPDATE": VoiceStateUpdate{}, - "RESUMED": Resumed{}, -} +import ( + "encoding/json" + "time" +) -// Connect is an empty struct for an event. +// 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 + +// Connect is the data for a Connect event. +// This is a sythetic event and is not dispatched by Discord. type Connect struct{} -// Disconnect is an empty struct for an event. +// Disconnect is the data for a Disconnect event. +// This is a sythetic event and is not dispatched by Discord. type Disconnect struct{} -// RateLimit is a struct for the RateLimited 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 } -// MessageCreate is a wrapper struct for an event. -type MessageCreate struct { - *Message +// Event provides a basic initial struct for all websocket events. +type Event struct { + Operation int `json:"op"` + Sequence int `json:"s"` + Type string `json:"t"` + RawData json.RawMessage `json:"d"` + // Struct contains one of the other types in this file. + Struct interface{} `json:"-"` } -// MessageUpdate is a wrapper struct for an event. -type MessageUpdate struct { - *Message +// 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"` + + // Undocumented fields + Settings *Settings `json:"user_settings"` + UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` + Relationships []*Relationship `json:"relationships"` + Presences []*Presence `json:"presences"` } -// MessageDelete is a wrapper struct for an event. -type MessageDelete struct { - *Message -} - -// MessageReactionAdd is a wrapper struct for an event. -type MessageReactionAdd struct { - *MessageReaction -} - -// MessageReactionRemove is a wrapper struct for an event. -type MessageReactionRemove struct { - *MessageReaction -} - -// ChannelCreate is a wrapper struct for an event. +// 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 { 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 { 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 } -// PresencesReplace is an array of Presences for an event. +// 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 +} + +// 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 +} + +// PresencesReplace is the data for a PresencesReplace event. type PresencesReplace []*Presence -// RelationshipAdd is a wrapper struct for an event. +// 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 { + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + Trace []string `json:"_trace"` +} + +// RelationshipAdd is the data for a RelationshipAdd event. type RelationshipAdd struct { *Relationship } -// RelationshipRemove is a wrapper struct for an event. +// RelationshipRemove is the data for a RelationshipRemove event. type RelationshipRemove struct { *Relationship } -// VoiceStateUpdate is a wrapper struct for an event. -type VoiceStateUpdate struct { - *VoiceState +// 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 a wrapper struct for an event. +// UserUpdate is the data for a UserUpdate event. type UserUpdate struct { *User } -// UserSettingsUpdate is a map for an event. +// UserSettingsUpdate is the data for a UserSettingsUpdate event. type UserSettingsUpdate map[string]interface{} -// UserGuildSettingsUpdate is a map for an event. +// UserGuildSettingsUpdate is the data for a UserGuildSettingsUpdate event. type UserGuildSettingsUpdate struct { *UserGuildSettings } + +// 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 +} diff --git a/restapi.go b/restapi.go index 3704886..5389c86 100644 --- a/restapi.go +++ b/restapi.go @@ -146,7 +146,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID return } s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) - s.handle(RateLimit{TooManyRequests: &rl, URL: urlStr}) + s.handleEvent(rateLimitEventType, RateLimit{TooManyRequests: &rl, URL: urlStr}) time.Sleep(rl.RetryAfter * time.Millisecond) // we can make the above smarter diff --git a/structs.go b/structs.go index f2d0a2e..548ee52 100644 --- a/structs.go +++ b/structs.go @@ -13,7 +13,6 @@ package discordgo import ( "encoding/json" - "reflect" "strconv" "sync" "time" @@ -74,13 +73,10 @@ type Session struct { // 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 + // Event handlers + handlersMu sync.RWMutex + handlers map[string][]*eventHandlerInstance + onceHandlers map[string][]*eventHandlerInstance // The websocket connection. wsConn *websocket.Conn @@ -110,12 +106,6 @@ type rateLimitMutex struct { // bucket map[string]*sync.Mutex // TODO :) } -// A Resumed struct holds the data received in a RESUMED event -type Resumed struct { - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - Trace []string `json:"_trace"` -} - // A VoiceRegion stores data for a specific voice region server. type VoiceRegion struct { ID string `json:"id"` @@ -385,32 +375,6 @@ type FriendSourceFlags struct { MutualFriends bool `json:"mutual_friends"` } -// An Event provides a basic initial struct for all websocket event. -type Event struct { - Operation int `json:"op"` - Sequence int `json:"s"` - Type string `json:"t"` - RawData json.RawMessage `json:"d"` - 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"` - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - 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"` -} - // A Relationship between the logged in user and Relationship.User type Relationship struct { User *User `json:"user"` @@ -433,67 +397,23 @@ type ReadState struct { 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"` -} - -// A PresenceUpdate stores data for the presence update websocket event. -type PresenceUpdate struct { - Presence - GuildID string `json:"guild_id"` - Roles []string `json:"roles"` -} - -// A MessageAck stores data for the message ack websocket event. -type MessageAck struct { - MessageID string `json:"message_id"` - ChannelID string `json:"channel_id"` -} - // An Ack is used to ack messages type Ack struct { Token string `json:"token"` } -// A GuildIntegrationsUpdate stores data for the guild integrations update -// websocket event. -type GuildIntegrationsUpdate struct { - GuildID string `json:"guild_id"` -} - -// A GuildRole stores data for guild role websocket events. +// A GuildRole stores data for guild roles. type GuildRole struct { Role *Role `json:"role"` GuildID string `json:"guild_id"` } -// 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 GuildBan stores data for a guild ban. type GuildBan struct { Reason string `json:"reason"` User *User `json:"user"` } -// A GuildEmojisUpdate stores data for a guild emoji update event. -type GuildEmojisUpdate struct { - GuildID string `json:"guild_id"` - Emojis []*Emoji `json:"emojis"` -} - -// A GuildMembersChunk stores data for the Guild Members Chunk websocket event. -type GuildMembersChunk struct { - GuildID string `json:"guild_id"` - Members []*Member `json:"members"` -} - // A GuildIntegration stores data for a guild integration. type GuildIntegration struct { ID string `json:"id"` @@ -553,12 +473,6 @@ type APIErrorMessage struct { Message string `json:"message"` } -// ChannelPinsUpdate stores data for the channel pins update event -type ChannelPinsUpdate struct { - LastPinTimestamp string `json:"last_pin_timestamp"` - ChannelID string `json:"channel_id"` -} - // Webhook stores the data for a webhook. type Webhook struct { ID string `json:"id"` diff --git a/tools/cmd/eventhandlers/main.go b/tools/cmd/eventhandlers/main.go new file mode 100644 index 0000000..f389408 --- /dev/null +++ b/tools/cmd/eventhandlers/main.go @@ -0,0 +1,123 @@ +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 +} + +// New returns a new instance of {{.}}. +func (eh {{privateName .}}EventHandler) New() interface{} { + return &{{.}}{} +} + +// 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/wsapi.go b/wsapi.go index 449531c..99a6236 100644 --- a/wsapi.go +++ b/wsapi.go @@ -18,7 +18,6 @@ import ( "fmt" "io" "net/http" - "reflect" "runtime" "time" @@ -121,9 +120,8 @@ func (s *Session) Open() (err error) { s.Unlock() - s.initialize() s.log(LogInformational, "emit connect event") - s.handle(&Connect{}) + s.handleEvent(connectEventType, &Connect{}) s.log(LogInformational, "exiting") return @@ -409,16 +407,12 @@ func (s *Session) onEvent(messageType int, message []byte) { // Store the message sequence s.sequence = e.Sequence - // Map event to registered event handlers and pass it along - // to any registered functions - i := eventToInterface[e.Type] - if i != nil { - - // Create a new instance of the event type. - i = reflect.New(reflect.TypeOf(i)).Interface() + // 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 err = json.Unmarshal(e.RawData, i); err != nil { + if err = json.Unmarshal(e.RawData, e.Struct); err != nil { s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) } @@ -429,30 +423,19 @@ func (s *Session) onEvent(messageType int, message []byte) { // 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. - go s.handle(i) - + s.handleEvent(e.Type, e.Struct) } else { s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) } - // Emit event to the OnEvent handler - e.Struct = i - go s.handle(e) + // 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"` @@ -712,7 +695,7 @@ func (s *Session) Close() (err error) { s.Unlock() s.log(LogInformational, "emit disconnect event") - s.handle(&Disconnect{}) + s.handleEvent(disconnectEventType, &Disconnect{}) return } From e6500210377edf0df966860dce48edd29bda23c9 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 8 Dec 2016 10:16:37 -0600 Subject: [PATCH 70/70] Test against 1.6 and 1.7, make lint fatal again. --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0febcbf..7594fea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - 1.6 - - tip + - 1.7 install: - go get github.com/bwmarrin/discordgo - go get -v . @@ -9,5 +9,5 @@ install: script: - diff <(gofmt -d .) <(echo -n) - go vet -x ./... - - golint ./... + - golint -set_exit_status ./... - go test -v -race ./...