From c40b9753542ab8a3cce8bd949a3dea67b5a38b70 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Fri, 28 Apr 2017 21:34:30 +0200 Subject: [PATCH 01/17] STOP USING PRINTF PLS --- user.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user.go b/user.go index b3a7e4b..cdc8a85 100644 --- a/user.go +++ b/user.go @@ -1,7 +1,5 @@ package discordgo -import "fmt" - // A User stores all data for an individual Discord user. type User struct { ID string `json:"id"` @@ -17,10 +15,10 @@ type User struct { // String returns a unique identifier of the form username#discriminator func (u *User) String() string { - return fmt.Sprintf("%s#%s", u.Username, u.Discriminator) + return u.Username + "#" + u.Discriminator } // Mention return a string which mentions the user func (u *User) Mention() string { - return fmt.Sprintf("<@%s>", u.ID) + return "<@" + u.ID + ">" } From 27db9ad6df27c62f81f77ec7b45c5f5d2763c18b Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Thu, 4 May 2017 20:23:17 +0200 Subject: [PATCH 02/17] DIE, PRINTF. DIE! --- message.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/message.go b/message.go index 13c2da0..b743327 100644 --- a/message.go +++ b/message.go @@ -10,7 +10,6 @@ package discordgo import ( - "fmt" "io" "regexp" ) @@ -174,7 +173,7 @@ func (m *Message) ContentWithMentionsReplaced() string { } content := m.Content for _, user := range m.Mentions { - content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username) + content = regexp.MustCompile("<@!?("+regexp.QuoteMeta(user.ID)+")>").ReplaceAllString(content, "@"+user.Username) } return content } From b813c5d0ca183265579f9cfd75a8693cce121261 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Sun, 23 Jul 2017 08:27:41 +0200 Subject: [PATCH 03/17] Merge branch 'develop' into 1 --- .travis.yml | 2 +- README.md | 1 - discord.go | 5 +- discord_test.go | 12 +++- endpoints.go | 36 ++++++------ message.go | 77 ++++++++++++++++++++++---- message_test.go | 41 ++++++++++++++ restapi.go | 142 ++++++++++++++++++++++++++++++++++-------------- restapi_test.go | 11 ++++ state.go | 28 ++++++---- structs.go | 19 ++++++- user.go | 15 +++++ voice.go | 6 +- wsapi.go | 64 ++++++++++++++++++---- 14 files changed, 357 insertions(+), 102 deletions(-) create mode 100644 message_test.go diff --git a/.travis.yml b/.travis.yml index 440159d..92beb2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,5 +10,5 @@ install: script: - diff <(gofmt -d .) <(echo -n) - go vet -x ./... - - golint -set_exit_status ./... + - golint ./... - go test -v -race ./... diff --git a/README.md b/README.md index ac9dbcd..eb9f14f 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,6 @@ that information in a nice format. - [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) - [![Go Walker](http://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/bwmarrin/discordgo) -- [Unofficial Discord API Documentation](https://discordapi.readthedocs.org/en/latest/) - Hand crafted documentation coming eventually. diff --git a/discord.go b/discord.go index ef5bf28..04d4719 100644 --- a/discord.go +++ b/discord.go @@ -20,8 +20,8 @@ import ( "time" ) -// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) -const VERSION = "0.16.0-dev" +// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) +const VERSION = "0.17.0-dev" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled") @@ -59,6 +59,7 @@ func New(args ...interface{}) (s *Session, err error) { MaxRestRetries: 3, Client: &http.Client{Timeout: (20 * time.Second)}, sequence: new(int64), + LastHeartbeatAck: time.Now().UTC(), } // If no arguments are passed return the empty Session interface. diff --git a/discord_test.go b/discord_test.go index afac0bc..ca4472a 100644 --- a/discord_test.go +++ b/discord_test.go @@ -11,9 +11,11 @@ import ( ////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////// VARS NEEDED FOR TESTING var ( - dg *Session // Stores global discordgo session + dg *Session // Stores a global discordgo user session + dgBot *Session // Stores a global discordgo bot session - envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating + envToken = os.Getenv("DG_TOKEN") // Token to use when authenticating the user account + envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account envEmail = os.Getenv("DG_EMAIL") // Email to use when authenticating envPassword = os.Getenv("DG_PASSWORD") // Password to use when authenticating envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests @@ -23,6 +25,12 @@ var ( ) func init() { + if envBotToken != "" { + if d, err := New(envBotToken); err == nil { + dgBot = d + } + } + if envEmail == "" || envPassword == "" || envToken == "" { return } diff --git a/endpoints.go b/endpoints.go index 96bcf28..0ecdf0b 100644 --- a/endpoints.go +++ b/endpoints.go @@ -18,13 +18,14 @@ var ( EndpointSmActive = EndpointSm + "active.json" EndpointSmUpcoming = EndpointSm + "upcoming.json" - EndpointDiscord = "https://discordapp.com/" - EndpointAPI = EndpointDiscord + "api/" - EndpointGuilds = EndpointAPI + "guilds/" - EndpointChannels = EndpointAPI + "channels/" - EndpointUsers = EndpointAPI + "users/" - EndpointGateway = EndpointAPI + "gateway" - EndpointWebhooks = EndpointAPI + "webhooks/" + EndpointDiscord = "https://discordapp.com/" + EndpointAPI = EndpointDiscord + "api/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + EndpointGatewayBot = EndpointGateway + "/bot" + EndpointWebhooks = EndpointAPI + "webhooks/" EndpointCDN = "https://cdn.discordapp.com/" EndpointCDNAttachments = EndpointCDN + "attachments/" @@ -54,16 +55,17 @@ var ( EndpointReport = EndpointAPI + "report" EndpointIntegrations = EndpointAPI + "integrations" - EndpointUser = func(uID string) string { return EndpointUsers + uID } - EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } - EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } - EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } - EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } - EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } - EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } - EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } - EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } - EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } + EndpointUser = func(uID string) string { return EndpointUsers + uID } + EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } + EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } diff --git a/message.go b/message.go index b743327..d46f3f3 100644 --- a/message.go +++ b/message.go @@ -12,6 +12,7 @@ package discordgo import ( "io" "regexp" + "strings" ) // A Message stores all data related to a specific Discord message. @@ -33,8 +34,9 @@ type Message struct { // File stores info about files you e.g. send in messages. type File struct { - Name string - Reader io.Reader + Name string + ContentType string + Reader io.Reader } // MessageSend stores all parameters you can send with ChannelMessageSendComplex. @@ -42,7 +44,10 @@ type MessageSend struct { Content string `json:"content,omitempty"` Embed *MessageEmbed `json:"embed,omitempty"` Tts bool `json:"tts"` - File *File `json:"file"` + Files []*File `json:"-"` + + // TODO: Remove this when compatibility is not required. + File *File `json:"-"` } // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which @@ -167,13 +172,65 @@ type MessageReactions struct { // ContentWithMentionsReplaced will replace all @ mentions with the // username of the mention. -func (m *Message) ContentWithMentionsReplaced() string { - if m.Mentions == nil { - return m.Content - } - content := m.Content +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + for _, user := range m.Mentions { - content = regexp.MustCompile("<@!?("+regexp.QuoteMeta(user.ID)+")>").ReplaceAllString(content, "@"+user.Username) + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+user.Username, + ).Replace(content) } - return content + return +} + +var patternChannels = regexp.MustCompile("<#[^>]*>") + +// ContentWithMoreMentionsReplaced will replace all @ mentions with the +// username of the mention, but also role IDs and more. +func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) { + content = m.Content + + if !s.StateEnabled { + content = m.ContentWithMentionsReplaced() + return + } + + channel, err := s.State.Channel(m.ChannelID) + if err != nil { + content = m.ContentWithMentionsReplaced() + return + } + + for _, user := range m.Mentions { + nick := user.Username + + member, err := s.State.Member(channel.GuildID, user.ID) + if err == nil && member.Nick != "" { + nick = member.Nick + } + + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+nick, + ).Replace(content) + } + for _, roleID := range m.MentionRoles { + role, err := s.State.Role(channel.GuildID, roleID) + if err != nil || !role.Mentionable { + continue + } + + content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) + } + + content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { + channel, err := s.State.Channel(mention[2 : len(mention)-1]) + if err != nil || channel.Type == "voice" { + return mention + } + + return "#" + channel.Name + }) + return } diff --git a/message_test.go b/message_test.go new file mode 100644 index 0000000..fd2f133 --- /dev/null +++ b/message_test.go @@ -0,0 +1,41 @@ +package discordgo + +import ( + "testing" +) + +func TestContentWithMoreMentionsReplaced(t *testing.T) { + s := &Session{StateEnabled: true, State: NewState()} + + user := &User{ + ID: "user", + Username: "User Name", + } + + s.StateEnabled = true + s.State.GuildAdd(&Guild{ID: "guild"}) + s.State.RoleAdd("guild", &Role{ + ID: "role", + Name: "Role Name", + Mentionable: true, + }) + s.State.MemberAdd(&Member{ + User: user, + Nick: "User Nick", + GuildID: "guild", + }) + s.State.ChannelAdd(&Channel{ + Name: "Channel Name", + GuildID: "guild", + ID: "channel", + }) + m := &Message{ + Content: "<&role> <@!user> <@user> <#channel>", + ChannelID: "channel", + MentionRoles: []string{"role"}, + Mentions: []*User{user}, + } + if result, _ := m.ContentWithMoreMentionsReplaced(s); result != "@Role Name @User Nick @User Name #Channel Name" { + t.Error(result) + } +} diff --git a/restapi.go b/restapi.go index 7c9fd81..bd944f2 100644 --- a/restapi.go +++ b/restapi.go @@ -23,14 +23,22 @@ import ( "log" "mime/multipart" "net/http" + "net/textproto" "net/url" "strconv" "strings" "time" ) -// ErrJSONUnmarshal is returned for JSON Unmarshall errors. -var ErrJSONUnmarshal = errors.New("json unmarshal") +// All error constants +var ( + ErrJSONUnmarshal = errors.New("json unmarshal") + ErrStatusOffline = errors.New("You can't set your Status to offline") + ErrVerificationLevelBounds = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") + ErrGuildNoIcon = errors.New("guild does not have an icon set") + ErrGuildNoSplash = errors.New("guild does not have a splash set") +) // Request 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) { @@ -302,8 +310,8 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri // If left blank, avatar will be set to null/blank data := struct { - Email string `json:"email"` - Password string `json:"password"` + Email string `json:"email,omitempty"` + Password string `json:"password,omitempty"` Username string `json:"username,omitempty"` Avatar string `json:"avatar,omitempty"` NewPassword string `json:"new_password,omitempty"` @@ -334,7 +342,7 @@ func (s *Session) UserSettings() (st *Settings, err error) { // status : The new status (Actual valid status are 'online','idle','dnd','invisible') func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { if status == StatusOffline { - err = errors.New("You can't set your Status to offline") + err = ErrStatusOffline return } @@ -595,7 +603,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error if g.VerificationLevel != nil { val := *g.VerificationLevel if val < 0 || val > 3 { - err = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + err = ErrVerificationLevelBounds return } } @@ -756,7 +764,21 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { // userID : The ID of a User func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) + return s.GuildMemberDeleteWithReason(guildID, userID, "") +} + +// GuildMemberDelete removes the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for the kick +func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) { + + uri := EndpointGuildMember(guildID, userID) + if reason != "" { + uri += "?reason=" + url.QueryEscape(reason) + } + + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, "")) return } @@ -988,7 +1010,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er count = 0 if days <= 0 { - err = errors.New("the number of days should be more than or equal to 1") + err = ErrPruneDaysBounds return } @@ -1018,7 +1040,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err count = 0 if days <= 0 { - err = errors.New("the number of days should be more than or equal to 1") + err = ErrPruneDaysBounds return } @@ -1120,7 +1142,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { } if g.Icon == "" { - err = errors.New("guild does not have an icon set") + err = ErrGuildNoIcon return } @@ -1142,7 +1164,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { } if g.Splash == "" { - err = errors.New("guild does not have a splash set") + err = ErrGuildNoSplash return } @@ -1309,6 +1331,8 @@ func (s *Session) ChannelMessageSend(channelID string, content string) (*Message }) } +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + // ChannelMessageSendComplex sends a message to the given channel. // channelID : The ID of a Channel. // data : The message struct to send. @@ -1319,48 +1343,62 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) endpoint := EndpointChannelMessages(channelID) - var response []byte + // TODO: Remove this when compatibility is not required. + files := data.Files if data.File != nil { + if files == nil { + files = []*File{data.File} + } else { + err = fmt.Errorf("cannot specify both File and Files") + return + } + } + + var response []byte + if len(files) > 0 { body := &bytes.Buffer{} bodywriter := multipart.NewWriter(body) - // What's a better way of doing this? Reflect? Generator? I'm open to suggestions - - if data.Content != "" { - if err = bodywriter.WriteField("content", data.Content); err != nil { - return - } - } - - if data.Embed != nil { - var embed []byte - embed, err = json.Marshal(data.Embed) - if err != nil { - return - } - err = bodywriter.WriteField("embed", string(embed)) - if err != nil { - return - } - } - - if data.Tts { - if err = bodywriter.WriteField("tts", "true"); err != nil { - return - } - } - - var writer io.Writer - writer, err = bodywriter.CreateFormFile("file", data.File.Name) + var payload []byte + payload, err = json.Marshal(data) if err != nil { return } - _, err = io.Copy(writer, data.File.Reader) + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) if err != nil { return } + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } + err = bodywriter.Close() if err != nil { return @@ -1678,6 +1716,28 @@ func (s *Session) Gateway() (gateway string, err error) { return } +// GatewayBot returns the websocket Gateway address and the recommended number of shards +func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) { + + response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot) + if err != nil { + return + } + + err = unmarshal(response, &st) + if err != nil { + return + } + + // Ensure the gateway always has a trailing slash. + // MacOS will fail to connect if we add query params without a trailing slash on the base domain. + if !strings.HasSuffix(st.URL, "/") { + st.URL += "/" + } + + return +} + // Functions specific to Webhooks // WebhookCreate returns a new Webhook. diff --git a/restapi_test.go b/restapi_test.go index a5d326b..7aa4e60 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -166,6 +166,17 @@ func TestGateway(t *testing.T) { } } +func TestGatewayBot(t *testing.T) { + + if dgBot == nil { + t.Skip("Skipping, dgBot not set.") + } + _, err := dgBot.GatewayBot() + if err != nil { + t.Errorf("GatewayBot() returned error: %+v", err) + } +} + func TestVoiceICE(t *testing.T) { if dg == nil { diff --git a/state.go b/state.go index c2c5519..7400ef6 100644 --- a/state.go +++ b/state.go @@ -21,6 +21,10 @@ import ( // ErrNilState is returned when the state is nil. var ErrNilState = errors.New("state not instantiated, please use discordgo.New() or assign Session.State") +// ErrStateNotFound is returned when the state cache +// requested is not found +var ErrStateNotFound = errors.New("state cache not found") + // A State contains the current known state. // As discord sends this in a READY blob, it seems reasonable to simply // use that struct as the data store. @@ -146,7 +150,7 @@ func (s *State) Guild(guildID string) (*Guild, error) { return g, nil } - return nil, errors.New("guild not found") + return nil, ErrStateNotFound } // PresenceAdd adds a presence to the current world state, or @@ -227,7 +231,7 @@ func (s *State) PresenceRemove(guildID string, presence *Presence) error { } } - return errors.New("presence not found") + return ErrStateNotFound } // Presence gets a presence by ID from a guild. @@ -247,7 +251,7 @@ func (s *State) Presence(guildID, userID string) (*Presence, error) { } } - return nil, errors.New("presence not found") + return nil, ErrStateNotFound } // TODO: Consider moving Guild state update methods onto *Guild. @@ -299,7 +303,7 @@ func (s *State) MemberRemove(member *Member) error { } } - return errors.New("member not found") + return ErrStateNotFound } // Member gets a member by ID from a guild. @@ -322,7 +326,7 @@ func (s *State) Member(guildID, userID string) (*Member, error) { } } - return nil, errors.New("member not found") + return nil, ErrStateNotFound } // RoleAdd adds a role to the current world state, or @@ -372,7 +376,7 @@ func (s *State) RoleRemove(guildID, roleID string) error { } } - return errors.New("role not found") + return ErrStateNotFound } // Role gets a role by ID from a guild. @@ -395,7 +399,7 @@ func (s *State) Role(guildID, roleID string) (*Role, error) { } } - return nil, errors.New("role not found") + return nil, ErrStateNotFound } // ChannelAdd adds a channel to the current world state, or @@ -428,7 +432,7 @@ func (s *State) ChannelAdd(channel *Channel) error { } else { guild, ok := s.guildMap[channel.GuildID] if !ok { - return errors.New("guild for channel not found") + return ErrStateNotFound } guild.Channels = append(guild.Channels, channel) @@ -507,7 +511,7 @@ func (s *State) Channel(channelID string) (*Channel, error) { return c, nil } - return nil, errors.New("channel not found") + return nil, ErrStateNotFound } // Emoji returns an emoji for a guild and emoji id. @@ -530,7 +534,7 @@ func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) { } } - return nil, errors.New("emoji not found") + return nil, ErrStateNotFound } // EmojiAdd adds an emoji to the current world state. @@ -647,7 +651,7 @@ func (s *State) messageRemoveByID(channelID, messageID string) error { } } - return errors.New("message not found") + return ErrStateNotFound } func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { @@ -701,7 +705,7 @@ func (s *State) Message(channelID, messageID string) (*Message, error) { } } - return nil, errors.New("message not found") + return nil, ErrStateNotFound } // OnReady takes a Ready event and updates all internal state. diff --git a/structs.go b/structs.go index 3a6ec05..9697fa5 100644 --- a/structs.go +++ b/structs.go @@ -78,6 +78,9 @@ type Session struct { // The http client used for REST requests Client *http.Client + // Stores the last HeartbeatAck that was recieved (in UTC) + LastHeartbeatAck time.Time + // Event handlers handlersMu sync.RWMutex handlers map[string][]*eventHandlerInstance @@ -304,7 +307,7 @@ type Game struct { // UnmarshalJSON unmarshals json to Game struct func (g *Game) UnmarshalJSON(bytes []byte) error { temp := &struct { - Name string `json:"name"` + Name json.Number `json:"name"` Type json.RawMessage `json:"type"` URL string `json:"url"` }{} @@ -312,8 +315,8 @@ func (g *Game) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - g.Name = temp.Name g.URL = temp.URL + g.Name = temp.Name.String() if temp.Type != nil { err = json.Unmarshal(temp.Type, &g.Type) @@ -509,6 +512,12 @@ type MessageReaction struct { ChannelID string `json:"channel_id"` } +// GatewayBotResponse stores the data for the gateway/bot response +type GatewayBotResponse struct { + URL string `json:"url"` + Shards int `json:"shards"` +} + // Constants for the different bit offsets of text channel permissions const ( PermissionReadMessages = 1 << (iota + 10) @@ -549,6 +558,8 @@ const ( PermissionAdministrator PermissionManageChannels PermissionManageServer + PermissionAddReactions + PermissionViewAuditLogs PermissionAllText = PermissionReadMessages | PermissionSendMessages | @@ -568,7 +579,9 @@ const ( PermissionAllVoice | PermissionCreateInstantInvite | PermissionManageRoles | - PermissionManageChannels + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs PermissionAll = PermissionAllChannel | PermissionKickMembers | PermissionBanMembers | diff --git a/user.go b/user.go index cdc8a85..8abffb0 100644 --- a/user.go +++ b/user.go @@ -1,5 +1,7 @@ package discordgo +import "strings" + // A User stores all data for an individual Discord user. type User struct { ID string `json:"id"` @@ -22,3 +24,16 @@ func (u *User) String() string { func (u *User) Mention() string { return "<@" + u.ID + ">" } + +// AvatarURL returns a URL to the user's avatar. +// size: The size of the user's avatar as a power of two +func (u *User) AvatarURL(size string) string { + var URL string + if strings.HasPrefix(u.Avatar, "a_") { + URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) + } else { + URL = EndpointUserAvatar(u.ID, u.Avatar) + } + + return URL + "?size=" + size +} diff --git a/voice.go b/voice.go index 8b566f4..5bbd0ad 100644 --- a/voice.go +++ b/voice.go @@ -814,7 +814,11 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) if c != nil { - c <- &p + select { + case c <- &p: + case <-close: + return + } } } } diff --git a/wsapi.go b/wsapi.go index adab402..213ea72 100644 --- a/wsapi.go +++ b/wsapi.go @@ -25,6 +25,18 @@ import ( "github.com/gorilla/websocket" ) +// ErrWSAlreadyOpen is thrown when you attempt to open +// a websocket that already is open. +var ErrWSAlreadyOpen = errors.New("web socket already opened") + +// ErrWSNotFound is thrown when you attempt to use a websocket +// that doesn't exist +var ErrWSNotFound = errors.New("no websocket connection exists") + +// ErrWSShardBounds is thrown when you try to use a shard ID that is +// less than the total shard count +var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount") + type resumePacket struct { Op int `json:"op"` Data struct { @@ -58,7 +70,7 @@ func (s *Session) Open() (err error) { } if s.wsConn != nil { - err = errors.New("web socket already opened") + err = ErrWSAlreadyOpen return } @@ -119,6 +131,7 @@ func (s *Session) Open() (err error) { // lock. s.listening = make(chan interface{}) go s.listen(s.wsConn, s.listening) + s.LastHeartbeatAck = time.Now().UTC() s.Unlock() @@ -187,10 +200,13 @@ type helloOp struct { Trace []string `json:"_trace"` } +// Number of heartbeat intervals to wait until forcing a connection restart. +const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond + // heartbeat sends regular heartbeats to Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. -func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { +func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) { s.log(LogInformational, "called") @@ -199,20 +215,26 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} } var err error - ticker := time.NewTicker(i * time.Millisecond) + ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) defer ticker.Stop() for { + s.RLock() + last := s.LastHeartbeatAck + s.RUnlock() sequence := atomic.LoadInt64(s.sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() - if err != nil { - s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) - s.Lock() - s.DataReady = false - s.Unlock() + if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { + if err != nil { + s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) + } else { + s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) + } + s.Close() + s.reconnect() return } s.Lock() @@ -250,7 +272,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } var usd updateStatusData @@ -307,7 +329,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } data := requestGuildMembersData{ @@ -386,7 +408,10 @@ func (s *Session) onEvent(messageType int, message []byte) { // Reconnect // Must immediately disconnect from gateway and reconnect to new gateway. if e.Operation == 7 { - // TODO + s.log(LogInformational, "Closing and reconnecting in response to Op7") + s.Close() + s.reconnect() + return } // Invalid Session @@ -414,6 +439,14 @@ func (s *Session) onEvent(messageType int, message []byte) { return } + if e.Operation == 11 { + s.Lock() + s.LastHeartbeatAck = time.Now().UTC() + s.Unlock() + s.log(LogInformational, "got heartbeat ACK") + return + } + // Do not try to Dispatch a non-Dispatch Message if e.Operation != 0 { // But we probably should be doing something with them. @@ -621,7 +654,7 @@ func (s *Session) identify() error { if s.ShardCount > 1 { if s.ShardID >= s.ShardCount { - return errors.New("ShardID must be less than ShardCount") + return ErrWSShardBounds } data.Shard = &[2]int{s.ShardID, s.ShardCount} @@ -676,6 +709,13 @@ func (s *Session) reconnect() { return } + // Certain race conditions can call reconnect() twice. If this happens, we + // just break out of the reconnect loop + if err == ErrWSAlreadyOpen { + s.log(LogInformational, "Websocket already exists, no need to reconnect") + return + } + s.log(LogError, "error reconnecting to gateway, %s", err) <-time.After(wait * time.Second) From 1d514dbfd16f8fcd998ffee8056caabe21b44037 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Sun, 23 Jul 2017 08:41:32 +0200 Subject: [PATCH 04/17] More merging... --- user.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/user.go b/user.go index 0d3af1e..8abffb0 100644 --- a/user.go +++ b/user.go @@ -37,16 +37,3 @@ func (u *User) AvatarURL(size string) string { return URL + "?size=" + size } - -// AvatarURL returns a URL to the user's avatar. -// size: The size of the user's avatar as a power of two -func (u *User) AvatarURL(size string) string { - var URL string - if strings.HasPrefix(u.Avatar, "a_") { - URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) - } else { - URL = EndpointUserAvatar(u.ID, u.Avatar) - } - - return URL + "?size=" + size -} From 2079ab8efbb0701bfb86bea6379144fba4c3f99f Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Sun, 23 Jul 2017 08:42:47 +0200 Subject: [PATCH 05/17] More printfs --- restapi.go | 19 +++++++++++-------- types.go | 3 +-- voice.go | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/restapi.go b/restapi.go index bd944f2..3da0ac3 100644 --- a/restapi.go +++ b/restapi.go @@ -87,7 +87,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID req.Header.Set("Content-Type", contentType) // TODO: Make a configurable static variable. - req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION)) + req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION) if s.Debug { for k, v := range req.Header { @@ -247,7 +247,7 @@ func (s *Session) Register(username string) (token string, err error) { // even use. func (s *Session) Logout() (err error) { - // _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token)) + // _, err = s.Request("POST", LOGOUT, `{"token": "` + s.Token + `"}`) if s.Token == "" { return @@ -410,7 +410,7 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui uri := EndpointUserGuilds("@me") if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) @@ -733,7 +733,7 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID)) @@ -1018,7 +1018,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er Pruned uint32 `json:"pruned"` }{} - uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) + uri := EndpointGuildPrune(guildID) + "?days=" + days body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) err = unmarshal(body, &p) @@ -1281,7 +1281,7 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID v.Set("around", aroundID) } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID)) @@ -1382,7 +1382,10 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) for i, file := range files { h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + h.Set("Content-Disposition", + `form-data; name="file`+strconv.Itoa(i)+ + `"; filename="`+quoteEscaper.Replace(file.Name)+ + `"`) contentType := file.ContentType if contentType == "" { contentType = "application/octet-stream" @@ -1943,7 +1946,7 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i } if len(v) > 0 { - uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + uri += "?" + v.Encode() } body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) diff --git a/types.go b/types.go index 780b6bb..78296f7 100644 --- a/types.go +++ b/types.go @@ -11,7 +11,6 @@ package discordgo import ( "encoding/json" - "fmt" "net/http" "time" ) @@ -54,5 +53,5 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro } func (r RESTError) Error() string { - return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) + return "HTTP " + r.Response.Status + ", " + r.ResponseBody } diff --git a/voice.go b/voice.go index 5bbd0ad..cd0f168 100644 --- a/voice.go +++ b/voice.go @@ -16,6 +16,7 @@ import ( "log" "net" "runtime" + "strconv" "strings" "sync" "time" @@ -301,7 +302,7 @@ func (v *VoiceConnection) open() (err error) { } // Connect to VoiceConnection Websocket - vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) + vg := "wss://" + strings.TrimSuffix(v.endpoint, ":80") v.log(LogInformational, "connecting to voice endpoint %s", vg) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) if err != nil { @@ -544,7 +545,7 @@ func (v *VoiceConnection) udpOpen() (err error) { return fmt.Errorf("empty endpoint") } - host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) + host := strings.TrimSuffix(v.endpoint, ":80") + ":" + strconv.Itoa(v.op2.Port) addr, err := net.ResolveUDPAddr("udp", host) if err != nil { v.log(LogWarning, "error resolving udp host %s, %s", host, err) From fede59cfa01d78dd3e2fb8ab40a53ad6cd393f20 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Sun, 23 Jul 2017 08:46:05 +0200 Subject: [PATCH 06/17] Fixed compilation issues --- restapi.go | 2 +- types.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/restapi.go b/restapi.go index 3da0ac3..6fb1238 100644 --- a/restapi.go +++ b/restapi.go @@ -1018,7 +1018,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er Pruned uint32 `json:"pruned"` }{} - uri := EndpointGuildPrune(guildID) + "?days=" + days + uri := EndpointGuildPrune(guildID) + "?days=" + strconv.FormatUint(uint64(days), 10) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) err = unmarshal(body, &p) diff --git a/types.go b/types.go index 78296f7..c0ce013 100644 --- a/types.go +++ b/types.go @@ -53,5 +53,5 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro } func (r RESTError) Error() string { - return "HTTP " + r.Response.Status + ", " + r.ResponseBody + return "HTTP " + r.Response.Status + ", " + string(r.ResponseBody) } From 9a36098f5caa2f9c5c987a3ce46490fa2bd6dc89 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Tue, 29 Aug 2017 07:46:24 +0200 Subject: [PATCH 07/17] Revert one usecase --- restapi.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/restapi.go b/restapi.go index 6fb1238..59c4907 100644 --- a/restapi.go +++ b/restapi.go @@ -1383,9 +1383,10 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) for i, file := range files { h := make(textproto.MIMEHeader) h.Set("Content-Disposition", - `form-data; name="file`+strconv.Itoa(i)+ - `"; filename="`+quoteEscaper.Replace(file.Name)+ - `"`) + fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, + i, + quoteEscaper.Replace(file.Name), + )) contentType := file.ContentType if contentType == "" { contentType = "application/octet-stream" From f73dec458e7d5d50208a5245e34a0a1b0a34e21a Mon Sep 17 00:00:00 2001 From: jD91mZM2 Date: Fri, 8 Sep 2017 17:01:32 +0200 Subject: [PATCH 08/17] Stuff... yay --- restapi.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/restapi.go b/restapi.go index ee563b9..7471c4f 100644 --- a/restapi.go +++ b/restapi.go @@ -87,7 +87,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID req.Header.Set("Content-Type", contentType) // TODO: Make a configurable static variable. - req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION) + req.Header.Set("User-Agent", "DiscordBot (https://github.com/bwmarrin/discordgo, v"+VERSION+")") if s.Debug { for k, v := range req.Header { @@ -1382,11 +1382,7 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) for i, file := range files { h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", - fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, - i, - quoteEscaper.Replace(file.Name), - )) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name),)) contentType := file.ContentType if contentType == "" { contentType = "application/octet-stream" From c768789edf02b0fa0b1451d900daa024fa392d8b Mon Sep 17 00:00:00 2001 From: jD91mZM2 Date: Fri, 8 Sep 2017 17:02:33 +0200 Subject: [PATCH 09/17] Delete useless comma --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 7471c4f..e607205 100644 --- a/restapi.go +++ b/restapi.go @@ -1382,7 +1382,7 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) for i, file := range files { h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name),)) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) contentType := file.ContentType if contentType == "" { contentType = "application/octet-stream" From 4523c46927cd079d9adea1221cfcb466f3512864 Mon Sep 17 00:00:00 2001 From: Anis B Date: Sat, 9 Dec 2017 16:23:21 +0100 Subject: [PATCH 10/17] Drop the default websocket close handler --- wsapi.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wsapi.go b/wsapi.go index de66f69..0cd503e 100644 --- a/wsapi.go +++ b/wsapi.go @@ -85,6 +85,10 @@ func (s *Session) Open() error { s.wsConn = nil // Just to be safe. return err } + + s.wsConn.SetCloseHandler(func(code int, text string) error { + return nil + }) defer func() { // because of this, all code below must set err to the error From e5c968d7a6c4fbe7d465a7712f6307d4cf472644 Mon Sep 17 00:00:00 2001 From: necro Date: Sat, 23 Jun 2018 16:06:45 -0400 Subject: [PATCH 11/17] Add Member.Mention --- structs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/structs.go b/structs.go index 6acbc3c..467ac26 100644 --- a/structs.go +++ b/structs.go @@ -602,6 +602,11 @@ type Member struct { Roles []string `json:"roles"` } +// Mention creates a member mention +func (m *Member) Mention() string { + return "<@!" + m.User.ID + ">" +} + // A Settings stores data for a specific users Discord client settings. type Settings struct { RenderEmbeds bool `json:"render_embeds"` From fbfba5b9694dc0b77cccaa1e58ee677d0d4015b6 Mon Sep 17 00:00:00 2001 From: Santiago Andaluz Date: Thu, 5 Jul 2018 13:46:54 -0500 Subject: [PATCH 12/17] Update Disconnect() comment It looks like dgo has moved away from ChannelVoiceLeave ever since discord allows bot to connect to more than one channel. Confusing comment seeing that it is very hard to find information about the function that it mentions. --- voice.go | 1 - 1 file changed, 1 deletion(-) diff --git a/voice.go b/voice.go index 3bbf621..769e547 100644 --- a/voice.go +++ b/voice.go @@ -135,7 +135,6 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err // Disconnect disconnects from this voice channel and closes the websocket // and udp connections to Discord. -// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave func (v *VoiceConnection) Disconnect() (err error) { // Send a OP4 with a nil channel to disconnect From 82be39a218b056d4b5f9e8460b776830fd92a1ac Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 25 Jul 2018 12:00:40 +0300 Subject: [PATCH 13/17] Remove redundant line s.StateEnabled variable already set to true above --- message_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/message_test.go b/message_test.go index 8bff87b..792bfc4 100644 --- a/message_test.go +++ b/message_test.go @@ -12,7 +12,6 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) { Username: "User Name", } - s.StateEnabled = true s.State.GuildAdd(&Guild{ID: "guild"}) s.State.RoleAdd("guild", &Role{ ID: "role", From 9c1fd5d8a9f27aded86b21be4d1591916dfe56e0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 31 Jul 2018 15:32:29 -0500 Subject: [PATCH 14/17] Changed Member.JoinedAt to type Timestamp It was previously just string, but has been updated to be consistent with the other structs. --- structs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structs.go b/structs.go index d089b5f..aa4d052 100644 --- a/structs.go +++ b/structs.go @@ -597,7 +597,7 @@ type Member struct { GuildID string `json:"guild_id"` // The time at which the member joined the guild, in ISO8601. - JoinedAt string `json:"joined_at"` + JoinedAt Timestamp `json:"joined_at"` // The nickname of the member, if they have one. Nick string `json:"nick"` From 38d030aaed778de69ee80bbd8f8de6dcba0a9129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20=28Netux=29=20Rodr=C3=ADguez?= Date: Fri, 17 Aug 2018 15:52:12 -0300 Subject: [PATCH 15/17] endpoints(): add EndpointEmojiAnimated (#572) --- endpoints.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/endpoints.go b/endpoints.go index 95f6fd9..b961908 100644 --- a/endpoints.go +++ b/endpoints.go @@ -136,7 +136,8 @@ var ( EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } - EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" } EndpointOauth2 = EndpointAPI + "oauth2/" EndpointApplications = EndpointOauth2 + "applications" From 34307e9a0b2de9e5731fbf67f85605870d86a47e Mon Sep 17 00:00:00 2001 From: Qais Patankar Date: Sat, 18 Aug 2018 17:10:18 +0100 Subject: [PATCH 16/17] Add ErrCodeUnknownWebhook (#573) --- structs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/structs.go b/structs.go index d089b5f..a7d298c 100644 --- a/structs.go +++ b/structs.go @@ -929,6 +929,7 @@ const ( ErrCodeUnknownToken = 10012 ErrCodeUnknownUser = 10013 ErrCodeUnknownEmoji = 10014 + ErrCodeUnknownWebhook = 10015 ErrCodeBotsCannotUseEndpoint = 20001 ErrCodeOnlyBotsCanUseEndpoint = 20002 From 0af46d54e73fde2f464b49e89eb52cf555be5de1 Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Sun, 19 Aug 2018 21:43:41 +0200 Subject: [PATCH 17/17] adds support for Webhooks Update event (#561) --- eventhandlers.go | 24 ++++++++++++++++++++++++ events.go | 6 ++++++ 2 files changed, 30 insertions(+) diff --git a/eventhandlers.go b/eventhandlers.go index 5cc157d..d2b9a98 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -50,6 +50,7 @@ const ( userUpdateEventType = "USER_UPDATE" voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" voiceStateUpdateEventType = "VOICE_STATE_UPDATE" + webhooksUpdateEventType = "WEBHOOKS_UPDATE" ) // channelCreateEventHandler is an event handler for ChannelCreate events. @@ -892,6 +893,26 @@ func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events. +type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate) + +// Type returns the event type for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Type() string { + return webhooksUpdateEventType +} + +// New returns a new instance of WebhooksUpdate. +func (eh webhooksUpdateEventHandler) New() interface{} { + return &WebhooksUpdate{} +} + +// Handle is the handler for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*WebhooksUpdate); ok { + eh(s, t) + } +} + func handlerForInterface(handler interface{}) EventHandler { switch v := handler.(type) { case func(*Session, interface{}): @@ -982,6 +1003,8 @@ func handlerForInterface(handler interface{}) EventHandler { return voiceServerUpdateEventHandler(v) case func(*Session, *VoiceStateUpdate): return voiceStateUpdateEventHandler(v) + case func(*Session, *WebhooksUpdate): + return webhooksUpdateEventHandler(v) } return nil @@ -1027,4 +1050,5 @@ func init() { registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) + registerInterfaceProvider(webhooksUpdateEventHandler(nil)) } diff --git a/events.go b/events.go index e784cac..c4fb520 100644 --- a/events.go +++ b/events.go @@ -254,3 +254,9 @@ type MessageDeleteBulk struct { ChannelID string `json:"channel_id"` GuildID string `json:"guild_id"` } + +// WebhooksUpdate is the data for a WebhooksUpdate event +type WebhooksUpdate struct { + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` +}