From c40b9753542ab8a3cce8bd949a3dea67b5a38b70 Mon Sep 17 00:00:00 2001 From: LEGOlord208 Date: Fri, 28 Apr 2017 21:34:30 +0200 Subject: [PATCH 01/64] STOP USING PRINTF PLS --- user.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/user.go b/user.go index b3a7e4b2c..cdc8a8515 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/64] DIE, PRINTF. DIE! --- message.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/message.go b/message.go index 13c2da078..b743327e2 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/64] Merge branch 'develop' into 1 --- .travis.yml | 2 +- README.md | 1 - discord.go | 5 +- discord_test.go | 12 ++++- endpoints.go | 36 +++++++------ message.go | 75 +++++++++++++++++++++++---- message_test.go | 41 +++++++++++++++ restapi.go | 134 +++++++++++++++++++++++++++++++++++------------- restapi_test.go | 11 ++++ state.go | 28 +++++----- structs.go | 19 +++++-- user.go | 15 ++++++ voice.go | 6 ++- wsapi.go | 64 ++++++++++++++++++----- 14 files changed, 352 insertions(+), 97 deletions(-) create mode 100644 message_test.go diff --git a/.travis.yml b/.travis.yml index 440159dc3..92beb2eb8 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 ac9dbcdea..eb9f14f04 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 ef5bf2806..04d471986 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 afac0bc5a..ca4472ada 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 96bcf28b1..0ecdf0bc3 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 b743327e2..d46f3f3f6 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 +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + + for _, user := range m.Mentions { + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+user.Username, + ).Replace(content) } - content := m.Content + return +} + +var patternChannels = regexp.MustCompile("<#[^>]*>") + +// ContentWithMoreMentionsReplaced will replace all @ mentions with the +// username of the mention, but also role IDs and more. +func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) { + content = m.Content + + if !s.StateEnabled { + content = m.ContentWithMentionsReplaced() + return + } + + channel, err := s.State.Channel(m.ChannelID) + if err != nil { + content = m.ContentWithMentionsReplaced() + return + } + for _, user := range m.Mentions { - content = regexp.MustCompile("<@!?("+regexp.QuoteMeta(user.ID)+")>").ReplaceAllString(content, "@"+user.Username) + 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) } - return 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 000000000..fd2f13360 --- /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 7c9fd811e..bd944f26d 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 + var payload []byte + payload, err = json.Marshal(data) + if err != nil { + return + } - if data.Content != "" { - if err = bodywriter.WriteField("content", data.Content); err != nil { - return - } + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return } - if data.Embed != nil { - var embed []byte - embed, err = json.Marshal(data.Embed) - 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" } - err = bodywriter.WriteField("embed", string(embed)) + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) if err != nil { return } - } - if data.Tts { - if err = bodywriter.WriteField("tts", "true"); err != nil { + if _, err = io.Copy(p, file.Reader); err != nil { return } } - var writer io.Writer - writer, err = bodywriter.CreateFormFile("file", data.File.Name) - if err != nil { - return - } - - _, err = io.Copy(writer, data.File.Reader) - if 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 a5d326bba..7aa4e604d 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 c2c551954..7400ef621 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 3a6ec0514..9697fa566 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 cdc8a8515..8abffb05b 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 8b566f440..5bbd0adaf 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 adab402dd..213ea72d7 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/64] More merging... --- user.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/user.go b/user.go index 0d3af1ec9..8abffb05b 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/64] 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 bd944f26d..3da0ac3f8 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 780b6bb97..78296f770 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 5bbd0adaf..cd0f1684c 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/64] 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 3da0ac3f8..6fb1238aa 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 78296f770..c0ce01315 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/64] Revert one usecase --- restapi.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/restapi.go b/restapi.go index 6fb1238aa..59c490738 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/64] Stuff... yay --- restapi.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/restapi.go b/restapi.go index ee563b97c..7471c4f45 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/64] Delete useless comma --- restapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index 7471c4f45..e60720588 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 550145114d67bd8d206039f7136422698849114a Mon Sep 17 00:00:00 2001 From: robbix1206 Date: Thu, 21 Sep 2017 20:11:41 +0200 Subject: [PATCH 10/64] Add ChannelID to WebhookeEdit (#434) --- restapi.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/restapi.go b/restapi.go index 1a682fa23..9a5883683 100644 --- a/restapi.go +++ b/restapi.go @@ -1822,12 +1822,13 @@ func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err er // 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) { +func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Role, err error) { data := struct { - Name string `json:"name,omitempty"` - Avatar string `json:"avatar,omitempty"` - }{name, avatar} + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + }{name, avatar, channelID} body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) if err != nil { From 4523c46927cd079d9adea1221cfcb466f3512864 Mon Sep 17 00:00:00 2001 From: Anis B Date: Sat, 9 Dec 2017 16:23:21 +0100 Subject: [PATCH 11/64] Drop the default websocket close handler --- wsapi.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wsapi.go b/wsapi.go index de66f6931..0cd503e0a 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 18dfe540ad6b60a4039901b724ccc8319c73eadb Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Thu, 11 Jan 2018 16:09:12 +0100 Subject: [PATCH 12/64] adds GuildAuditLog() (#504) * adds GuildAuditLogs() * adds missing comments to GuildAuditLog --- endpoints.go | 1 + restapi.go | 36 +++++++++++++++++++++++++ structs.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/endpoints.go b/endpoints.go index 335e224dd..8e1a9992f 100644 --- a/endpoints.go +++ b/endpoints.go @@ -88,6 +88,7 @@ var ( EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } + EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } diff --git a/restapi.go b/restapi.go index 5dc0467f3..dcbf1b82e 100644 --- a/restapi.go +++ b/restapi.go @@ -1206,6 +1206,42 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) return } +// GuildAuditLog returns the audit log for a Guild. +// guildID : The ID of a Guild. +// userID : If provided the log will be filtered for the given ID. +// beforeID : If provided all log entries returned will be before the given ID. +// actionType : If provided the log will be filtered for the given Action Type. +// limit : The number messages that can be returned. (default 50, min 1, max 100) +func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) { + + uri := EndpointGuildAuditLogs(guildID) + + v := url.Values{} + if userID != "" { + v.Set("user_id", userID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + if actionType > 0 { + v.Set("action_type", strconv.Itoa(actionType)) + } + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildAuditLogs(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Channels // ------------------------------------------------------------------------------------------------ diff --git a/structs.go b/structs.go index 19d2bad73..d2916fc4b 100644 --- a/structs.go +++ b/structs.go @@ -494,6 +494,82 @@ type GuildEmbed struct { ChannelID string `json:"channel_id"` } +// A GuildAuditLog stores data for a guild audit log. +type GuildAuditLog struct { + Webhooks []struct { + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` + ID string `json:"id"` + Avatar string `json:"avatar"` + Name string `json:"name"` + } `json:"webhooks,omitempty"` + Users []struct { + Username string `json:"username"` + Discriminator string `json:"discriminator"` + Bot bool `json:"bot"` + ID string `json:"id"` + Avatar string `json:"avatar"` + } `json:"users,omitempty"` + AuditLogEntries []struct { + TargetID string `json:"target_id"` + Changes []struct { + NewValue interface{} `json:"new_value"` + OldValue interface{} `json:"old_value"` + Key string `json:"key"` + } `json:"changes,omitempty"` + UserID string `json:"user_id"` + ID string `json:"id"` + ActionType int `json:"action_type"` + Options struct { + DeleteMembersDay string `json:"delete_member_days"` + MembersRemoved string `json:"members_removed"` + ChannelID string `json:"channel_id"` + Count string `json:"count"` + ID string `json:"id"` + Type string `json:"type"` + RoleName string `json:"role_name"` + } `json:"options,omitempty"` + Reason string `json:"reason"` + } `json:"audit_log_entries"` +} + +// Block contains Discord Audit Log Action Types +const ( + AuditLogActionGuildUpdate = 1 + + AuditLogActionChannelCreate = 10 + AuditLogActionChannelUpdate = 11 + AuditLogActionChannelDelete = 12 + AuditLogActionChannelOverwriteCreate = 13 + AuditLogActionChannelOverwriteUpdate = 14 + AuditLogActionChannelOverwriteDelete = 15 + + AuditLogActionMemberKick = 20 + AuditLogActionMemberPrune = 21 + AuditLogActionMemberBanAdd = 22 + AuditLogActionMemberBanRemove = 23 + AuditLogActionMemberUpdate = 24 + AuditLogActionMemberRoleUpdate = 25 + + AuditLogActionRoleCreate = 30 + AuditLogActionRoleUpdate = 31 + AuditLogActionRoleDelete = 32 + + AuditLogActionInviteCreate = 40 + AuditLogActionInviteUpdate = 41 + AuditLogActionInviteDelete = 42 + + AuditLogActionWebhookCreate = 50 + AuditLogActionWebhookUpdate = 51 + AuditLogActionWebhookDelete = 52 + + AuditLogActionEmojiCreate = 60 + AuditLogActionEmojiUpdate = 61 + AuditLogActionEmojiDelete = 62 + + AuditLogActionMessageDelete = 72 +) + // A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. type UserGuildSettingsChannelOverride struct { Muted bool `json:"muted"` From 8591961512a5b77b96492743050e96674350bdaa Mon Sep 17 00:00:00 2001 From: Eric Wohltman Date: Thu, 11 Jan 2018 19:48:25 -0500 Subject: [PATCH 13/64] Small update to supported Game Activity Types (#500) --- structs.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/structs.go b/structs.go index d2916fc4b..c58c9163e 100644 --- a/structs.go +++ b/structs.go @@ -334,6 +334,8 @@ type GameType int const ( GameTypeGame GameType = iota GameTypeStreaming + GameTypeListening + GameTypeWatching ) // A Game struct holds the name of the "playing .." game for a user From a0faf9a0d0a5f51ad5bc1bca493ef8c61019c67b Mon Sep 17 00:00:00 2001 From: Carson Hoffman Date: Mon, 22 Jan 2018 17:32:00 -0500 Subject: [PATCH 14/64] Removed unnecessary error check (#505) --- wsapi.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/wsapi.go b/wsapi.go index de66f6931..3b18e65f5 100644 --- a/wsapi.go +++ b/wsapi.go @@ -732,11 +732,8 @@ func (s *Session) identify() error { s.wsMutex.Lock() err := s.wsConn.WriteJSON(op) s.wsMutex.Unlock() - if err != nil { - return err - } - return nil + return err } func (s *Session) reconnect() { From 5765bb7804e71146bcba0623823791d18f3a7f16 Mon Sep 17 00:00:00 2001 From: Carson Hoffman Date: Sun, 28 Jan 2018 20:02:52 -0500 Subject: [PATCH 15/64] Added Role.Mention function --- structs.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/structs.go b/structs.go index c58c9163e..351e85591 100644 --- a/structs.go +++ b/structs.go @@ -13,6 +13,7 @@ package discordgo import ( "encoding/json" + "fmt" "net/http" "sync" "time" @@ -289,6 +290,11 @@ type Role struct { Permissions int `json:"permissions"` } +// Mention returns a string which mentions the role +func (r *Role) Mention() string { + return fmt.Sprintf("<@&%s>", r.ID) +} + // Roles are a collection of Role type Roles []*Role From e7e5f85367f7dc38b133fd32975ae8dc225df20b Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Fri, 9 Feb 2018 05:08:56 +0100 Subject: [PATCH 16/64] adds default avatar generation to AvatarURL method (#488) * adds default avatar generation to AvatarURL method * converts discriminator string to integer in endpoints.go --- endpoints.go | 22 ++++++++++++++-------- user.go | 4 +++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/endpoints.go b/endpoints.go index 8e1a9992f..2c6959592 100644 --- a/endpoints.go +++ b/endpoints.go @@ -11,6 +11,8 @@ package discordgo +import "strconv" + // APIVersion is the Discord API version used for the REST and Websocket API. var APIVersion = "6" @@ -61,14 +63,18 @@ var ( 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 } + EndpointDefaultUserAvatar = func(uDiscriminator string) string { + uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator) + return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".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 } EndpointGuild = func(gID string) string { return EndpointGuilds + gID } EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } diff --git a/user.go b/user.go index a710f2865..101b90686 100644 --- a/user.go +++ b/user.go @@ -34,7 +34,9 @@ func (u *User) Mention() string { // be added to the URL. func (u *User) AvatarURL(size string) string { var URL string - if strings.HasPrefix(u.Avatar, "a_") { + if u.Avatar == "" { + URL = EndpointDefaultUserAvatar(u.Discriminator) + } else if strings.HasPrefix(u.Avatar, "a_") { URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) } else { URL = EndpointUserAvatar(u.ID, u.Avatar) From e8cd93cf1502cbbe43ca3158ae7e8c45a9f2f2e8 Mon Sep 17 00:00:00 2001 From: Daniel Thorpe Date: Fri, 9 Feb 2018 04:50:07 +0000 Subject: [PATCH 17/64] Add a listening status method (#508) * Bump to v0.17.0 * Add members from GuildMembersChunk to state (#454) * Revert "Add members from GuildMembersChunk to state (#454)" (#455) This reverts commit e4487b30d4d846b1fdc08fd3982bd5b9965a8cc9. * travis: update go versions * Allowed setting the game to null. * Allowed for setting status to listening. * Changed variable names for better consistency * Reverted updateStatus to maintain API compatibility, instead added now UpdateListeningStatus method which directly calls UpdateStatusComplex. * Fix version to 0.18.0-alpha * Make the new UpdateListeningStatus read a bit better. * Updated setting game to nil for better compatibility. * Remove the explicit nil check, as no longer seems to be needed. * Fix comment styling. * Remove idle as unused. --- .travis.yml | 6 +++--- wsapi.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c181ddaf8..fe626fcfd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: go go: - - 1.7 - - 1.8 - - 1.9 + - 1.7.x + - 1.8.x + - 1.9.x install: - go get github.com/bwmarrin/discordgo - go get -v . diff --git a/wsapi.go b/wsapi.go index 3b18e65f5..f9d6029e7 100644 --- a/wsapi.go +++ b/wsapi.go @@ -379,6 +379,23 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { return s.UpdateStreamingStatus(idle, game, "") } +// UpdateListeningStatus is used to set the user to "Listening to..." +// If game!="" then set to what user is listening to +// Else, set user to active and no game. +func (s *Session) UpdateListeningStatus(game string) (err error) { + usd := UpdateStatusData{ + Status: "online", + } + if game != "" { + usd.Game = &Game{ + Name: game, + Type: GameTypeListening, + URL: "", + } + } + return s.UpdateStatusComplex(usd) +} + type requestGuildMembersData struct { GuildID string `json:"guild_id"` Query string `json:"query"` From 7f8369a45fc1d2df208d7bbfdbd9226a85815143 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Thu, 8 Feb 2018 21:00:24 -0800 Subject: [PATCH 18/64] Clean up status methods. --- wsapi.go | 71 +++++++++++++++++++++++++------------------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/wsapi.go b/wsapi.go index f9d6029e7..80e85ac30 100644 --- a/wsapi.go +++ b/wsapi.go @@ -323,16 +323,8 @@ type updateStatusOp struct { Data UpdateStatusData `json:"d"` } -// UpdateStreamingStatus is used to update the user's streaming status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// If game!="" and url!="" then set the status type to streaming with the URL set. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { - - s.log(LogInformational, "called") - - usd := UpdateStatusData{ +func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData { + usd := &UpdateStatusData{ Status: "online", } @@ -341,10 +333,6 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err } if game != "" { - gameType := GameTypeGame - if url != "" { - gameType = GameTypeStreaming - } usd.Game = &Game{ Name: game, Type: gameType, @@ -352,7 +340,35 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err } } - return s.UpdateStatusComplex(usd) + return usd +} + +// UpdateStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStatus(idle int, game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, "")) +} + +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// If game!="" and url!="" then set the status type to streaming with the URL set. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { + gameType := GameTypeGame + if url != "" { + gameType = GameTypeStreaming + } + return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url)) +} + +// UpdateListeningStatus is used to set the user to "Listening to..." +// If game!="" then set to what user is listening to +// Else, set user to active and no game. +func (s *Session) UpdateListeningStatus(game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, "")) } // UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. @@ -371,31 +387,6 @@ func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { return } -// UpdateStatus is used to update the user's status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStatus(idle int, game string) (err error) { - return s.UpdateStreamingStatus(idle, game, "") -} - -// UpdateListeningStatus is used to set the user to "Listening to..." -// If game!="" then set to what user is listening to -// Else, set user to active and no game. -func (s *Session) UpdateListeningStatus(game string) (err error) { - usd := UpdateStatusData{ - Status: "online", - } - if game != "" { - usd.Game = &Game{ - Name: game, - Type: GameTypeListening, - URL: "", - } - } - return s.UpdateStatusComplex(usd) -} - type requestGuildMembersData struct { GuildID string `json:"guild_id"` Query string `json:"query"` From ffa9956c9b41e8e2a10c26a254389854e016b006 Mon Sep 17 00:00:00 2001 From: Wim Date: Thu, 15 Feb 2018 00:05:22 +0100 Subject: [PATCH 19/64] Fix role mention replacement (#509) * Fix role mention replacement * Fix message test too --- message.go | 2 +- message_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/message.go b/message.go index 19345b953..4fd468fd8 100644 --- a/message.go +++ b/message.go @@ -237,7 +237,7 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e continue } - content = strings.Replace(content, "<&"+role.ID+">", "@"+role.Name, -1) + content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1) } content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { diff --git a/message_test.go b/message_test.go index fd2f13360..8bff87b1b 100644 --- a/message_test.go +++ b/message_test.go @@ -30,7 +30,7 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) { ID: "channel", }) m := &Message{ - Content: "<&role> <@!user> <@user> <#channel>", + Content: "<@&role> <@!user> <@user> <#channel>", ChannelID: "channel", MentionRoles: []string{"role"}, Mentions: []*User{user}, From eb11ffb51cedfcb345f328914914e0f4bcdcd6af Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Thu, 22 Feb 2018 14:44:09 +0100 Subject: [PATCH 20/64] makes GuildChannelCreate use ChannelType (#510) --- restapi.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/restapi.go b/restapi.go index dcbf1b82e..72e02e45b 100644 --- a/restapi.go +++ b/restapi.go @@ -880,12 +880,12 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { // GuildChannelCreate creates a new channel in the given guild // guildID : The ID of a Guild. // name : Name of the channel (2-100 chars length) -// ctype : Tpye of the channel (voice or text) -func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) { +// ctype : Type of the channel +func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { data := struct { - Name string `json:"name"` - Type string `json:"type"` + Name string `json:"name"` + Type ChannelType `json:"type"` }{name, ctype} body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) From 1ff9565306521e243058696a42b06f04a6a2be32 Mon Sep 17 00:00:00 2001 From: Necroforger Date: Wed, 7 Mar 2018 18:45:09 -0500 Subject: [PATCH 21/64] Added UserConnections function and structs (#514) * Added UserConnections function and structs * Rename Connection to UserConnection * Embed Integrations inside GuildIntegration and remove GuildIntegrationAccount * Replaced GuildIntegration with Integration --- restapi.go | 17 ++++++++++++++++- structs.go | 52 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/restapi.go b/restapi.go index dcbf1b82e..aa76a72f4 100644 --- a/restapi.go +++ b/restapi.go @@ -361,6 +361,21 @@ func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { return } +// UserConnections returns the user's connections +func (s *Session) UserConnections() (conn []*UserConnection, err error) { + response, err := s.RequestWithBucketID("GET", EndpointUserConnections("@me"), nil, EndpointUserConnections("@me")) + if err != nil { + return nil, err + } + + err = unmarshal(response, &conn) + if err != nil { + return + } + + return +} + // UserChannels returns an array of Channel structures for all private // channels. func (s *Session) UserChannels() (st []*Channel, err error) { @@ -1075,7 +1090,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err // 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) { +func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) if err != nil { diff --git a/structs.go b/structs.go index 351e85591..aa52a1eea 100644 --- a/structs.go +++ b/structs.go @@ -112,6 +112,37 @@ type Session struct { wsMutex sync.Mutex } +// UserConnection is a Connection returned from the UserConnections endpoint +type UserConnection struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Revoked bool `json:"revoked"` + Integrations []*Integration `json:"integrations"` +} + +// Integration stores integration information +type Integration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account IntegrationAccount `json:"account"` + SyncedAt Timestamp `json:"synced_at"` +} + +// IntegrationAccount is integration account information +// sent by the UserConnections endpoint +type IntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + // A VoiceRegion stores data for a specific voice region server. type VoiceRegion struct { ID string `json:"id"` @@ -475,27 +506,6 @@ type GuildBan struct { User *User `json:"user"` } -// A GuildIntegration stores data for a guild integration. -type GuildIntegration struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Enabled bool `json:"enabled"` - Syncing bool `json:"syncing"` - RoleID string `json:"role_id"` - ExpireBehavior int `json:"expire_behavior"` - ExpireGracePeriod int `json:"expire_grace_period"` - User *User `json:"user"` - Account *GuildIntegrationAccount `json:"account"` - SyncedAt int `json:"synced_at"` -} - -// A GuildIntegrationAccount stores data for a guild integration account. -type GuildIntegrationAccount struct { - ID string `json:"id"` - Name string `json:"name"` -} - // A GuildEmbed stores data for a guild embed. type GuildEmbed struct { Enabled bool `json:"enabled"` From 964060f34c9da77c681647d877e8914bbaa154b7 Mon Sep 17 00:00:00 2001 From: Carson Hoffman Date: Tue, 13 Mar 2018 10:48:50 -0400 Subject: [PATCH 22/64] General documentation improvements --- event.go | 11 ++- message.go | 61 +++++++++++---- state.go | 3 +- structs.go | 221 +++++++++++++++++++++++++++++++++++++++++------------ user.go | 36 +++++++-- 5 files changed, 258 insertions(+), 74 deletions(-) diff --git a/event.go b/event.go index bba396cbb..97cc00a2b 100644 --- a/event.go +++ b/event.go @@ -98,7 +98,9 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { // 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. +// The first parameter is a *Session, and the second parameter is a pointer +// to a struct corresponding to the event for which you want to listen. +// // eg: // Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { // }) @@ -106,6 +108,13 @@ func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { // or: // Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { // }) +// +// List of events can be found at this page, with corresponding names in the +// library for each event: https://discordapp.com/developers/docs/topics/gateway#event-names +// There are also synthetic events fired by the library internally which are +// available for handling, like Connect, Disconnect, and RateLimit. +// events.go contains all of the Discord WSAPI and synthetic events that can be handled. +// // The return value of this method is a function, that when called will remove the // event handler. func (s *Session) AddHandler(handler interface{}) func() { diff --git a/message.go b/message.go index 4fd468fd8..b67c01d21 100644 --- a/message.go +++ b/message.go @@ -32,20 +32,53 @@ const ( // A Message stores all data related to a specific Discord message. type Message struct { - ID string `json:"id"` - ChannelID string `json:"channel_id"` - Content string `json:"content"` - Timestamp Timestamp `json:"timestamp"` - EditedTimestamp Timestamp `json:"edited_timestamp"` - MentionRoles []string `json:"mention_roles"` - Tts bool `json:"tts"` - MentionEveryone bool `json:"mention_everyone"` - Author *User `json:"author"` - Attachments []*MessageAttachment `json:"attachments"` - Embeds []*MessageEmbed `json:"embeds"` - Mentions []*User `json:"mentions"` - Reactions []*MessageReactions `json:"reactions"` - Type MessageType `json:"type"` + // The ID of the message. + ID string `json:"id"` + + // The ID of the channel in which the message was sent. + ChannelID string `json:"channel_id"` + + // The content of the message. + Content string `json:"content"` + + // The time at which the messsage was sent. + // CAUTION: this field may be removed in a + // future API version; it is safer to calculate + // the creation time via the ID. + Timestamp Timestamp `json:"timestamp"` + + // The time at which the last edit of the message + // occurred, if it has been edited. + EditedTimestamp Timestamp `json:"edited_timestamp"` + + // The roles mentioned in the message. + MentionRoles []string `json:"mention_roles"` + + // Whether the message is text-to-speech. + Tts bool `json:"tts"` + + // Whether the message mentions everyone. + MentionEveryone bool `json:"mention_everyone"` + + // The author of the message. This is not guaranteed to be a + // valid user (webhook-sent messages do not possess a full author). + Author *User `json:"author"` + + // A list of attachments present in the message. + Attachments []*MessageAttachment `json:"attachments"` + + // A list of embeds present in the message. Multiple + // embeds can currently only be sent by webhooks. + Embeds []*MessageEmbed `json:"embeds"` + + // A list of users mentioned in the message. + Mentions []*User `json:"mentions"` + + // A list of reactions to the message. + Reactions []*MessageReactions `json:"reactions"` + + // The type of the message. + Type MessageType `json:"type"` } // File stores info about files you e.g. send in messages. diff --git a/state.go b/state.go index 8158708b3..662814c0e 100644 --- a/state.go +++ b/state.go @@ -32,6 +32,7 @@ type State struct { sync.RWMutex Ready + // MaxMessageCount represents how many messages per channel the state will store. MaxMessageCount int TrackChannels bool TrackEmojis bool @@ -607,7 +608,7 @@ func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error { // MessageAdd adds a message to the current world state, or updates it if it exists. // If the channel cannot be found, the message is discarded. -// Messages are kept in state up to s.MaxMessageCount +// Messages are kept in state up to s.MaxMessageCount per channel. func (s *State) MessageAdd(message *Message) error { if s == nil { return ErrNilState diff --git a/structs.go b/structs.go index aa52a1eea..3e59aa5af 100644 --- a/structs.go +++ b/structs.go @@ -193,19 +193,47 @@ const ( // A Channel holds all data related to an individual Discord channel. type Channel struct { - ID string `json:"id"` - GuildID string `json:"guild_id"` - Name string `json:"name"` - Topic string `json:"topic"` - Type ChannelType `json:"type"` - LastMessageID string `json:"last_message_id"` - NSFW bool `json:"nsfw"` - Position int `json:"position"` - Bitrate int `json:"bitrate"` - Recipients []*User `json:"recipients"` - Messages []*Message `json:"-"` + // The ID of the channel. + ID string `json:"id"` + + // The ID of the guild to which the channel belongs, if it is in a guild. + // Else, this ID is empty (e.g. DM channels). + GuildID string `json:"guild_id"` + + // The name of the channel. + Name string `json:"name"` + + // The topic of the channel. + Topic string `json:"topic"` + + // The type of the channel. + Type ChannelType `json:"type"` + + // The ID of the last message sent in the channel. This is not + // guaranteed to be an ID of a valid message. + LastMessageID string `json:"last_message_id"` + + // Whether the channel is marked as NSFW. + NSFW bool `json:"nsfw"` + + // The position of the channel, used for sorting in client. + Position int `json:"position"` + + // The bitrate of the channel, if it is a voice channel. + Bitrate int `json:"bitrate"` + + // The recipients of the channel. This is only populated in DM channels. + Recipients []*User `json:"recipients"` + + // The messages in the channel. This is only present in state-cached channels, + // and State.MaxMessageCount must be non-zero. + Messages []*Message `json:"-"` + + // A list of permission overwrites present for the channel. PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` - ParentID string `json:"parent_id"` + + // The ID of the parent channel, if the channel is under a category + ParentID string `json:"parent_id"` } // A ChannelEdit holds Channel Feild data for a channel edit. @@ -263,28 +291,89 @@ const ( // A Guild holds all data related to a specific Discord Guild. Guilds are also // sometimes referred to as Servers in the Discord client. type Guild struct { - ID string `json:"id"` - Name string `json:"name"` - Icon string `json:"icon"` - Region string `json:"region"` - AfkChannelID string `json:"afk_channel_id"` - EmbedChannelID string `json:"embed_channel_id"` - OwnerID string `json:"owner_id"` - JoinedAt Timestamp `json:"joined_at"` - Splash string `json:"splash"` - AfkTimeout int `json:"afk_timeout"` - MemberCount int `json:"member_count"` - VerificationLevel VerificationLevel `json:"verification_level"` - EmbedEnabled bool `json:"embed_enabled"` - Large bool `json:"large"` // ?? - DefaultMessageNotifications int `json:"default_message_notifications"` - Roles []*Role `json:"roles"` - Emojis []*Emoji `json:"emojis"` - Members []*Member `json:"members"` - Presences []*Presence `json:"presences"` - Channels []*Channel `json:"channels"` - VoiceStates []*VoiceState `json:"voice_states"` - Unavailable bool `json:"unavailable"` + // The ID of the guild. + ID string `json:"id"` + + // The name of the guild. (2–100 characters) + Name string `json:"name"` + + // The hash of the guild's icon. Use Session.GuildIcon + // to retrieve the icon itself. + Icon string `json:"icon"` + + // The voice region of the guild. + Region string `json:"region"` + + // The ID of the AFK voice channel. + AfkChannelID string `json:"afk_channel_id"` + + // The ID of the embed channel ID, used for embed widgets. + EmbedChannelID string `json:"embed_channel_id"` + + // The user ID of the owner of the guild. + OwnerID string `json:"owner_id"` + + // The time at which the current user joined the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + JoinedAt Timestamp `json:"joined_at"` + + // The hash of the guild's splash. + Splash string `json:"splash"` + + // The timeout, in seconds, before a user is considered AFK in voice. + AfkTimeout int `json:"afk_timeout"` + + // The number of members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + MemberCount int `json:"member_count"` + + // The verification level required for the guild. + VerificationLevel VerificationLevel `json:"verification_level"` + + // Whether the guild has embedding enabled. + EmbedEnabled bool `json:"embed_enabled"` + + // Whether the guild is considered large. This is + // determined by a member threshold in the identify packet, + // and is currently hard-coded at 250 members in the library. + Large bool `json:"large"` + + // The default message notification setting for the guild. + // 0 == all messages, 1 == mentions only. + DefaultMessageNotifications int `json:"default_message_notifications"` + + // A list of roles in the guild. + Roles []*Role `json:"roles"` + + // A list of the custom emojis present in the guild. + Emojis []*Emoji `json:"emojis"` + + // A list of the members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Members []*Member `json:"members"` + + // A list of partial presence objects for members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Presences []*Presence `json:"presences"` + + // A list of channels in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Channels []*Channel `json:"channels"` + + // A list of voice states for the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + VoiceStates []*VoiceState `json:"voice_states"` + + // Whether this guild is currently unavailable (most likely due to outage). + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Unavailable bool `json:"unavailable"` } // A UserGuild holds a brief version of a Guild @@ -311,14 +400,32 @@ type GuildParams struct { // A Role stores information about Discord guild member roles. type Role struct { - ID string `json:"id"` - Name string `json:"name"` - Managed bool `json:"managed"` - Mentionable bool `json:"mentionable"` - Hoist bool `json:"hoist"` - Color int `json:"color"` - Position int `json:"position"` - Permissions int `json:"permissions"` + // The ID of the role. + ID string `json:"id"` + + // The name of the role. + Name string `json:"name"` + + // Whether this role is managed by an integration, and + // thus cannot be manually added to, or taken from, members. + Managed bool `json:"managed"` + + // Whether this role is mentionable. + Mentionable bool `json:"mentionable"` + + // Whether this role is hoisted (shows up separately in member list). + Hoist bool `json:"hoist"` + + // The hex color of this role. + Color int `json:"color"` + + // The position of this role in the guild's role hierarchy. + Position int `json:"position"` + + // The permissions of the role on the guild (doesn't include channel overrides). + // This is a combination of bit masks; the presence of a certain permission can + // be checked by performing a bitwise AND between this int and the permission. + Permissions int `json:"permissions"` } // Mention returns a string which mentions the role @@ -418,15 +525,29 @@ type Assets struct { SmallText string `json:"small_text,omitempty"` } -// A Member stores user information for Guild members. +// A Member stores user information for Guild members. A guild +// member represents a certain user's presence in a guild. type Member struct { - GuildID string `json:"guild_id"` - JoinedAt string `json:"joined_at"` - Nick string `json:"nick"` - Deaf bool `json:"deaf"` - Mute bool `json:"mute"` - User *User `json:"user"` - Roles []string `json:"roles"` + // The guild ID on which the member exists. + GuildID string `json:"guild_id"` + + // The time at which the member joined the guild, in ISO8601. + JoinedAt string `json:"joined_at"` + + // The nickname of the member, if they have one. + Nick string `json:"nick"` + + // Whether the member is deafened at a guild level. + Deaf bool `json:"deaf"` + + // Whether the member is muted at a guild level. + Mute bool `json:"mute"` + + // The underlying user on which the member is based. + User *User `json:"user"` + + // A list of IDs of the roles which are possessed by the member. + Roles []string `json:"roles"` } // A Settings stores data for a specific users Discord client settings. diff --git a/user.go b/user.go index 101b90686..5de4a8244 100644 --- a/user.go +++ b/user.go @@ -7,15 +7,35 @@ import ( // A User stores all data for an individual Discord user. type User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - Avatar string `json:"avatar"` + // The ID of the user. + ID string `json:"id"` + + // The email of the user. This is only present when + // the application possesses the email scope for the user. + Email string `json:"email"` + + // The user's username. + Username string `json:"username"` + + // The hash of the user's avatar. Use Session.UserAvatar + // to retrieve the avatar itself. + Avatar string `json:"avatar"` + + // The discriminator of the user (4 numbers after name). Discriminator string `json:"discriminator"` - Token string `json:"token"` - Verified bool `json:"verified"` - MFAEnabled bool `json:"mfa_enabled"` - Bot bool `json:"bot"` + + // The token of the user. This is only present for + // the user represented by the current session. + Token string `json:"token"` + + // Whether the user's email is verified. + Verified bool `json:"verified"` + + // Whether the user has multi-factor authentication enabled. + MFAEnabled bool `json:"mfa_enabled"` + + // Whether the user is a bot. + Bot bool `json:"bot"` } // String returns a unique identifier of the form username#discriminator From 1cdc34e413fd9bee9fefd4765b06261601d5a8ca Mon Sep 17 00:00:00 2001 From: antihax Date: Sun, 25 Mar 2018 20:25:19 -0700 Subject: [PATCH 23/64] Add GuildMemberAdd --- restapi.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/restapi.go b/restapi.go index aa76a72f4..ea97608c0 100644 --- a/restapi.go +++ b/restapi.go @@ -776,6 +776,32 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { return } +// GuildMemberAdd force joins a user to the guild. +// accessToken : Valid access_token for the user. +// guildID : The ID of a Guild. +// userID : The ID of a User. +// nick : Value to set users nickname to +// roles : A list of role ID's to set on the member. +// mute : If the user is muted. +// deaf : If the user is deafened. +func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) { + + data := struct { + AccessToken string `json:"access_token"` + Nick string `json:"nick,omitempty"` + Roles []string `json:"roles,omitempty"` + Mute bool `json:"mute,omitempty"` + Deaf bool `json:"deaf,omitempty"` + }{accessToken, nick, roles, mute, deaf} + + _, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + if err != nil { + return err + } + + return err +} + // GuildMemberDelete removes the given user from the given guild. // guildID : The ID of a Guild. // userID : The ID of a User From dd26841a4f812c4ac3e2ac4d243798c6e91f5abf Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Tue, 3 Apr 2018 16:55:56 +0200 Subject: [PATCH 24/64] adds missing fields to guild struct --- structs.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/structs.go b/structs.go index 3e59aa5af..f17fb4c8c 100644 --- a/structs.go +++ b/structs.go @@ -288,6 +288,25 @@ const ( VerificationLevelHigh ) +// ExplicitContentFilterLevel type definition +type ExplicitContentFilterLevel int + +// Constants for ExplicitContentFilterLevel levels from 0 to 2 inclusive +const ( + ExplicitContentFilterDisabled ExplicitContentFilterLevel = iota + ExplicitContentFilterMembersWithoutRoles + ExplicitContentFilterAllMembers +) + +// MfaLevel type definition +type MfaLevel int + +// Constants for MfaLevel levels from 0 to 1 inclusive +const ( + MfaLevelNone MfaLevel = iota + MfaLevelElevated +) + // A Guild holds all data related to a specific Discord Guild. Guilds are also // sometimes referred to as Servers in the Discord client. type Guild struct { @@ -374,6 +393,24 @@ type Guild struct { // This field is only present in GUILD_CREATE events and websocket // update events, and thus is only present in state-cached guilds. Unavailable bool `json:"unavailable"` + + // The explicit content filter level + ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"` + + // The list of enabled guild features + Features []string `json:"features"` + + // Required MFA level for the guild + MfaLevel MfaLevel `json:"mfa_level"` + + // Whether or not the Server Widget is enabled + WidgetEnabled bool `json:"widget_enabled"` + + // The Channel ID for the Server Widget + WidgetChannelID string `json:"widget_channel_id"` + + // The Channel ID to which system messages are sent (eg join and leave messages) + SystemChannelID string `json:"system_channel_id"` } // A UserGuild holds a brief version of a Guild From 169895af550c982da028fcf3011f28b174008cd8 Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Sat, 7 Apr 2018 23:53:07 +0100 Subject: [PATCH 25/64] Removed state checking from (s *Session) Guild(...) to be consistent with other methods (#530) --- restapi.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/restapi.go b/restapi.go index 56bb1860b..b80f8c964 100644 --- a/restapi.go +++ b/restapi.go @@ -577,14 +577,6 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss // Guild returns a Guild structure of a specific Guild. // guildID : The ID of a Guild func (s *Session) Guild(guildID string) (st *Guild, err error) { - if s.StateEnabled { - // Attempt to grab the guild from State first. - st, err = s.State.Guild(guildID) - if err == nil { - return - } - } - body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return From aad0e0b32acaac075bed60517cfba344c9afb4e9 Mon Sep 17 00:00:00 2001 From: tsudoko Date: Tue, 10 Apr 2018 05:26:23 +0200 Subject: [PATCH 26/64] Add Icon channel field (#531) --- structs.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/structs.go b/structs.go index f17fb4c8c..9fa6ce668 100644 --- a/structs.go +++ b/structs.go @@ -216,6 +216,9 @@ type Channel struct { // Whether the channel is marked as NSFW. NSFW bool `json:"nsfw"` + // Icon of the group DM channel. + Icon string `json:"icon"` + // The position of the channel, used for sorting in client. Position int `json:"position"` From 293b41141dbc60d25d2b69528c2dbb7c216c3bd4 Mon Sep 17 00:00:00 2001 From: Carson Hoffman Date: Thu, 12 Apr 2018 23:40:50 -0400 Subject: [PATCH 27/64] Fix for #532 (#533) --- state.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/state.go b/state.go index 662814c0e..695f47cb1 100644 --- a/state.go +++ b/state.go @@ -300,7 +300,12 @@ func (s *State) MemberAdd(member *Member) error { members[member.User.ID] = member guild.Members = append(guild.Members, member) } else { - *m = *member // Update the actual data, which will also update the member pointer in the slice + // We are about to replace `m` in the state with `member`, but first we need to + // make sure we preserve any fields that the `member` doesn't contain from `m`. + if member.JoinedAt == "" { + member.JoinedAt = m.JoinedAt + } + *m = *member } return nil From 22ec91f6cfb72fab7e9cac6a0039cb1689c6c0e5 Mon Sep 17 00:00:00 2001 From: Rens Rikkerink Date: Thu, 19 Apr 2018 17:41:00 +0200 Subject: [PATCH 28/64] Added Mention() func for Channel (#536) * Added Mention() func for Channel * Changed variable name Adding the func in Github was so simple I didn't bother seeing if it'd compile, I shortly after realised my mistake. --- structs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/structs.go b/structs.go index 9fa6ce668..439a15e76 100644 --- a/structs.go +++ b/structs.go @@ -239,6 +239,11 @@ type Channel struct { ParentID string `json:"parent_id"` } +// Mention returns a string which mentions the channel +func (c *Channel) Mention() string { + return fmt.Sprintf("<#%s>", c.ID) +} + // A ChannelEdit holds Channel Feild data for a channel edit. type ChannelEdit struct { Name string `json:"name,omitempty"` From d32a8f518d21589a90a2ee6d8c26729b22b08523 Mon Sep 17 00:00:00 2001 From: Noah Santschi-Cooney Date: Thu, 19 Apr 2018 17:14:04 +0100 Subject: [PATCH 29/64] Fixed ContentWithMoreMentionsReplaced() for roles (#521) * Bump to v0.17.0 * Add members from GuildMembersChunk to state (#454) * Revert "Add members from GuildMembersChunk to state (#454)" (#455) This reverts commit e4487b30d4d846b1fdc08fd3982bd5b9965a8cc9. * travis: update go versions * Fixed ContentWithMoreMentionsReplaced() for roles Discord breaking the API again? Roles seem to always be in the form `<@&id>` now * Update message_test.go * Removed changes * Removed changes Need to learn git *properly* :( From da902d321c475325942f7292d663cafcd445f2f3 Mon Sep 17 00:00:00 2001 From: Rens Rikkerink Date: Fri, 20 Apr 2018 16:17:00 +0200 Subject: [PATCH 30/64] Reduce data sent to the DAPI in GuildChannelsReorder (#537) This function currently has an issue where it sends too much data, causing Discord to reject the request as it believes you're trying to amend several fields by sending all of them. This change resolves that by creating a simplified version of the Channel struct that only sends the data required for reordering. --- restapi.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index b80f8c964..e5ef9f8b8 100644 --- a/restapi.go +++ b/restapi.go @@ -935,7 +935,17 @@ func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (s // channels : Updated channels. func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { - _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID)) + data := make([]struct { + ID string `json:"id"` + Position int `json:"position"` + }, len(channels)) + + for i, c := range channels { + data[i].ID = c.ID + data[i].Position = c.Position + } + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) return } From ecd6077268288fbb5e54ca287d3b38c04a3373e3 Mon Sep 17 00:00:00 2001 From: Matthew Roseman Date: Sat, 21 Apr 2018 21:55:51 -0400 Subject: [PATCH 31/64] fixed small typo in oauth2_test.go Signed-off-by: Matthew Roseman --- oauth2_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_test.go b/oauth2_test.go index 0ff0ca0e7..1d5451bdf 100644 --- a/oauth2_test.go +++ b/oauth2_test.go @@ -9,7 +9,7 @@ import ( func ExampleApplication() { - // Authentication Token pulled from environment variable DG_TOKEN + // Authentication Token pulled from environment variable DGU_TOKEN Token := os.Getenv("DGU_TOKEN") if Token == "" { return From 82c8cf21b216a2507bfd815484b4a91183662d4c Mon Sep 17 00:00:00 2001 From: Dim Date: Wed, 2 May 2018 20:07:23 -0400 Subject: [PATCH 32/64] Add GuildID to multiple structs (#541) --- events.go | 3 +++ message.go | 3 +++ structs.go | 1 + 3 files changed, 7 insertions(+) diff --git a/events.go b/events.go index c78fbdd2c..e784cac7c 100644 --- a/events.go +++ b/events.go @@ -70,6 +70,7 @@ type ChannelDelete struct { type ChannelPinsUpdate struct { LastPinTimestamp string `json:"last_pin_timestamp"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` } // GuildCreate is the data for a GuildCreate event. @@ -212,6 +213,7 @@ type RelationshipRemove struct { type TypingStart struct { UserID string `json:"user_id"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` Timestamp int `json:"timestamp"` } @@ -250,4 +252,5 @@ type VoiceStateUpdate struct { type MessageDeleteBulk struct { Messages []string `json:"ids"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` } diff --git a/message.go b/message.go index b67c01d21..5edcb8f2e 100644 --- a/message.go +++ b/message.go @@ -38,6 +38,9 @@ type Message struct { // The ID of the channel in which the message was sent. ChannelID string `json:"channel_id"` + // The ID of the guild in which the message was sent. + GuildID string `json:"guild_id,omitempty"` + // The content of the message. Content string `json:"content"` diff --git a/structs.go b/structs.go index 439a15e76..bba5a0189 100644 --- a/structs.go +++ b/structs.go @@ -813,6 +813,7 @@ type MessageReaction struct { MessageID string `json:"message_id"` Emoji Emoji `json:"emoji"` ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` } // GatewayBotResponse stores the data for the gateway/bot response From 80ac3826416d1ff0add27d7644eeaba681e55b98 Mon Sep 17 00:00:00 2001 From: Rens Rikkerink Date: Wed, 9 May 2018 14:55:49 +0200 Subject: [PATCH 33/64] Added GuildChannelCreateComplex --- restapi.go | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/restapi.go b/restapi.go index e5ef9f8b8..436f02a8d 100644 --- a/restapi.go +++ b/restapi.go @@ -910,17 +910,22 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { return } -// GuildChannelCreate creates a new channel in the given guild -// guildID : The ID of a Guild. -// name : Name of the channel (2-100 chars length) -// ctype : Type of the channel -func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { - - data := struct { - Name string `json:"name"` - Type ChannelType `json:"type"` - }{name, ctype} - +// GuildChannelCreateData is provided to GuildChannelCreateComplex +type GuildChannelCreateData struct { + Name string `json:"name"` + Type ChannelType `json:"type"` + Topic string `json:"topic,omitempty"` + Bitrate int `json:"bitrate,omitempty"` + UserLimit int `json:"user_limit,omitempty"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID string `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` +} + +// GuildChannelCreateComplex creates a new channel in the given guild +// guildID : The ID of a Guild +// data : A data struct describing the new Channel, Name and Type are mandatory, other fields depending on the type +func (s *Session) GuildChannelCreateComplex(guildID string, data GuildChannelCreateData) (st *Channel, err error) { body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) if err != nil { return @@ -930,6 +935,17 @@ func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (s return } +// GuildChannelCreate creates a new channel in the given guild +// guildID : The ID of a Guild. +// name : Name of the channel (2-100 chars length) +// ctype : Type of the channel +func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { + return s.GuildChannelCreateComplex(guildID, GuildChannelCreateData{ + Name: name, + Type: ctype, + }) +} + // GuildChannelsReorder updates the order of channels in a guild // guildID : The ID of a Guild. // channels : Updated channels. From 8f686efd241c5355b1fb9e716ea073509b3fc3b6 Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Sun, 20 May 2018 14:37:02 +0200 Subject: [PATCH 34/64] adds GuildEmojiCreate, GuildEmojiEdit, and GuildEmojiDelete --- endpoints.go | 2 ++ restapi.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/endpoints.go b/endpoints.go index 2c6959592..95f6fd9af 100644 --- a/endpoints.go +++ b/endpoints.go @@ -95,6 +95,8 @@ var ( EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } + EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" } + EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } diff --git a/restapi.go b/restapi.go index e5ef9f8b8..e4c7d2b27 100644 --- a/restapi.go +++ b/restapi.go @@ -1285,6 +1285,56 @@ func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, li return } +// GuildEmojiCreate creates a new emoji +// guildID : The ID of a Guild. +// image : the base64 encoded emoji image, has to be smaller than 256KB +// roles : roles for which this emoji will be whitelisted, can be nil +func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Image string `json:"image"` + Roles []string `json:"roles,omitempty"` + }{name, image, roles} + + body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiEdit modifies an emoji +// guildID : The ID of a Guild. +// image : the base64 encoded emoji image, has to be smaller than 256KB +// roles : roles for which this emoji will be whitelisted, can be nil +func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Roles []string `json:"roles,omitempty"` + }{name, roles} + + body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiDelete deletes an Emoji. +// guildID : The ID of a Guild. +// emojiID : The ID of the Emoji. +func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID)) + return +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Channels // ------------------------------------------------------------------------------------------------ From 1bf986726ad3cb3fc9acdce9b44facab6b133bf6 Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Sun, 20 May 2018 14:40:55 +0200 Subject: [PATCH 35/64] fixes comments --- restapi.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/restapi.go b/restapi.go index e4c7d2b27..632fb1e5f 100644 --- a/restapi.go +++ b/restapi.go @@ -1287,8 +1287,9 @@ func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, li // GuildEmojiCreate creates a new emoji // guildID : The ID of a Guild. -// image : the base64 encoded emoji image, has to be smaller than 256KB -// roles : roles for which this emoji will be whitelisted, can be nil +// name : The Name of the Emoji. +// image : The base64 encoded emoji image, has to be smaller than 256KB. +// roles : The roles for which this emoji will be whitelisted, can be nil. func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) { data := struct { @@ -1308,8 +1309,9 @@ func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) // GuildEmojiEdit modifies an emoji // guildID : The ID of a Guild. -// image : the base64 encoded emoji image, has to be smaller than 256KB -// roles : roles for which this emoji will be whitelisted, can be nil +// emojiID : The ID of an Emoji. +// name : The Name of the Emoji. +// roles : The roles for which this emoji will be whitelisted, can be nil. func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) { data := struct { @@ -1328,7 +1330,7 @@ func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) // GuildEmojiDelete deletes an Emoji. // guildID : The ID of a Guild. -// emojiID : The ID of the Emoji. +// emojiID : The ID of an Emoji. func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { _, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID)) From 9aae26445f536c1a2e3fafd5f5f057c3fa2112bf Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Wed, 23 May 2018 16:27:14 +0200 Subject: [PATCH 36/64] adds InviteWithCounts --- restapi.go | 13 +++++++++++++ structs.go | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/restapi.go b/restapi.go index e5ef9f8b8..2855e79ac 100644 --- a/restapi.go +++ b/restapi.go @@ -1717,6 +1717,19 @@ func (s *Session) Invite(inviteID string) (st *Invite, err error) { return } +// InviteWithCounts returns an Invite structure of the given invite including approximate member counts +// inviteID : The invite code +func (s *Session) InviteWithCounts(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID)+"?with_counts=true", nil, EndpointInvite("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + // InviteDelete deletes an existing invite // inviteID : the code of an invite func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { diff --git a/structs.go b/structs.go index bba5a0189..1906e2dea 100644 --- a/structs.go +++ b/structs.go @@ -177,6 +177,10 @@ type Invite struct { Revoked bool `json:"revoked"` Temporary bool `json:"temporary"` Unique bool `json:"unique"` + + // will only be filled when using InviteWithCounts + ApproximatePresenceCount int `json:"approximate_presence_count"` + ApproximateMemberCount int `json:"approximate_member_count"` } // ChannelType is the type of a Channel From bb42325c3a1cc2706ea4557b733fe8a99416eaf7 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Sat, 26 May 2018 16:52:00 -0700 Subject: [PATCH 37/64] Add an explicit log and error for when a request is attempted with an unauthorized token that is not a bot token. (#553) --- restapi.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/restapi.go b/restapi.go index 7be51ea39..e58b4e7b8 100644 --- a/restapi.go +++ b/restapi.go @@ -38,6 +38,7 @@ var ( 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") + ErrUnauthorized = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header") ) // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr @@ -129,13 +130,9 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b } switch resp.StatusCode { - case http.StatusOK: case http.StatusCreated: case http.StatusNoContent: - - // TODO check for 401 response, invalidate token if we get one. - case http.StatusBadGateway: // Retry sending request if possible if sequence < s.MaxRestRetries { @@ -145,7 +142,6 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b } else { err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) } - case 429: // TOO MANY REQUESTS - Rate limiting rl := TooManyRequests{} err = json.Unmarshal(response, &rl) @@ -161,7 +157,12 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b // this method can cause longer delays than required response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence) - + case http.StatusUnauthorized: + if strings.Index(s.Token, "Bot ") != 0 { + s.log(LogInformational, ErrUnauthorized.Error()) + err = ErrUnauthorized + } + fallthrough default: // Error condition err = newRestError(req, resp, response) } From 703389b219ff6e014858b4945a29e4348d9a7572 Mon Sep 17 00:00:00 2001 From: Shawn Toffel Date: Wed, 30 May 2018 00:04:52 +0000 Subject: [PATCH 38/64] #554 Added locale to user struct --- user.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/user.go b/user.go index 5de4a8244..618a7d59e 100644 --- a/user.go +++ b/user.go @@ -21,6 +21,9 @@ type User struct { // to retrieve the avatar itself. Avatar string `json:"avatar"` + // The user's chosen language option. + Locale string `json:"locale"` + // The discriminator of the user (4 numbers after name). Discriminator string `json:"discriminator"` From e28d444422189fb99789df2ce1e2b4e5a740cf3c Mon Sep 17 00:00:00 2001 From: Cory Ory Date: Thu, 7 Jun 2018 02:20:03 +0100 Subject: [PATCH 39/64] Add userlimit to the channel struct (#557) --- structs.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/structs.go b/structs.go index 1906e2dea..6acbc3cf3 100644 --- a/structs.go +++ b/structs.go @@ -239,6 +239,9 @@ type Channel struct { // A list of permission overwrites present for the channel. PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` + // The user limit of the voice channel. + UserLimit int `json:"user_limit"` + // The ID of the parent channel, if the channel is under a category ParentID string `json:"parent_id"` } From 67729f3d95e80c3c60f97bd083d76f8244696f4d Mon Sep 17 00:00:00 2001 From: Aaron Meese Date: Wed, 6 Jun 2018 19:29:09 -0600 Subject: [PATCH 40/64] Update README.md (#542) Added hyphens to bullet points to make them match the 'Special Thanks' style and a proper comma --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index eb9f14f04..d526d35c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# DiscordGo +# DiscordGo [![GoDoc](https://godoc.org/github.com/bwmarrin/discordgo?status.svg)](https://godoc.org/github.com/bwmarrin/discordgo) [![Go report](http://goreportcard.com/badge/bwmarrin/discordgo)](http://goreportcard.com/report/bwmarrin/discordgo) [![Build Status](https://travis-ci.org/bwmarrin/discordgo.svg?branch=master)](https://travis-ci.org/bwmarrin/discordgo) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23discordgo-blue.svg)](https://discord.gg/0f1SbxBZjYoCtNPP) [![Discord API](https://img.shields.io/badge/Discord%20API-%23go_discordgo-blue.svg)](https://discord.gg/0SBTUU1wZTWT6sqd) @@ -15,11 +15,11 @@ to add the official DiscordGo test bot **dgo** to your server. This provides indispensable help to this project. * See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of -additional voice helper functions and features for DiscordGo +additional voice helper functions and features for DiscordGo. * See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with -Discord (and DiscordGo) +Discord (and DiscordGo). **For help with this package or general Go discussion, please join the [Discord Gophers](https://discord.gg/0f1SbxBZjYq9jLBk) chat server.** @@ -39,9 +39,9 @@ the breaking changes get documented before pushing to master. *So, what should you use?* -If you can accept the constant changing nature of *develop* then it is the +If you can accept the constant changing nature of *develop*, it is the recommended branch to use. Otherwise, if you want to tail behind development -slightly and have a more stable package with documented releases then use *master* +slightly and have a more stable package with documented releases, use *master*. ### Installing @@ -96,10 +96,10 @@ that information in a nice format. ## Examples Below is a list of examples and other projects using DiscordGo. Please submit -an issue if you would like your project added or removed from this list +an issue if you would like your project added or removed from this list. -- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) A collection of example programs written with DiscordGo -- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) A curated list of high quality projects using DiscordGo +- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) - A collection of example programs written with DiscordGo +- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) - A curated list of high quality projects using DiscordGo ## Troubleshooting For help with common problems please reference the @@ -114,7 +114,7 @@ Contributions are very welcomed, however please follow the below guidelines. discussed. - Fork the develop branch and make your changes. - Try to match current naming conventions as closely as possible. -- This package is intended to be a low level direct mapping of the Discord API +- This package is intended to be a low level direct mapping of the Discord API, so please avoid adding enhancements outside of that scope without first discussing it. - Create a Pull Request with your changes against the develop branch. @@ -127,4 +127,4 @@ comparison and list of other Discord API libraries. ## Special Thanks -[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs +[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs. From 6f45ea3535c212339d7602a908a1df12b774f8fd Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Fri, 8 Jun 2018 14:25:06 +0200 Subject: [PATCH 41/64] adds Unique field to ChannelInviteCreate --- restapi.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/restapi.go b/restapi.go index e58b4e7b8..476f78335 100644 --- a/restapi.go +++ b/restapi.go @@ -1718,7 +1718,8 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e MaxAge int `json:"max_age"` MaxUses int `json:"max_uses"` Temporary bool `json:"temporary"` - }{i.MaxAge, i.MaxUses, i.Temporary} + Unique bool `json:"unique"` + }{i.MaxAge, i.MaxUses, i.Temporary, i.Unique} body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) if err != nil { From d7a58a4f11c0d553eac6ad976e31bcd2e2e5857a Mon Sep 17 00:00:00 2001 From: Altarrel Date: Mon, 18 Jun 2018 16:42:32 -0400 Subject: [PATCH 42/64] Minor spelling fixes (#559) --- discord.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord.go b/discord.go index df911cbeb..681541efa 100644 --- a/discord.go +++ b/discord.go @@ -6,8 +6,8 @@ // license that can be found in the LICENSE file. // This file contains high level helper functions and easy entry points for the -// entire discordgo package. These functions are beling developed and are very -// experimental at this point. They will most likley change so please use the +// entire discordgo package. These functions are being developed and are very +// experimental at this point. They will most likely change so please use the // low level functions if that's a problem. // Package discordgo provides Discord binding for Go From e5c968d7a6c4fbe7d465a7712f6307d4cf472644 Mon Sep 17 00:00:00 2001 From: necro Date: Sat, 23 Jun 2018 16:06:45 -0400 Subject: [PATCH 43/64] Add Member.Mention --- structs.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/structs.go b/structs.go index 6acbc3cf3..467ac267d 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 44/64] 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 3bbf6212b..769e5477a 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 d6e795c573539dd0eea92917e5c94acbd0508169 Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Mon, 23 Jul 2018 20:09:49 -0700 Subject: [PATCH 45/64] Make sure MemberCount isn't stomped when Guilds are added. --- state.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/state.go b/state.go index 695f47cb1..3a4643fd2 100644 --- a/state.go +++ b/state.go @@ -99,6 +99,9 @@ func (s *State) GuildAdd(guild *Guild) error { if g, ok := s.guildMap[guild.ID]; ok { // We are about to replace `g` in the state with `guild`, but first we need to // make sure we preserve any fields that the `guild` doesn't contain from `g`. + if guild.MemberCount == 0 { + guild.MemberCount = g.MemberCount + } if guild.Roles == nil { guild.Roles = g.Roles } From 82be39a218b056d4b5f9e8460b776830fd92a1ac Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 25 Jul 2018 12:00:40 +0300 Subject: [PATCH 46/64] 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 8bff87b1b..792bfc404 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 1593c55523958ab2f7c0473931915efa638718a0 Mon Sep 17 00:00:00 2001 From: Rens Rikkerink <1952177+ikkerens@users.noreply.github.com> Date: Tue, 31 Jul 2018 20:36:58 +0200 Subject: [PATCH 47/64] Added .MessageFormat() to emoji struct (#569) * Added .ChatName() to emoji struct * Renamed ChatName to MessageFormat * Fix lint --- structs.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/structs.go b/structs.go index 6acbc3cf3..d089b5f26 100644 --- a/structs.go +++ b/structs.go @@ -281,6 +281,19 @@ type Emoji struct { Animated bool `json:"animated"` } +// MessageFormat returns a correctly formatted Emoji for use in Message content and embeds +func (e *Emoji) MessageFormat() string { + if e.ID != "" && e.Name != "" { + if e.Animated { + return "" + } + + return "<:" + e.APIName() + ">" + } + + return e.APIName() +} + // APIName returns an correctly formatted API name for use in the MessageReactions endpoints. func (e *Emoji) APIName() string { if e.ID != "" && e.Name != "" { From 9c1fd5d8a9f27aded86b21be4d1591916dfe56e0 Mon Sep 17 00:00:00 2001 From: Unknown Date: Tue, 31 Jul 2018 15:32:29 -0500 Subject: [PATCH 48/64] 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 d089b5f26..aa4d05253 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 49/64] endpoints(): add EndpointEmojiAnimated (#572) --- endpoints.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/endpoints.go b/endpoints.go index 95f6fd9af..b96190897 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 50/64] Add ErrCodeUnknownWebhook (#573) --- structs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/structs.go b/structs.go index d089b5f26..a7d298c8a 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 51/64] 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 5cc157de2..d2b9a98b7 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 e784cac7c..c4fb52055 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"` +} From 22c5f24532e162b2fa421319c39c288ea35a0430 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Thu, 23 Aug 2018 00:33:57 +0000 Subject: [PATCH 52/64] Fixed a couple of error msgs --- voice.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/voice.go b/voice.go index 3bbf6212b..85fb6b8a1 100644 --- a/voice.go +++ b/voice.go @@ -103,7 +103,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { defer v.Unlock() if err != nil { v.speaking = false - v.log(LogError, "Speaking() write json error:", err) + v.log(LogError, "Speaking() write json errorR, %s", err) return } @@ -180,7 +180,7 @@ func (v *VoiceConnection) Close() { v.log(LogInformational, "closing udp") err := v.udpConn.Close() if err != nil { - v.log(LogError, "error closing udp connection: ", err) + v.log(LogError, "error closing udp connection, %s", err) } v.udpConn = nil } From a089b52f64e87992794a62d3640468ad95dd4244 Mon Sep 17 00:00:00 2001 From: Christopher F Date: Wed, 22 Aug 2018 21:04:44 -0400 Subject: [PATCH 53/64] feature: add ChannelVoiceJoinManual This resolves #577. ChannelVoiceJoinManual is a wrapper over sending an OP4 to Discord, for initiating a voice server connection. The library's builtin voice connection management locks/maps are skipped, and the library will not attempt to manage this voice connection. Users are expected to hook the VoiceServerUpdate event and forward the data to an outside voice manager, such as Lavalink. --- wsapi.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/wsapi.go b/wsapi.go index 80e85ac30..93c3bea93 100644 --- a/wsapi.go +++ b/wsapi.go @@ -623,6 +623,30 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi return } +// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it. +// +// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere. +// +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) { + + s.log(LogInformational, "called") + + // Send the request to Discord that we want to join the voice channel + data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(data) + s.wsMutex.Unlock() + if err != nil { + return + } + + return +} + // onVoiceStateUpdate handles Voice State Update events on the data websocket. func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { From e7ec3c1e374e1c8dc88365df1faa0187470901a2 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Mon, 27 Aug 2018 15:37:58 +0000 Subject: [PATCH 54/64] Fix a typo. --- voice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/voice.go b/voice.go index b904508b4..aa630b128 100644 --- a/voice.go +++ b/voice.go @@ -104,7 +104,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { defer v.Unlock() if err != nil { v.speaking = false - v.log(LogError, "Speaking() write json errorR, %s", err) + v.log(LogError, "Speaking() write json error, %s", err) return } From 27581e260f189029083ee7e9191869d0ff218f31 Mon Sep 17 00:00:00 2001 From: Necroforger Date: Thu, 30 Aug 2018 20:08:21 -0400 Subject: [PATCH 55/64] Add PermissionManageWebhooks and PermissionManageEmojis to PermissionAll (#580) --- structs.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/structs.go b/structs.go index 97f726ca8..f242f001b 100644 --- a/structs.go +++ b/structs.go @@ -915,7 +915,9 @@ const ( PermissionKickMembers | PermissionBanMembers | PermissionManageServer | - PermissionAdministrator + PermissionAdministrator | + PermissionManageWebhooks | + PermissionManageEmojis ) // Block contains Discord JSON Error Response codes From 8dc444f78a3d4c5db37dd5f645d5bfc990c2ab46 Mon Sep 17 00:00:00 2001 From: Bruce Marriner Date: Fri, 31 Aug 2018 14:26:02 +0000 Subject: [PATCH 56/64] Moved noisy heartbeat logs to Debug --- wsapi.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wsapi.go b/wsapi.go index 3da541255..1e2a866ad 100644 --- a/wsapi.go +++ b/wsapi.go @@ -85,7 +85,7 @@ 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 }) @@ -287,7 +287,7 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} last := s.LastHeartbeatAck s.RUnlock() sequence := atomic.LoadInt64(s.sequence) - s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) + s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() @@ -520,7 +520,7 @@ func (s *Session) onEvent(messageType int, message []byte) (*Event, error) { s.Lock() s.LastHeartbeatAck = time.Now().UTC() s.Unlock() - s.log(LogInformational, "got heartbeat ACK") + s.log(LogDebug, "got heartbeat ACK") return e, nil } From 6d2c944ad6d5a22911f3b872e253d44a836aee7b Mon Sep 17 00:00:00 2001 From: Sebastian Winkler Date: Thu, 20 Sep 2018 21:38:28 +0200 Subject: [PATCH 57/64] adds rate_limit_per_user to ChannelEdit (#585) --- structs.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/structs.go b/structs.go index f242f001b..a7b30dd4d 100644 --- a/structs.go +++ b/structs.go @@ -251,7 +251,7 @@ func (c *Channel) Mention() string { return fmt.Sprintf("<#%s>", c.ID) } -// A ChannelEdit holds Channel Feild data for a channel edit. +// A ChannelEdit holds Channel Field data for a channel edit. type ChannelEdit struct { Name string `json:"name,omitempty"` Topic string `json:"topic,omitempty"` @@ -261,6 +261,7 @@ type ChannelEdit struct { UserLimit int `json:"user_limit,omitempty"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID string `json:"parent_id,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` } // A PermissionOverwrite holds permission overwrite data for a Channel From c8554477e44900990a8fbad29ca17ecb3412e832 Mon Sep 17 00:00:00 2001 From: Connor Wright <325329@student.lincolncollege.ac.uk> Date: Wed, 26 Sep 2018 13:22:21 -0700 Subject: [PATCH 58/64] Add HeartbeatLatency method (#593) * Latency method * fixed typo * fixed linter error * Renamed Latency to HeartbeatLatency * HeartbeatLatency now returns time.Time * return time.Duration instead, since .Sub() returns that * Add full-stops to end of comments --- structs.go | 3 +++ wsapi.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/structs.go b/structs.go index a7b30dd4d..4465ec52c 100644 --- a/structs.go +++ b/structs.go @@ -85,6 +85,9 @@ type Session struct { // Stores the last HeartbeatAck that was recieved (in UTC) LastHeartbeatAck time.Time + // Stores the last Heartbeat sent (in UTC) + LastHeartbeatSent time.Time + // used to deal with rate limits Ratelimiter *RateLimiter diff --git a/wsapi.go b/wsapi.go index 1e2a866ad..8ecaaa772 100644 --- a/wsapi.go +++ b/wsapi.go @@ -267,6 +267,13 @@ type helloOp struct { // FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond +// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send. +func (s *Session) HeartbeatLatency() time.Duration { + + return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent) + +} + // 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. @@ -289,6 +296,7 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} sequence := atomic.LoadInt64(s.sequence) s.log(LogDebug, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() + s.LastHeartbeatSent = time.Now().UTC() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { From 8fb9c2a11dec6f71d180301562fa27e132537207 Mon Sep 17 00:00:00 2001 From: Soumil07 Date: Wed, 24 Oct 2018 21:00:37 +0530 Subject: [PATCH 59/64] track membercount on memberAdd and leave (#602) * track membercount on memberAdd and leave * requested changes --- state.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/state.go b/state.go index 3a4643fd2..e6f08c730 100644 --- a/state.go +++ b/state.go @@ -814,6 +814,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { case *GuildDelete: err = s.GuildRemove(t.Guild) case *GuildMemberAdd: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount++ + + // Caches member if tracking is enabled. if s.TrackMembers { err = s.MemberAdd(t.Member) } @@ -822,6 +830,14 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { err = s.MemberAdd(t.Member) } case *GuildMemberRemove: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount-- + + // Removes member from the cache if tracking is enabled. if s.TrackMembers { err = s.MemberRemove(t.Member) } From 797adeefdeb3f40fe4848f62492371cf7425cf2b Mon Sep 17 00:00:00 2001 From: Robert M Date: Wed, 24 Oct 2018 18:15:31 -0400 Subject: [PATCH 60/64] Fixed travis test failing for older versions of go (#604) * Fixed travis test failing for older versions of go * changed supported go version --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe626fcfd..2656ae539 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: go go: - - 1.7.x - - 1.8.x - 1.9.x + - 1.10.x + - 1.11.x install: - go get github.com/bwmarrin/discordgo - go get -v . - - go get -v github.com/golang/lint/golint + - go get -v golang.org/x/lint/golint script: - diff <(gofmt -d .) <(echo -n) - go vet -x ./... From 412fc067e6cd66494d76259aee3925f8dd18c222 Mon Sep 17 00:00:00 2001 From: Soumil07 Date: Fri, 26 Oct 2018 20:27:14 +0530 Subject: [PATCH 61/64] Add WebhookID member to message property (#607) * track membercount on memberAdd and leave * requested changes * add WebhookID member to message --- message.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/message.go b/message.go index 5edcb8f2e..2b609920b 100644 --- a/message.go +++ b/message.go @@ -82,6 +82,9 @@ type Message struct { // The type of the message. Type MessageType `json:"type"` + + // The webhook ID of the message, if it was generated by a webhook + WebhookID string `json:"webhook_id"` } // File stores info about files you e.g. send in messages. From b86482cd7704fe20c09aaa3ec1e06846f764429e Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Fri, 2 Nov 2018 10:37:52 -0700 Subject: [PATCH 62/64] Backport retrieving Guilds from State to develop. Fix bugs when Guild is Unavailable --- restapi.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/restapi.go b/restapi.go index 16a354792..84a2a31e7 100644 --- a/restapi.go +++ b/restapi.go @@ -578,6 +578,14 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss // Guild returns a Guild structure of a specific Guild. // guildID : The ID of a Guild func (s *Session) Guild(guildID string) (st *Guild, err error) { + if s.StateEnabled { + // Attempt to grab the guild from State first. + st, err = s.State.Guild(guildID) + if err == nil && !st.Unavailable { + return + } + } + body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return From 9b7b311486800e3c62aaf1c2ce2503303b32973f Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Fri, 2 Nov 2018 11:12:51 -0700 Subject: [PATCH 63/64] Add go module support --- go.mod | 6 ++++++ go.sum | 4 ++++ 2 files changed, 10 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..2ff886807 --- /dev/null +++ b/go.mod @@ -0,0 +1,6 @@ +module github.com/bwmarrin/discordgo + +require ( + github.com/gorilla/websocket v1.4.0 + golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..a86b0501e --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= From a040eaef9b170c879c13213375aa0baa71d86d3b Mon Sep 17 00:00:00 2001 From: Chris Rhodes Date: Fri, 2 Nov 2018 11:13:46 -0700 Subject: [PATCH 64/64] Version 0.18.0 --- discord.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord.go b/discord.go index 681541efa..c6751426b 100644 --- a/discord.go +++ b/discord.go @@ -21,7 +21,7 @@ import ( ) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) -const VERSION = "0.18.0-alpha" +const VERSION = "0.18.0" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled")