diff --git a/vendor/github.com/bwmarrin/discordgo/discord.go b/vendor/github.com/bwmarrin/discordgo/discord.go index 4081c65a..2f0b6fd4 100644 --- a/vendor/github.com/bwmarrin/discordgo/discord.go +++ b/vendor/github.com/bwmarrin/discordgo/discord.go @@ -13,10 +13,18 @@ // Package discordgo provides Discord binding for Go package discordgo -import "fmt" +import ( + "errors" + "fmt" + "net/http" + "time" +) -// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) -const VERSION = "0.15.0" +// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) +const VERSION = "0.16.0" + +// ErrMFA will be risen by New when the user has 2FA. +var ErrMFA = errors.New("account has 2FA enabled") // New creates a new Discord session and will automate some startup // tasks if given enough information to do so. Currently you can pass zero @@ -31,6 +39,12 @@ const VERSION = "0.15.0" // With an email, password and auth token - Discord will verify the auth // token, if it is invalid it will sign in with the provided // credentials. This is the Discord recommended way to sign in. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func New(args ...interface{}) (s *Session, err error) { // Create an empty Session interface. @@ -43,6 +57,8 @@ func New(args ...interface{}) (s *Session, err error) { ShardID: 0, ShardCount: 1, MaxRestRetries: 3, + Client: &http.Client{Timeout: (20 * time.Second)}, + sequence: new(int64), } // If no arguments are passed return the empty Session interface. @@ -60,7 +76,7 @@ func New(args ...interface{}) (s *Session, err error) { case []string: if len(v) > 3 { - err = fmt.Errorf("Too many string parameters provided.") + err = fmt.Errorf("too many string parameters provided") return } @@ -91,7 +107,7 @@ func New(args ...interface{}) (s *Session, err error) { } else if s.Token == "" { s.Token = v } else { - err = fmt.Errorf("Too many string parameters provided.") + err = fmt.Errorf("too many string parameters provided") return } @@ -99,7 +115,7 @@ func New(args ...interface{}) (s *Session, err error) { // TODO: Parse configuration struct default: - err = fmt.Errorf("Unsupported parameter type provided.") + err = fmt.Errorf("unsupported parameter type provided") return } } @@ -113,7 +129,11 @@ func New(args ...interface{}) (s *Session, err error) { } else { err = s.Login(auth, pass) if err != nil || s.Token == "" { - err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) + if s.MFA { + err = ErrMFA + } else { + err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) + } return } } diff --git a/vendor/github.com/bwmarrin/discordgo/endpoints.go b/vendor/github.com/bwmarrin/discordgo/endpoints.go index f63240ff..96bcf28b 100644 --- a/vendor/github.com/bwmarrin/discordgo/endpoints.go +++ b/vendor/github.com/bwmarrin/discordgo/endpoints.go @@ -26,6 +26,13 @@ var ( EndpointGateway = EndpointAPI + "gateway" EndpointWebhooks = EndpointAPI + "webhooks/" + EndpointCDN = "https://cdn.discordapp.com/" + EndpointCDNAttachments = EndpointCDN + "attachments/" + EndpointCDNAvatars = EndpointCDN + "avatars/" + EndpointCDNIcons = EndpointCDN + "icons/" + EndpointCDNSplashes = EndpointCDN + "splashes/" + EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" + EndpointAuth = EndpointAPI + "auth/" EndpointLogin = EndpointAuth + "login" EndpointLogout = EndpointAuth + "logout" @@ -48,7 +55,7 @@ var ( EndpointIntegrations = EndpointAPI + "integrations" EndpointUser = func(uID string) string { return EndpointUsers + uID } - EndpointUserAvatar = func(uID, aID string) string { return EndpointUsers + uID + "/avatars/" + aID + ".jpg" } + 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 } @@ -56,6 +63,7 @@ var ( 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" } @@ -73,8 +81,8 @@ var ( EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" } EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } - EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" } - EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" } + 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" } EndpointChannel = func(cID string) string { return EndpointChannels + cID } @@ -89,6 +97,8 @@ var ( EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } + EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } + EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } diff --git a/vendor/github.com/bwmarrin/discordgo/event.go b/vendor/github.com/bwmarrin/discordgo/event.go index 245f0c1f..906c2aa8 100644 --- a/vendor/github.com/bwmarrin/discordgo/event.go +++ b/vendor/github.com/bwmarrin/discordgo/event.go @@ -1,7 +1,5 @@ package discordgo -import "fmt" - // EventHandler is an interface for Discord events. type EventHandler interface { // Type returns the type of event this handler belongs to. @@ -45,12 +43,15 @@ var registeredInterfaceProviders = map[string]EventInterfaceProvider{} // registerInterfaceProvider registers a provider so that DiscordGo can // access it's New() method. -func registerInterfaceProvider(eh EventInterfaceProvider) error { +func registerInterfaceProvider(eh EventInterfaceProvider) { if _, ok := registeredInterfaceProviders[eh.Type()]; ok { - return fmt.Errorf("event %s already registered", eh.Type()) + return + // XXX: + // if we should error here, we need to do something with it. + // fmt.Errorf("event %s already registered", eh.Type()) } registeredInterfaceProviders[eh.Type()] = eh - return nil + return } // eventHandlerInstance is a wrapper around an event handler, as functions @@ -210,14 +211,15 @@ func (s *Session) onInterface(i interface{}) { setGuildIds(t.Guild) case *GuildUpdate: setGuildIds(t.Guild) - case *Resumed: - s.onResumed(t) case *VoiceServerUpdate: go s.onVoiceServerUpdate(t) case *VoiceStateUpdate: go s.onVoiceStateUpdate(t) } - s.State.onInterface(s, i) + err := s.State.onInterface(s, i) + if err != nil { + s.log(LogDebug, "error dispatching internal event, %s", err) + } } // onReady handles the ready event. @@ -225,14 +227,4 @@ func (s *Session) onReady(r *Ready) { // Store the SessionID within the Session struct. s.sessionID = r.SessionID - - // Start the heartbeat to keep the connection alive. - go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) -} - -// onResumed handles the resumed event. -func (s *Session) onResumed(r *Resumed) { - - // Start the heartbeat to keep the connection alive. - go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) } diff --git a/vendor/github.com/bwmarrin/discordgo/eventhandlers.go b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go index 6d78bacd..5cc157de 100644 --- a/vendor/github.com/bwmarrin/discordgo/eventhandlers.go +++ b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go @@ -7,46 +7,49 @@ package discordgo // Event type values are used to match the events returned by Discord. // EventTypes surrounded by __ are synthetic and are internal to DiscordGo. const ( - channelCreateEventType = "CHANNEL_CREATE" - channelDeleteEventType = "CHANNEL_DELETE" - channelPinsUpdateEventType = "CHANNEL_PINS_UPDATE" - channelUpdateEventType = "CHANNEL_UPDATE" - connectEventType = "__CONNECT__" - disconnectEventType = "__DISCONNECT__" - eventEventType = "__EVENT__" - guildBanAddEventType = "GUILD_BAN_ADD" - guildBanRemoveEventType = "GUILD_BAN_REMOVE" - guildCreateEventType = "GUILD_CREATE" - guildDeleteEventType = "GUILD_DELETE" - guildEmojisUpdateEventType = "GUILD_EMOJIS_UPDATE" - guildIntegrationsUpdateEventType = "GUILD_INTEGRATIONS_UPDATE" - guildMemberAddEventType = "GUILD_MEMBER_ADD" - guildMemberRemoveEventType = "GUILD_MEMBER_REMOVE" - guildMemberUpdateEventType = "GUILD_MEMBER_UPDATE" - guildMembersChunkEventType = "GUILD_MEMBERS_CHUNK" - guildRoleCreateEventType = "GUILD_ROLE_CREATE" - guildRoleDeleteEventType = "GUILD_ROLE_DELETE" - guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" - guildUpdateEventType = "GUILD_UPDATE" - messageAckEventType = "MESSAGE_ACK" - messageCreateEventType = "MESSAGE_CREATE" - messageDeleteEventType = "MESSAGE_DELETE" - messageReactionAddEventType = "MESSAGE_REACTION_ADD" - messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" - messageUpdateEventType = "MESSAGE_UPDATE" - presenceUpdateEventType = "PRESENCE_UPDATE" - presencesReplaceEventType = "PRESENCES_REPLACE" - rateLimitEventType = "__RATE_LIMIT__" - readyEventType = "READY" - relationshipAddEventType = "RELATIONSHIP_ADD" - relationshipRemoveEventType = "RELATIONSHIP_REMOVE" - resumedEventType = "RESUMED" - typingStartEventType = "TYPING_START" - userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE" - userSettingsUpdateEventType = "USER_SETTINGS_UPDATE" - userUpdateEventType = "USER_UPDATE" - voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" - voiceStateUpdateEventType = "VOICE_STATE_UPDATE" + channelCreateEventType = "CHANNEL_CREATE" + channelDeleteEventType = "CHANNEL_DELETE" + channelPinsUpdateEventType = "CHANNEL_PINS_UPDATE" + channelUpdateEventType = "CHANNEL_UPDATE" + connectEventType = "__CONNECT__" + disconnectEventType = "__DISCONNECT__" + eventEventType = "__EVENT__" + guildBanAddEventType = "GUILD_BAN_ADD" + guildBanRemoveEventType = "GUILD_BAN_REMOVE" + guildCreateEventType = "GUILD_CREATE" + guildDeleteEventType = "GUILD_DELETE" + guildEmojisUpdateEventType = "GUILD_EMOJIS_UPDATE" + guildIntegrationsUpdateEventType = "GUILD_INTEGRATIONS_UPDATE" + guildMemberAddEventType = "GUILD_MEMBER_ADD" + guildMemberRemoveEventType = "GUILD_MEMBER_REMOVE" + guildMemberUpdateEventType = "GUILD_MEMBER_UPDATE" + guildMembersChunkEventType = "GUILD_MEMBERS_CHUNK" + guildRoleCreateEventType = "GUILD_ROLE_CREATE" + guildRoleDeleteEventType = "GUILD_ROLE_DELETE" + guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" + guildUpdateEventType = "GUILD_UPDATE" + messageAckEventType = "MESSAGE_ACK" + messageCreateEventType = "MESSAGE_CREATE" + messageDeleteEventType = "MESSAGE_DELETE" + messageDeleteBulkEventType = "MESSAGE_DELETE_BULK" + messageReactionAddEventType = "MESSAGE_REACTION_ADD" + messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" + messageReactionRemoveAllEventType = "MESSAGE_REACTION_REMOVE_ALL" + messageUpdateEventType = "MESSAGE_UPDATE" + presenceUpdateEventType = "PRESENCE_UPDATE" + presencesReplaceEventType = "PRESENCES_REPLACE" + rateLimitEventType = "__RATE_LIMIT__" + readyEventType = "READY" + relationshipAddEventType = "RELATIONSHIP_ADD" + relationshipRemoveEventType = "RELATIONSHIP_REMOVE" + resumedEventType = "RESUMED" + typingStartEventType = "TYPING_START" + userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE" + userNoteUpdateEventType = "USER_NOTE_UPDATE" + userSettingsUpdateEventType = "USER_SETTINGS_UPDATE" + userUpdateEventType = "USER_UPDATE" + voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" + voiceStateUpdateEventType = "VOICE_STATE_UPDATE" ) // channelCreateEventHandler is an event handler for ChannelCreate events. @@ -137,11 +140,6 @@ func (eh connectEventHandler) Type() string { return connectEventType } -// New returns a new instance of Connect. -func (eh connectEventHandler) New() interface{} { - return &Connect{} -} - // Handle is the handler for Connect events. func (eh connectEventHandler) Handle(s *Session, i interface{}) { if t, ok := i.(*Connect); ok { @@ -157,11 +155,6 @@ func (eh disconnectEventHandler) Type() string { return disconnectEventType } -// New returns a new instance of Disconnect. -func (eh disconnectEventHandler) New() interface{} { - return &Disconnect{} -} - // Handle is the handler for Disconnect events. func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { if t, ok := i.(*Disconnect); ok { @@ -177,11 +170,6 @@ func (eh eventEventHandler) Type() string { return eventEventType } -// New returns a new instance of Event. -func (eh eventEventHandler) New() interface{} { - return &Event{} -} - // Handle is the handler for Event events. func (eh eventEventHandler) Handle(s *Session, i interface{}) { if t, ok := i.(*Event); ok { @@ -529,6 +517,26 @@ func (eh messageDeleteEventHandler) Handle(s *Session, i interface{}) { } } +// messageDeleteBulkEventHandler is an event handler for MessageDeleteBulk events. +type messageDeleteBulkEventHandler func(*Session, *MessageDeleteBulk) + +// Type returns the event type for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Type() string { + return messageDeleteBulkEventType +} + +// New returns a new instance of MessageDeleteBulk. +func (eh messageDeleteBulkEventHandler) New() interface{} { + return &MessageDeleteBulk{} +} + +// Handle is the handler for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageDeleteBulk); ok { + eh(s, t) + } +} + // messageReactionAddEventHandler is an event handler for MessageReactionAdd events. type messageReactionAddEventHandler func(*Session, *MessageReactionAdd) @@ -569,6 +577,26 @@ func (eh messageReactionRemoveEventHandler) Handle(s *Session, i interface{}) { } } +// messageReactionRemoveAllEventHandler is an event handler for MessageReactionRemoveAll events. +type messageReactionRemoveAllEventHandler func(*Session, *MessageReactionRemoveAll) + +// Type returns the event type for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Type() string { + return messageReactionRemoveAllEventType +} + +// New returns a new instance of MessageReactionRemoveAll. +func (eh messageReactionRemoveAllEventHandler) New() interface{} { + return &MessageReactionRemoveAll{} +} + +// Handle is the handler for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionRemoveAll); ok { + eh(s, t) + } +} + // messageUpdateEventHandler is an event handler for MessageUpdate events. type messageUpdateEventHandler func(*Session, *MessageUpdate) @@ -637,11 +665,6 @@ func (eh rateLimitEventHandler) Type() string { return rateLimitEventType } -// New returns a new instance of RateLimit. -func (eh rateLimitEventHandler) New() interface{} { - return &RateLimit{} -} - // Handle is the handler for RateLimit events. func (eh rateLimitEventHandler) Handle(s *Session, i interface{}) { if t, ok := i.(*RateLimit); ok { @@ -769,6 +792,26 @@ func (eh userGuildSettingsUpdateEventHandler) Handle(s *Session, i interface{}) } } +// userNoteUpdateEventHandler is an event handler for UserNoteUpdate events. +type userNoteUpdateEventHandler func(*Session, *UserNoteUpdate) + +// Type returns the event type for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Type() string { + return userNoteUpdateEventType +} + +// New returns a new instance of UserNoteUpdate. +func (eh userNoteUpdateEventHandler) New() interface{} { + return &UserNoteUpdate{} +} + +// Handle is the handler for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserNoteUpdate); ok { + eh(s, t) + } +} + // userSettingsUpdateEventHandler is an event handler for UserSettingsUpdate events. type userSettingsUpdateEventHandler func(*Session, *UserSettingsUpdate) @@ -901,10 +944,14 @@ func handlerForInterface(handler interface{}) EventHandler { return messageCreateEventHandler(v) case func(*Session, *MessageDelete): return messageDeleteEventHandler(v) + case func(*Session, *MessageDeleteBulk): + return messageDeleteBulkEventHandler(v) case func(*Session, *MessageReactionAdd): return messageReactionAddEventHandler(v) case func(*Session, *MessageReactionRemove): return messageReactionRemoveEventHandler(v) + case func(*Session, *MessageReactionRemoveAll): + return messageReactionRemoveAllEventHandler(v) case func(*Session, *MessageUpdate): return messageUpdateEventHandler(v) case func(*Session, *PresenceUpdate): @@ -925,6 +972,8 @@ func handlerForInterface(handler interface{}) EventHandler { return typingStartEventHandler(v) case func(*Session, *UserGuildSettingsUpdate): return userGuildSettingsUpdateEventHandler(v) + case func(*Session, *UserNoteUpdate): + return userNoteUpdateEventHandler(v) case func(*Session, *UserSettingsUpdate): return userSettingsUpdateEventHandler(v) case func(*Session, *UserUpdate): @@ -937,6 +986,7 @@ func handlerForInterface(handler interface{}) EventHandler { return nil } + func init() { registerInterfaceProvider(channelCreateEventHandler(nil)) registerInterfaceProvider(channelDeleteEventHandler(nil)) @@ -959,8 +1009,10 @@ func init() { registerInterfaceProvider(messageAckEventHandler(nil)) registerInterfaceProvider(messageCreateEventHandler(nil)) registerInterfaceProvider(messageDeleteEventHandler(nil)) + registerInterfaceProvider(messageDeleteBulkEventHandler(nil)) registerInterfaceProvider(messageReactionAddEventHandler(nil)) registerInterfaceProvider(messageReactionRemoveEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveAllEventHandler(nil)) registerInterfaceProvider(messageUpdateEventHandler(nil)) registerInterfaceProvider(presenceUpdateEventHandler(nil)) registerInterfaceProvider(presencesReplaceEventHandler(nil)) @@ -970,6 +1022,7 @@ func init() { registerInterfaceProvider(resumedEventHandler(nil)) registerInterfaceProvider(typingStartEventHandler(nil)) registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userNoteUpdateEventHandler(nil)) registerInterfaceProvider(userSettingsUpdateEventHandler(nil)) registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) diff --git a/vendor/github.com/bwmarrin/discordgo/events.go b/vendor/github.com/bwmarrin/discordgo/events.go index 19c11bda..c78fbdd2 100644 --- a/vendor/github.com/bwmarrin/discordgo/events.go +++ b/vendor/github.com/bwmarrin/discordgo/events.go @@ -2,7 +2,6 @@ package discordgo import ( "encoding/json" - "time" ) // This file contains all the possible structs that can be @@ -28,7 +27,7 @@ type RateLimit struct { // Event provides a basic initial struct for all websocket events. type Event struct { Operation int `json:"op"` - Sequence int `json:"s"` + Sequence int64 `json:"s"` Type string `json:"t"` RawData json.RawMessage `json:"d"` // Struct contains one of the other types in this file. @@ -37,19 +36,19 @@ type Event struct { // A Ready stores all data for the websocket READY event. type Ready struct { - Version int `json:"v"` - SessionID string `json:"session_id"` - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - User *User `json:"user"` - ReadState []*ReadState `json:"read_state"` - PrivateChannels []*Channel `json:"private_channels"` - Guilds []*Guild `json:"guilds"` + Version int `json:"v"` + SessionID string `json:"session_id"` + User *User `json:"user"` + ReadState []*ReadState `json:"read_state"` + PrivateChannels []*Channel `json:"private_channels"` + Guilds []*Guild `json:"guilds"` // Undocumented fields Settings *Settings `json:"user_settings"` UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` Relationships []*Relationship `json:"relationships"` Presences []*Presence `json:"presences"` + Notes map[string]string `json:"notes"` } // ChannelCreate is the data for a ChannelCreate event. @@ -179,6 +178,11 @@ type MessageReactionRemove struct { *MessageReaction } +// MessageReactionRemoveAll is the data for a MessageReactionRemoveAll event. +type MessageReactionRemoveAll struct { + *MessageReaction +} + // PresencesReplace is the data for a PresencesReplace event. type PresencesReplace []*Presence @@ -191,8 +195,7 @@ type PresenceUpdate struct { // Resumed is the data for a Resumed event. type Resumed struct { - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - Trace []string `json:"_trace"` + Trace []string `json:"_trace"` } // RelationshipAdd is the data for a RelationshipAdd event. @@ -225,6 +228,12 @@ type UserGuildSettingsUpdate struct { *UserGuildSettings } +// UserNoteUpdate is the data for a UserNoteUpdate event. +type UserNoteUpdate struct { + ID string `json:"id"` + Note string `json:"note"` +} + // VoiceServerUpdate is the data for a VoiceServerUpdate event. type VoiceServerUpdate struct { Token string `json:"token"` @@ -236,3 +245,9 @@ type VoiceServerUpdate struct { type VoiceStateUpdate struct { *VoiceState } + +// MessageDeleteBulk is the data for a MessageDeleteBulk event +type MessageDeleteBulk struct { + Messages []string `json:"ids"` + ChannelID string `json:"channel_id"` +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go b/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go index ff5e5214..21ceb76b 100644 --- a/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go +++ b/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go @@ -6,7 +6,9 @@ import ( "fmt" "io" "os" + "os/signal" "strings" + "syscall" "time" "github.com/bwmarrin/discordgo" @@ -21,6 +23,7 @@ var token string var buffer = make([][]byte, 0) func main() { + if token == "" { fmt.Println("No token provided. Please run: airhorn -t ") return @@ -56,21 +59,37 @@ func main() { fmt.Println("Error opening Discord session: ", err) } + // Wait here until CTRL-C or other term signal is received. fmt.Println("Airhorn is now running. Press CTRL-C to exit.") - // Simple way to keep program running until CTRL-C is pressed. - <-make(chan struct{}) - return + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() } +// This function will be called (due to AddHandler above) when the bot receives +// the "ready" event from Discord. func ready(s *discordgo.Session, event *discordgo.Ready) { + // Set the playing status. - _ = s.UpdateStatus(0, "!airhorn") + s.UpdateStatus(0, "!airhorn") } // This function will be called (due to AddHandler above) every time a new // message is created on any channel that the autenticated bot has access to. func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + + // check if the message is "!airhorn" if strings.HasPrefix(m.Content, "!airhorn") { + // Find the channel that the message came from. c, err := s.State.Channel(m.ChannelID) if err != nil { @@ -85,7 +104,7 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { return } - // Look for the message sender in that guilds current voice states. + // Look for the message sender in that guild's current voice states. for _, vs := range g.VoiceStates { if vs.UserID == m.Author.ID { err = playSound(s, g.ID, vs.ChannelID) @@ -102,6 +121,7 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { // This function will be called (due to AddHandler above) every time a new // guild is joined. func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { + if event.Guild.Unavailable { return } @@ -116,8 +136,8 @@ func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { // loadSound attempts to load an encoded sound file from disk. func loadSound() error { - file, err := os.Open("airhorn.dca") + file, err := os.Open("airhorn.dca") if err != nil { fmt.Println("Error opening dca file :", err) return err @@ -131,7 +151,7 @@ func loadSound() error { // If this is the end of the file, just return. if err == io.EOF || err == io.ErrUnexpectedEOF { - file.Close() + err := file.Close() if err != nil { return err } @@ -160,6 +180,7 @@ func loadSound() error { // playSound plays the current buffer to the provided channel. func playSound(s *discordgo.Session, guildID, channelID string) (err error) { + // Join the provided voice channel. vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) if err != nil { @@ -170,7 +191,7 @@ func playSound(s *discordgo.Session, guildID, channelID string) (err error) { time.Sleep(250 * time.Millisecond) // Start speaking. - _ = vc.Speaking(true) + vc.Speaking(true) // Send the buffer data. for _, buff := range buffer { @@ -178,13 +199,13 @@ func playSound(s *discordgo.Session, guildID, channelID string) (err error) { } // Stop speaking - _ = vc.Speaking(false) + vc.Speaking(false) // Sleep for a specificed amount of time before ending. time.Sleep(250 * time.Millisecond) // Disconnect from the provided voice channel. - _ = vc.Disconnect() + vc.Disconnect() return nil } diff --git a/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go b/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go index bd0e3b88..286fe169 100644 --- a/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go +++ b/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go @@ -1,38 +1,42 @@ package main import ( + "encoding/json" "flag" "fmt" + "os" "github.com/bwmarrin/discordgo" ) // Variables used for command line options var ( - Email string - Password string Token string - AppName string + Name string DeleteID string ListOnly bool ) func init() { - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") - flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&Token, "t", "", "Owner Account Token") + flag.StringVar(&Name, "n", "", "Name to give App/Bot") flag.StringVar(&DeleteID, "d", "", "Application ID to delete") flag.BoolVar(&ListOnly, "l", false, "List Applications Only") - flag.StringVar(&AppName, "a", "", "App/Bot Name") flag.Parse() + + if Token == "" { + flag.Usage() + os.Exit(1) + } } func main() { var err error + // Create a new Discord session using the provided login information. - dg, err := discordgo.New(Email, Password, Token) + dg, err := discordgo.New(Token) if err != nil { fmt.Println("error creating Discord session,", err) return @@ -41,18 +45,17 @@ func main() { // If -l set, only display a list of existing applications // for the given account. if ListOnly { - aps, err2 := dg.Applications() - if err2 != nil { + + aps, err := dg.Applications() + if err != nil { fmt.Println("error fetching applications,", err) return } - for k, v := range aps { - fmt.Printf("%d : --------------------------------------\n", k) - fmt.Printf("ID: %s\n", v.ID) - fmt.Printf("Name: %s\n", v.Name) - fmt.Printf("Secret: %s\n", v.Secret) - fmt.Printf("Description: %s\n", v.Description) + for _, v := range aps { + fmt.Println("-----------------------------------------------------") + b, _ := json.MarshalIndent(v, "", " ") + fmt.Println(string(b)) } return } @@ -66,9 +69,14 @@ func main() { return } + if Name == "" { + flag.Usage() + os.Exit(1) + } + // Create a new application. ap := &discordgo.Application{} - ap.Name = AppName + ap.Name = Name ap, err = dg.ApplicationCreate(ap) if err != nil { fmt.Println("error creating new applicaiton,", err) @@ -76,9 +84,8 @@ func main() { } fmt.Printf("Application created successfully:\n") - fmt.Printf("ID: %s\n", ap.ID) - fmt.Printf("Name: %s\n", ap.Name) - fmt.Printf("Secret: %s\n\n", ap.Secret) + b, _ := json.MarshalIndent(ap, "", " ") + fmt.Println(string(b)) // Create the bot account under the application we just created bot, err := dg.ApplicationBotCreate(ap.ID) @@ -88,11 +95,9 @@ func main() { } fmt.Printf("Bot account created successfully.\n") - fmt.Printf("ID: %s\n", bot.ID) - fmt.Printf("Username: %s\n", bot.Username) - fmt.Printf("Token: %s\n\n", bot.Token) + b, _ = json.MarshalIndent(bot, "", " ") + fmt.Println(string(b)) + fmt.Println("Please save the above posted info in a secure place.") fmt.Println("You will need that information to login with your bot account.") - - return } diff --git a/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go b/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go deleted file mode 100644 index adfe0b1d..00000000 --- a/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "encoding/base64" - "flag" - "fmt" - "io/ioutil" - "net/http" - - "github.com/bwmarrin/discordgo" -) - -// Variables used for command line parameters -var ( - Email string - Password string - Token string - Avatar string - BotID string - BotUsername string -) - -func init() { - - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") - flag.StringVar(&Token, "t", "", "Account Token") - flag.StringVar(&Avatar, "f", "./avatar.jpg", "Avatar File Name") - flag.Parse() -} - -func main() { - - // Create a new Discord session using the provided login information. - // Use discordgo.New(Token) to just use a token for login. - dg, err := discordgo.New(Email, Password, Token) - if err != nil { - fmt.Println("error creating Discord session,", err) - return - } - - bot, err := dg.User("@me") - if err != nil { - fmt.Println("error fetching the bot details,", err) - return - } - - BotID = bot.ID - BotUsername = bot.Username - changeAvatar(dg) - - fmt.Println("Bot is now running. Press CTRL-C to exit.") - // Simple way to keep program running until CTRL-C is pressed. - <-make(chan struct{}) - return -} - -// Helper function to change the avatar -func changeAvatar(s *discordgo.Session) { - img, err := ioutil.ReadFile(Avatar) - if err != nil { - fmt.Println(err) - } - - base64 := base64.StdEncoding.EncodeToString(img) - - avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64) - - _, err = s.UserUpdate("", "", BotUsername, avatar, "") - if err != nil { - fmt.Println(err) - } -} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/avatar/main.go b/vendor/github.com/bwmarrin/discordgo/examples/avatar/main.go new file mode 100644 index 00000000..e0a9c880 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/avatar/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Token string + AvatarFile string + AvatarURL string +) + +func init() { + + flag.StringVar(&Token, "t", "", "Bot Token") + flag.StringVar(&AvatarFile, "f", "", "Avatar File Name") + flag.StringVar(&AvatarURL, "u", "", "URL to the avatar image") + flag.Parse() + + if Token == "" || (AvatarFile == "" && AvatarURL == "") { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Declare these here so they can be used in the below two if blocks and + // still carry over to the end of this function. + var base64img string + var contentType string + + // If we're using a URL link for the Avatar + if AvatarURL != "" { + + resp, err := http.Get(AvatarURL) + if err != nil { + fmt.Println("Error retrieving the file, ", err) + return + } + + defer func() { + _ = resp.Body.Close() + }() + + img, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading the response, ", err) + return + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // If we're using a local file for the Avatar + if AvatarFile != "" { + img, err := ioutil.ReadFile(AvatarFile) + if err != nil { + fmt.Println(err) + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // Now lets format our base64 image into the proper format Discord wants + // and then call UserUpdate to set it as our user's Avatar. + avatar := fmt.Sprintf("data:%s;base64,%s", contentType, base64img) + _, err = dg.UserUpdate("", "", "", avatar, "") + if err != nil { + fmt.Println(err) + } +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go b/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go deleted file mode 100644 index 26170df5..00000000 --- a/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "encoding/base64" - "flag" - "fmt" - "io/ioutil" - "net/http" - - "github.com/bwmarrin/discordgo" -) - -// Variables used for command line parameters -var ( - Email string - Password string - Token string - URL string - BotID string - BotUsername string -) - -func init() { - - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") - flag.StringVar(&Token, "t", "", "Account Token") - flag.StringVar(&URL, "l", "http://bwmarrin.github.io/discordgo/img/discordgo.png", "Link to the avatar image") - flag.Parse() -} - -func main() { - - // Create a new Discord session using the provided login information. - // Use discordgo.New(Token) to just use a token for login. - dg, err := discordgo.New(Email, Password, Token) - if err != nil { - fmt.Println("error creating Discord session,", err) - return - } - - bot, err := dg.User("@me") - if err != nil { - fmt.Println("error fetching the bot details,", err) - return - } - - BotID = bot.ID - BotUsername = bot.Username - changeAvatar(dg) - - fmt.Println("Bot is now running. Press CTRL-C to exit.") - // Simple way to keep program running until CTRL-C is pressed. - <-make(chan struct{}) - return -} - -// Helper function to change the avatar -func changeAvatar(s *discordgo.Session) { - - resp, err := http.Get(URL) - if err != nil { - fmt.Println("Error retrieving the file, ", err) - return - } - - defer func() { - _ = resp.Body.Close() - }() - - img, err := ioutil.ReadAll(resp.Body) - if err != nil { - fmt.Println("Error reading the response, ", err) - return - } - - base64 := base64.StdEncoding.EncodeToString(img) - - avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64) - - _, err = s.UserUpdate("", "", BotUsername, avatar, "") - if err != nil { - fmt.Println("Error setting the avatar, ", err) - } - -} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go b/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go index 5914fc8a..9375eadc 100644 --- a/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go +++ b/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "os" "github.com/bwmarrin/discordgo" ) @@ -18,6 +19,11 @@ func init() { flag.StringVar(&Email, "e", "", "Account Email") flag.StringVar(&Password, "p", "", "Account Password") flag.Parse() + + if Email == "" || Password == "" { + flag.Usage() + os.Exit(1) + } } func main() { @@ -29,5 +35,6 @@ func main() { return } + // Print out your token. fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) } diff --git a/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go b/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go deleted file mode 100644 index 0191bb06..00000000 --- a/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "time" - - "github.com/bwmarrin/discordgo" -) - -// Variables used for command line parameters -var ( - Token string -) - -func init() { - - flag.StringVar(&Token, "t", "", "Bot Token") - flag.Parse() -} - -func main() { - - // Create a new Discord session using the provided bot token. - dg, err := discordgo.New("Bot " + Token) - if err != nil { - fmt.Println("error creating Discord session,", err) - return - } - - // Register messageCreate as a callback for the messageCreate events. - dg.AddHandler(messageCreate) - - // Open the websocket and begin listening. - err = dg.Open() - if err != nil { - fmt.Println("error opening connection,", err) - return - } - - fmt.Println("Bot is now running. Press CTRL-C to exit.") - // Simple way to keep program running until CTRL-C is pressed. - <-make(chan struct{}) - return -} - -// This function will be called (due to AddHandler above) every time a new -// message is created on any channel that the autenticated bot has access to. -func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - - // Print message to stdout. - fmt.Printf("%20s %20s %20s > %s\n", m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content) -} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go b/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go index 2edd957e..155e782f 100644 --- a/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go +++ b/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go @@ -3,6 +3,9 @@ package main import ( "flag" "fmt" + "os" + "os/signal" + "syscall" "github.com/bwmarrin/discordgo" ) @@ -10,7 +13,6 @@ import ( // Variables used for command line parameters var ( Token string - BotID string ) func init() { @@ -28,29 +30,24 @@ func main() { return } - // Get the account information. - u, err := dg.User("@me") - if err != nil { - fmt.Println("error obtaining account details,", err) - } - - // Store the account ID for later use. - BotID = u.ID - - // Register messageCreate as a callback for the messageCreate events. + // Register the messageCreate func as a callback for MessageCreate events. dg.AddHandler(messageCreate) - // Open the websocket and begin listening. + // Open a websocket connection to Discord and begin listening. err = dg.Open() if err != nil { fmt.Println("error opening connection,", err) return } + // Wait here until CTRL-C or other term signal is received. fmt.Println("Bot is now running. Press CTRL-C to exit.") - // Simple way to keep program running until CTRL-C is pressed. - <-make(chan struct{}) - return + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() } // This function will be called (due to AddHandler above) every time a new @@ -58,17 +55,17 @@ func main() { func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { // Ignore all messages created by the bot itself - if m.Author.ID == BotID { + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { return } - // If the message is "ping" reply with "Pong!" if m.Content == "ping" { - _, _ = s.ChannelMessageSend(m.ChannelID, "Pong!") + s.ChannelMessageSend(m.ChannelID, "Pong!") } // If the message is "pong" reply with "Ping!" if m.Content == "pong" { - _, _ = s.ChannelMessageSend(m.ChannelID, "Ping!") + s.ChannelMessageSend(m.ChannelID, "Ping!") } } diff --git a/vendor/github.com/bwmarrin/discordgo/message.go b/vendor/github.com/bwmarrin/discordgo/message.go index d7abda60..13c2da07 100644 --- a/vendor/github.com/bwmarrin/discordgo/message.go +++ b/vendor/github.com/bwmarrin/discordgo/message.go @@ -11,6 +11,7 @@ package discordgo import ( "fmt" + "io" "regexp" ) @@ -31,6 +32,53 @@ type Message struct { Reactions []*MessageReactions `json:"reactions"` } +// File stores info about files you e.g. send in messages. +type File struct { + Name string + Reader io.Reader +} + +// MessageSend stores all parameters you can send with ChannelMessageSendComplex. +type MessageSend struct { + Content string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + Tts bool `json:"tts"` + File *File `json:"file"` +} + +// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which +// is also where you should get the instance from. +type MessageEdit struct { + Content *string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + + ID string + Channel string +} + +// NewMessageEdit returns a MessageEdit struct, initialized +// with the Channel and ID. +func NewMessageEdit(channelID string, messageID string) *MessageEdit { + return &MessageEdit{ + Channel: channelID, + ID: messageID, + } +} + +// SetContent is the same as setting the variable Content, +// except it doesn't take a pointer. +func (m *MessageEdit) SetContent(str string) *MessageEdit { + m.Content = &str + return m +} + +// SetEmbed is a convenience function for setting the embed, +// so you can chain commands. +func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit { + m.Embed = embed + return m +} + // A MessageAttachment stores data for message attachments. type MessageAttachment struct { ID string `json:"id"` diff --git a/vendor/github.com/bwmarrin/discordgo/oauth2.go b/vendor/github.com/bwmarrin/discordgo/oauth2.go index 14ba6bbe..108b32fe 100644 --- a/vendor/github.com/bwmarrin/discordgo/oauth2.go +++ b/vendor/github.com/bwmarrin/discordgo/oauth2.go @@ -15,13 +15,18 @@ package discordgo // An Application struct stores values for a Discord OAuth2 Application type Application struct { - ID string `json:"id,omitempty"` - Name string `json:"name"` - Description string `json:"description,omitempty"` - Icon string `json:"icon,omitempty"` - Secret string `json:"secret,omitempty"` - RedirectURIs *[]string `json:"redirect_uris,omitempty"` - Owner *User `json:"owner"` + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Secret string `json:"secret,omitempty"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + BotRequireCodeGrant bool `json:"bot_require_code_grant,omitempty"` + BotPublic bool `json:"bot_public,omitempty"` + RPCApplicationState int `json:"rpc_application_state,omitempty"` + Flags int `json:"flags,omitempty"` + Owner *User `json:"owner"` + Bot *User `json:"bot"` } // Application returns an Application structure of a specific Application diff --git a/vendor/github.com/bwmarrin/discordgo/ratelimit.go b/vendor/github.com/bwmarrin/discordgo/ratelimit.go index bc320f0e..876e98a9 100644 --- a/vendor/github.com/bwmarrin/discordgo/ratelimit.go +++ b/vendor/github.com/bwmarrin/discordgo/ratelimit.go @@ -4,13 +4,14 @@ import ( "net/http" "strconv" "sync" + "sync/atomic" "time" ) // RateLimiter holds all ratelimit buckets type RateLimiter struct { sync.Mutex - global *Bucket + global *int64 buckets map[string]*Bucket globalRateLimit time.Duration } @@ -20,7 +21,7 @@ func NewRatelimiter() *RateLimiter { return &RateLimiter{ buckets: make(map[string]*Bucket), - global: &Bucket{Key: "global"}, + global: new(int64), } } @@ -58,8 +59,10 @@ func (r *RateLimiter) LockBucket(bucketID string) *Bucket { } // Check for global ratelimits - r.global.Lock() - r.global.Unlock() + sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) + if now := time.Now(); now.Before(sleepTo) { + time.Sleep(sleepTo.Sub(now)) + } b.remaining-- return b @@ -72,7 +75,7 @@ type Bucket struct { remaining int limit int reset time.Time - global *Bucket + global *int64 } // Release unlocks the bucket and reads the headers to update the buckets ratelimit info @@ -89,41 +92,25 @@ func (b *Bucket) Release(headers http.Header) error { global := headers.Get("X-RateLimit-Global") retryAfter := headers.Get("Retry-After") - // If it's global just keep the main ratelimit mutex locked - if global != "" { - parsedAfter, err := strconv.Atoi(retryAfter) - if err != nil { - return err - } - - // Lock it in a new goroutine so that this isn't a blocking call - go func() { - // Make sure if several requests were waiting we don't sleep for n * retry-after - // where n is the amount of requests that were going on - sleepTo := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) - - b.global.Lock() - - sleepDuration := sleepTo.Sub(time.Now()) - if sleepDuration > 0 { - time.Sleep(sleepDuration) - } - - b.global.Unlock() - }() - - return nil - } - - // Update reset time if either retry after or reset headers are present - // Prefer retryafter because it's more accurate with time sync and whatnot + // Update global and per bucket reset time if the proper headers are available + // If global is set, then it will block all buckets until after Retry-After + // If Retry-After without global is provided it will use that for the new reset + // time since it's more accurate than X-RateLimit-Reset. + // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset if retryAfter != "" { parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) if err != nil { return err } - b.reset = time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + resetAt := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + + // Lock either this single bucket or all buckets + if global != "" { + atomic.StoreInt64(b.global, resetAt.UnixNano()) + } else { + b.reset = resetAt + } } else if reset != "" { // Calculate the reset time by using the date header returned from discord discordTime, err := http.ParseTime(headers.Get("Date")) diff --git a/vendor/github.com/bwmarrin/discordgo/restapi.go b/vendor/github.com/bwmarrin/discordgo/restapi.go index 5389c860..cb482e68 100644 --- a/vendor/github.com/bwmarrin/discordgo/restapi.go +++ b/vendor/github.com/bwmarrin/discordgo/restapi.go @@ -29,8 +29,15 @@ import ( "time" ) -// ErrJSONUnmarshal is returned for JSON Unmarshall errors. -var ErrJSONUnmarshal = errors.New("json unmarshal") +// All error constants +var ( + ErrJSONUnmarshal = errors.New("json unmarshal") + ErrStatusOffline = errors.New("You can't set your Status to offline") + ErrVerificationLevelBounds = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") + ErrGuildNoIcon = errors.New("guild does not have an icon set") + ErrGuildNoSplash = errors.New("guild does not have a splash set") +) // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) { @@ -87,9 +94,7 @@ func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID } } - client := &http.Client{Timeout: (20 * time.Second)} - - resp, err := client.Do(req) + resp, err := s.Client.Do(req) if err != nil { bucket.Release(nil) return @@ -175,6 +180,12 @@ func unmarshal(data []byte, v interface{}) error { // ------------------------------------------------------------------------------------------------ // Login asks the Discord server for an authentication token. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. func (s *Session) Login(email, password string) (err error) { data := struct { @@ -189,6 +200,7 @@ func (s *Session) Login(email, password string) (err error) { temp := struct { Token string `json:"token"` + MFA bool `json:"mfa"` }{} err = unmarshal(response, &temp) @@ -197,6 +209,7 @@ func (s *Session) Login(email, password string) (err error) { } s.Token = temp.Token + s.MFA = temp.MFA return } @@ -264,15 +277,21 @@ func (s *Session) User(userID string) (st *User, err error) { return } -// UserAvatar returns an image.Image of a users Avatar. +// UserAvatar is deprecated. Please use UserAvatarDecode // userID : A user ID or "@me" which is a shortcut of current user ID func (s *Session) UserAvatar(userID string) (img image.Image, err error) { u, err := s.User(userID) if err != nil { return } + img, err = s.UserAvatarDecode(u) + return +} - body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(userID, u.Avatar), nil, EndpointUserAvatar("", "")) +// UserAvatarDecode returns an image.Image of a user's Avatar +// user : The user which avatar should be retrieved +func (s *Session) UserAvatarDecode(u *User) (img image.Image, err error) { + body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(u.ID, u.Avatar), nil, EndpointUserAvatar("", "")) if err != nil { return } @@ -292,7 +311,7 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri data := struct { Email string `json:"email"` Password string `json:"password"` - Username string `json:"username"` + Username string `json:"username,omitempty"` Avatar string `json:"avatar,omitempty"` NewPassword string `json:"new_password,omitempty"` }{email, password, username, avatar, newPassword} @@ -322,7 +341,7 @@ func (s *Session) UserSettings() (st *Settings, err error) { // status : The new status (Actual valid status are 'online','idle','dnd','invisible') func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { if status == StatusOffline { - err = errors.New("You can't set your Status to offline") + err = ErrStatusOffline return } @@ -370,9 +389,30 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) } // UserGuilds returns an array of UserGuild structures for all guilds. -func (s *Session) UserGuilds() (st []*UserGuild, err error) { +// limit : The number guilds that can be returned. (max 100) +// beforeID : If provided all guilds returned will be before given ID. +// afterID : If provided all guilds returned will be after given ID. +func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGuild, err error) { - body, err := s.RequestWithBucketID("GET", EndpointUserGuilds("@me"), nil, EndpointUserGuilds("")) + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if afterID != "" { + v.Set("after", afterID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + + uri := EndpointUserGuilds("@me") + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) if err != nil { return } @@ -402,6 +442,13 @@ func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSetti // NOTE: This function is now deprecated and will be removed in the future. // Please see the same function inside state.go func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + // Try to just get permissions from state. + apermissions, err = s.State.UserChannelPermissions(userID, channelID) + if err == nil { + return + } + + // Otherwise try get as much data from state as possible, falling back to the network. channel, err := s.State.Channel(channelID) if err != nil || channel == nil { channel, err = s.Channel(channelID) @@ -431,6 +478,19 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } + return memberPermissions(guild, channel, member), nil +} + +// Calculates the permissions for a member. +// https://support.discordapp.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured- +func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) { + userID := member.User.ID + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + for _, role := range guild.Roles { if role.ID == guild.ID { apermissions |= role.Permissions @@ -447,21 +507,36 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - if apermissions&PermissionAdministrator > 0 { + if apermissions&PermissionAdministrator == PermissionAdministrator { apermissions |= PermissionAll } + // Apply @everyone overrides from the channel. + for _, overwrite := range channel.PermissionOverwrites { + if guild.ID == overwrite.ID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } + } + + denies := 0 + allows := 0 + // Member overwrites can override role overrides, so do two passes for _, overwrite := range channel.PermissionOverwrites { for _, roleID := range member.Roles { if overwrite.Type == "role" && roleID == overwrite.ID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow + denies |= overwrite.Deny + allows |= overwrite.Allow break } } } + apermissions &= ^denies + apermissions |= allows + for _, overwrite := range channel.PermissionOverwrites { if overwrite.Type == "member" && overwrite.ID == userID { apermissions &= ^overwrite.Deny @@ -470,11 +545,11 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - if apermissions&PermissionAdministrator > 0 { + if apermissions&PermissionAdministrator == PermissionAdministrator { apermissions |= PermissionAllChannel } - return + return apermissions } // ------------------------------------------------------------------------------------------------ @@ -527,7 +602,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error if g.VerificationLevel != nil { val := *g.VerificationLevel if val < 0 || val > 3 { - err = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + err = ErrVerificationLevelBounds return } } @@ -551,13 +626,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error } } - data := struct { - Name string `json:"name,omitempty"` - Region string `json:"region,omitempty"` - VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` - }{g.Name, g.Region, g.VerificationLevel} - - body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), data, EndpointGuild(guildID)) + body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), g, EndpointGuild(guildID)) if err != nil { return } @@ -607,11 +676,28 @@ func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { // userID : The ID of a User // days : The number of days of previous comments to delete. func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { + return s.GuildBanCreateWithReason(guildID, userID, "", days) +} + +// GuildBanCreateWithReason bans the given user from the given guild also providing a reaso. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for this ban +// days : The number of days of previous comments to delete. +func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days int) (err error) { uri := EndpointGuildBan(guildID, userID) + queryParams := url.Values{} if days > 0 { - uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days) + queryParams.Set("delete-message-days", strconv.Itoa(days)) + } + if reason != "" { + queryParams.Set("reason", reason) + } + + if len(queryParams) > 0 { + uri += "?" + queryParams.Encode() } _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) @@ -722,12 +808,17 @@ func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) // GuildMemberNickname updates the nickname of a guild member // guildID : The ID of a guild // userID : The ID of a user +// userID : The ID of a user or "@me" which is a shortcut of the current user ID func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) { data := struct { Nick string `json:"nick"` }{nickname} + if userID == "@me" { + userID += "/nick" + } + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) return } @@ -738,7 +829,7 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err // roleID : The ID of a Role to be assigned to the user. func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) { - _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, userID, roleID)) + _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) return } @@ -749,7 +840,7 @@ func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) // roleID : The ID of a Role to be removed from the user. func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, userID, roleID)) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) return } @@ -904,7 +995,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er count = 0 if days <= 0 { - err = errors.New("The number of days should be more than or equal to 1.") + err = ErrPruneDaysBounds return } @@ -934,7 +1025,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err count = 0 if days <= 0 { - err = errors.New("The number of days should be more than or equal to 1.") + err = ErrPruneDaysBounds return } @@ -1036,7 +1127,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { } if g.Icon == "" { - err = errors.New("Guild does not have an icon set.") + err = ErrGuildNoIcon return } @@ -1058,7 +1149,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { } if g.Splash == "" { - err = errors.New("Guild does not have a splash set.") + err = ErrGuildNoSplash return } @@ -1156,7 +1247,8 @@ func (s *Session) ChannelTyping(channelID string) (err error) { // limit : The number messages that can be returned. (max 100) // beforeID : If provided all messages returned will be before given ID. // afterID : If provided all messages returned will be after given ID. -func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID string) (st []*Message, err error) { +// aroundID : If provided all messages returned will be around given ID. +func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID, aroundID string) (st []*Message, err error) { uri := EndpointChannelMessages(channelID) @@ -1170,6 +1262,9 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID if beforeID != "" { v.Set("before", beforeID) } + if aroundID != "" { + v.Set("around", aroundID) + } if len(v) > 0 { uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } @@ -1212,20 +1307,76 @@ func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st return } -// channelMessageSend sends a message to the given channel. +// ChannelMessageSend sends a message to the given channel. // channelID : The ID of a Channel. // content : The message to send. -// tts : Whether to send the message with TTS. -func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *Message, err error) { +func (s *Session) ChannelMessageSend(channelID string, content string) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + }) +} - // TODO: nonce string ? - data := struct { - Content string `json:"content"` - TTS bool `json:"tts"` - }{content, tts} +// ChannelMessageSendComplex sends a message to the given channel. +// channelID : The ID of a Channel. +// data : The message struct to send. +func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) (st *Message, err error) { + if data.Embed != nil && data.Embed.Type == "" { + data.Embed.Type = "rich" + } - // Send the message to the given channel - response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID)) + endpoint := EndpointChannelMessages(channelID) + + var response []byte + if data.File != nil { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + // What's a better way of doing this? Reflect? Generator? I'm open to suggestions + + if data.Content != "" { + if err = bodywriter.WriteField("content", data.Content); err != nil { + return + } + } + + if data.Embed != nil { + var embed []byte + embed, err = json.Marshal(data.Embed) + if err != nil { + return + } + err = bodywriter.WriteField("embed", string(embed)) + if err != nil { + return + } + } + + if data.Tts { + if err = bodywriter.WriteField("tts", "true"); err != nil { + return + } + } + + var writer io.Writer + writer, err = bodywriter.CreateFormFile("file", data.File.Name) + if err != nil { + return + } + + _, err = io.Copy(writer, data.File.Reader) + if err != nil { + return + } + + err = bodywriter.Close() + if err != nil { + return + } + + response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + } else { + response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + } if err != nil { return } @@ -1234,55 +1385,42 @@ func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *M return } -// ChannelMessageSend sends a message to the given channel. -// channelID : The ID of a Channel. -// content : The message to send. -func (s *Session) ChannelMessageSend(channelID string, content string) (st *Message, err error) { - - return s.channelMessageSend(channelID, content, false) -} - // ChannelMessageSendTTS sends a message to the given channel with Text to Speech. // channelID : The ID of a Channel. // content : The message to send. -func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *Message, err error) { - - return s.channelMessageSend(channelID, content, true) +func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + Tts: true, + }) } -// ChannelMessageSendEmbed sends a message to the given channel with embedded data (bot only). +// ChannelMessageSendEmbed sends a message to the given channel with embedded data. // channelID : The ID of a Channel. // embed : The embed data to send. -func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (st *Message, err error) { - if embed != nil && embed.Type == "" { - embed.Type = "rich" - } - - data := struct { - Embed *MessageEmbed `json:"embed"` - }{embed} - - // Send the message to the given channel - response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID)) - if err != nil { - return - } - - err = unmarshal(response, &st) - return +func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Embed: embed, + }) } // ChannelMessageEdit edits an existing message, replacing it entirely with // the given content. -// channeld : The ID of a Channel -// messageID : the ID of a Message -func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *Message, err error) { +// channelID : The ID of a Channel +// messageID : The ID of a Message +// content : The contents of the message +func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetContent(content)) +} - data := struct { - Content string `json:"content"` - }{content} +// ChannelMessageEditComplex edits an existing message, replacing it entirely with +// the given MessageEdit struct +func (s *Session) ChannelMessageEditComplex(m *MessageEdit) (st *Message, err error) { + if m.Embed != nil && m.Embed.Type == "" { + m.Embed.Type = "rich" + } - response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(channelID, messageID), data, EndpointChannelMessage(channelID, "")) + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(m.Channel, m.ID), m, EndpointChannelMessage(m.Channel, "")) if err != nil { return } @@ -1291,26 +1429,12 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st * return } -// ChannelMessageEditEmbed edits an existing message with embedded data (bot only). +// ChannelMessageEditEmbed edits an existing message with embedded data. // channelID : The ID of a Channel // messageID : The ID of a Message // embed : The embed data to send -func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (st *Message, err error) { - if embed != nil && embed.Type == "" { - embed.Type = "rich" - } - - data := struct { - Embed *MessageEmbed `json:"embed"` - }{embed} - - response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(channelID, messageID), data, EndpointChannelMessage(channelID, "")) - if err != nil { - return - } - - err = unmarshal(response, &st) - return +func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbed(embed)) } // ChannelMessageDelete deletes a message from the Channel. @@ -1385,48 +1509,18 @@ func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err er // channelID : The ID of a Channel. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) { - return s.ChannelFileSendWithMessage(channelID, "", name, r) +func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}}) } // ChannelFileSendWithMessage sends a file to the given channel with an message. +// DEPRECATED. Use ChannelMessageSendComplex instead. // channelID : The ID of a Channel. // content: Optional Message content. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (st *Message, err error) { - - body := &bytes.Buffer{} - bodywriter := multipart.NewWriter(body) - - if len(content) != 0 { - if err := bodywriter.WriteField("content", content); err != nil { - return nil, err - } - } - - writer, err := bodywriter.CreateFormFile("file", name) - if err != nil { - return nil, err - } - - _, err = io.Copy(writer, r) - if err != nil { - return - } - - err = bodywriter.Close() - if err != nil { - return - } - - response, err := s.request("POST", EndpointChannelMessages(channelID), bodywriter.FormDataContentType(), body.Bytes(), EndpointChannelMessages(channelID), 0) - if err != nil { - return - } - - err = unmarshal(response, &st) - return +func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}, Content: content}) } // ChannelInvites returns an array of Invite structures for the given channel @@ -1563,7 +1657,7 @@ func (s *Session) VoiceICE() (st *VoiceICE, err error) { // Functions specific to Discord Websockets // ------------------------------------------------------------------------------------------------ -// Gateway returns the a websocket Gateway address +// Gateway returns the websocket Gateway address func (s *Session) Gateway() (gateway string, err error) { response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway) @@ -1808,6 +1902,20 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i return } +// ------------------------------------------------------------------------------------------------ +// Functions specific to user notes +// ------------------------------------------------------------------------------------------------ + +// UserNoteSet sets the note for a specific user. +func (s *Session) UserNoteSet(userID string, message string) (err error) { + data := struct { + Note string `json:"note"` + }{message} + + _, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes("")) + return +} + // ------------------------------------------------------------------------------------------------ // Functions specific to Discord Relationships (Friends list) // ------------------------------------------------------------------------------------------------ diff --git a/vendor/github.com/bwmarrin/discordgo/state.go b/vendor/github.com/bwmarrin/discordgo/state.go index 25dd3d16..7400ef62 100644 --- a/vendor/github.com/bwmarrin/discordgo/state.go +++ b/vendor/github.com/bwmarrin/discordgo/state.go @@ -14,11 +14,16 @@ package discordgo import ( "errors" + "sort" "sync" ) // ErrNilState is returned when the state is nil. -var ErrNilState = errors.New("State not instantiated, please use discordgo.New() or assign Session.State.") +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 @@ -33,6 +38,7 @@ type State struct { TrackMembers bool TrackRoles bool TrackVoice bool + TrackPresences bool guildMap map[string]*Guild channelMap map[string]*Channel @@ -45,13 +51,14 @@ func NewState() *State { PrivateChannels: []*Channel{}, Guilds: []*Guild{}, }, - TrackChannels: true, - TrackEmojis: true, - TrackMembers: true, - TrackRoles: true, - TrackVoice: true, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), + TrackChannels: true, + TrackEmojis: true, + TrackMembers: true, + TrackRoles: true, + TrackVoice: true, + TrackPresences: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), } } @@ -143,7 +150,108 @@ 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 +// updates it if it already exists. +func (s *State) PresenceAdd(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + //guild.Presences[i] = presence + + //Update status + guild.Presences[i].Game = presence.Game + guild.Presences[i].Roles = presence.Roles + if presence.Status != "" { + guild.Presences[i].Status = presence.Status + } + if presence.Nick != "" { + guild.Presences[i].Nick = presence.Nick + } + + //Update the optionally sent user information + //ID Is a mandatory field so you should not need to check if it is empty + guild.Presences[i].User.ID = presence.User.ID + + if presence.User.Avatar != "" { + guild.Presences[i].User.Avatar = presence.User.Avatar + } + if presence.User.Discriminator != "" { + guild.Presences[i].User.Discriminator = presence.User.Discriminator + } + if presence.User.Email != "" { + guild.Presences[i].User.Email = presence.User.Email + } + if presence.User.Token != "" { + guild.Presences[i].User.Token = presence.User.Token + } + if presence.User.Username != "" { + guild.Presences[i].User.Username = presence.User.Username + } + + return nil + } + } + + guild.Presences = append(guild.Presences, presence) + return nil +} + +// PresenceRemove removes a presence from the current world state. +func (s *State) PresenceRemove(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + guild.Presences = append(guild.Presences[:i], guild.Presences[i+1:]...) + return nil + } + } + + return ErrStateNotFound +} + +// Presence gets a presence by ID from a guild. +func (s *State) Presence(guildID, userID string) (*Presence, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + for _, p := range guild.Presences { + if p.User.ID == userID { + return p, nil + } + } + + return nil, ErrStateNotFound } // TODO: Consider moving Guild state update methods onto *Guild. @@ -195,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. @@ -218,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 @@ -268,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. @@ -291,10 +399,10 @@ func (s *State) Role(guildID, roleID string) (*Role, error) { } } - return nil, errors.New("Role not found.") + return nil, ErrStateNotFound } -// ChannelAdd adds a guild to the current world state, or +// ChannelAdd adds a channel to the current world state, or // updates it if it already exists. // Channels may exist either as PrivateChannels or inside // a guild. @@ -324,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) @@ -403,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. @@ -426,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. @@ -523,7 +631,12 @@ func (s *State) MessageRemove(message *Message) error { return ErrNilState } - c, err := s.Channel(message.ChannelID) + return s.messageRemoveByID(message.ChannelID, message.ID) +} + +// messageRemoveByID removes a message by channelID and messageID from the world state. +func (s *State) messageRemoveByID(channelID, messageID string) error { + c, err := s.Channel(channelID) if err != nil { return err } @@ -532,13 +645,13 @@ func (s *State) MessageRemove(message *Message) error { defer s.Unlock() for i, m := range c.Messages { - if m.ID == message.ID { + if m.ID == messageID { c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) return nil } } - return errors.New("Message not found.") + return ErrStateNotFound } func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { @@ -592,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. @@ -608,10 +721,9 @@ func (s *State) onReady(se *Session, r *Ready) (err error) { // if state is disabled, store the bare essentials. if !se.StateEnabled { ready := Ready{ - Version: r.Version, - SessionID: r.SessionID, - HeartbeatInterval: r.HeartbeatInterval, - User: r.User, + Version: r.Version, + SessionID: r.SessionID, + User: r.User, } s.Ready = ready @@ -710,10 +822,55 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) { if s.MaxMessageCount != 0 { err = s.MessageRemove(t.Message) } + case *MessageDeleteBulk: + if s.MaxMessageCount != 0 { + for _, mID := range t.Messages { + s.messageRemoveByID(t.ChannelID, mID) + } + } case *VoiceStateUpdate: if s.TrackVoice { err = s.voiceStateUpdate(t) } + case *PresenceUpdate: + if s.TrackPresences { + s.PresenceAdd(t.GuildID, &t.Presence) + } + if s.TrackMembers { + if t.Status == StatusOffline { + return + } + + var m *Member + m, err = s.Member(t.GuildID, t.User.ID) + + if err != nil { + // Member not found; this is a user coming online + m = &Member{ + GuildID: t.GuildID, + Nick: t.Nick, + User: t.User, + Roles: t.Roles, + } + + } else { + + if t.Nick != "" { + m.Nick = t.Nick + } + + if t.User.Username != "" { + m.User.Username = t.User.Username + } + + // PresenceUpdates always contain a list of roles, so there's no need to check for an empty list here + m.Roles = t.Roles + + } + + err = s.MemberAdd(m) + } + } return @@ -747,48 +904,46 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i return } - for _, role := range guild.Roles { - if role.ID == guild.ID { - apermissions |= role.Permissions - break - } + return memberPermissions(guild, channel, member), nil +} + +// UserColor returns the color of a user in a channel. +// While colors are defined at a Guild level, determining for a channel is more useful in message handlers. +// 0 is returned in cases of error, which is the color of @everyone. +// userID : The ID of the user to calculate the color for. +// channelID : The ID of the channel to calculate the color for. +func (s *State) UserColor(userID, channelID string) int { + if s == nil { + return 0 } - for _, role := range guild.Roles { + channel, err := s.Channel(channelID) + if err != nil { + return 0 + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return 0 + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return 0 + } + + roles := Roles(guild.Roles) + sort.Sort(roles) + + for _, role := range roles { for _, roleID := range member.Roles { if role.ID == roleID { - apermissions |= role.Permissions - break + if role.Color != 0 { + return role.Color + } } } } - if apermissions&PermissionAdministrator > 0 { - apermissions |= PermissionAll - } - - // Member overwrites can override role overrides, so do two passes - for _, overwrite := range channel.PermissionOverwrites { - for _, roleID := range member.Roles { - if overwrite.Type == "role" && roleID == overwrite.ID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break - } - } - } - - for _, overwrite := range channel.PermissionOverwrites { - if overwrite.Type == "member" && overwrite.ID == userID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break - } - } - - if apermissions&PermissionAdministrator > 0 { - apermissions |= PermissionAllChannel - } - - return + return 0 } diff --git a/vendor/github.com/bwmarrin/discordgo/structs.go b/vendor/github.com/bwmarrin/discordgo/structs.go index 548ee52c..32f435ce 100644 --- a/vendor/github.com/bwmarrin/discordgo/structs.go +++ b/vendor/github.com/bwmarrin/discordgo/structs.go @@ -13,6 +13,7 @@ package discordgo import ( "encoding/json" + "net/http" "strconv" "sync" "time" @@ -28,6 +29,7 @@ type Session struct { // Authentication token for this session Token string + MFA bool // Debug for printing JSON request/responses Debug bool // Deprecated, will be removed. @@ -73,6 +75,9 @@ type Session struct { // StateEnabled is true. State *State + // The http client used for REST requests + Client *http.Client + // Event handlers handlersMu sync.RWMutex handlers map[string][]*eventHandlerInstance @@ -88,7 +93,7 @@ type Session struct { ratelimiter *RateLimiter // sequence tracks the current gateway api websocket sequence number - sequence int + sequence *int64 // stores sessions current Discord Gateway gateway string @@ -100,12 +105,6 @@ type Session struct { wsMutex sync.Mutex } -type rateLimitMutex struct { - sync.Mutex - url map[string]*sync.Mutex - // bucket map[string]*sync.Mutex // TODO :) -} - // A VoiceRegion stores data for a specific voice region server. type VoiceRegion struct { ID string `json:"id"` @@ -235,9 +234,15 @@ type UserGuild struct { // A GuildParams stores all the data needed to update discord guild settings type GuildParams struct { - Name string `json:"name"` - Region string `json:"region"` - VerificationLevel *VerificationLevel `json:"verification_level"` + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` + DefaultMessageNotifications int `json:"default_message_notifications,omitempty"` // TODO: Separate type? + AfkChannelID string `json:"afk_channel_id,omitempty"` + AfkTimeout int `json:"afk_timeout,omitempty"` + Icon string `json:"icon,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + Splash string `json:"splash,omitempty"` } // A Role stores information about Discord guild member roles. @@ -252,6 +257,21 @@ type Role struct { Permissions int `json:"permissions"` } +// Roles are a collection of Role +type Roles []*Role + +func (r Roles) Len() int { + return len(r) +} + +func (r Roles) Less(i, j int) bool { + return r[i].Position > r[j].Position +} + +func (r Roles) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + // A VoiceState stores the voice states of Guilds type VoiceState struct { UserID string `json:"user_id"` @@ -284,7 +304,7 @@ type Game struct { // UnmarshalJSON unmarshals json to Game struct func (g *Game) UnmarshalJSON(bytes []byte) error { temp := &struct { - Name string `json:"name"` + Name json.Number `json:"name"` Type json.RawMessage `json:"type"` URL string `json:"url"` }{} @@ -292,8 +312,8 @@ func (g *Game) UnmarshalJSON(bytes []byte) error { if err != nil { return err } - g.Name = temp.Name g.URL = temp.URL + g.Name = temp.Name.String() if temp.Type != nil { err = json.Unmarshal(temp.Type, &g.Type) @@ -324,19 +344,6 @@ type Member struct { Roles []string `json:"roles"` } -// 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"` - Discriminator string `json:"discriminator"` - Token string `json:"token"` - Verified bool `json:"verified"` - MFAEnabled bool `json:"mfa_enabled"` - Bot bool `json:"bot"` -} - // A Settings stores data for a specific users Discord client settings. type Settings struct { RenderEmbeds bool `json:"render_embeds"` @@ -542,6 +549,8 @@ const ( PermissionAdministrator PermissionManageChannels PermissionManageServer + PermissionAddReactions + PermissionViewAuditLogs PermissionAllText = PermissionReadMessages | PermissionSendMessages | @@ -561,9 +570,12 @@ const ( PermissionAllVoice | PermissionCreateInstantInvite | PermissionManageRoles | - PermissionManageChannels + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs PermissionAll = PermissionAllChannel | PermissionKickMembers | PermissionBanMembers | - PermissionManageServer + PermissionManageServer | + PermissionAdministrator ) diff --git a/vendor/github.com/bwmarrin/discordgo/tools/cmd/eventhandlers/main.go b/vendor/github.com/bwmarrin/discordgo/tools/cmd/eventhandlers/main.go index f3894085..839f009d 100644 --- a/vendor/github.com/bwmarrin/discordgo/tools/cmd/eventhandlers/main.go +++ b/vendor/github.com/bwmarrin/discordgo/tools/cmd/eventhandlers/main.go @@ -37,18 +37,18 @@ type {{privateName .}}EventHandler func(*Session, *{{.}}) func (eh {{privateName .}}EventHandler) Type() string { return {{privateName .}}EventType } - +{{if isDiscordEvent .}} // New returns a new instance of {{.}}. func (eh {{privateName .}}EventHandler) New() interface{} { return &{{.}}{} -} - +}{{end}} // Handle is the handler for {{.}} events. func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) { if t, ok := i.(*{{.}}); ok { eh(s, t) } } + {{end}} func handlerForInterface(handler interface{}) EventHandler { switch v := handler.(type) { @@ -60,6 +60,7 @@ func handlerForInterface(handler interface{}) EventHandler { return nil } + func init() { {{range .}}{{if isDiscordEvent .}} registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}} } diff --git a/vendor/github.com/bwmarrin/discordgo/user.go b/vendor/github.com/bwmarrin/discordgo/user.go new file mode 100644 index 00000000..b3a7e4b2 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/user.go @@ -0,0 +1,26 @@ +package discordgo + +import "fmt" + +// 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"` + Discriminator string `json:"discriminator"` + Token string `json:"token"` + Verified bool `json:"verified"` + MFAEnabled bool `json:"mfa_enabled"` + 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) +} + +// Mention return a string which mentions the user +func (u *User) Mention() string { + return fmt.Sprintf("<@%s>", u.ID) +} diff --git a/vendor/github.com/bwmarrin/discordgo/voice.go b/vendor/github.com/bwmarrin/discordgo/voice.go index 43de329e..da7b8c90 100644 --- a/vendor/github.com/bwmarrin/discordgo/voice.go +++ b/vendor/github.com/bwmarrin/discordgo/voice.go @@ -15,7 +15,6 @@ import ( "fmt" "log" "net" - "runtime" "strings" "sync" "time" @@ -93,18 +92,22 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { } if v.wsConn == nil { - return fmt.Errorf("No VoiceConnection websocket.") + return fmt.Errorf("no VoiceConnection websocket") } data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} v.wsMutex.Lock() err = v.wsConn.WriteJSON(data) v.wsMutex.Unlock() + + v.Lock() + defer v.Unlock() if err != nil { v.speaking = false log.Println("Speaking() write json error:", err) return } + v.speaking = b return @@ -139,9 +142,9 @@ func (v *VoiceConnection) Disconnect() (err error) { // Send a OP4 with a nil channel to disconnect if v.sessionID != "" { data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} - v.wsMutex.Lock() + v.session.wsMutex.Lock() err = v.session.wsConn.WriteJSON(data) - v.wsMutex.Unlock() + v.session.wsMutex.Unlock() v.sessionID = "" } @@ -149,7 +152,10 @@ func (v *VoiceConnection) Disconnect() (err error) { v.Close() v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) + + v.session.Lock() delete(v.session.VoiceConnections, v.GuildID) + v.session.Unlock() return } @@ -185,7 +191,9 @@ func (v *VoiceConnection) Close() { // To cleanly close a connection, a client should send a close // frame and wait for the server to close the connection. + v.wsMutex.Lock() err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + v.wsMutex.Unlock() if err != nil { v.log(LogError, "error closing websocket, %s", err) } @@ -246,12 +254,15 @@ func (v *VoiceConnection) waitUntilConnected() error { i := 0 for { - if v.Ready { + v.RLock() + ready := v.Ready + v.RUnlock() + if ready { return nil } if i > 10 { - return fmt.Errorf("Timeout waiting for voice.") + return fmt.Errorf("timeout waiting for voice") } time.Sleep(1 * time.Second) @@ -282,7 +293,7 @@ func (v *VoiceConnection) open() (err error) { break } if i > 20 { // only loop for up to 1 second total - return fmt.Errorf("Did not receive voice Session ID in time.") + return fmt.Errorf("did not receive voice Session ID in time") } time.Sleep(50 * time.Millisecond) i++ @@ -409,8 +420,6 @@ func (v *VoiceConnection) onEvent(message []byte) { go v.opusReceiver(v.udpConn, v.close, v.OpusRecv) } - // Send the ready event - v.connected <- true return case 3: // HEARTBEAT response @@ -418,6 +427,9 @@ func (v *VoiceConnection) onEvent(message []byte) { return case 4: // udp encryption secret key + v.Lock() + defer v.Unlock() + v.op4 = voiceOP4{} if err := json.Unmarshal(e.RawData, &v.op4); err != nil { v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData)) @@ -466,6 +478,7 @@ func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struc var err error ticker := time.NewTicker(i * time.Millisecond) + defer ticker.Stop() for { v.log(LogDebug, "sending heartbeat packet") v.wsMutex.Lock() @@ -616,6 +629,7 @@ func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct packet := make([]byte, 8) ticker := time.NewTicker(i) + defer ticker.Stop() for { binary.LittleEndian.PutUint64(packet, sequence) @@ -644,12 +658,16 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{} return } - runtime.LockOSThread() - // VoiceConnection is now ready to receive audio packets // TODO: this needs reviewed as I think there must be a better way. + v.Lock() v.Ready = true - defer func() { v.Ready = false }() + v.Unlock() + defer func() { + v.Lock() + v.Ready = false + v.Unlock() + }() var sequence uint16 var timestamp uint32 @@ -665,6 +683,7 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{} // start a send loop that loops until buf chan is closed ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000))) + defer ticker.Stop() for { // Get data from chan. If chan is closed, return. @@ -678,7 +697,10 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{} // else, continue loop } - if !v.speaking { + v.RLock() + speaking := v.speaking + v.RUnlock() + if !speaking { err := v.Speaking(true) if err != nil { v.log(LogError, "error sending speaking packet, %s", err) @@ -691,7 +713,9 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{} // encrypt the opus data copy(nonce[:], udpHeader) + v.RLock() sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey) + v.RUnlock() // block here until we're exactly at the right time :) // Then send rtp audio packet to Discord over UDP @@ -742,7 +766,6 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct return } - p := Packet{} recvbuf := make([]byte, 1024) var nonce [24]byte @@ -778,6 +801,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct } // build a audio packet struct + p := Packet{} p.Type = recvbuf[0:2] p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4]) p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8]) @@ -837,6 +861,8 @@ func (v *VoiceConnection) reconnect() { return } + v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) + // if the reconnect above didn't work lets just send a disconnect // packet to reset things. // Send a OP4 with a nil channel to disconnect @@ -848,6 +874,5 @@ func (v *VoiceConnection) reconnect() { v.log(LogError, "error sending disconnect packet, %s", err) } - v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) } } diff --git a/vendor/github.com/bwmarrin/discordgo/wsapi.go b/vendor/github.com/bwmarrin/discordgo/wsapi.go index 99a6236a..09128505 100644 --- a/vendor/github.com/bwmarrin/discordgo/wsapi.go +++ b/vendor/github.com/bwmarrin/discordgo/wsapi.go @@ -19,17 +19,30 @@ import ( "io" "net/http" "runtime" + "sync/atomic" "time" "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 { Token string `json:"token"` SessionID string `json:"session_id"` - Sequence int `json:"seq"` + Sequence int64 `json:"seq"` } `json:"d"` } @@ -57,7 +70,7 @@ func (s *Session) Open() (err error) { } if s.wsConn != nil { - err = errors.New("Web socket already opened.") + err = ErrWSAlreadyOpen return } @@ -74,7 +87,7 @@ func (s *Session) Open() (err error) { } // Add the version and encoding to the URL - s.gateway = fmt.Sprintf("%s?v=4&encoding=json", s.gateway) + s.gateway = fmt.Sprintf("%s?v=5&encoding=json", s.gateway) } header := http.Header{} @@ -89,13 +102,14 @@ func (s *Session) Open() (err error) { return } - if s.sessionID != "" && s.sequence > 0 { + sequence := atomic.LoadInt64(s.sequence) + if s.sessionID != "" && sequence > 0 { p := resumePacket{} p.Op = 6 p.Data.Token = s.Token p.Data.SessionID = s.sessionID - p.Data.Sequence = s.sequence + p.Data.Sequence = sequence s.log(LogInformational, "sending resume packet to gateway") err = s.wsConn.WriteJSON(p) @@ -176,8 +190,13 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { } type heartbeatOp struct { - Op int `json:"op"` - Data int `json:"d"` + Op int `json:"op"` + Data int64 `json:"d"` +} + +type helloOp struct { + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + Trace []string `json:"_trace"` } // heartbeat sends regular heartbeats to Discord so it knows the client @@ -193,12 +212,13 @@ func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{} var err error ticker := time.NewTicker(i * time.Millisecond) + defer ticker.Stop() for { - - s.log(LogInformational, "sending gateway websocket heartbeat seq %d", s.sequence) + sequence := atomic.LoadInt64(s.sequence) + s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) s.wsMutex.Lock() - err = wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + 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) @@ -242,7 +262,7 @@ func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } var usd updateStatusData @@ -299,7 +319,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err s.RLock() defer s.RUnlock() if s.wsConn == nil { - return errors.New("no websocket connection exists") + return ErrWSNotFound } data := requestGuildMembersData{ @@ -365,7 +385,7 @@ func (s *Session) onEvent(messageType int, message []byte) { if e.Operation == 1 { s.log(LogInformational, "sending heartbeat in response to Op1") s.wsMutex.Lock() - err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + err = s.wsConn.WriteJSON(heartbeatOp{1, atomic.LoadInt64(s.sequence)}) s.wsMutex.Unlock() if err != nil { s.log(LogError, "error sending heartbeat in response to Op1") @@ -396,6 +416,16 @@ func (s *Session) onEvent(messageType int, message []byte) { return } + if e.Operation == 10 { + var h helloOp + if err = json.Unmarshal(e.RawData, &h); err != nil { + s.log(LogError, "error unmarshalling helloOp, %s", err) + } else { + go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval) + } + return + } + // Do not try to Dispatch a non-Dispatch Message if e.Operation != 0 { // But we probably should be doing something with them. @@ -405,7 +435,7 @@ func (s *Session) onEvent(messageType int, message []byte) { } // Store the message sequence - s.sequence = e.Sequence + atomic.StoreInt64(s.sequence, e.Sequence) // Map event to registered event handlers and pass it along to any registered handlers. if eh, ok := registeredInterfaceProviders[e.Type]; ok { @@ -458,18 +488,24 @@ func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *Voi s.log(LogInformational, "called") + s.RLock() voice, _ = s.VoiceConnections[gID] + s.RUnlock() if voice == nil { voice = &VoiceConnection{} + s.Lock() s.VoiceConnections[gID] = voice + s.Unlock() } + voice.Lock() voice.GuildID = gID voice.ChannelID = cID voice.deaf = deaf voice.mute = mute voice.session = s + voice.Unlock() // Send the request to Discord that we want to join the voice channel data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} @@ -500,7 +536,9 @@ func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { } // Check if we have a voice connection to update + s.RLock() voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() if !exists { return } @@ -511,8 +549,11 @@ func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { } // Store the SessionID for later use. + voice.Lock() voice.UserID = st.UserID voice.sessionID = st.SessionID + voice.ChannelID = st.ChannelID + voice.Unlock() } // onVoiceServerUpdate handles the Voice Server Update data websocket event. @@ -524,7 +565,9 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) { s.log(LogInformational, "called") + s.RLock() voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() // If no VoiceConnection exists, just skip this if !exists { @@ -536,9 +579,11 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) { voice.Close() // Store values for later use + voice.Lock() voice.token = st.Token voice.endpoint = st.Endpoint voice.GuildID = st.GuildID + voice.Unlock() // Open a conenction to the voice server err := voice.open() @@ -588,7 +633,7 @@ func (s *Session) identify() error { if s.ShardCount > 1 { if s.ShardID >= s.ShardCount { - return errors.New("ShardID must be less than ShardCount") + return ErrWSShardBounds } data.Shard = &[2]int{s.ShardID, s.ShardCount} @@ -628,6 +673,8 @@ func (s *Session) reconnect() { // However, there seems to be cases where something "weird" // happens. So we're doing this for now just to improve // stability in those edge cases. + s.RLock() + defer s.RUnlock() for _, v := range s.VoiceConnections { s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) @@ -675,7 +722,9 @@ func (s *Session) Close() (err error) { s.log(LogInformational, "sending close frame") // To cleanly close a connection, a client should send a close // frame and wait for the server to close the connection. + s.wsMutex.Lock() err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + s.wsMutex.Unlock() if err != nil { s.log(LogInformational, "error closing websocket, %s", err) } diff --git a/vendor/manifest b/vendor/manifest index d311a3ab..e1f81c95 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -45,7 +45,7 @@ "importpath": "github.com/bwmarrin/discordgo", "repository": "https://github.com/bwmarrin/discordgo", "vcs": "git", - "revision": "5835676872d7fa65ee37b32672079c4ede2c1855", + "revision": "d420e28024ad527390b43aa7f64e029083e11989", "branch": "master", "notests": true },