diff --git a/.travis.yml b/.travis.yml index fe626fc..2656ae5 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 ./... diff --git a/README.md b/README.md index acc72bf..cb5a665 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. diff --git a/discord.go b/discord.go index 99fda30..cdac67f 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 @@ -21,7 +21,7 @@ import ( ) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) -const VERSION = "0.18.0" +const VERSION = "0.19.0" // ErrMFA will be risen by New when the user has 2FA. var ErrMFA = errors.New("account has 2FA enabled") diff --git a/endpoints.go b/endpoints.go index 335e224..b961908 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" } @@ -88,6 +94,9 @@ 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" } + 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" } @@ -127,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" diff --git a/event.go b/event.go index bba396c..97cc00a 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/eventhandlers.go b/eventhandlers.go index 5cc157d..d2b9a98 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -50,6 +50,7 @@ const ( userUpdateEventType = "USER_UPDATE" voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" voiceStateUpdateEventType = "VOICE_STATE_UPDATE" + webhooksUpdateEventType = "WEBHOOKS_UPDATE" ) // channelCreateEventHandler is an event handler for ChannelCreate events. @@ -892,6 +893,26 @@ func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events. +type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate) + +// Type returns the event type for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Type() string { + return webhooksUpdateEventType +} + +// New returns a new instance of WebhooksUpdate. +func (eh webhooksUpdateEventHandler) New() interface{} { + return &WebhooksUpdate{} +} + +// Handle is the handler for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*WebhooksUpdate); ok { + eh(s, t) + } +} + func handlerForInterface(handler interface{}) EventHandler { switch v := handler.(type) { case func(*Session, interface{}): @@ -982,6 +1003,8 @@ func handlerForInterface(handler interface{}) EventHandler { return voiceServerUpdateEventHandler(v) case func(*Session, *VoiceStateUpdate): return voiceStateUpdateEventHandler(v) + case func(*Session, *WebhooksUpdate): + return webhooksUpdateEventHandler(v) } return nil @@ -1027,4 +1050,5 @@ func init() { registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) + registerInterfaceProvider(webhooksUpdateEventHandler(nil)) } diff --git a/events.go b/events.go index c78fbdd..c4fb520 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,11 @@ type VoiceStateUpdate struct { type MessageDeleteBulk struct { Messages []string `json:"ids"` 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"` } diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2ff8868 --- /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 0000000..a86b050 --- /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= diff --git a/message.go b/message.go index 19345b9..2b60992 100644 --- a/message.go +++ b/message.go @@ -32,20 +32,59 @@ 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 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"` + + // 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"` + + // 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. @@ -237,7 +276,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 fd2f133..792bfc4 100644 --- a/message_test.go +++ b/message_test.go @@ -12,7 +12,6 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) { Username: "User Name", } - s.StateEnabled = true s.State.GuildAdd(&Guild{ID: "guild"}) s.State.RoleAdd("guild", &Role{ ID: "role", @@ -30,7 +29,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}, diff --git a/oauth2_test.go b/oauth2_test.go index 0ff0ca0..1d5451b 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 diff --git a/restapi.go b/restapi.go index 2249dcc..de0a255 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 @@ -89,7 +90,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b 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 { @@ -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) } @@ -249,7 +250,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 @@ -361,6 +362,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) { @@ -412,7 +428,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("")) @@ -565,7 +581,7 @@ 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 { + if err == nil && !st.Unavailable { return } } @@ -735,7 +751,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)) @@ -761,6 +777,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 @@ -877,17 +919,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 : Tpye of the channel (voice or text) -func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) { - - data := struct { - Name string `json:"name"` - Type string `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 @@ -897,12 +944,33 @@ func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, 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. 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 } @@ -1021,7 +1089,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=" + strconv.FormatUint(uint64(days), 10) body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) if err != nil { return @@ -1075,7 +1143,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 { @@ -1206,6 +1274,94 @@ 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 +} + +// GuildEmojiCreate creates a new emoji +// guildID : The ID of a Guild. +// 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 { + 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. +// 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 { + 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 an 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 // ------------------------------------------------------------------------------------------------ @@ -1291,7 +1447,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)) @@ -1586,7 +1742,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 { @@ -1638,6 +1795,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) { @@ -1830,12 +2000,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 { @@ -1965,7 +2136,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/state.go b/state.go index 8158708..e6f08c7 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 @@ -98,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 } @@ -299,7 +303,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 @@ -607,7 +616,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 @@ -805,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) } @@ -813,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) } diff --git a/structs.go b/structs.go index 19d2bad..4465ec5 100644 --- a/structs.go +++ b/structs.go @@ -13,6 +13,7 @@ package discordgo import ( "encoding/json" + "fmt" "net/http" "sync" "time" @@ -84,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 @@ -111,6 +115,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"` @@ -145,6 +180,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 @@ -161,22 +200,61 @@ 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"` + + // Icon of the group DM channel. + Icon string `json:"icon"` + + // 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 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"` } -// A ChannelEdit holds Channel Feild data for a channel edit. +// Mention returns a string which mentions the channel +func (c *Channel) Mention() string { + return fmt.Sprintf("<#%s>", c.ID) +} + +// A ChannelEdit holds Channel Field data for a channel edit. type ChannelEdit struct { Name string `json:"name,omitempty"` Topic string `json:"topic,omitempty"` @@ -186,6 +264,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 @@ -206,6 +285,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 != "" { @@ -228,31 +320,129 @@ 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 { - 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"` + + // 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 @@ -279,14 +469,37 @@ 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 +func (r *Role) Mention() string { + return fmt.Sprintf("<@&%s>", r.ID) } // Roles are a collection of Role @@ -334,6 +547,8 @@ type GameType int const ( GameTypeGame GameType = iota GameTypeStreaming + GameTypeListening + GameTypeWatching ) // A Game struct holds the name of the "playing .." game for a user @@ -379,15 +594,34 @@ 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 Timestamp `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"` +} + +// 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. @@ -467,33 +701,88 @@ 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"` 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"` @@ -553,6 +842,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 @@ -629,7 +919,9 @@ const ( PermissionKickMembers | PermissionBanMembers | PermissionManageServer | - PermissionAdministrator + PermissionAdministrator | + PermissionManageWebhooks | + PermissionManageEmojis ) // Block contains Discord JSON Error Response codes @@ -648,6 +940,7 @@ const ( ErrCodeUnknownToken = 10012 ErrCodeUnknownUser = 10013 ErrCodeUnknownEmoji = 10014 + ErrCodeUnknownWebhook = 10015 ErrCodeBotsCannotUseEndpoint = 20001 ErrCodeOnlyBotsCanUseEndpoint = 20002 diff --git a/types.go b/types.go index 780b6bb..c0ce013 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 + ", " + string(r.ResponseBody) } diff --git a/user.go b/user.go index a710f28..a9af31a 100644 --- a/user.go +++ b/user.go @@ -1,31 +1,51 @@ package discordgo -import ( - "fmt" - "strings" -) +import "strings" // A User stores all data for an individual Discord user. type User struct { - ID string `json:"id"` - Email string `json:"email"` - Username string `json:"username"` - Avatar string `json:"avatar"` + // 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 user's chosen language option. + Locale string `json:"locale"` + + // 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 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 + ">" } // AvatarURL returns a URL to the user's avatar. @@ -34,7 +54,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) diff --git a/voice.go b/voice.go index 3bbf621..aa630b1 100644 --- a/voice.go +++ b/voice.go @@ -14,6 +14,7 @@ import ( "encoding/json" "fmt" "net" + "strconv" "strings" "sync" "time" @@ -103,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 error:", err) + v.log(LogError, "Speaking() write json error, %s", err) return } @@ -135,7 +136,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 @@ -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 } @@ -299,7 +299,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 { @@ -542,7 +542,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) diff --git a/wsapi.go b/wsapi.go index de66f69..8ecaaa7 100644 --- a/wsapi.go +++ b/wsapi.go @@ -86,6 +86,10 @@ func (s *Session) Open() error { 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 // when exiting with an error :) Maybe someone has a better @@ -263,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. @@ -283,8 +294,9 @@ 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() + s.LastHeartbeatSent = time.Now().UTC() err = wsConn.WriteJSON(heartbeatOp{1, sequence}) s.wsMutex.Unlock() if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { @@ -323,16 +335,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 +345,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 +352,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,14 +399,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, "") -} - type requestGuildMembersData struct { GuildID string `json:"guild_id"` Query string `json:"query"` @@ -508,7 +528,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 } @@ -615,6 +635,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) { @@ -732,11 +776,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() {