Update vendor (bwmarrin/discordgo)

This commit is contained in:
Wim 2017-07-16 14:38:45 +02:00
parent aec5e3d77b
commit 5db24aa901
24 changed files with 1072 additions and 659 deletions

View File

@ -13,10 +13,18 @@
// Package discordgo provides Discord binding for Go // Package discordgo provides Discord binding for Go
package discordgo package discordgo
import "fmt" import (
"errors"
"fmt"
"net/http"
"time"
)
// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.15.0" 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 // New creates a new Discord session and will automate some startup
// tasks if given enough information to do so. Currently you can pass zero // 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 // With an email, password and auth token - Discord will verify the auth
// token, if it is invalid it will sign in with the provided // token, if it is invalid it will sign in with the provided
// credentials. This is the Discord recommended way to sign in. // 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) { func New(args ...interface{}) (s *Session, err error) {
// Create an empty Session interface. // Create an empty Session interface.
@ -43,6 +57,8 @@ func New(args ...interface{}) (s *Session, err error) {
ShardID: 0, ShardID: 0,
ShardCount: 1, ShardCount: 1,
MaxRestRetries: 3, MaxRestRetries: 3,
Client: &http.Client{Timeout: (20 * time.Second)},
sequence: new(int64),
} }
// If no arguments are passed return the empty Session interface. // If no arguments are passed return the empty Session interface.
@ -60,7 +76,7 @@ func New(args ...interface{}) (s *Session, err error) {
case []string: case []string:
if len(v) > 3 { if len(v) > 3 {
err = fmt.Errorf("Too many string parameters provided.") err = fmt.Errorf("too many string parameters provided")
return return
} }
@ -91,7 +107,7 @@ func New(args ...interface{}) (s *Session, err error) {
} else if s.Token == "" { } else if s.Token == "" {
s.Token = v s.Token = v
} else { } else {
err = fmt.Errorf("Too many string parameters provided.") err = fmt.Errorf("too many string parameters provided")
return return
} }
@ -99,7 +115,7 @@ func New(args ...interface{}) (s *Session, err error) {
// TODO: Parse configuration struct // TODO: Parse configuration struct
default: default:
err = fmt.Errorf("Unsupported parameter type provided.") err = fmt.Errorf("unsupported parameter type provided")
return return
} }
} }
@ -113,7 +129,11 @@ func New(args ...interface{}) (s *Session, err error) {
} else { } else {
err = s.Login(auth, pass) err = s.Login(auth, pass)
if err != nil || s.Token == "" { if err != nil || s.Token == "" {
if s.MFA {
err = ErrMFA
} else {
err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) err = fmt.Errorf("Unable to fetch discord authentication token. %v", err)
}
return return
} }
} }

View File

@ -26,6 +26,13 @@ var (
EndpointGateway = EndpointAPI + "gateway" EndpointGateway = EndpointAPI + "gateway"
EndpointWebhooks = EndpointAPI + "webhooks/" 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/" EndpointAuth = EndpointAPI + "auth/"
EndpointLogin = EndpointAuth + "login" EndpointLogin = EndpointAuth + "login"
EndpointLogout = EndpointAuth + "logout" EndpointLogout = EndpointAuth + "logout"
@ -48,7 +55,7 @@ var (
EndpointIntegrations = EndpointAPI + "integrations" EndpointIntegrations = EndpointAPI + "integrations"
EndpointUser = func(uID string) string { return EndpointUsers + uID } 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" } EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" }
EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" }
EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } 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" } EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" }
EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" }
EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } 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 } EndpointGuild = func(gID string) string { return EndpointGuilds + gID }
EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" }
@ -73,8 +81,8 @@ var (
EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" }
EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" } EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" }
EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" }
EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" } EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" }
EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" } EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" }
EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" }
EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannel = func(cID string) string { return EndpointChannels + cID }
@ -89,6 +97,8 @@ var (
EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" }
EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } 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" } EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" }
EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID }
EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token }

View File

@ -1,7 +1,5 @@
package discordgo package discordgo
import "fmt"
// EventHandler is an interface for Discord events. // EventHandler is an interface for Discord events.
type EventHandler interface { type EventHandler interface {
// Type returns the type of event this handler belongs to. // 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 // registerInterfaceProvider registers a provider so that DiscordGo can
// access it's New() method. // access it's New() method.
func registerInterfaceProvider(eh EventInterfaceProvider) error { func registerInterfaceProvider(eh EventInterfaceProvider) {
if _, ok := registeredInterfaceProviders[eh.Type()]; ok { 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 registeredInterfaceProviders[eh.Type()] = eh
return nil return
} }
// eventHandlerInstance is a wrapper around an event handler, as functions // eventHandlerInstance is a wrapper around an event handler, as functions
@ -210,14 +211,15 @@ func (s *Session) onInterface(i interface{}) {
setGuildIds(t.Guild) setGuildIds(t.Guild)
case *GuildUpdate: case *GuildUpdate:
setGuildIds(t.Guild) setGuildIds(t.Guild)
case *Resumed:
s.onResumed(t)
case *VoiceServerUpdate: case *VoiceServerUpdate:
go s.onVoiceServerUpdate(t) go s.onVoiceServerUpdate(t)
case *VoiceStateUpdate: case *VoiceStateUpdate:
go s.onVoiceStateUpdate(t) 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. // onReady handles the ready event.
@ -225,14 +227,4 @@ func (s *Session) onReady(r *Ready) {
// Store the SessionID within the Session struct. // Store the SessionID within the Session struct.
s.sessionID = r.SessionID 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)
} }

View File

@ -31,8 +31,10 @@ const (
messageAckEventType = "MESSAGE_ACK" messageAckEventType = "MESSAGE_ACK"
messageCreateEventType = "MESSAGE_CREATE" messageCreateEventType = "MESSAGE_CREATE"
messageDeleteEventType = "MESSAGE_DELETE" messageDeleteEventType = "MESSAGE_DELETE"
messageDeleteBulkEventType = "MESSAGE_DELETE_BULK"
messageReactionAddEventType = "MESSAGE_REACTION_ADD" messageReactionAddEventType = "MESSAGE_REACTION_ADD"
messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE"
messageReactionRemoveAllEventType = "MESSAGE_REACTION_REMOVE_ALL"
messageUpdateEventType = "MESSAGE_UPDATE" messageUpdateEventType = "MESSAGE_UPDATE"
presenceUpdateEventType = "PRESENCE_UPDATE" presenceUpdateEventType = "PRESENCE_UPDATE"
presencesReplaceEventType = "PRESENCES_REPLACE" presencesReplaceEventType = "PRESENCES_REPLACE"
@ -43,6 +45,7 @@ const (
resumedEventType = "RESUMED" resumedEventType = "RESUMED"
typingStartEventType = "TYPING_START" typingStartEventType = "TYPING_START"
userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE" userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE"
userNoteUpdateEventType = "USER_NOTE_UPDATE"
userSettingsUpdateEventType = "USER_SETTINGS_UPDATE" userSettingsUpdateEventType = "USER_SETTINGS_UPDATE"
userUpdateEventType = "USER_UPDATE" userUpdateEventType = "USER_UPDATE"
voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" voiceServerUpdateEventType = "VOICE_SERVER_UPDATE"
@ -137,11 +140,6 @@ func (eh connectEventHandler) Type() string {
return connectEventType return connectEventType
} }
// New returns a new instance of Connect.
func (eh connectEventHandler) New() interface{} {
return &Connect{}
}
// Handle is the handler for Connect events. // Handle is the handler for Connect events.
func (eh connectEventHandler) Handle(s *Session, i interface{}) { func (eh connectEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*Connect); ok { if t, ok := i.(*Connect); ok {
@ -157,11 +155,6 @@ func (eh disconnectEventHandler) Type() string {
return disconnectEventType return disconnectEventType
} }
// New returns a new instance of Disconnect.
func (eh disconnectEventHandler) New() interface{} {
return &Disconnect{}
}
// Handle is the handler for Disconnect events. // Handle is the handler for Disconnect events.
func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { func (eh disconnectEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*Disconnect); ok { if t, ok := i.(*Disconnect); ok {
@ -177,11 +170,6 @@ func (eh eventEventHandler) Type() string {
return eventEventType return eventEventType
} }
// New returns a new instance of Event.
func (eh eventEventHandler) New() interface{} {
return &Event{}
}
// Handle is the handler for Event events. // Handle is the handler for Event events.
func (eh eventEventHandler) Handle(s *Session, i interface{}) { func (eh eventEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*Event); ok { 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. // messageReactionAddEventHandler is an event handler for MessageReactionAdd events.
type messageReactionAddEventHandler func(*Session, *MessageReactionAdd) 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. // messageUpdateEventHandler is an event handler for MessageUpdate events.
type messageUpdateEventHandler func(*Session, *MessageUpdate) type messageUpdateEventHandler func(*Session, *MessageUpdate)
@ -637,11 +665,6 @@ func (eh rateLimitEventHandler) Type() string {
return rateLimitEventType return rateLimitEventType
} }
// New returns a new instance of RateLimit.
func (eh rateLimitEventHandler) New() interface{} {
return &RateLimit{}
}
// Handle is the handler for RateLimit events. // Handle is the handler for RateLimit events.
func (eh rateLimitEventHandler) Handle(s *Session, i interface{}) { func (eh rateLimitEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*RateLimit); ok { 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. // userSettingsUpdateEventHandler is an event handler for UserSettingsUpdate events.
type userSettingsUpdateEventHandler func(*Session, *UserSettingsUpdate) type userSettingsUpdateEventHandler func(*Session, *UserSettingsUpdate)
@ -901,10 +944,14 @@ func handlerForInterface(handler interface{}) EventHandler {
return messageCreateEventHandler(v) return messageCreateEventHandler(v)
case func(*Session, *MessageDelete): case func(*Session, *MessageDelete):
return messageDeleteEventHandler(v) return messageDeleteEventHandler(v)
case func(*Session, *MessageDeleteBulk):
return messageDeleteBulkEventHandler(v)
case func(*Session, *MessageReactionAdd): case func(*Session, *MessageReactionAdd):
return messageReactionAddEventHandler(v) return messageReactionAddEventHandler(v)
case func(*Session, *MessageReactionRemove): case func(*Session, *MessageReactionRemove):
return messageReactionRemoveEventHandler(v) return messageReactionRemoveEventHandler(v)
case func(*Session, *MessageReactionRemoveAll):
return messageReactionRemoveAllEventHandler(v)
case func(*Session, *MessageUpdate): case func(*Session, *MessageUpdate):
return messageUpdateEventHandler(v) return messageUpdateEventHandler(v)
case func(*Session, *PresenceUpdate): case func(*Session, *PresenceUpdate):
@ -925,6 +972,8 @@ func handlerForInterface(handler interface{}) EventHandler {
return typingStartEventHandler(v) return typingStartEventHandler(v)
case func(*Session, *UserGuildSettingsUpdate): case func(*Session, *UserGuildSettingsUpdate):
return userGuildSettingsUpdateEventHandler(v) return userGuildSettingsUpdateEventHandler(v)
case func(*Session, *UserNoteUpdate):
return userNoteUpdateEventHandler(v)
case func(*Session, *UserSettingsUpdate): case func(*Session, *UserSettingsUpdate):
return userSettingsUpdateEventHandler(v) return userSettingsUpdateEventHandler(v)
case func(*Session, *UserUpdate): case func(*Session, *UserUpdate):
@ -937,6 +986,7 @@ func handlerForInterface(handler interface{}) EventHandler {
return nil return nil
} }
func init() { func init() {
registerInterfaceProvider(channelCreateEventHandler(nil)) registerInterfaceProvider(channelCreateEventHandler(nil))
registerInterfaceProvider(channelDeleteEventHandler(nil)) registerInterfaceProvider(channelDeleteEventHandler(nil))
@ -959,8 +1009,10 @@ func init() {
registerInterfaceProvider(messageAckEventHandler(nil)) registerInterfaceProvider(messageAckEventHandler(nil))
registerInterfaceProvider(messageCreateEventHandler(nil)) registerInterfaceProvider(messageCreateEventHandler(nil))
registerInterfaceProvider(messageDeleteEventHandler(nil)) registerInterfaceProvider(messageDeleteEventHandler(nil))
registerInterfaceProvider(messageDeleteBulkEventHandler(nil))
registerInterfaceProvider(messageReactionAddEventHandler(nil)) registerInterfaceProvider(messageReactionAddEventHandler(nil))
registerInterfaceProvider(messageReactionRemoveEventHandler(nil)) registerInterfaceProvider(messageReactionRemoveEventHandler(nil))
registerInterfaceProvider(messageReactionRemoveAllEventHandler(nil))
registerInterfaceProvider(messageUpdateEventHandler(nil)) registerInterfaceProvider(messageUpdateEventHandler(nil))
registerInterfaceProvider(presenceUpdateEventHandler(nil)) registerInterfaceProvider(presenceUpdateEventHandler(nil))
registerInterfaceProvider(presencesReplaceEventHandler(nil)) registerInterfaceProvider(presencesReplaceEventHandler(nil))
@ -970,6 +1022,7 @@ func init() {
registerInterfaceProvider(resumedEventHandler(nil)) registerInterfaceProvider(resumedEventHandler(nil))
registerInterfaceProvider(typingStartEventHandler(nil)) registerInterfaceProvider(typingStartEventHandler(nil))
registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil)) registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil))
registerInterfaceProvider(userNoteUpdateEventHandler(nil))
registerInterfaceProvider(userSettingsUpdateEventHandler(nil)) registerInterfaceProvider(userSettingsUpdateEventHandler(nil))
registerInterfaceProvider(userUpdateEventHandler(nil)) registerInterfaceProvider(userUpdateEventHandler(nil))
registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) registerInterfaceProvider(voiceServerUpdateEventHandler(nil))

View File

@ -2,7 +2,6 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"time"
) )
// This file contains all the possible structs that can be // 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. // Event provides a basic initial struct for all websocket events.
type Event struct { type Event struct {
Operation int `json:"op"` Operation int `json:"op"`
Sequence int `json:"s"` Sequence int64 `json:"s"`
Type string `json:"t"` Type string `json:"t"`
RawData json.RawMessage `json:"d"` RawData json.RawMessage `json:"d"`
// Struct contains one of the other types in this file. // Struct contains one of the other types in this file.
@ -39,7 +38,6 @@ type Event struct {
type Ready struct { type Ready struct {
Version int `json:"v"` Version int `json:"v"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
User *User `json:"user"` User *User `json:"user"`
ReadState []*ReadState `json:"read_state"` ReadState []*ReadState `json:"read_state"`
PrivateChannels []*Channel `json:"private_channels"` PrivateChannels []*Channel `json:"private_channels"`
@ -50,6 +48,7 @@ type Ready struct {
UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"`
Relationships []*Relationship `json:"relationships"` Relationships []*Relationship `json:"relationships"`
Presences []*Presence `json:"presences"` Presences []*Presence `json:"presences"`
Notes map[string]string `json:"notes"`
} }
// ChannelCreate is the data for a ChannelCreate event. // ChannelCreate is the data for a ChannelCreate event.
@ -179,6 +178,11 @@ type MessageReactionRemove struct {
*MessageReaction *MessageReaction
} }
// MessageReactionRemoveAll is the data for a MessageReactionRemoveAll event.
type MessageReactionRemoveAll struct {
*MessageReaction
}
// PresencesReplace is the data for a PresencesReplace event. // PresencesReplace is the data for a PresencesReplace event.
type PresencesReplace []*Presence type PresencesReplace []*Presence
@ -191,7 +195,6 @@ type PresenceUpdate struct {
// Resumed is the data for a Resumed event. // Resumed is the data for a Resumed event.
type Resumed struct { type Resumed struct {
HeartbeatInterval time.Duration `json:"heartbeat_interval"`
Trace []string `json:"_trace"` Trace []string `json:"_trace"`
} }
@ -225,6 +228,12 @@ type UserGuildSettingsUpdate struct {
*UserGuildSettings *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. // VoiceServerUpdate is the data for a VoiceServerUpdate event.
type VoiceServerUpdate struct { type VoiceServerUpdate struct {
Token string `json:"token"` Token string `json:"token"`
@ -236,3 +245,9 @@ type VoiceServerUpdate struct {
type VoiceStateUpdate struct { type VoiceStateUpdate struct {
*VoiceState *VoiceState
} }
// MessageDeleteBulk is the data for a MessageDeleteBulk event
type MessageDeleteBulk struct {
Messages []string `json:"ids"`
ChannelID string `json:"channel_id"`
}

View File

@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"os/signal"
"strings" "strings"
"syscall"
"time" "time"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
@ -21,6 +23,7 @@ var token string
var buffer = make([][]byte, 0) var buffer = make([][]byte, 0)
func main() { func main() {
if token == "" { if token == "" {
fmt.Println("No token provided. Please run: airhorn -t <bot token>") fmt.Println("No token provided. Please run: airhorn -t <bot token>")
return return
@ -56,21 +59,37 @@ func main() {
fmt.Println("Error opening Discord session: ", err) 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.") fmt.Println("Airhorn is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed. sc := make(chan os.Signal, 1)
<-make(chan struct{}) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
return <-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) { func ready(s *discordgo.Session, event *discordgo.Ready) {
// Set the playing status. // 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 // 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. // message is created on any channel that the autenticated bot has access to.
func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { 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") { if strings.HasPrefix(m.Content, "!airhorn") {
// Find the channel that the message came from. // Find the channel that the message came from.
c, err := s.State.Channel(m.ChannelID) c, err := s.State.Channel(m.ChannelID)
if err != nil { if err != nil {
@ -85,7 +104,7 @@ func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
return 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 { for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID { if vs.UserID == m.Author.ID {
err = playSound(s, g.ID, vs.ChannelID) 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 // This function will be called (due to AddHandler above) every time a new
// guild is joined. // guild is joined.
func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
if event.Guild.Unavailable { if event.Guild.Unavailable {
return return
} }
@ -116,8 +136,8 @@ func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
// loadSound attempts to load an encoded sound file from disk. // loadSound attempts to load an encoded sound file from disk.
func loadSound() error { func loadSound() error {
file, err := os.Open("airhorn.dca")
file, err := os.Open("airhorn.dca")
if err != nil { if err != nil {
fmt.Println("Error opening dca file :", err) fmt.Println("Error opening dca file :", err)
return err return err
@ -131,7 +151,7 @@ func loadSound() error {
// If this is the end of the file, just return. // If this is the end of the file, just return.
if err == io.EOF || err == io.ErrUnexpectedEOF { if err == io.EOF || err == io.ErrUnexpectedEOF {
file.Close() err := file.Close()
if err != nil { if err != nil {
return err return err
} }
@ -160,6 +180,7 @@ func loadSound() error {
// playSound plays the current buffer to the provided channel. // playSound plays the current buffer to the provided channel.
func playSound(s *discordgo.Session, guildID, channelID string) (err error) { func playSound(s *discordgo.Session, guildID, channelID string) (err error) {
// Join the provided voice channel. // Join the provided voice channel.
vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true)
if err != nil { if err != nil {
@ -170,7 +191,7 @@ func playSound(s *discordgo.Session, guildID, channelID string) (err error) {
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
// Start speaking. // Start speaking.
_ = vc.Speaking(true) vc.Speaking(true)
// Send the buffer data. // Send the buffer data.
for _, buff := range buffer { for _, buff := range buffer {
@ -178,13 +199,13 @@ func playSound(s *discordgo.Session, guildID, channelID string) (err error) {
} }
// Stop speaking // Stop speaking
_ = vc.Speaking(false) vc.Speaking(false)
// Sleep for a specificed amount of time before ending. // Sleep for a specificed amount of time before ending.
time.Sleep(250 * time.Millisecond) time.Sleep(250 * time.Millisecond)
// Disconnect from the provided voice channel. // Disconnect from the provided voice channel.
_ = vc.Disconnect() vc.Disconnect()
return nil return nil
} }

View File

@ -1,38 +1,42 @@
package main package main
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
// Variables used for command line options // Variables used for command line options
var ( var (
Email string
Password string
Token string Token string
AppName string Name string
DeleteID string DeleteID string
ListOnly bool ListOnly bool
) )
func init() { func init() {
flag.StringVar(&Email, "e", "", "Account Email") flag.StringVar(&Token, "t", "", "Owner Account Token")
flag.StringVar(&Password, "p", "", "Account Password") flag.StringVar(&Name, "n", "", "Name to give App/Bot")
flag.StringVar(&Token, "t", "", "Account Token")
flag.StringVar(&DeleteID, "d", "", "Application ID to delete") flag.StringVar(&DeleteID, "d", "", "Application ID to delete")
flag.BoolVar(&ListOnly, "l", false, "List Applications Only") flag.BoolVar(&ListOnly, "l", false, "List Applications Only")
flag.StringVar(&AppName, "a", "", "App/Bot Name")
flag.Parse() flag.Parse()
if Token == "" {
flag.Usage()
os.Exit(1)
}
} }
func main() { func main() {
var err error var err error
// Create a new Discord session using the provided login information. // 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 { if err != nil {
fmt.Println("error creating Discord session,", err) fmt.Println("error creating Discord session,", err)
return return
@ -41,18 +45,17 @@ func main() {
// If -l set, only display a list of existing applications // If -l set, only display a list of existing applications
// for the given account. // for the given account.
if ListOnly { if ListOnly {
aps, err2 := dg.Applications()
if err2 != nil { aps, err := dg.Applications()
if err != nil {
fmt.Println("error fetching applications,", err) fmt.Println("error fetching applications,", err)
return return
} }
for k, v := range aps { for _, v := range aps {
fmt.Printf("%d : --------------------------------------\n", k) fmt.Println("-----------------------------------------------------")
fmt.Printf("ID: %s\n", v.ID) b, _ := json.MarshalIndent(v, "", " ")
fmt.Printf("Name: %s\n", v.Name) fmt.Println(string(b))
fmt.Printf("Secret: %s\n", v.Secret)
fmt.Printf("Description: %s\n", v.Description)
} }
return return
} }
@ -66,9 +69,14 @@ func main() {
return return
} }
if Name == "" {
flag.Usage()
os.Exit(1)
}
// Create a new application. // Create a new application.
ap := &discordgo.Application{} ap := &discordgo.Application{}
ap.Name = AppName ap.Name = Name
ap, err = dg.ApplicationCreate(ap) ap, err = dg.ApplicationCreate(ap)
if err != nil { if err != nil {
fmt.Println("error creating new applicaiton,", err) fmt.Println("error creating new applicaiton,", err)
@ -76,9 +84,8 @@ func main() {
} }
fmt.Printf("Application created successfully:\n") fmt.Printf("Application created successfully:\n")
fmt.Printf("ID: %s\n", ap.ID) b, _ := json.MarshalIndent(ap, "", " ")
fmt.Printf("Name: %s\n", ap.Name) fmt.Println(string(b))
fmt.Printf("Secret: %s\n\n", ap.Secret)
// Create the bot account under the application we just created // Create the bot account under the application we just created
bot, err := dg.ApplicationBotCreate(ap.ID) bot, err := dg.ApplicationBotCreate(ap.ID)
@ -88,11 +95,9 @@ func main() {
} }
fmt.Printf("Bot account created successfully.\n") fmt.Printf("Bot account created successfully.\n")
fmt.Printf("ID: %s\n", bot.ID) b, _ = json.MarshalIndent(bot, "", " ")
fmt.Printf("Username: %s\n", bot.Username) fmt.Println(string(b))
fmt.Printf("Token: %s\n\n", bot.Token)
fmt.Println("Please save the above posted info in a secure place.") 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.") fmt.Println("You will need that information to login with your bot account.")
return
} }

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -3,6 +3,7 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
@ -18,6 +19,11 @@ func init() {
flag.StringVar(&Email, "e", "", "Account Email") flag.StringVar(&Email, "e", "", "Account Email")
flag.StringVar(&Password, "p", "", "Account Password") flag.StringVar(&Password, "p", "", "Account Password")
flag.Parse() flag.Parse()
if Email == "" || Password == "" {
flag.Usage()
os.Exit(1)
}
} }
func main() { func main() {
@ -29,5 +35,6 @@ func main() {
return return
} }
// Print out your token.
fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token)
} }

View File

@ -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)
}

View File

@ -3,6 +3,9 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"os"
"os/signal"
"syscall"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
) )
@ -10,7 +13,6 @@ import (
// Variables used for command line parameters // Variables used for command line parameters
var ( var (
Token string Token string
BotID string
) )
func init() { func init() {
@ -28,29 +30,24 @@ func main() {
return return
} }
// Get the account information. // Register the messageCreate func as a callback for MessageCreate events.
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.
dg.AddHandler(messageCreate) dg.AddHandler(messageCreate)
// Open the websocket and begin listening. // Open a websocket connection to Discord and begin listening.
err = dg.Open() err = dg.Open()
if err != nil { if err != nil {
fmt.Println("error opening connection,", err) fmt.Println("error opening connection,", err)
return return
} }
// Wait here until CTRL-C or other term signal is received.
fmt.Println("Bot is now running. Press CTRL-C to exit.") fmt.Println("Bot is now running. Press CTRL-C to exit.")
// Simple way to keep program running until CTRL-C is pressed. sc := make(chan os.Signal, 1)
<-make(chan struct{}) signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
return <-sc
// Cleanly close down the Discord session.
dg.Close()
} }
// This function will be called (due to AddHandler above) every time a new // 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) { func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself // 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 return
} }
// If the message is "ping" reply with "Pong!" // If the message is "ping" reply with "Pong!"
if m.Content == "ping" { if m.Content == "ping" {
_, _ = s.ChannelMessageSend(m.ChannelID, "Pong!") s.ChannelMessageSend(m.ChannelID, "Pong!")
} }
// If the message is "pong" reply with "Ping!" // If the message is "pong" reply with "Ping!"
if m.Content == "pong" { if m.Content == "pong" {
_, _ = s.ChannelMessageSend(m.ChannelID, "Ping!") s.ChannelMessageSend(m.ChannelID, "Ping!")
} }
} }

View File

@ -11,6 +11,7 @@ package discordgo
import ( import (
"fmt" "fmt"
"io"
"regexp" "regexp"
) )
@ -31,6 +32,53 @@ type Message struct {
Reactions []*MessageReactions `json:"reactions"` 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. // A MessageAttachment stores data for message attachments.
type MessageAttachment struct { type MessageAttachment struct {
ID string `json:"id"` ID string `json:"id"`

View File

@ -21,7 +21,12 @@ type Application struct {
Icon string `json:"icon,omitempty"` Icon string `json:"icon,omitempty"`
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
RedirectURIs *[]string `json:"redirect_uris,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"` Owner *User `json:"owner"`
Bot *User `json:"bot"`
} }
// Application returns an Application structure of a specific Application // Application returns an Application structure of a specific Application

View File

@ -4,13 +4,14 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"time" "time"
) )
// RateLimiter holds all ratelimit buckets // RateLimiter holds all ratelimit buckets
type RateLimiter struct { type RateLimiter struct {
sync.Mutex sync.Mutex
global *Bucket global *int64
buckets map[string]*Bucket buckets map[string]*Bucket
globalRateLimit time.Duration globalRateLimit time.Duration
} }
@ -20,7 +21,7 @@ func NewRatelimiter() *RateLimiter {
return &RateLimiter{ return &RateLimiter{
buckets: make(map[string]*Bucket), 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 // Check for global ratelimits
r.global.Lock() sleepTo := time.Unix(0, atomic.LoadInt64(r.global))
r.global.Unlock() if now := time.Now(); now.Before(sleepTo) {
time.Sleep(sleepTo.Sub(now))
}
b.remaining-- b.remaining--
return b return b
@ -72,7 +75,7 @@ type Bucket struct {
remaining int remaining int
limit int limit int
reset time.Time reset time.Time
global *Bucket global *int64
} }
// Release unlocks the bucket and reads the headers to update the buckets ratelimit info // 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") global := headers.Get("X-RateLimit-Global")
retryAfter := headers.Get("Retry-After") retryAfter := headers.Get("Retry-After")
// If it's global just keep the main ratelimit mutex locked // Update global and per bucket reset time if the proper headers are available
if global != "" { // If global is set, then it will block all buckets until after Retry-After
parsedAfter, err := strconv.Atoi(retryAfter) // If Retry-After without global is provided it will use that for the new reset
if err != nil { // time since it's more accurate than X-RateLimit-Reset.
return err // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset
}
// Lock it in a new goroutine so that this isn't a blocking call
go func() {
// Make sure if several requests were waiting we don't sleep for n * retry-after
// where n is the amount of requests that were going on
sleepTo := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond)
b.global.Lock()
sleepDuration := sleepTo.Sub(time.Now())
if sleepDuration > 0 {
time.Sleep(sleepDuration)
}
b.global.Unlock()
}()
return nil
}
// Update reset time if either retry after or reset headers are present
// Prefer retryafter because it's more accurate with time sync and whatnot
if retryAfter != "" { if retryAfter != "" {
parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64)
if err != nil { if err != nil {
return err 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 != "" { } else if reset != "" {
// Calculate the reset time by using the date header returned from discord // Calculate the reset time by using the date header returned from discord
discordTime, err := http.ParseTime(headers.Get("Date")) discordTime, err := http.ParseTime(headers.Get("Date"))

View File

@ -29,8 +29,15 @@ import (
"time" "time"
) )
// ErrJSONUnmarshal is returned for JSON Unmarshall errors. // All error constants
var ErrJSONUnmarshal = errors.New("json unmarshal") 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 // 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) { 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 := s.Client.Do(req)
resp, err := client.Do(req)
if err != nil { if err != nil {
bucket.Release(nil) bucket.Release(nil)
return return
@ -175,6 +180,12 @@ func unmarshal(data []byte, v interface{}) error {
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------
// Login asks the Discord server for an authentication token. // 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) { func (s *Session) Login(email, password string) (err error) {
data := struct { data := struct {
@ -189,6 +200,7 @@ func (s *Session) Login(email, password string) (err error) {
temp := struct { temp := struct {
Token string `json:"token"` Token string `json:"token"`
MFA bool `json:"mfa"`
}{} }{}
err = unmarshal(response, &temp) err = unmarshal(response, &temp)
@ -197,6 +209,7 @@ func (s *Session) Login(email, password string) (err error) {
} }
s.Token = temp.Token s.Token = temp.Token
s.MFA = temp.MFA
return return
} }
@ -264,15 +277,21 @@ func (s *Session) User(userID string) (st *User, err error) {
return 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 // 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) { func (s *Session) UserAvatar(userID string) (img image.Image, err error) {
u, err := s.User(userID) u, err := s.User(userID)
if err != nil { if err != nil {
return 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 { if err != nil {
return return
} }
@ -292,7 +311,7 @@ func (s *Session) UserUpdate(email, password, username, avatar, newPassword stri
data := struct { data := struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
NewPassword string `json:"new_password,omitempty"` NewPassword string `json:"new_password,omitempty"`
}{email, password, username, avatar, newPassword} }{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') // status : The new status (Actual valid status are 'online','idle','dnd','invisible')
func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) {
if status == StatusOffline { if status == StatusOffline {
err = errors.New("You can't set your Status to offline") err = ErrStatusOffline
return 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. // 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 { if err != nil {
return 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. // NOTE: This function is now deprecated and will be removed in the future.
// Please see the same function inside state.go // Please see the same function inside state.go
func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { 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) channel, err := s.State.Channel(channelID)
if err != nil || channel == nil { if err != nil || channel == nil {
channel, err = s.Channel(channelID) 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 { for _, role := range guild.Roles {
if role.ID == guild.ID { if role.ID == guild.ID {
apermissions |= role.Permissions apermissions |= role.Permissions
@ -447,20 +507,35 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions
} }
} }
if apermissions&PermissionAdministrator > 0 { if apermissions&PermissionAdministrator == PermissionAdministrator {
apermissions |= PermissionAll apermissions |= PermissionAll
} }
// Member overwrites can override role overrides, so do two passes // Apply @everyone overrides from the channel.
for _, overwrite := range channel.PermissionOverwrites { for _, overwrite := range channel.PermissionOverwrites {
for _, roleID := range member.Roles { if guild.ID == overwrite.ID {
if overwrite.Type == "role" && roleID == overwrite.ID {
apermissions &= ^overwrite.Deny apermissions &= ^overwrite.Deny
apermissions |= overwrite.Allow apermissions |= overwrite.Allow
break 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 {
denies |= overwrite.Deny
allows |= overwrite.Allow
break
} }
}
}
apermissions &= ^denies
apermissions |= allows
for _, overwrite := range channel.PermissionOverwrites { for _, overwrite := range channel.PermissionOverwrites {
if overwrite.Type == "member" && overwrite.ID == userID { if overwrite.Type == "member" && overwrite.ID == userID {
@ -470,11 +545,11 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions
} }
} }
if apermissions&PermissionAdministrator > 0 { if apermissions&PermissionAdministrator == PermissionAdministrator {
apermissions |= PermissionAllChannel 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 { if g.VerificationLevel != nil {
val := *g.VerificationLevel val := *g.VerificationLevel
if val < 0 || val > 3 { if val < 0 || val > 3 {
err = errors.New("VerificationLevel out of bounds, should be between 0 and 3") err = ErrVerificationLevelBounds
return return
} }
} }
@ -551,13 +626,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error
} }
} }
data := struct { body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), g, EndpointGuild(guildID))
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))
if err != nil { if err != nil {
return return
} }
@ -607,11 +676,28 @@ func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) {
// userID : The ID of a User // userID : The ID of a User
// days : The number of days of previous comments to delete. // days : The number of days of previous comments to delete.
func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { 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) uri := EndpointGuildBan(guildID, userID)
queryParams := url.Values{}
if days > 0 { 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, "")) _, 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 // GuildMemberNickname updates the nickname of a guild member
// guildID : The ID of a guild // guildID : The ID of a guild
// userID : The ID of a user // 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) { func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) {
data := struct { data := struct {
Nick string `json:"nick"` Nick string `json:"nick"`
}{nickname} }{nickname}
if userID == "@me" {
userID += "/nick"
}
_, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, ""))
return 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. // roleID : The ID of a Role to be assigned to the user.
func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) { 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 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. // roleID : The ID of a Role to be removed from the user.
func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err error) { 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 return
} }
@ -904,7 +995,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er
count = 0 count = 0
if days <= 0 { if days <= 0 {
err = errors.New("The number of days should be more than or equal to 1.") err = ErrPruneDaysBounds
return return
} }
@ -934,7 +1025,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err
count = 0 count = 0
if days <= 0 { if days <= 0 {
err = errors.New("The number of days should be more than or equal to 1.") err = ErrPruneDaysBounds
return return
} }
@ -1036,7 +1127,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) {
} }
if g.Icon == "" { if g.Icon == "" {
err = errors.New("Guild does not have an icon set.") err = ErrGuildNoIcon
return return
} }
@ -1058,7 +1149,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) {
} }
if g.Splash == "" { if g.Splash == "" {
err = errors.New("Guild does not have a splash set.") err = ErrGuildNoSplash
return return
} }
@ -1156,7 +1247,8 @@ func (s *Session) ChannelTyping(channelID string) (err error) {
// limit : The number messages that can be returned. (max 100) // limit : The number messages that can be returned. (max 100)
// beforeID : If provided all messages returned will be before given ID. // beforeID : If provided all messages returned will be before given ID.
// afterID : If provided all messages returned will be after 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) uri := EndpointChannelMessages(channelID)
@ -1170,6 +1262,9 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID
if beforeID != "" { if beforeID != "" {
v.Set("before", beforeID) v.Set("before", beforeID)
} }
if aroundID != "" {
v.Set("around", aroundID)
}
if len(v) > 0 { if len(v) > 0 {
uri = fmt.Sprintf("%s?%s", uri, v.Encode()) uri = fmt.Sprintf("%s?%s", uri, v.Encode())
} }
@ -1212,20 +1307,76 @@ func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st
return return
} }
// channelMessageSend sends a message to the given channel. // ChannelMessageSend sends a message to the given channel.
// channelID : The ID of a Channel. // channelID : The ID of a Channel.
// content : The message to send. // content : The message to send.
// tts : Whether to send the message with TTS. func (s *Session) ChannelMessageSend(channelID string, content string) (*Message, error) {
func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *Message, err error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{
Content: content,
})
}
// TODO: nonce string ? // ChannelMessageSendComplex sends a message to the given channel.
data := struct { // channelID : The ID of a Channel.
Content string `json:"content"` // data : The message struct to send.
TTS bool `json:"tts"` func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) (st *Message, err error) {
}{content, tts} if data.Embed != nil && data.Embed.Type == "" {
data.Embed.Type = "rich"
}
// Send the message to the given channel endpoint := EndpointChannelMessages(channelID)
response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, 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 { if err != nil {
return return
} }
@ -1234,55 +1385,42 @@ func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *M
return 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. // ChannelMessageSendTTS sends a message to the given channel with Text to Speech.
// channelID : The ID of a Channel. // channelID : The ID of a Channel.
// content : The message to send. // content : The message to send.
func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *Message, err error) { func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) {
return s.ChannelMessageSendComplex(channelID, &MessageSend{
return s.channelMessageSend(channelID, content, true) 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. // channelID : The ID of a Channel.
// embed : The embed data to send. // embed : The embed data to send.
func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (st *Message, err error) { func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (*Message, error) {
if embed != nil && embed.Type == "" { return s.ChannelMessageSendComplex(channelID, &MessageSend{
embed.Type = "rich" Embed: embed,
} })
data := struct {
Embed *MessageEmbed `json:"embed"`
}{embed}
// Send the message to the given channel
response, err := s.RequestWithBucketID("POST", EndpointChannelMessages(channelID), data, EndpointChannelMessages(channelID))
if err != nil {
return
}
err = unmarshal(response, &st)
return
} }
// ChannelMessageEdit edits an existing message, replacing it entirely with // ChannelMessageEdit edits an existing message, replacing it entirely with
// the given content. // the given content.
// channeld : The ID of a Channel // channelID : The ID of a Channel
// messageID : the ID of a Message // messageID : The ID of a Message
func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *Message, err error) { // 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 { // ChannelMessageEditComplex edits an existing message, replacing it entirely with
Content string `json:"content"` // the given MessageEdit struct
}{content} 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 { if err != nil {
return return
} }
@ -1291,26 +1429,12 @@ func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *
return 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 // channelID : The ID of a Channel
// messageID : The ID of a Message // messageID : The ID of a Message
// embed : The embed data to send // embed : The embed data to send
func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (st *Message, err error) { func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (*Message, error) {
if embed != nil && embed.Type == "" { return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbed(embed))
embed.Type = "rich"
}
data := struct {
Embed *MessageEmbed `json:"embed"`
}{embed}
response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(channelID, messageID), data, EndpointChannelMessage(channelID, ""))
if err != nil {
return
}
err = unmarshal(response, &st)
return
} }
// ChannelMessageDelete deletes a message from the Channel. // 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. // channelID : The ID of a Channel.
// name: The name of the file. // name: The name of the file.
// io.Reader : A reader for the file contents. // io.Reader : A reader for the file contents.
func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) { func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) {
return s.ChannelFileSendWithMessage(channelID, "", name, r) return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}})
} }
// ChannelFileSendWithMessage sends a file to the given channel with an message. // ChannelFileSendWithMessage sends a file to the given channel with an message.
// DEPRECATED. Use ChannelMessageSendComplex instead.
// channelID : The ID of a Channel. // channelID : The ID of a Channel.
// content: Optional Message content. // content: Optional Message content.
// name: The name of the file. // name: The name of the file.
// io.Reader : A reader for the file contents. // io.Reader : A reader for the file contents.
func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (st *Message, err error) { 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})
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
} }
// ChannelInvites returns an array of Invite structures for the given channel // 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 // 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) { func (s *Session) Gateway() (gateway string, err error) {
response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway) response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway)
@ -1808,6 +1902,20 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i
return 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) // Functions specific to Discord Relationships (Friends list)
// ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------

View File

@ -14,11 +14,16 @@ package discordgo
import ( import (
"errors" "errors"
"sort"
"sync" "sync"
) )
// ErrNilState is returned when the state is nil. // 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. // A State contains the current known state.
// As discord sends this in a READY blob, it seems reasonable to simply // As discord sends this in a READY blob, it seems reasonable to simply
@ -33,6 +38,7 @@ type State struct {
TrackMembers bool TrackMembers bool
TrackRoles bool TrackRoles bool
TrackVoice bool TrackVoice bool
TrackPresences bool
guildMap map[string]*Guild guildMap map[string]*Guild
channelMap map[string]*Channel channelMap map[string]*Channel
@ -50,6 +56,7 @@ func NewState() *State {
TrackMembers: true, TrackMembers: true,
TrackRoles: true, TrackRoles: true,
TrackVoice: true, TrackVoice: true,
TrackPresences: true,
guildMap: make(map[string]*Guild), guildMap: make(map[string]*Guild),
channelMap: make(map[string]*Channel), channelMap: make(map[string]*Channel),
} }
@ -143,7 +150,108 @@ func (s *State) Guild(guildID string) (*Guild, error) {
return g, nil 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. // 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. // 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 // 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. // 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. // updates it if it already exists.
// Channels may exist either as PrivateChannels or inside // Channels may exist either as PrivateChannels or inside
// a guild. // a guild.
@ -324,7 +432,7 @@ func (s *State) ChannelAdd(channel *Channel) error {
} else { } else {
guild, ok := s.guildMap[channel.GuildID] guild, ok := s.guildMap[channel.GuildID]
if !ok { if !ok {
return errors.New("Guild for channel not found.") return ErrStateNotFound
} }
guild.Channels = append(guild.Channels, channel) guild.Channels = append(guild.Channels, channel)
@ -403,7 +511,7 @@ func (s *State) Channel(channelID string) (*Channel, error) {
return c, nil return c, nil
} }
return nil, errors.New("Channel not found.") return nil, ErrStateNotFound
} }
// Emoji returns an emoji for a guild and emoji id. // 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. // EmojiAdd adds an emoji to the current world state.
@ -523,7 +631,12 @@ func (s *State) MessageRemove(message *Message) error {
return ErrNilState 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 { if err != nil {
return err return err
} }
@ -532,13 +645,13 @@ func (s *State) MessageRemove(message *Message) error {
defer s.Unlock() defer s.Unlock()
for i, m := range c.Messages { 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:]...) c.Messages = append(c.Messages[:i], c.Messages[i+1:]...)
return nil return nil
} }
} }
return errors.New("Message not found.") return ErrStateNotFound
} }
func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { 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. // OnReady takes a Ready event and updates all internal state.
@ -610,7 +723,6 @@ func (s *State) onReady(se *Session, r *Ready) (err error) {
ready := Ready{ ready := Ready{
Version: r.Version, Version: r.Version,
SessionID: r.SessionID, SessionID: r.SessionID,
HeartbeatInterval: r.HeartbeatInterval,
User: r.User, User: r.User,
} }
@ -710,10 +822,55 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) {
if s.MaxMessageCount != 0 { if s.MaxMessageCount != 0 {
err = s.MessageRemove(t.Message) err = s.MessageRemove(t.Message)
} }
case *MessageDeleteBulk:
if s.MaxMessageCount != 0 {
for _, mID := range t.Messages {
s.messageRemoveByID(t.ChannelID, mID)
}
}
case *VoiceStateUpdate: case *VoiceStateUpdate:
if s.TrackVoice { if s.TrackVoice {
err = s.voiceStateUpdate(t) 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 return
@ -747,48 +904,46 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i
return return
} }
for _, role := range guild.Roles { return memberPermissions(guild, channel, member), nil
if role.ID == guild.ID {
apermissions |= role.Permissions
break
}
} }
for _, role := range guild.Roles { // 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
}
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 { for _, roleID := range member.Roles {
if role.ID == roleID { if role.ID == roleID {
apermissions |= role.Permissions if role.Color != 0 {
break return role.Color
}
} }
} }
} }
if apermissions&PermissionAdministrator > 0 { return 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
} }

View File

@ -13,6 +13,7 @@ package discordgo
import ( import (
"encoding/json" "encoding/json"
"net/http"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -28,6 +29,7 @@ type Session struct {
// Authentication token for this session // Authentication token for this session
Token string Token string
MFA bool
// Debug for printing JSON request/responses // Debug for printing JSON request/responses
Debug bool // Deprecated, will be removed. Debug bool // Deprecated, will be removed.
@ -73,6 +75,9 @@ type Session struct {
// StateEnabled is true. // StateEnabled is true.
State *State State *State
// The http client used for REST requests
Client *http.Client
// Event handlers // Event handlers
handlersMu sync.RWMutex handlersMu sync.RWMutex
handlers map[string][]*eventHandlerInstance handlers map[string][]*eventHandlerInstance
@ -88,7 +93,7 @@ type Session struct {
ratelimiter *RateLimiter ratelimiter *RateLimiter
// sequence tracks the current gateway api websocket sequence number // sequence tracks the current gateway api websocket sequence number
sequence int sequence *int64
// stores sessions current Discord Gateway // stores sessions current Discord Gateway
gateway string gateway string
@ -100,12 +105,6 @@ type Session struct {
wsMutex sync.Mutex 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. // A VoiceRegion stores data for a specific voice region server.
type VoiceRegion struct { type VoiceRegion struct {
ID string `json:"id"` ID string `json:"id"`
@ -235,9 +234,15 @@ type UserGuild struct {
// A GuildParams stores all the data needed to update discord guild settings // A GuildParams stores all the data needed to update discord guild settings
type GuildParams struct { type GuildParams struct {
Name string `json:"name"` Name string `json:"name,omitempty"`
Region string `json:"region"` Region string `json:"region,omitempty"`
VerificationLevel *VerificationLevel `json:"verification_level"` 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. // A Role stores information about Discord guild member roles.
@ -252,6 +257,21 @@ type Role struct {
Permissions int `json:"permissions"` 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 // A VoiceState stores the voice states of Guilds
type VoiceState struct { type VoiceState struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
@ -284,7 +304,7 @@ type Game struct {
// UnmarshalJSON unmarshals json to Game struct // UnmarshalJSON unmarshals json to Game struct
func (g *Game) UnmarshalJSON(bytes []byte) error { func (g *Game) UnmarshalJSON(bytes []byte) error {
temp := &struct { temp := &struct {
Name string `json:"name"` Name json.Number `json:"name"`
Type json.RawMessage `json:"type"` Type json.RawMessage `json:"type"`
URL string `json:"url"` URL string `json:"url"`
}{} }{}
@ -292,8 +312,8 @@ func (g *Game) UnmarshalJSON(bytes []byte) error {
if err != nil { if err != nil {
return err return err
} }
g.Name = temp.Name
g.URL = temp.URL g.URL = temp.URL
g.Name = temp.Name.String()
if temp.Type != nil { if temp.Type != nil {
err = json.Unmarshal(temp.Type, &g.Type) err = json.Unmarshal(temp.Type, &g.Type)
@ -324,19 +344,6 @@ type Member struct {
Roles []string `json:"roles"` 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. // A Settings stores data for a specific users Discord client settings.
type Settings struct { type Settings struct {
RenderEmbeds bool `json:"render_embeds"` RenderEmbeds bool `json:"render_embeds"`
@ -542,6 +549,8 @@ const (
PermissionAdministrator PermissionAdministrator
PermissionManageChannels PermissionManageChannels
PermissionManageServer PermissionManageServer
PermissionAddReactions
PermissionViewAuditLogs
PermissionAllText = PermissionReadMessages | PermissionAllText = PermissionReadMessages |
PermissionSendMessages | PermissionSendMessages |
@ -561,9 +570,12 @@ const (
PermissionAllVoice | PermissionAllVoice |
PermissionCreateInstantInvite | PermissionCreateInstantInvite |
PermissionManageRoles | PermissionManageRoles |
PermissionManageChannels PermissionManageChannels |
PermissionAddReactions |
PermissionViewAuditLogs
PermissionAll = PermissionAllChannel | PermissionAll = PermissionAllChannel |
PermissionKickMembers | PermissionKickMembers |
PermissionBanMembers | PermissionBanMembers |
PermissionManageServer PermissionManageServer |
PermissionAdministrator
) )

View File

@ -37,18 +37,18 @@ type {{privateName .}}EventHandler func(*Session, *{{.}})
func (eh {{privateName .}}EventHandler) Type() string { func (eh {{privateName .}}EventHandler) Type() string {
return {{privateName .}}EventType return {{privateName .}}EventType
} }
{{if isDiscordEvent .}}
// New returns a new instance of {{.}}. // New returns a new instance of {{.}}.
func (eh {{privateName .}}EventHandler) New() interface{} { func (eh {{privateName .}}EventHandler) New() interface{} {
return &{{.}}{} return &{{.}}{}
} }{{end}}
// Handle is the handler for {{.}} events. // Handle is the handler for {{.}} events.
func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) { func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*{{.}}); ok { if t, ok := i.(*{{.}}); ok {
eh(s, t) eh(s, t)
} }
} }
{{end}} {{end}}
func handlerForInterface(handler interface{}) EventHandler { func handlerForInterface(handler interface{}) EventHandler {
switch v := handler.(type) { switch v := handler.(type) {
@ -60,6 +60,7 @@ func handlerForInterface(handler interface{}) EventHandler {
return nil return nil
} }
func init() { {{range .}}{{if isDiscordEvent .}} func init() { {{range .}}{{if isDiscordEvent .}}
registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}} registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}}
} }

26
vendor/github.com/bwmarrin/discordgo/user.go generated vendored Normal file
View File

@ -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)
}

View File

@ -15,7 +15,6 @@ import (
"fmt" "fmt"
"log" "log"
"net" "net"
"runtime"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -93,18 +92,22 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
} }
if v.wsConn == nil { if v.wsConn == nil {
return fmt.Errorf("No VoiceConnection websocket.") return fmt.Errorf("no VoiceConnection websocket")
} }
data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}}
v.wsMutex.Lock() v.wsMutex.Lock()
err = v.wsConn.WriteJSON(data) err = v.wsConn.WriteJSON(data)
v.wsMutex.Unlock() v.wsMutex.Unlock()
v.Lock()
defer v.Unlock()
if err != nil { if err != nil {
v.speaking = false v.speaking = false
log.Println("Speaking() write json error:", err) log.Println("Speaking() write json error:", err)
return return
} }
v.speaking = b v.speaking = b
return return
@ -139,9 +142,9 @@ func (v *VoiceConnection) Disconnect() (err error) {
// Send a OP4 with a nil channel to disconnect // Send a OP4 with a nil channel to disconnect
if v.sessionID != "" { if v.sessionID != "" {
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}}
v.wsMutex.Lock() v.session.wsMutex.Lock()
err = v.session.wsConn.WriteJSON(data) err = v.session.wsConn.WriteJSON(data)
v.wsMutex.Unlock() v.session.wsMutex.Unlock()
v.sessionID = "" v.sessionID = ""
} }
@ -149,7 +152,10 @@ func (v *VoiceConnection) Disconnect() (err error) {
v.Close() v.Close()
v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID)
v.session.Lock()
delete(v.session.VoiceConnections, v.GuildID) delete(v.session.VoiceConnections, v.GuildID)
v.session.Unlock()
return return
} }
@ -185,7 +191,9 @@ func (v *VoiceConnection) Close() {
// To cleanly close a connection, a client should send a close // To cleanly close a connection, a client should send a close
// frame and wait for the server to close the connection. // frame and wait for the server to close the connection.
v.wsMutex.Lock()
err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
v.wsMutex.Unlock()
if err != nil { if err != nil {
v.log(LogError, "error closing websocket, %s", err) v.log(LogError, "error closing websocket, %s", err)
} }
@ -246,12 +254,15 @@ func (v *VoiceConnection) waitUntilConnected() error {
i := 0 i := 0
for { for {
if v.Ready { v.RLock()
ready := v.Ready
v.RUnlock()
if ready {
return nil return nil
} }
if i > 10 { if i > 10 {
return fmt.Errorf("Timeout waiting for voice.") return fmt.Errorf("timeout waiting for voice")
} }
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
@ -282,7 +293,7 @@ func (v *VoiceConnection) open() (err error) {
break break
} }
if i > 20 { // only loop for up to 1 second total 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) time.Sleep(50 * time.Millisecond)
i++ i++
@ -409,8 +420,6 @@ func (v *VoiceConnection) onEvent(message []byte) {
go v.opusReceiver(v.udpConn, v.close, v.OpusRecv) go v.opusReceiver(v.udpConn, v.close, v.OpusRecv)
} }
// Send the ready event
v.connected <- true
return return
case 3: // HEARTBEAT response case 3: // HEARTBEAT response
@ -418,6 +427,9 @@ func (v *VoiceConnection) onEvent(message []byte) {
return return
case 4: // udp encryption secret key case 4: // udp encryption secret key
v.Lock()
defer v.Unlock()
v.op4 = voiceOP4{} v.op4 = voiceOP4{}
if err := json.Unmarshal(e.RawData, &v.op4); err != nil { if err := json.Unmarshal(e.RawData, &v.op4); err != nil {
v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData)) 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 var err error
ticker := time.NewTicker(i * time.Millisecond) ticker := time.NewTicker(i * time.Millisecond)
defer ticker.Stop()
for { for {
v.log(LogDebug, "sending heartbeat packet") v.log(LogDebug, "sending heartbeat packet")
v.wsMutex.Lock() v.wsMutex.Lock()
@ -616,6 +629,7 @@ func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct
packet := make([]byte, 8) packet := make([]byte, 8)
ticker := time.NewTicker(i) ticker := time.NewTicker(i)
defer ticker.Stop()
for { for {
binary.LittleEndian.PutUint64(packet, sequence) binary.LittleEndian.PutUint64(packet, sequence)
@ -644,12 +658,16 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{}
return return
} }
runtime.LockOSThread()
// VoiceConnection is now ready to receive audio packets // VoiceConnection is now ready to receive audio packets
// TODO: this needs reviewed as I think there must be a better way. // TODO: this needs reviewed as I think there must be a better way.
v.Lock()
v.Ready = true v.Ready = true
defer func() { v.Ready = false }() v.Unlock()
defer func() {
v.Lock()
v.Ready = false
v.Unlock()
}()
var sequence uint16 var sequence uint16
var timestamp uint32 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 // start a send loop that loops until buf chan is closed
ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000))) ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000)))
defer ticker.Stop()
for { for {
// Get data from chan. If chan is closed, return. // 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 // else, continue loop
} }
if !v.speaking { v.RLock()
speaking := v.speaking
v.RUnlock()
if !speaking {
err := v.Speaking(true) err := v.Speaking(true)
if err != nil { if err != nil {
v.log(LogError, "error sending speaking packet, %s", err) 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 // encrypt the opus data
copy(nonce[:], udpHeader) copy(nonce[:], udpHeader)
v.RLock()
sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey) sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey)
v.RUnlock()
// block here until we're exactly at the right time :) // block here until we're exactly at the right time :)
// Then send rtp audio packet to Discord over UDP // Then send rtp audio packet to Discord over UDP
@ -742,7 +766,6 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
return return
} }
p := Packet{}
recvbuf := make([]byte, 1024) recvbuf := make([]byte, 1024)
var nonce [24]byte var nonce [24]byte
@ -778,6 +801,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
} }
// build a audio packet struct // build a audio packet struct
p := Packet{}
p.Type = recvbuf[0:2] p.Type = recvbuf[0:2]
p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4]) p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4])
p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8]) p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8])
@ -837,6 +861,8 @@ func (v *VoiceConnection) reconnect() {
return 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 // if the reconnect above didn't work lets just send a disconnect
// packet to reset things. // packet to reset things.
// Send a OP4 with a nil channel to disconnect // 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(LogError, "error sending disconnect packet, %s", err)
} }
v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err)
} }
} }

View File

@ -19,17 +19,30 @@ import (
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
"sync/atomic"
"time" "time"
"github.com/gorilla/websocket" "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 { type resumePacket struct {
Op int `json:"op"` Op int `json:"op"`
Data struct { Data struct {
Token string `json:"token"` Token string `json:"token"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
Sequence int `json:"seq"` Sequence int64 `json:"seq"`
} `json:"d"` } `json:"d"`
} }
@ -57,7 +70,7 @@ func (s *Session) Open() (err error) {
} }
if s.wsConn != nil { if s.wsConn != nil {
err = errors.New("Web socket already opened.") err = ErrWSAlreadyOpen
return return
} }
@ -74,7 +87,7 @@ func (s *Session) Open() (err error) {
} }
// Add the version and encoding to the URL // 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{} header := http.Header{}
@ -89,13 +102,14 @@ func (s *Session) Open() (err error) {
return return
} }
if s.sessionID != "" && s.sequence > 0 { sequence := atomic.LoadInt64(s.sequence)
if s.sessionID != "" && sequence > 0 {
p := resumePacket{} p := resumePacket{}
p.Op = 6 p.Op = 6
p.Data.Token = s.Token p.Data.Token = s.Token
p.Data.SessionID = s.sessionID p.Data.SessionID = s.sessionID
p.Data.Sequence = s.sequence p.Data.Sequence = sequence
s.log(LogInformational, "sending resume packet to gateway") s.log(LogInformational, "sending resume packet to gateway")
err = s.wsConn.WriteJSON(p) err = s.wsConn.WriteJSON(p)
@ -177,7 +191,12 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
type heartbeatOp struct { type heartbeatOp struct {
Op int `json:"op"` Op int `json:"op"`
Data int `json:"d"` 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 // 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 var err error
ticker := time.NewTicker(i * time.Millisecond) ticker := time.NewTicker(i * time.Millisecond)
defer ticker.Stop()
for { for {
sequence := atomic.LoadInt64(s.sequence)
s.log(LogInformational, "sending gateway websocket heartbeat seq %d", s.sequence) s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence)
s.wsMutex.Lock() s.wsMutex.Lock()
err = wsConn.WriteJSON(heartbeatOp{1, s.sequence}) err = wsConn.WriteJSON(heartbeatOp{1, sequence})
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) 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() s.RLock()
defer s.RUnlock() defer s.RUnlock()
if s.wsConn == nil { if s.wsConn == nil {
return errors.New("no websocket connection exists") return ErrWSNotFound
} }
var usd updateStatusData var usd updateStatusData
@ -299,7 +319,7 @@ func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err err
s.RLock() s.RLock()
defer s.RUnlock() defer s.RUnlock()
if s.wsConn == nil { if s.wsConn == nil {
return errors.New("no websocket connection exists") return ErrWSNotFound
} }
data := requestGuildMembersData{ data := requestGuildMembersData{
@ -365,7 +385,7 @@ func (s *Session) onEvent(messageType int, message []byte) {
if e.Operation == 1 { if e.Operation == 1 {
s.log(LogInformational, "sending heartbeat in response to Op1") s.log(LogInformational, "sending heartbeat in response to Op1")
s.wsMutex.Lock() s.wsMutex.Lock()
err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence}) err = s.wsConn.WriteJSON(heartbeatOp{1, atomic.LoadInt64(s.sequence)})
s.wsMutex.Unlock() s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogError, "error sending heartbeat in response to Op1") s.log(LogError, "error sending heartbeat in response to Op1")
@ -396,6 +416,16 @@ func (s *Session) onEvent(messageType int, message []byte) {
return 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 // Do not try to Dispatch a non-Dispatch Message
if e.Operation != 0 { if e.Operation != 0 {
// But we probably should be doing something with them. // 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 // 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. // Map event to registered event handlers and pass it along to any registered handlers.
if eh, ok := registeredInterfaceProviders[e.Type]; ok { 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.log(LogInformational, "called")
s.RLock()
voice, _ = s.VoiceConnections[gID] voice, _ = s.VoiceConnections[gID]
s.RUnlock()
if voice == nil { if voice == nil {
voice = &VoiceConnection{} voice = &VoiceConnection{}
s.Lock()
s.VoiceConnections[gID] = voice s.VoiceConnections[gID] = voice
s.Unlock()
} }
voice.Lock()
voice.GuildID = gID voice.GuildID = gID
voice.ChannelID = cID voice.ChannelID = cID
voice.deaf = deaf voice.deaf = deaf
voice.mute = mute voice.mute = mute
voice.session = s voice.session = s
voice.Unlock()
// Send the request to Discord that we want to join the voice channel // Send the request to Discord that we want to join the voice channel
data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} 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 // Check if we have a voice connection to update
s.RLock()
voice, exists := s.VoiceConnections[st.GuildID] voice, exists := s.VoiceConnections[st.GuildID]
s.RUnlock()
if !exists { if !exists {
return return
} }
@ -511,8 +549,11 @@ func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) {
} }
// Store the SessionID for later use. // Store the SessionID for later use.
voice.Lock()
voice.UserID = st.UserID voice.UserID = st.UserID
voice.sessionID = st.SessionID voice.sessionID = st.SessionID
voice.ChannelID = st.ChannelID
voice.Unlock()
} }
// onVoiceServerUpdate handles the Voice Server Update data websocket event. // onVoiceServerUpdate handles the Voice Server Update data websocket event.
@ -524,7 +565,9 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
s.log(LogInformational, "called") s.log(LogInformational, "called")
s.RLock()
voice, exists := s.VoiceConnections[st.GuildID] voice, exists := s.VoiceConnections[st.GuildID]
s.RUnlock()
// If no VoiceConnection exists, just skip this // If no VoiceConnection exists, just skip this
if !exists { if !exists {
@ -536,9 +579,11 @@ func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) {
voice.Close() voice.Close()
// Store values for later use // Store values for later use
voice.Lock()
voice.token = st.Token voice.token = st.Token
voice.endpoint = st.Endpoint voice.endpoint = st.Endpoint
voice.GuildID = st.GuildID voice.GuildID = st.GuildID
voice.Unlock()
// Open a conenction to the voice server // Open a conenction to the voice server
err := voice.open() err := voice.open()
@ -588,7 +633,7 @@ func (s *Session) identify() error {
if s.ShardCount > 1 { if s.ShardCount > 1 {
if s.ShardID >= s.ShardCount { if s.ShardID >= s.ShardCount {
return errors.New("ShardID must be less than ShardCount") return ErrWSShardBounds
} }
data.Shard = &[2]int{s.ShardID, s.ShardCount} 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" // However, there seems to be cases where something "weird"
// happens. So we're doing this for now just to improve // happens. So we're doing this for now just to improve
// stability in those edge cases. // stability in those edge cases.
s.RLock()
defer s.RUnlock()
for _, v := range s.VoiceConnections { for _, v := range s.VoiceConnections {
s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) 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") s.log(LogInformational, "sending close frame")
// To cleanly close a connection, a client should send a close // To cleanly close a connection, a client should send a close
// frame and wait for the server to close the connection. // frame and wait for the server to close the connection.
s.wsMutex.Lock()
err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
s.wsMutex.Unlock()
if err != nil { if err != nil {
s.log(LogInformational, "error closing websocket, %s", err) s.log(LogInformational, "error closing websocket, %s", err)
} }

2
vendor/manifest vendored
View File

@ -45,7 +45,7 @@
"importpath": "github.com/bwmarrin/discordgo", "importpath": "github.com/bwmarrin/discordgo",
"repository": "https://github.com/bwmarrin/discordgo", "repository": "https://github.com/bwmarrin/discordgo",
"vcs": "git", "vcs": "git",
"revision": "5835676872d7fa65ee37b32672079c4ede2c1855", "revision": "d420e28024ad527390b43aa7f64e029083e11989",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },