diff options
author | Wim <wim@42.be> | 2016-09-19 20:53:26 +0200 |
---|---|---|
committer | Wim <wim@42.be> | 2016-09-19 20:53:26 +0200 |
commit | a0b84beb9bae3514940a0cde40948e885184f555 (patch) | |
tree | 0e8d22c5e2292c24e46699e59f442795b6158b3a /vendor/github.com/bwmarrin | |
parent | 0816e968318be5a4b165ac8fd30c032c6ecce61c (diff) | |
download | matterbridge-msglm-a0b84beb9bae3514940a0cde40948e885184f555.tar.gz matterbridge-msglm-a0b84beb9bae3514940a0cde40948e885184f555.tar.bz2 matterbridge-msglm-a0b84beb9bae3514940a0cde40948e885184f555.zip |
Add Discord support
Diffstat (limited to 'vendor/github.com/bwmarrin')
19 files changed, 5654 insertions, 0 deletions
diff --git a/vendor/github.com/bwmarrin/discordgo/LICENSE b/vendor/github.com/bwmarrin/discordgo/LICENSE new file mode 100644 index 00000000..8d062ea5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2015, Bruce Marriner +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of discordgo nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/github.com/bwmarrin/discordgo/discord.go b/vendor/github.com/bwmarrin/discordgo/discord.go new file mode 100644 index 00000000..d1cfddf5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/discord.go @@ -0,0 +1,257 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains high level helper functions and easy entry points for the +// entire discordgo package. These functions are beling developed and are very +// experimental at this point. They will most likley change so please use the +// low level functions if that's a problem. + +// Package discordgo provides Discord binding for Go +package discordgo + +import ( + "fmt" + "reflect" +) + +// VERSION of Discordgo, follows Symantic Versioning. (http://semver.org/) +const VERSION = "0.13.0" + +// New creates a new Discord session and will automate some startup +// tasks if given enough information to do so. Currently you can pass zero +// arguments and it will return an empty Discord session. +// There are 3 ways to call New: +// With a single auth token - All requests will use the token blindly, +// no verification of the token will be done and requests may fail. +// With an email and password - Discord will sign in with the provided +// credentials. +// With an email, password and auth token - Discord will verify the auth +// token, if it is invalid it will sign in with the provided +// credentials. This is the Discord recommended way to sign in. +func New(args ...interface{}) (s *Session, err error) { + + // Create an empty Session interface. + s = &Session{ + State: NewState(), + StateEnabled: true, + Compress: true, + ShouldReconnectOnError: true, + ShardID: 0, + ShardCount: 1, + } + + // If no arguments are passed return the empty Session interface. + if args == nil { + return + } + + // Variables used below when parsing func arguments + var auth, pass string + + // Parse passed arguments + for _, arg := range args { + + switch v := arg.(type) { + + case []string: + if len(v) > 3 { + err = fmt.Errorf("Too many string parameters provided.") + return + } + + // First string is either token or username + if len(v) > 0 { + auth = v[0] + } + + // If second string exists, it must be a password. + if len(v) > 1 { + pass = v[1] + } + + // If third string exists, it must be an auth token. + if len(v) > 2 { + s.Token = v[2] + } + + case string: + // First string must be either auth token or username. + // Second string must be a password. + // Only 2 input strings are supported. + + if auth == "" { + auth = v + } else if pass == "" { + pass = v + } else if s.Token == "" { + s.Token = v + } else { + err = fmt.Errorf("Too many string parameters provided.") + return + } + + // case Config: + // TODO: Parse configuration struct + + default: + err = fmt.Errorf("Unsupported parameter type provided.") + return + } + } + + // If only one string was provided, assume it is an auth token. + // Otherwise get auth token from Discord, if a token was specified + // Discord will verify it for free, or log the user in if it is + // invalid. + if pass == "" { + s.Token = auth + } else { + err = s.Login(auth, pass) + if err != nil || s.Token == "" { + err = fmt.Errorf("Unable to fetch discord authentication token. %v", err) + return + } + } + + // The Session is now able to have RestAPI methods called on it. + // It is recommended that you now call Open() so that events will trigger. + + return +} + +// validateHandler takes an event handler func, and returns the type of event. +// eg. +// Session.validateHandler(func (s *discordgo.Session, m *discordgo.MessageCreate)) +// will return the reflect.Type of *discordgo.MessageCreate +func (s *Session) validateHandler(handler interface{}) reflect.Type { + + handlerType := reflect.TypeOf(handler) + + if handlerType.NumIn() != 2 { + panic("Unable to add event handler, handler must be of the type func(*discordgo.Session, *discordgo.EventType).") + } + + if handlerType.In(0) != reflect.TypeOf(s) { + panic("Unable to add event handler, first argument must be of type *discordgo.Session.") + } + + eventType := handlerType.In(1) + + // Support handlers of type interface{}, this is a special handler, which is triggered on every event. + if eventType.Kind() == reflect.Interface { + eventType = nil + } + + return eventType +} + +// AddHandler allows you to add an event handler that will be fired anytime +// the Discord WSAPI event that matches the interface fires. +// eventToInterface in events.go has a list of all the Discord WSAPI events +// and their respective interface. +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +// The return value of this method is a function, that when called will remove the +// event handler. +func (s *Session) AddHandler(handler interface{}) func() { + + s.initialize() + + eventType := s.validateHandler(handler) + + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + h := reflect.ValueOf(handler) + + s.handlers[eventType] = append(s.handlers[eventType], h) + + // This must be done as we need a consistent reference to the + // reflected value, otherwise a RemoveHandler method would have + // been nice. + return func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + handlers := s.handlers[eventType] + for i, v := range handlers { + if h == v { + s.handlers[eventType] = append(handlers[:i], handlers[i+1:]...) + return + } + } + } +} + +// handle calls any handlers that match the event type and any handlers of +// interface{}. +func (s *Session) handle(event interface{}) { + + s.handlersMu.RLock() + defer s.handlersMu.RUnlock() + + if s.handlers == nil { + return + } + + handlerParameters := []reflect.Value{reflect.ValueOf(s), reflect.ValueOf(event)} + + if handlers, ok := s.handlers[nil]; ok { + for _, handler := range handlers { + go handler.Call(handlerParameters) + } + } + + if handlers, ok := s.handlers[reflect.TypeOf(event)]; ok { + for _, handler := range handlers { + go handler.Call(handlerParameters) + } + } +} + +// initialize adds all internal handlers and state tracking handlers. +func (s *Session) initialize() { + + s.log(LogInformational, "called") + + s.handlersMu.Lock() + if s.handlers != nil { + s.handlersMu.Unlock() + return + } + + s.handlers = map[interface{}][]reflect.Value{} + s.handlersMu.Unlock() + + s.AddHandler(s.onReady) + s.AddHandler(s.onResumed) + s.AddHandler(s.onVoiceServerUpdate) + s.AddHandler(s.onVoiceStateUpdate) + s.AddHandler(s.State.onInterface) +} + +// onReady handles the ready event. +func (s *Session) onReady(se *Session, r *Ready) { + + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID + + // Start the heartbeat to keep the connection alive. + go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) +} + +// onResumed handles the resumed event. +func (s *Session) onResumed(se *Session, r *Resumed) { + + // Start the heartbeat to keep the connection alive. + go s.heartbeat(s.wsConn, s.listening, r.HeartbeatInterval) +} diff --git a/vendor/github.com/bwmarrin/discordgo/endpoints.go b/vendor/github.com/bwmarrin/discordgo/endpoints.go new file mode 100644 index 00000000..682433d6 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/endpoints.go @@ -0,0 +1,99 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains variables for all known Discord end points. All functions +// throughout the Discordgo package use these variables for all connections +// to Discord. These are all exported and you may modify them if needed. + +package discordgo + +// Known Discord API Endpoints. +var ( + EndpointStatus = "https://status.discordapp.com/api/v2/" + EndpointSm = EndpointStatus + "scheduled-maintenances/" + EndpointSmActive = EndpointSm + "active.json" + EndpointSmUpcoming = EndpointSm + "upcoming.json" + + EndpointDiscord = "https://discordapp.com/" + EndpointAPI = EndpointDiscord + "api/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + + EndpointAuth = EndpointAPI + "auth/" + EndpointLogin = EndpointAuth + "login" + EndpointLogout = EndpointAuth + "logout" + EndpointVerify = EndpointAuth + "verify" + EndpointVerifyResend = EndpointAuth + "verify/resend" + EndpointForgotPassword = EndpointAuth + "forgot" + EndpointResetPassword = EndpointAuth + "reset" + EndpointRegister = EndpointAuth + "register" + + EndpointVoice = EndpointAPI + "/voice/" + EndpointVoiceRegions = EndpointVoice + "regions" + EndpointVoiceIce = EndpointVoice + "ice" + + EndpointTutorial = EndpointAPI + "tutorial/" + EndpointTutorialIndicators = EndpointTutorial + "indicators" + + EndpointTrack = EndpointAPI + "track" + EndpointSso = EndpointAPI + "sso" + EndpointReport = EndpointAPI + "report" + EndpointIntegrations = EndpointAPI + "integrations" + + EndpointUser = func(uID string) string { return EndpointUsers + uID } + EndpointUserAvatar = func(uID, aID string) string { return EndpointUsers + uID + "/avatars/" + aID + ".jpg" } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + + EndpointGuild = func(gID string) string { return EndpointGuilds + gID } + EndpointGuildInivtes = func(gID string) string { return EndpointGuilds + gID + "/invites" } + EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } + EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } + EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } + EndpointGuildBans = func(gID string) string { return EndpointGuilds + gID + "/bans" } + EndpointGuildBan = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID } + EndpointGuildIntegrations = func(gID string) string { return EndpointGuilds + gID + "/integrations" } + EndpointGuildIntegration = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID } + EndpointGuildIntegrationSync = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID + "/sync" } + EndpointGuildRoles = func(gID string) string { return EndpointGuilds + gID + "/roles" } + EndpointGuildRole = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID } + EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } + EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" } + EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } + EndpointGuildIcon = func(gID, hash string) string { return EndpointGuilds + gID + "/icons/" + hash + ".jpg" } + EndpointGuildSplash = func(gID, hash string) string { return EndpointGuilds + gID + "/splashes/" + hash + ".jpg" } + + EndpointChannel = func(cID string) string { return EndpointChannels + cID } + EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } + EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID } + EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" } + EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" } + EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } + EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } + EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } + EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk_delete" } + EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } + EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } + + EndpointInvite = func(iID string) string { return EndpointAPI + "invite/" + iID } + + EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } + + EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } + + EndpointOauth2 = EndpointAPI + "oauth2/" + EndpointApplications = EndpointOauth2 + "applications" + EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } + EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" } +) diff --git a/vendor/github.com/bwmarrin/discordgo/events.go b/vendor/github.com/bwmarrin/discordgo/events.go new file mode 100644 index 00000000..72aabf67 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/events.go @@ -0,0 +1,159 @@ +package discordgo + +// eventToInterface is a mapping of Discord WSAPI events to their +// DiscordGo event container. +// Each Discord WSAPI event maps to a unique interface. +// Use Session.AddHandler with one of these types to handle that +// type of event. +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +var eventToInterface = map[string]interface{}{ + "CHANNEL_CREATE": ChannelCreate{}, + "CHANNEL_UPDATE": ChannelUpdate{}, + "CHANNEL_DELETE": ChannelDelete{}, + "GUILD_CREATE": GuildCreate{}, + "GUILD_UPDATE": GuildUpdate{}, + "GUILD_DELETE": GuildDelete{}, + "GUILD_BAN_ADD": GuildBanAdd{}, + "GUILD_BAN_REMOVE": GuildBanRemove{}, + "GUILD_MEMBER_ADD": GuildMemberAdd{}, + "GUILD_MEMBER_UPDATE": GuildMemberUpdate{}, + "GUILD_MEMBER_REMOVE": GuildMemberRemove{}, + "GUILD_ROLE_CREATE": GuildRoleCreate{}, + "GUILD_ROLE_UPDATE": GuildRoleUpdate{}, + "GUILD_ROLE_DELETE": GuildRoleDelete{}, + "GUILD_INTEGRATIONS_UPDATE": GuildIntegrationsUpdate{}, + "GUILD_EMOJIS_UPDATE": GuildEmojisUpdate{}, + "MESSAGE_ACK": MessageAck{}, + "MESSAGE_CREATE": MessageCreate{}, + "MESSAGE_UPDATE": MessageUpdate{}, + "MESSAGE_DELETE": MessageDelete{}, + "PRESENCE_UPDATE": PresenceUpdate{}, + "PRESENCES_REPLACE": PresencesReplace{}, + "READY": Ready{}, + "USER_UPDATE": UserUpdate{}, + "USER_SETTINGS_UPDATE": UserSettingsUpdate{}, + "USER_GUILD_SETTINGS_UPDATE": UserGuildSettingsUpdate{}, + "TYPING_START": TypingStart{}, + "VOICE_SERVER_UPDATE": VoiceServerUpdate{}, + "VOICE_STATE_UPDATE": VoiceStateUpdate{}, + "RESUMED": Resumed{}, +} + +// Connect is an empty struct for an event. +type Connect struct{} + +// Disconnect is an empty struct for an event. +type Disconnect struct{} + +// RateLimit is a struct for the RateLimited event +type RateLimit struct { + *TooManyRequests + URL string +} + +// MessageCreate is a wrapper struct for an event. +type MessageCreate struct { + *Message +} + +// MessageUpdate is a wrapper struct for an event. +type MessageUpdate struct { + *Message +} + +// MessageDelete is a wrapper struct for an event. +type MessageDelete struct { + *Message +} + +// ChannelCreate is a wrapper struct for an event. +type ChannelCreate struct { + *Channel +} + +// ChannelUpdate is a wrapper struct for an event. +type ChannelUpdate struct { + *Channel +} + +// ChannelDelete is a wrapper struct for an event. +type ChannelDelete struct { + *Channel +} + +// GuildCreate is a wrapper struct for an event. +type GuildCreate struct { + *Guild +} + +// GuildUpdate is a wrapper struct for an event. +type GuildUpdate struct { + *Guild +} + +// GuildDelete is a wrapper struct for an event. +type GuildDelete struct { + *Guild +} + +// GuildBanAdd is a wrapper struct for an event. +type GuildBanAdd struct { + *GuildBan +} + +// GuildBanRemove is a wrapper struct for an event. +type GuildBanRemove struct { + *GuildBan +} + +// GuildMemberAdd is a wrapper struct for an event. +type GuildMemberAdd struct { + *Member +} + +// GuildMemberUpdate is a wrapper struct for an event. +type GuildMemberUpdate struct { + *Member +} + +// GuildMemberRemove is a wrapper struct for an event. +type GuildMemberRemove struct { + *Member +} + +// GuildRoleCreate is a wrapper struct for an event. +type GuildRoleCreate struct { + *GuildRole +} + +// GuildRoleUpdate is a wrapper struct for an event. +type GuildRoleUpdate struct { + *GuildRole +} + +// PresencesReplace is an array of Presences for an event. +type PresencesReplace []*Presence + +// VoiceStateUpdate is a wrapper struct for an event. +type VoiceStateUpdate struct { + *VoiceState +} + +// UserUpdate is a wrapper struct for an event. +type UserUpdate struct { + *User +} + +// UserSettingsUpdate is a map for an event. +type UserSettingsUpdate map[string]interface{} + +// UserGuildSettingsUpdate is a map for an event. +type UserGuildSettingsUpdate struct { + *UserGuildSettings +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go b/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go new file mode 100644 index 00000000..cc61301c --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/airhorn/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/bwmarrin/discordgo" +) + +func init() { + flag.StringVar(&token, "t", "", "Account Token") + flag.Parse() +} + +var token string +var buffer = make([][]byte, 0) + +func main() { + if token == "" { + fmt.Println("No token provided. Please run: airhorn -t <bot token>") + return + } + + // Load the sound file. + err := loadSound() + if err != nil { + fmt.Println("Error loading sound: ", err) + fmt.Println("Please copy $GOPATH/src/github.com/bwmarrin/examples/airhorn/airhorn.dca to this directory.") + return + } + + // Create a new Discord session using the provided token. + dg, err := discordgo.New(token) + if err != nil { + fmt.Println("Error creating Discord session: ", err) + return + } + + // Register ready as a callback for the ready events. + dg.AddHandler(ready) + + // Register messageCreate as a callback for the messageCreate events. + dg.AddHandler(messageCreate) + + // Register guildCreate as a callback for the guildCreate events. + dg.AddHandler(guildCreate) + + // Open the websocket and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("Error opening Discord session: ", err) + } + + fmt.Println("Airhorn is now running. Press CTRL-C to exit.") + // Simple way to keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} + +func ready(s *discordgo.Session, event *discordgo.Ready) { + // Set the playing status. + _ = s.UpdateStatus(0, "!airhorn") +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + if strings.HasPrefix(m.Content, "!airhorn") { + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // Could not find channel. + return + } + + // Find the guild for that channel. + g, err := s.State.Guild(c.GuildID) + if err != nil { + // Could not find guild. + return + } + + // Look for the message sender in that guilds current voice states. + for _, vs := range g.VoiceStates { + if vs.UserID == m.Author.ID { + err = playSound(s, g.ID, vs.ChannelID) + if err != nil { + fmt.Println("Error playing sound:", err) + } + + return + } + } + } +} + +// This function will be called (due to AddHandler above) every time a new +// guild is joined. +func guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) { + if event.Guild.Unavailable != nil { + return + } + + for _, channel := range event.Guild.Channels { + if channel.ID == event.Guild.ID { + _, _ = s.ChannelMessageSend(channel.ID, "Airhorn is ready! Type !airhorn while in a voice channel to play a sound.") + return + } + } +} + +// loadSound attempts to load an encoded sound file from disk. +func loadSound() error { + file, err := os.Open("airhorn.dca") + + if err != nil { + fmt.Println("Error opening dca file :", err) + return err + } + + var opuslen int16 + + for { + // Read opus frame length from dca file. + err = binary.Read(file, binary.LittleEndian, &opuslen) + + // If this is the end of the file, just return. + if err == io.EOF || err == io.ErrUnexpectedEOF { + return nil + } + + if err != nil { + fmt.Println("Error reading from dca file :", err) + return err + } + + // Read encoded pcm from dca file. + InBuf := make([]byte, opuslen) + err = binary.Read(file, binary.LittleEndian, &InBuf) + + // Should not be any end of file errors + if err != nil { + fmt.Println("Error reading from dca file :", err) + return err + } + + // Append encoded pcm data to the buffer. + buffer = append(buffer, InBuf) + } +} + +// playSound plays the current buffer to the provided channel. +func playSound(s *discordgo.Session, guildID, channelID string) (err error) { + // Join the provided voice channel. + vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) + if err != nil { + return err + } + + // Sleep for a specified amount of time before playing the sound + time.Sleep(250 * time.Millisecond) + + // Start speaking. + _ = vc.Speaking(true) + + // Send the buffer data. + for _, buff := range buffer { + vc.OpusSend <- buff + } + + // Stop speaking + _ = vc.Speaking(false) + + // Sleep for a specificed amount of time before ending. + time.Sleep(250 * time.Millisecond) + + // Disconnect from the provided voice channel. + _ = vc.Disconnect() + + return nil +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go b/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go new file mode 100644 index 00000000..bd0e3b88 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/appmaker/main.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line options +var ( + Email string + Password string + Token string + AppName string + DeleteID string + ListOnly bool +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&DeleteID, "d", "", "Application ID to delete") + flag.BoolVar(&ListOnly, "l", false, "List Applications Only") + flag.StringVar(&AppName, "a", "", "App/Bot Name") + flag.Parse() +} + +func main() { + + var err error + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(Email, Password, Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // If -l set, only display a list of existing applications + // for the given account. + if ListOnly { + aps, err2 := dg.Applications() + if err2 != nil { + fmt.Println("error fetching applications,", err) + return + } + + for k, v := range aps { + fmt.Printf("%d : --------------------------------------\n", k) + fmt.Printf("ID: %s\n", v.ID) + fmt.Printf("Name: %s\n", v.Name) + fmt.Printf("Secret: %s\n", v.Secret) + fmt.Printf("Description: %s\n", v.Description) + } + return + } + + // if -d set, delete the given Application + if DeleteID != "" { + err = dg.ApplicationDelete(DeleteID) + if err != nil { + fmt.Println("error deleting application,", err) + } + return + } + + // Create a new application. + ap := &discordgo.Application{} + ap.Name = AppName + ap, err = dg.ApplicationCreate(ap) + if err != nil { + fmt.Println("error creating new applicaiton,", err) + return + } + + fmt.Printf("Application created successfully:\n") + fmt.Printf("ID: %s\n", ap.ID) + fmt.Printf("Name: %s\n", ap.Name) + fmt.Printf("Secret: %s\n\n", ap.Secret) + + // Create the bot account under the application we just created + bot, err := dg.ApplicationBotCreate(ap.ID) + if err != nil { + fmt.Println("error creating bot account,", err) + return + } + + fmt.Printf("Bot account created successfully.\n") + fmt.Printf("ID: %s\n", bot.ID) + fmt.Printf("Username: %s\n", bot.Username) + fmt.Printf("Token: %s\n\n", bot.Token) + fmt.Println("Please save the above posted info in a secure place.") + fmt.Println("You will need that information to login with your bot account.") + + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go b/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go new file mode 100644 index 00000000..adfe0b1d --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/avatar/localfile/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "net/http" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string + Token string + Avatar string + BotID string + BotUsername string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&Avatar, "f", "./avatar.jpg", "Avatar File Name") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided login information. + // Use discordgo.New(Token) to just use a token for login. + dg, err := discordgo.New(Email, Password, Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + bot, err := dg.User("@me") + if err != nil { + fmt.Println("error fetching the bot details,", err) + return + } + + BotID = bot.ID + BotUsername = bot.Username + changeAvatar(dg) + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + // Simple way to keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} + +// Helper function to change the avatar +func changeAvatar(s *discordgo.Session) { + img, err := ioutil.ReadFile(Avatar) + if err != nil { + fmt.Println(err) + } + + base64 := base64.StdEncoding.EncodeToString(img) + + avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64) + + _, err = s.UserUpdate("", "", BotUsername, avatar, "") + if err != nil { + fmt.Println(err) + } +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go b/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go new file mode 100644 index 00000000..26170df5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/avatar/url/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "net/http" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string + Token string + URL string + BotID string + BotUsername string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") + flag.StringVar(&URL, "l", "http://bwmarrin.github.io/discordgo/img/discordgo.png", "Link to the avatar image") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided login information. + // Use discordgo.New(Token) to just use a token for login. + dg, err := discordgo.New(Email, Password, Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + bot, err := dg.User("@me") + if err != nil { + fmt.Println("error fetching the bot details,", err) + return + } + + BotID = bot.ID + BotUsername = bot.Username + changeAvatar(dg) + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + // Simple way to keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} + +// Helper function to change the avatar +func changeAvatar(s *discordgo.Session) { + + resp, err := http.Get(URL) + if err != nil { + fmt.Println("Error retrieving the file, ", err) + return + } + + defer func() { + _ = resp.Body.Close() + }() + + img, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading the response, ", err) + return + } + + base64 := base64.StdEncoding.EncodeToString(img) + + avatar := fmt.Sprintf("data:%s;base64,%s", http.DetectContentType(img), base64) + + _, err = s.UserUpdate("", "", BotUsername, avatar, "") + if err != nil { + fmt.Println("Error setting the avatar, ", err) + } + +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go b/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go new file mode 100644 index 00000000..5914fc8a --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/mytoken/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(Email, Password) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go b/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go new file mode 100644 index 00000000..c3861ac0 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/new_basic/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "flag" + "fmt" + "time" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string + Token string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") + 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 + } + + // Register messageCreate as a callback for the messageCreate events. + dg.AddHandler(messageCreate) + + // Open the websocket and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + fmt.Println("Bot is now running. Press CTRL-C to exit.") + // Simple way to keep program running until CTRL-C is pressed. + <-make(chan struct{}) + return +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Print message to stdout. + fmt.Printf("%20s %20s %20s > %s\n", m.ChannelID, time.Now().Format(time.Stamp), m.Author.Username, m.Content) +} diff --git a/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go b/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go new file mode 100644 index 00000000..e6893ca1 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/examples/pingpong/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Email string + Password string + Token string + BotID string +) + +func init() { + + flag.StringVar(&Email, "e", "", "Account Email") + flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(Email, Password, Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Get the account information. + u, err := dg.User("@me") + if err != nil { + fmt.Println("error obtaining account details,", err) + } + + // Store the account ID for later use. + BotID = u.ID + + // Register messageCreate as a callback for the messageCreate events. + 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) { + + // Ignore all messages created by the bot itself + if m.Author.ID == BotID { + return + } + + // If the message is "ping" reply with "Pong!" + if m.Content == "ping" { + _, _ = s.ChannelMessageSend(m.ChannelID, "Pong!") + } + + // If the message is "pong" reply with "Ping!" + if m.Content == "pong" { + _, _ = s.ChannelMessageSend(m.ChannelID, "Ping!") + } +} diff --git a/vendor/github.com/bwmarrin/discordgo/logging.go b/vendor/github.com/bwmarrin/discordgo/logging.go new file mode 100644 index 00000000..70d78d60 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/logging.go @@ -0,0 +1,95 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to discordgo package logging + +package discordgo + +import ( + "fmt" + "log" + "runtime" + "strings" +) + +const ( + + // LogError level is used for critical errors that could lead to data loss + // or panic that would not be returned to a calling function. + LogError int = iota + + // LogWarning level is used for very abnormal events and errors that are + // also returend to a calling function. + LogWarning + + // LogInformational level is used for normal non-error activity + LogInformational + + // LogDebug level is for very detailed non-error activity. This is + // very spammy and will impact performance. + LogDebug +) + +// msglog provides package wide logging consistancy for discordgo +// the format, a... portion this command follows that of fmt.Printf +// msgL : LogLevel of the message +// caller : 1 + the number of callers away from the message source +// format : Printf style message format +// a ... : comma seperated list of values to pass +func msglog(msgL, caller int, format string, a ...interface{}) { + + pc, file, line, _ := runtime.Caller(caller) + + files := strings.Split(file, "/") + file = files[len(files)-1] + + name := runtime.FuncForPC(pc).Name() + fns := strings.Split(name, ".") + name = fns[len(fns)-1] + + msg := fmt.Sprintf(format, a...) + + log.Printf("[DG%d] %s:%d:%s() %s\n", msgL, file, line, name, msg) +} + +// helper function that wraps msglog for the Session struct +// This adds a check to insure the message is only logged +// if the session log level is equal or higher than the +// message log level +func (s *Session) log(msgL int, format string, a ...interface{}) { + + if msgL > s.LogLevel { + return + } + + msglog(msgL, 2, format, a...) +} + +// helper function that wraps msglog for the VoiceConnection struct +// This adds a check to insure the message is only logged +// if the voice connection log level is equal or higher than the +// message log level +func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { + + if msgL > v.LogLevel { + return + } + + msglog(msgL, 2, format, a...) +} + +// printJSON is a helper function to display JSON data in a easy to read format. +/* NOT USED ATM +func printJSON(body []byte) { + var prettyJSON bytes.Buffer + error := json.Indent(&prettyJSON, body, "", "\t") + if error != nil { + log.Print("JSON parse error: ", error) + } + log.Println(string(prettyJSON.Bytes())) +} +*/ diff --git a/vendor/github.com/bwmarrin/discordgo/message.go b/vendor/github.com/bwmarrin/discordgo/message.go new file mode 100644 index 00000000..8966c161 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/message.go @@ -0,0 +1,82 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to the Message struct + +package discordgo + +import ( + "fmt" + "regexp" +) + +// A Message stores all data related to a specific Discord message. +type Message struct { + ID string `json:"id"` + ChannelID string `json:"channel_id"` + Content string `json:"content"` + Timestamp string `json:"timestamp"` + EditedTimestamp string `json:"edited_timestamp"` + MentionRoles []string `json:"mention_roles"` + Tts bool `json:"tts"` + MentionEveryone bool `json:"mention_everyone"` + Author *User `json:"author"` + Attachments []*MessageAttachment `json:"attachments"` + Embeds []*MessageEmbed `json:"embeds"` + Mentions []*User `json:"mentions"` +} + +// A MessageAttachment stores data for message attachments. +type MessageAttachment struct { + ID string `json:"id"` + URL string `json:"url"` + ProxyURL string `json:"proxy_url"` + Filename string `json:"filename"` + Width int `json:"width"` + Height int `json:"height"` + Size int `json:"size"` +} + +// An MessageEmbed stores data for message embeds. +type MessageEmbed struct { + URL string `json:"url"` + Type string `json:"type"` + Title string `json:"title"` + Description string `json:"description"` + Thumbnail *struct { + URL string `json:"url"` + ProxyURL string `json:"proxy_url"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"thumbnail"` + Provider *struct { + URL string `json:"url"` + Name string `json:"name"` + } `json:"provider"` + Author *struct { + URL string `json:"url"` + Name string `json:"name"` + } `json:"author"` + Video *struct { + URL string `json:"url"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"video"` +} + +// ContentWithMentionsReplaced will replace all @<id> mentions with the +// username of the mention. +func (m *Message) ContentWithMentionsReplaced() string { + if m.Mentions == nil { + return m.Content + } + content := m.Content + for _, user := range m.Mentions { + content = regexp.MustCompile(fmt.Sprintf("<@!?(%s)>", user.ID)).ReplaceAllString(content, "@"+user.Username) + } + return content +} diff --git a/vendor/github.com/bwmarrin/discordgo/oauth2.go b/vendor/github.com/bwmarrin/discordgo/oauth2.go new file mode 100644 index 00000000..de2848db --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/oauth2.go @@ -0,0 +1,120 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains functions related to Discord OAuth2 endpoints + +package discordgo + +// ------------------------------------------------------------------------------------------------ +// Code specific to Discord OAuth2 Applications +// ------------------------------------------------------------------------------------------------ + +// An Application struct stores values for a Discord OAuth2 Application +type Application struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Icon string `json:"icon,omitempty"` + Secret string `json:"secret,omitempty"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` +} + +// Application returns an Application structure of a specific Application +// appID : The ID of an Application +func (s *Session) Application(appID string) (st *Application, err error) { + + body, err := s.Request("GET", EndpointApplication(appID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// Applications returns all applications for the authenticated user +func (s *Session) Applications() (st []*Application, err error) { + + body, err := s.Request("GET", EndpointApplications, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationCreate creates a new Application +// name : Name of Application / Bot +// uris : Redirect URIs (Not required) +func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error) { + + data := struct { + Name string `json:"name"` + Description string `json:"description"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + }{ap.Name, ap.Description, ap.RedirectURIs} + + body, err := s.Request("POST", EndpointApplications, data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationUpdate updates an existing Application +// var : desc +func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) { + + data := struct { + Name string `json:"name"` + Description string `json:"description"` + RedirectURIs *[]string `json:"redirect_uris,omitempty"` + }{ap.Name, ap.Description, ap.RedirectURIs} + + body, err := s.Request("PUT", EndpointApplication(appID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ApplicationDelete deletes an existing Application +// appID : The ID of an Application +func (s *Session) ApplicationDelete(appID string) (err error) { + + _, err = s.Request("DELETE", EndpointApplication(appID), nil) + if err != nil { + return + } + + return +} + +// ------------------------------------------------------------------------------------------------ +// Code specific to Discord OAuth2 Application Bots +// ------------------------------------------------------------------------------------------------ + +// ApplicationBotCreate creates an Application Bot Account +// +// appID : The ID of an Application +// +// NOTE: func name may change, if I can think up something better. +func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) { + + body, err := s.Request("POST", EndpointApplicationsBot(appID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/restapi.go b/vendor/github.com/bwmarrin/discordgo/restapi.go new file mode 100644 index 00000000..337a7f82 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/restapi.go @@ -0,0 +1,1403 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains functions for interacting with the Discord REST/JSON API +// at the lowest level. + +package discordgo + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "image" + _ "image/jpeg" // For JPEG decoding + _ "image/png" // For PNG decoding + "io" + "io/ioutil" + "log" + "mime/multipart" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" +) + +// ErrJSONUnmarshal is returned for JSON Unmarshall errors. +var ErrJSONUnmarshal = errors.New("json unmarshal") + +// Request makes a (GET/POST/...) Requests to Discord REST API with JSON data. +// All the other Discord REST Calls in this file use this function. +func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) { + + var body []byte + if data != nil { + body, err = json.Marshal(data) + if err != nil { + return + } + } + + return s.request(method, urlStr, "application/json", body) +} + +// request makes a (GET/POST/...) Requests to Discord REST API. +func (s *Session) request(method, urlStr, contentType string, b []byte) (response []byte, err error) { + + // rate limit mutex for this url + // TODO: review for performance improvements + // ideally we just ignore endpoints that we've never + // received a 429 on. But this simple method works and + // is a lot less complex :) It also might even be more + // performat due to less checks and maps. + var mu *sync.Mutex + + s.rateLimit.Lock() + if s.rateLimit.url == nil { + s.rateLimit.url = make(map[string]*sync.Mutex) + } + + bu := strings.Split(urlStr, "?") + mu, _ = s.rateLimit.url[bu[0]] + if mu == nil { + mu = new(sync.Mutex) + s.rateLimit.url[urlStr] = mu + } + s.rateLimit.Unlock() + + mu.Lock() // lock this URL for ratelimiting + if s.Debug { + log.Printf("API REQUEST %8s :: %s\n", method, urlStr) + log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) + } + + req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(b)) + if err != nil { + return + } + + // Not used on initial login.. + // TODO: Verify if a login, otherwise complain about no-token + if s.Token != "" { + req.Header.Set("authorization", s.Token) + } + + req.Header.Set("Content-Type", contentType) + // TODO: Make a configurable static variable. + req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION)) + + if s.Debug { + for k, v := range req.Header { + log.Printf("API REQUEST HEADER :: [%s] = %+v\n", k, v) + } + } + + client := &http.Client{Timeout: (20 * time.Second)} + + resp, err := client.Do(req) + mu.Unlock() // unlock ratelimit mutex + if err != nil { + return + } + defer func() { + err2 := resp.Body.Close() + if err2 != nil { + log.Println("error closing resp body") + } + }() + + response, err = ioutil.ReadAll(resp.Body) + if err != nil { + return + } + + if s.Debug { + + log.Printf("API RESPONSE STATUS :: %s\n", resp.Status) + for k, v := range resp.Header { + log.Printf("API RESPONSE HEADER :: [%s] = %+v\n", k, v) + } + log.Printf("API RESPONSE BODY :: [%s]\n\n\n", response) + } + + switch resp.StatusCode { + + case http.StatusOK: + case http.StatusCreated: + case http.StatusNoContent: + + // TODO check for 401 response, invalidate token if we get one. + + case 429: // TOO MANY REQUESTS - Rate limiting + + mu.Lock() // lock URL ratelimit mutex + + rl := TooManyRequests{} + err = json.Unmarshal(response, &rl) + if err != nil { + s.log(LogError, "rate limit unmarshal error, %s", err) + mu.Unlock() + return + } + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + s.handle(RateLimit{TooManyRequests: &rl, URL: urlStr}) + + time.Sleep(rl.RetryAfter * time.Millisecond) + // we can make the above smarter + // this method can cause longer delays then required + + mu.Unlock() // we have to unlock here + response, err = s.request(method, urlStr, contentType, b) + + default: // Error condition + err = fmt.Errorf("HTTP %s, %s", resp.Status, response) + } + + return +} + +func unmarshal(data []byte, v interface{}) error { + err := json.Unmarshal(data, v) + if err != nil { + return ErrJSONUnmarshal + } + + return nil +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Sessions +// ------------------------------------------------------------------------------------------------ + +// Login asks the Discord server for an authentication token. +func (s *Session) Login(email, password string) (err error) { + + data := struct { + Email string `json:"email"` + Password string `json:"password"` + }{email, password} + + response, err := s.Request("POST", EndpointLogin, data) + if err != nil { + return + } + + temp := struct { + Token string `json:"token"` + }{} + + err = unmarshal(response, &temp) + if err != nil { + return + } + + s.Token = temp.Token + return +} + +// Register sends a Register request to Discord, and returns the authentication token +// Note that this account is temporary and should be verified for future use. +// Another option is to save the authentication token external, but this isn't recommended. +func (s *Session) Register(username string) (token string, err error) { + + data := struct { + Username string `json:"username"` + }{username} + + response, err := s.Request("POST", EndpointRegister, data) + if err != nil { + return + } + + temp := struct { + Token string `json:"token"` + }{} + + err = unmarshal(response, &temp) + if err != nil { + return + } + + token = temp.Token + return +} + +// Logout sends a logout request to Discord. +// This does not seem to actually invalidate the token. So you can still +// make API calls even after a Logout. So, it seems almost pointless to +// even use. +func (s *Session) Logout() (err error) { + + // _, err = s.Request("POST", LOGOUT, fmt.Sprintf(`{"token": "%s"}`, s.Token)) + + if s.Token == "" { + return + } + + data := struct { + Token string `json:"token"` + }{s.Token} + + _, err = s.Request("POST", EndpointLogout, data) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Users +// ------------------------------------------------------------------------------------------------ + +// User returns the user details of the given userID +// userID : A user ID or "@me" which is a shortcut of current user ID +func (s *Session) User(userID string) (st *User, err error) { + + body, err := s.Request("GET", EndpointUser(userID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserAvatar returns an image.Image of a users Avatar. +// userID : A user ID or "@me" which is a shortcut of current user ID +func (s *Session) UserAvatar(userID string) (img image.Image, err error) { + u, err := s.User(userID) + if err != nil { + return + } + + body, err := s.Request("GET", EndpointUserAvatar(userID, u.Avatar), nil) + if err != nil { + return + } + + img, _, err = image.Decode(bytes.NewReader(body)) + return +} + +// UserUpdate updates a users settings. +func (s *Session) UserUpdate(email, password, username, avatar, newPassword string) (st *User, err error) { + + // NOTE: Avatar must be either the hash/id of existing Avatar or + // data:image/png;base64,BASE64_STRING_OF_NEW_AVATAR_PNG + // to set a new avatar. + // If left blank, avatar will be set to null/blank + + data := struct { + Email string `json:"email"` + Password string `json:"password"` + Username string `json:"username"` + Avatar string `json:"avatar,omitempty"` + NewPassword string `json:"new_password,omitempty"` + }{email, password, username, avatar, newPassword} + + body, err := s.Request("PATCH", EndpointUser("@me"), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserSettings returns the settings for a given user +func (s *Session) UserSettings() (st *Settings, err error) { + + body, err := s.Request("GET", EndpointUserSettings("@me"), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserChannels returns an array of Channel structures for all private +// channels. +func (s *Session) UserChannels() (st []*Channel, err error) { + + body, err := s.Request("GET", EndpointUserChannels("@me"), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserChannelCreate creates a new User (Private) Channel with another User +// recipientID : A user ID for the user to which this channel is opened with. +func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) { + + data := struct { + RecipientID string `json:"recipient_id"` + }{recipientID} + + body, err := s.Request("POST", EndpointUserChannels("@me"), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserGuilds returns an array of Guild structures for all guilds. +func (s *Session) UserGuilds() (st []*Guild, err error) { + + body, err := s.Request("GET", EndpointUserGuilds("@me"), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserGuildSettingsEdit Edits the users notification settings for a guild +// guildID : The ID of the guild to edit the settings on +// settings : The settings to update +func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) { + + body, err := s.Request("PATCH", EndpointUserGuildSettings("@me", guildID), settings) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// NOTE: This function is now deprecated and will be removed in the future. +// Please see the same function inside state.go +// UserChannelPermissions returns the permission of a user in a channel. +// userID : The ID of the user to calculate permissions for. +// channelID : The ID of the channel to calculate permission for. +func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + channel, err := s.State.Channel(channelID) + if err != nil || channel == nil { + channel, err = s.Channel(channelID) + if err != nil { + return + } + } + + guild, err := s.State.Guild(channel.GuildID) + if err != nil || guild == nil { + guild, err = s.Guild(channel.GuildID) + if err != nil { + return + } + } + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + member, err := s.State.Member(guild.ID, userID) + if err != nil || member == nil { + member, err = s.GuildMember(guild.ID, userID) + if err != nil { + return + } + } + + for _, role := range guild.Roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + apermissions |= role.Permissions + break + } + } + } + + if apermissions&PermissionManageRoles > 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&PermissionManageRoles > 0 { + apermissions |= PermissionAllChannel + } + + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Guilds +// ------------------------------------------------------------------------------------------------ + +// Guild returns a Guild structure of a specific Guild. +// guildID : The ID of a Guild +func (s *Session) Guild(guildID string) (st *Guild, err error) { + if s.StateEnabled { + // Attempt to grab the guild from State first. + st, err = s.State.Guild(guildID) + if err == nil { + return + } + } + + body, err := s.Request("GET", EndpointGuild(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildCreate creates a new Guild +// name : A name for the Guild (2-100 characters) +func (s *Session) GuildCreate(name string) (st *Guild, err error) { + + data := struct { + Name string `json:"name"` + }{name} + + body, err := s.Request("POST", EndpointGuilds, data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildEdit edits a new Guild +// guildID : The ID of a Guild +// g : A GuildParams struct with the values Name, Region and VerificationLevel defined. +func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error) { + + // Bounds checking for VerificationLevel, interval: [0, 3] + if g.VerificationLevel != nil { + val := *g.VerificationLevel + if val < 0 || val > 3 { + err = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + return + } + } + + //Bounds checking for regions + if g.Region != "" { + isValid := false + regions, _ := s.VoiceRegions() + for _, r := range regions { + if g.Region == r.ID { + isValid = true + } + } + if !isValid { + var valid []string + for _, r := range regions { + valid = append(valid, r.ID) + } + err = fmt.Errorf("Region not a valid region (%q)", valid) + return + } + } + + data := struct { + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` + }{g.Name, g.Region, g.VerificationLevel} + + body, err := s.Request("PATCH", EndpointGuild(guildID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildDelete deletes a Guild. +// guildID : The ID of a Guild +func (s *Session) GuildDelete(guildID string) (st *Guild, err error) { + + body, err := s.Request("DELETE", EndpointGuild(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildLeave leaves a Guild. +// guildID : The ID of a Guild +func (s *Session) GuildLeave(guildID string) (err error) { + + _, err = s.Request("DELETE", EndpointUserGuild("@me", guildID), nil) + return +} + +// GuildBans returns an array of User structures for all bans of a +// given guild. +// guildID : The ID of a Guild. +func (s *Session) GuildBans(guildID string) (st []*User, err error) { + + body, err := s.Request("GET", EndpointGuildBans(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildBanCreate bans the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +// days : The number of days of previous comments to delete. +func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { + + uri := EndpointGuildBan(guildID, userID) + + if days > 0 { + uri = fmt.Sprintf("%s?delete-message-days=%d", uri, days) + } + + _, err = s.Request("PUT", uri, nil) + return +} + +// GuildBanDelete removes the given user from the guild bans +// guildID : The ID of a Guild. +// userID : The ID of a User +func (s *Session) GuildBanDelete(guildID, userID string) (err error) { + + _, err = s.Request("DELETE", EndpointGuildBan(guildID, userID), nil) + return +} + +// GuildMembers returns a list of members for a guild. +// guildID : The ID of a Guild. +// offset : A number of members to skip +// limit : max number of members to return (max 1000) +func (s *Session) GuildMembers(guildID string, offset, limit int) (st []*Member, err error) { + + uri := EndpointGuildMembers(guildID) + + v := url.Values{} + + if offset > 0 { + v.Set("offset", strconv.Itoa(offset)) + } + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.Request("GET", uri, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildMember returns a member of a guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { + + body, err := s.Request("GET", EndpointGuildMember(guildID, userID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildMemberDelete removes the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { + + _, err = s.Request("DELETE", EndpointGuildMember(guildID, userID), nil) + return +} + +// GuildMemberEdit edits the roles of a member. +// guildID : The ID of a Guild. +// userID : The ID of a User. +// roles : A list of role ID's to set on the member. +func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err error) { + + data := struct { + Roles []string `json:"roles"` + }{roles} + + _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + if err != nil { + return + } + + return +} + +// GuildMemberMove moves a guild member from one voice channel to another/none +// guildID : The ID of a Guild. +// userID : The ID of a User. +// channelID : The ID of a channel to move user to, or null? +// NOTE : I am not entirely set on the name of this function and it may change +// prior to the final 1.0.0 release of Discordgo +func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) { + + data := struct { + ChannelID string `json:"channel_id"` + }{channelID} + + _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + if err != nil { + return + } + + return +} + +// GuildMemberNickname updates the nickname of a guild member +// guildID : The ID of a guild +// userID : The ID of a user +func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) { + + data := struct { + Nick string `json:"nick"` + }{nickname} + + _, err = s.Request("PATCH", EndpointGuildMember(guildID, userID), data) + return +} + +// GuildChannels returns an array of Channel structures for all channels of a +// given guild. +// guildID : The ID of a Guild. +func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { + + body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildChannelCreate creates a new channel in the given guild +// guildID : The ID of a Guild. +// name : Name of the channel (2-100 chars length) +// ctype : Tpye of the channel (voice or text) +func (s *Session) GuildChannelCreate(guildID, name, ctype string) (st *Channel, err error) { + + data := struct { + Name string `json:"name"` + Type string `json:"type"` + }{name, ctype} + + body, err := s.Request("POST", EndpointGuildChannels(guildID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildChannelsReorder updates the order of channels in a guild +// guildID : The ID of a Guild. +// channels : Updated channels. +func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { + + _, err = s.Request("PATCH", EndpointGuildChannels(guildID), channels) + return +} + +// GuildInvites returns an array of Invite structures for the given guild +// guildID : The ID of a Guild. +func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { + body, err := s.Request("GET", EndpointGuildInvites(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildRoles returns all roles for a given guild. +// guildID : The ID of a Guild. +func (s *Session) GuildRoles(guildID string) (st []*Role, err error) { + + body, err := s.Request("GET", EndpointGuildRoles(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return // TODO return pointer +} + +// GuildRoleCreate returns a new Guild Role. +// guildID: The ID of a Guild. +func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) { + + body, err := s.Request("POST", EndpointGuildRoles(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildRoleEdit updates an existing Guild Role with new values +// guildID : The ID of a Guild. +// roleID : The ID of a Role. +// name : The name of the Role. +// color : The color of the role (decimal, not hex). +// hoist : Whether to display the role's users separately. +// perm : The permissions for the role. +func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int) (st *Role, err error) { + + // Prevent sending a color int that is too big. + if color > 0xFFFFFF { + err = fmt.Errorf("color value cannot be larger than 0xFFFFFF") + } + + data := struct { + Name string `json:"name"` // The color the role should have (as a decimal, not hex) + Color int `json:"color"` // Whether to display the role's users separately + Hoist bool `json:"hoist"` // The role's name (overwrites existing) + Permissions int `json:"permissions"` // The overall permissions number of the role (overwrites existing) + }{name, color, hoist, perm} + + body, err := s.Request("PATCH", EndpointGuildRole(guildID, roleID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildRoleReorder reoders guild roles +// guildID : The ID of a Guild. +// roles : A list of ordered roles. +func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, err error) { + + body, err := s.Request("PATCH", EndpointGuildRoles(guildID), roles) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildRoleDelete deletes an existing role. +// guildID : The ID of a Guild. +// roleID : The ID of a Role. +func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { + + _, err = s.Request("DELETE", EndpointGuildRole(guildID, roleID), nil) + + return +} + +// GuildIntegrations returns an array of Integrations for a guild. +// guildID : The ID of a Guild. +func (s *Session) GuildIntegrations(guildID string) (st []*GuildIntegration, err error) { + + body, err := s.Request("GET", EndpointGuildIntegrations(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildIntegrationCreate creates a Guild Integration. +// guildID : The ID of a Guild. +// integrationType : The Integration type. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationCreate(guildID, integrationType, integrationID string) (err error) { + + data := struct { + Type string `json:"type"` + ID string `json:"id"` + }{integrationType, integrationID} + + _, err = s.Request("POST", EndpointGuildIntegrations(guildID), data) + return +} + +// GuildIntegrationEdit edits a Guild Integration. +// guildID : The ID of a Guild. +// integrationType : The Integration type. +// integrationID : The ID of an integration. +// expireBehavior : The behavior when an integration subscription lapses (see the integration object documentation). +// expireGracePeriod : Period (in seconds) where the integration will ignore lapsed subscriptions. +// enableEmoticons : Whether emoticons should be synced for this integration (twitch only currently). +func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBehavior, expireGracePeriod int, enableEmoticons bool) (err error) { + + data := struct { + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + EnableEmoticons bool `json:"enable_emoticons"` + }{expireBehavior, expireGracePeriod, enableEmoticons} + + _, err = s.Request("PATCH", EndpointGuildIntegration(guildID, integrationID), data) + return +} + +// GuildIntegrationDelete removes the given integration from the Guild. +// guildID : The ID of a Guild. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err error) { + + _, err = s.Request("DELETE", EndpointGuildIntegration(guildID, integrationID), nil) + return +} + +// GuildIntegrationSync syncs an integration. +// guildID : The ID of a Guild. +// integrationID : The ID of an integration. +func (s *Session) GuildIntegrationSync(guildID, integrationID string) (err error) { + + _, err = s.Request("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil) + return +} + +// GuildIcon returns an image.Image of a guild icon. +// guildID : The ID of a Guild. +func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { + g, err := s.Guild(guildID) + if err != nil { + return + } + + if g.Icon == "" { + err = errors.New("Guild does not have an icon set.") + return + } + + body, err := s.Request("GET", EndpointGuildIcon(guildID, g.Icon), nil) + if err != nil { + return + } + + img, _, err = image.Decode(bytes.NewReader(body)) + return +} + +// GuildSplash returns an image.Image of a guild splash image. +// guildID : The ID of a Guild. +func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { + g, err := s.Guild(guildID) + if err != nil { + return + } + + if g.Splash == "" { + err = errors.New("Guild does not have a splash set.") + return + } + + body, err := s.Request("GET", EndpointGuildSplash(guildID, g.Splash), nil) + if err != nil { + return + } + + img, _, err = image.Decode(bytes.NewReader(body)) + return +} + +// GuildEmbed returns the embed for a Guild. +// guildID : The ID of a Guild. +func (s *Session) GuildEmbed(guildID string) (st *GuildEmbed, err error) { + + body, err := s.Request("GET", EndpointGuildEmbed(guildID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildEmbedEdit returns the embed for a Guild. +// guildID : The ID of a Guild. +func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) (err error) { + + data := GuildEmbed{enabled, channelID} + + _, err = s.Request("PATCH", EndpointGuildEmbed(guildID), data) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Channels +// ------------------------------------------------------------------------------------------------ + +// Channel returns a Channel strucutre of a specific Channel. +// channelID : The ID of the Channel you want returned. +func (s *Session) Channel(channelID string) (st *Channel, err error) { + body, err := s.Request("GET", EndpointChannel(channelID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelEdit edits the given channel +// channelID : The ID of a Channel +// name : The new name to assign the channel. +func (s *Session) ChannelEdit(channelID, name string) (st *Channel, err error) { + + data := struct { + Name string `json:"name"` + }{name} + + body, err := s.Request("PATCH", EndpointChannel(channelID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelDelete deletes the given channel +// channelID : The ID of a Channel +func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) { + + body, err := s.Request("DELETE", EndpointChannel(channelID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelTyping broadcasts to all members that authenticated user is typing in +// the given channel. +// channelID : The ID of a Channel +func (s *Session) ChannelTyping(channelID string) (err error) { + + _, err = s.Request("POST", EndpointChannelTyping(channelID), nil) + return +} + +// ChannelMessages returns an array of Message structures for messages within +// a given channel. +// channelID : The ID of a Channel. +// limit : The number messages that can be returned. (max 100) +// beforeID : If provided all messages returned will be before given ID. +// afterID : If provided all messages returned will be after given ID. +func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID string) (st []*Message, err error) { + + uri := EndpointChannelMessages(channelID) + + v := url.Values{} + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if afterID != "" { + v.Set("after", afterID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.Request("GET", uri, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelMessage gets a single message by ID from a given channel. +// channeld : The ID of a Channel +// messageID : the ID of a Message +func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err error) { + + response, err := s.Request("GET", EndpointChannelMessage(channelID, messageID), nil) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// ChannelMessageAck acknowledges and marks the given message as read +// channeld : The ID of a Channel +// messageID : the ID of a Message +func (s *Session) ChannelMessageAck(channelID, messageID string) (err error) { + + _, err = s.request("POST", EndpointChannelMessageAck(channelID, messageID), "", nil) + return +} + +// channelMessageSend sends a message to the given channel. +// channelID : The ID of a Channel. +// content : The message to send. +// tts : Whether to send the message with TTS. +func (s *Session) channelMessageSend(channelID, content string, tts bool) (st *Message, err error) { + + // TODO: nonce string ? + data := struct { + Content string `json:"content"` + TTS bool `json:"tts"` + }{content, tts} + + // Send the message to the given channel + response, err := s.Request("POST", EndpointChannelMessages(channelID), data) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// ChannelMessageSend sends a message to the given channel. +// channelID : The ID of a Channel. +// content : The message to send. +func (s *Session) ChannelMessageSend(channelID string, content string) (st *Message, err error) { + + return s.channelMessageSend(channelID, content, false) +} + +// ChannelMessageSendTTS sends a message to the given channel with Text to Speech. +// channelID : The ID of a Channel. +// content : The message to send. +func (s *Session) ChannelMessageSendTTS(channelID string, content string) (st *Message, err error) { + + return s.channelMessageSend(channelID, content, true) +} + +// ChannelMessageEdit edits an existing message, replacing it entirely with +// the given content. +// channeld : The ID of a Channel +// messageID : the ID of a Message +func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (st *Message, err error) { + + data := struct { + Content string `json:"content"` + }{content} + + response, err := s.Request("PATCH", EndpointChannelMessage(channelID, messageID), data) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// ChannelMessageDelete deletes a message from the Channel. +func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { + + _, err = s.Request("DELETE", EndpointChannelMessage(channelID, messageID), nil) + return +} + +// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. +// If only one messageID is in the slice call channelMessageDelete funciton. +// If the slice is empty do nothing. +// channelID : The ID of the channel for the messages to delete. +// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. +func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) (err error) { + + if len(messages) == 0 { + return + } + + if len(messages) == 1 { + err = s.ChannelMessageDelete(channelID, messages[0]) + return + } + + if len(messages) > 100 { + messages = messages[:100] + } + + data := struct { + Messages []string `json:"messages"` + }{messages} + + _, err = s.Request("POST", EndpointChannelMessagesBulkDelete(channelID), data) + return +} + +// ChannelMessagePin pins a message within a given channel. +// channelID: The ID of a channel. +// messageID: The ID of a message. +func (s *Session) ChannelMessagePin(channelID, messageID string) (err error) { + + _, err = s.Request("PUT", EndpointChannelMessagePin(channelID, messageID), nil) + return +} + +// ChannelMessageUnpin unpins a message within a given channel. +// channelID: The ID of a channel. +// messageID: The ID of a message. +func (s *Session) ChannelMessageUnpin(channelID, messageID string) (err error) { + + _, err = s.Request("DELETE", EndpointChannelMessagePin(channelID, messageID), nil) + return +} + +// ChannelMessagesPinned returns an array of Message structures for pinned messages +// within a given channel +// channelID : The ID of a Channel. +func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err error) { + + body, err := s.Request("GET", EndpointChannelMessagesPins(channelID), nil) + + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelFileSend sends a file to the given channel. +// channelID : The ID of a Channel. +// io.Reader : A reader for the file contents. +func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (st *Message, err error) { + + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + 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()) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// ChannelInvites returns an array of Invite structures for the given channel +// channelID : The ID of a Channel +func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { + + body, err := s.Request("GET", EndpointChannelInvites(channelID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelInviteCreate creates a new invite for the given channel. +// channelID : The ID of a Channel +// i : An Invite struct with the values MaxAge, MaxUses, Temporary, +// and XkcdPass defined. +func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { + + data := struct { + MaxAge int `json:"max_age"` + MaxUses int `json:"max_uses"` + Temporary bool `json:"temporary"` + XKCDPass string `json:"xkcdpass"` + }{i.MaxAge, i.MaxUses, i.Temporary, i.XkcdPass} + + body, err := s.Request("POST", EndpointChannelInvites(channelID), data) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelPermissionSet creates a Permission Override for the given channel. +// NOTE: This func name may changed. Using Set instead of Create because +// you can both create a new override or update an override with this function. +func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, allow, deny int) (err error) { + + data := struct { + ID string `json:"id"` + Type string `json:"type"` + Allow int `json:"allow"` + Deny int `json:"deny"` + }{targetID, targetType, allow, deny} + + _, err = s.Request("PUT", EndpointChannelPermission(channelID, targetID), data) + return +} + +// ChannelPermissionDelete deletes a specific permission override for the given channel. +// NOTE: Name of this func may change. +func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error) { + + _, err = s.Request("DELETE", EndpointChannelPermission(channelID, targetID), nil) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Invites +// ------------------------------------------------------------------------------------------------ + +// Invite returns an Invite structure of the given invite +// inviteID : The invite code (or maybe xkcdpass?) +func (s *Session) Invite(inviteID string) (st *Invite, err error) { + + body, err := s.Request("GET", EndpointInvite(inviteID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// InviteDelete deletes an existing invite +// inviteID : the code (or maybe xkcdpass?) of an invite +func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { + + body, err := s.Request("DELETE", EndpointInvite(inviteID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// InviteAccept accepts an Invite to a Guild or Channel +// inviteID : The invite code (or maybe xkcdpass?) +func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { + + body, err := s.Request("POST", EndpointInvite(inviteID), nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Voice +// ------------------------------------------------------------------------------------------------ + +// VoiceRegions returns the voice server regions +func (s *Session) VoiceRegions() (st []*VoiceRegion, err error) { + + body, err := s.Request("GET", EndpointVoiceRegions, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// VoiceICE returns the voice server ICE information +func (s *Session) VoiceICE() (st *VoiceICE, err error) { + + body, err := s.Request("GET", EndpointVoiceIce, nil) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Websockets +// ------------------------------------------------------------------------------------------------ + +// Gateway returns the a websocket Gateway address +func (s *Session) Gateway() (gateway string, err error) { + + response, err := s.Request("GET", EndpointGateway, nil) + if err != nil { + return + } + + temp := struct { + URL string `json:"url"` + }{} + + err = unmarshal(response, &temp) + if err != nil { + return + } + + gateway = temp.URL + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/state.go b/vendor/github.com/bwmarrin/discordgo/state.go new file mode 100644 index 00000000..e9eb4d67 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/state.go @@ -0,0 +1,746 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to state tracking. If enabled, state +// tracking will capture the initial READY packet and many other websocket +// events and maintain an in-memory state of of guilds, channels, users, and +// so forth. This information can be accessed through the Session.State struct. + +package discordgo + +import ( + "errors" + "sync" +) + +// ErrNilState is returned when the state is nil. +var ErrNilState = errors.New("State not instantiated, please use discordgo.New() or assign Session.State.") + +// A State contains the current known state. +// As discord sends this in a READY blob, it seems reasonable to simply +// use that struct as the data store. +type State struct { + sync.RWMutex + Ready + + MaxMessageCount int + TrackChannels bool + TrackEmojis bool + TrackMembers bool + TrackRoles bool + TrackVoice bool + + guildMap map[string]*Guild + channelMap map[string]*Channel +} + +// NewState creates an empty state. +func NewState() *State { + return &State{ + Ready: Ready{ + PrivateChannels: []*Channel{}, + Guilds: []*Guild{}, + }, + TrackChannels: true, + TrackEmojis: true, + TrackMembers: true, + TrackRoles: true, + TrackVoice: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + } +} + +// OnReady takes a Ready event and updates all internal state. +func (s *State) OnReady(r *Ready) error { + if s == nil { + return ErrNilState + } + + s.Lock() + defer s.Unlock() + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + + for _, c := range g.Channels { + c.GuildID = g.ID + s.channelMap[c.ID] = c + } + } + + for _, c := range s.PrivateChannels { + s.channelMap[c.ID] = c + } + + return nil +} + +// GuildAdd adds a guild to the current world state, or +// updates it if it already exists. +func (s *State) GuildAdd(guild *Guild) error { + if s == nil { + return ErrNilState + } + + s.Lock() + defer s.Unlock() + + // Update the channels to point to the right guild, adding them to the channelMap as we go + for _, c := range guild.Channels { + c.GuildID = guild.ID + s.channelMap[c.ID] = c + } + + // If the guild exists, replace it. + if g, ok := s.guildMap[guild.ID]; ok { + // If this guild already exists with data, don't stomp on props. + if g.Unavailable != nil && !*g.Unavailable { + guild.Members = g.Members + guild.Presences = g.Presences + guild.Channels = g.Channels + guild.VoiceStates = g.VoiceStates + } + + *g = *guild + return nil + } + + s.Guilds = append(s.Guilds, guild) + s.guildMap[guild.ID] = guild + + return nil +} + +// GuildRemove removes a guild from current world state. +func (s *State) GuildRemove(guild *Guild) error { + if s == nil { + return ErrNilState + } + + _, err := s.Guild(guild.ID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + delete(s.guildMap, guild.ID) + + for i, g := range s.Guilds { + if g.ID == guild.ID { + s.Guilds = append(s.Guilds[:i], s.Guilds[i+1:]...) + return nil + } + } + + return nil +} + +// Guild gets a guild by ID. +// Useful for querying if @me is in a guild: +// _, err := discordgo.Session.State.Guild(guildID) +// isInGuild := err == nil +func (s *State) Guild(guildID string) (*Guild, error) { + if s == nil { + return nil, ErrNilState + } + + s.RLock() + defer s.RUnlock() + + if g, ok := s.guildMap[guildID]; ok { + return g, nil + } + + return nil, errors.New("Guild not found.") +} + +// TODO: Consider moving Guild state update methods onto *Guild. + +// MemberAdd adds a member to the current world state, or +// updates it if it already exists. +func (s *State) MemberAdd(member *Member) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(member.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, m := range guild.Members { + if m.User.ID == member.User.ID { + guild.Members[i] = member + return nil + } + } + + guild.Members = append(guild.Members, member) + return nil +} + +// MemberRemove removes a member from current world state. +func (s *State) MemberRemove(member *Member) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(member.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, m := range guild.Members { + if m.User.ID == member.User.ID { + guild.Members = append(guild.Members[:i], guild.Members[i+1:]...) + return nil + } + } + + return errors.New("Member not found.") +} + +// Member gets a member by ID from a guild. +func (s *State) Member(guildID, userID string) (*Member, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + s.RLock() + defer s.RUnlock() + + for _, m := range guild.Members { + if m.User.ID == userID { + return m, nil + } + } + + return nil, errors.New("Member not found.") +} + +// RoleAdd adds a role to the current world state, or +// updates it if it already exists. +func (s *State) RoleAdd(guildID string, role *Role) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, r := range guild.Roles { + if r.ID == role.ID { + guild.Roles[i] = role + return nil + } + } + + guild.Roles = append(guild.Roles, role) + return nil +} + +// RoleRemove removes a role from current world state by ID. +func (s *State) RoleRemove(guildID, roleID string) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, r := range guild.Roles { + if r.ID == roleID { + guild.Roles = append(guild.Roles[:i], guild.Roles[i+1:]...) + return nil + } + } + + return errors.New("Role not found.") +} + +// Role gets a role by ID from a guild. +func (s *State) Role(guildID, roleID string) (*Role, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + s.RLock() + defer s.RUnlock() + + for _, r := range guild.Roles { + if r.ID == roleID { + return r, nil + } + } + + return nil, errors.New("Role not found.") +} + +// ChannelAdd adds a guild to the current world state, or +// updates it if it already exists. +// Channels may exist either as PrivateChannels or inside +// a guild. +func (s *State) ChannelAdd(channel *Channel) error { + if s == nil { + return ErrNilState + } + + s.Lock() + defer s.Unlock() + + // If the channel exists, replace it + if c, ok := s.channelMap[channel.ID]; ok { + channel.Messages = c.Messages + channel.PermissionOverwrites = c.PermissionOverwrites + + *c = *channel + return nil + } + + if channel.IsPrivate { + s.PrivateChannels = append(s.PrivateChannels, channel) + } else { + guild, ok := s.guildMap[channel.GuildID] + if !ok { + return errors.New("Guild for channel not found.") + } + + guild.Channels = append(guild.Channels, channel) + } + + s.channelMap[channel.ID] = channel + + return nil +} + +// ChannelRemove removes a channel from current world state. +func (s *State) ChannelRemove(channel *Channel) error { + if s == nil { + return ErrNilState + } + + _, err := s.Channel(channel.ID) + if err != nil { + return err + } + + if channel.IsPrivate { + s.Lock() + defer s.Unlock() + + for i, c := range s.PrivateChannels { + if c.ID == channel.ID { + s.PrivateChannels = append(s.PrivateChannels[:i], s.PrivateChannels[i+1:]...) + break + } + } + } else { + guild, err := s.Guild(channel.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, c := range guild.Channels { + if c.ID == channel.ID { + guild.Channels = append(guild.Channels[:i], guild.Channels[i+1:]...) + break + } + } + } + + delete(s.channelMap, channel.ID) + + return nil +} + +// GuildChannel gets a channel by ID from a guild. +// This method is Deprecated, use Channel(channelID) +func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) { + return s.Channel(channelID) +} + +// PrivateChannel gets a private channel by ID. +// This method is Deprecated, use Channel(channelID) +func (s *State) PrivateChannel(channelID string) (*Channel, error) { + return s.Channel(channelID) +} + +// Channel gets a channel by ID, it will look in all guilds an private channels. +func (s *State) Channel(channelID string) (*Channel, error) { + if s == nil { + return nil, ErrNilState + } + + s.RLock() + defer s.RUnlock() + + if c, ok := s.channelMap[channelID]; ok { + return c, nil + } + + return nil, errors.New("Channel not found.") +} + +// Emoji returns an emoji for a guild and emoji id. +func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + s.RLock() + defer s.RUnlock() + + for _, e := range guild.Emojis { + if e.ID == emojiID { + return e, nil + } + } + + return nil, errors.New("Emoji not found.") +} + +// EmojiAdd adds an emoji to the current world state. +func (s *State) EmojiAdd(guildID string, emoji *Emoji) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, e := range guild.Emojis { + if e.ID == emoji.ID { + guild.Emojis[i] = emoji + return nil + } + } + + guild.Emojis = append(guild.Emojis, emoji) + return nil +} + +// EmojisAdd adds multiple emojis to the world state. +func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error { + for _, e := range emojis { + if err := s.EmojiAdd(guildID, e); err != nil { + return err + } + } + return nil +} + +// MessageAdd adds a message to the current world state, or updates it if it exists. +// If the channel cannot be found, the message is discarded. +// Messages are kept in state up to s.MaxMessageCount +func (s *State) MessageAdd(message *Message) error { + if s == nil { + return ErrNilState + } + + c, err := s.Channel(message.ChannelID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + // If the message exists, merge in the new message contents. + for _, m := range c.Messages { + if m.ID == message.ID { + if message.Content != "" { + m.Content = message.Content + } + if message.EditedTimestamp != "" { + m.EditedTimestamp = message.EditedTimestamp + } + if message.Mentions != nil { + m.Mentions = message.Mentions + } + if message.Embeds != nil { + m.Embeds = message.Embeds + } + if message.Attachments != nil { + m.Attachments = message.Attachments + } + + return nil + } + } + + c.Messages = append(c.Messages, message) + + if len(c.Messages) > s.MaxMessageCount { + c.Messages = c.Messages[len(c.Messages)-s.MaxMessageCount:] + } + return nil +} + +// MessageRemove removes a message from the world state. +func (s *State) MessageRemove(message *Message) error { + if s == nil { + return ErrNilState + } + + c, err := s.Channel(message.ChannelID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, m := range c.Messages { + if m.ID == message.ID { + c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) + return nil + } + } + + return errors.New("Message not found.") +} + +func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { + guild, err := s.Guild(update.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + // Handle Leaving Channel + if update.ChannelID == "" { + for i, state := range guild.VoiceStates { + if state.UserID == update.UserID { + guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...) + return nil + } + } + } else { + for i, state := range guild.VoiceStates { + if state.UserID == update.UserID { + guild.VoiceStates[i] = update.VoiceState + return nil + } + } + + guild.VoiceStates = append(guild.VoiceStates, update.VoiceState) + } + + return nil +} + +// Message gets a message by channel and message ID. +func (s *State) Message(channelID, messageID string) (*Message, error) { + if s == nil { + return nil, ErrNilState + } + + c, err := s.Channel(channelID) + if err != nil { + return nil, err + } + + s.RLock() + defer s.RUnlock() + + for _, m := range c.Messages { + if m.ID == messageID { + return m, nil + } + } + + return nil, errors.New("Message not found.") +} + +// onInterface handles all events related to states. +func (s *State) onInterface(se *Session, i interface{}) (err error) { + if s == nil { + return ErrNilState + } + if !se.StateEnabled { + return nil + } + + switch t := i.(type) { + case *Ready: + err = s.OnReady(t) + case *GuildCreate: + err = s.GuildAdd(t.Guild) + case *GuildUpdate: + err = s.GuildAdd(t.Guild) + case *GuildDelete: + err = s.GuildRemove(t.Guild) + case *GuildMemberAdd: + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } + case *GuildMemberUpdate: + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } + case *GuildMemberRemove: + if s.TrackMembers { + err = s.MemberRemove(t.Member) + } + case *GuildRoleCreate: + if s.TrackRoles { + err = s.RoleAdd(t.GuildID, t.Role) + } + case *GuildRoleUpdate: + if s.TrackRoles { + err = s.RoleAdd(t.GuildID, t.Role) + } + case *GuildRoleDelete: + if s.TrackRoles { + err = s.RoleRemove(t.GuildID, t.RoleID) + } + case *GuildEmojisUpdate: + if s.TrackEmojis { + err = s.EmojisAdd(t.GuildID, t.Emojis) + } + case *ChannelCreate: + if s.TrackChannels { + err = s.ChannelAdd(t.Channel) + } + case *ChannelUpdate: + if s.TrackChannels { + err = s.ChannelAdd(t.Channel) + } + case *ChannelDelete: + if s.TrackChannels { + err = s.ChannelRemove(t.Channel) + } + case *MessageCreate: + if s.MaxMessageCount != 0 { + err = s.MessageAdd(t.Message) + } + case *MessageUpdate: + if s.MaxMessageCount != 0 { + err = s.MessageAdd(t.Message) + } + case *MessageDelete: + if s.MaxMessageCount != 0 { + err = s.MessageRemove(t.Message) + } + case *VoiceStateUpdate: + if s.TrackVoice { + err = s.voiceStateUpdate(t) + } + } + + return +} + +// UserChannelPermissions returns the permission of a user in a channel. +// userID : The ID of the user to calculate permissions for. +// channelID : The ID of the channel to calculate permission for. +func (s *State) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + + channel, err := s.Channel(channelID) + if err != nil { + return + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return + } + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return + } + + for _, role := range guild.Roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + apermissions |= role.Permissions + break + } + } + } + + if apermissions&PermissionManageRoles > 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&PermissionManageRoles > 0 { + apermissions |= PermissionAllChannel + } + + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/structs.go b/vendor/github.com/bwmarrin/discordgo/structs.go new file mode 100644 index 00000000..19a291f8 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/structs.go @@ -0,0 +1,521 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains all structures for the discordgo package. These +// may be moved about later into separate files but I find it easier to have +// them all located together. + +package discordgo + +import ( + "encoding/json" + "reflect" + "sync" + "time" + + "github.com/gorilla/websocket" +) + +// A Session represents a connection to the Discord API. +type Session struct { + sync.RWMutex + + // General configurable settings. + + // Authentication token for this session + Token string + + // Debug for printing JSON request/responses + Debug bool // Deprecated, will be removed. + LogLevel int + + // Should the session reconnect the websocket on errors. + ShouldReconnectOnError bool + + // Should the session request compressed websocket data. + Compress bool + + // Sharding + ShardID int + ShardCount int + + // Should state tracking be enabled. + // State tracking is the best way for getting the the users + // active guilds and the members of the guilds. + StateEnabled bool + + // Exposed but should not be modified by User. + + // Whether the Data Websocket is ready + DataReady bool // NOTE: Maye be deprecated soon + + // Status stores the currect status of the websocket connection + // this is being tested, may stay, may go away. + status int32 + + // Whether the Voice Websocket is ready + VoiceReady bool // NOTE: Deprecated. + + // Whether the UDP Connection is ready + UDPReady bool // NOTE: Deprecated + + // Stores a mapping of guild id's to VoiceConnections + VoiceConnections map[string]*VoiceConnection + + // Managed state object, updated internally with events when + // StateEnabled is true. + State *State + + handlersMu sync.RWMutex + // This is a mapping of event struct to a reflected value + // for event handlers. + // We store the reflected value instead of the function + // reference as it is more performant, instead of re-reflecting + // the function each event. + handlers map[interface{}][]reflect.Value + + // The websocket connection. + wsConn *websocket.Conn + + // When nil, the session is not listening. + listening chan interface{} + + // used to deal with rate limits + // may switch to slices later + // TODO: performance test map vs slices + rateLimit rateLimitMutex + + // sequence tracks the current gateway api websocket sequence number + sequence int + + // stores sessions current Discord Gateway + gateway string + + // stores session ID of current Gateway connection + sessionID string + + // used to make sure gateway websocket writes do not happen concurrently + wsMutex sync.Mutex +} + +type rateLimitMutex struct { + sync.Mutex + url map[string]*sync.Mutex + // bucket map[string]*sync.Mutex // TODO :) +} + +// A Resumed struct holds the data received in a RESUMED event +type Resumed struct { + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + Trace []string `json:"_trace"` +} + +// A VoiceRegion stores data for a specific voice region server. +type VoiceRegion struct { + ID string `json:"id"` + Name string `json:"name"` + Hostname string `json:"sample_hostname"` + Port int `json:"sample_port"` +} + +// A VoiceICE stores data for voice ICE servers. +type VoiceICE struct { + TTL string `json:"ttl"` + Servers []*ICEServer `json:"servers"` +} + +// A ICEServer stores data for a specific voice ICE server. +type ICEServer struct { + URL string `json:"url"` + Username string `json:"username"` + Credential string `json:"credential"` +} + +// A Invite stores all data related to a specific Discord Guild or Channel invite. +type Invite struct { + Guild *Guild `json:"guild"` + Channel *Channel `json:"channel"` + Inviter *User `json:"inviter"` + Code string `json:"code"` + CreatedAt string `json:"created_at"` // TODO make timestamp + MaxAge int `json:"max_age"` + Uses int `json:"uses"` + MaxUses int `json:"max_uses"` + XkcdPass string `json:"xkcdpass"` + Revoked bool `json:"revoked"` + Temporary bool `json:"temporary"` +} + +// A Channel holds all data related to an individual Discord channel. +type Channel struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + Name string `json:"name"` + Topic string `json:"topic"` + Type string `json:"type"` + LastMessageID string `json:"last_message_id"` + Position int `json:"position"` + Bitrate int `json:"bitrate"` + IsPrivate bool `json:"is_private"` + Recipient *User `json:"recipient"` + Messages []*Message `json:"-"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` +} + +// A PermissionOverwrite holds permission overwrite data for a Channel +type PermissionOverwrite struct { + ID string `json:"id"` + Type string `json:"type"` + Deny int `json:"deny"` + Allow int `json:"allow"` +} + +// Emoji struct holds data related to Emoji's +type Emoji struct { + ID string `json:"id"` + Name string `json:"name"` + Roles []string `json:"roles"` + Managed bool `json:"managed"` + RequireColons bool `json:"require_colons"` +} + +// VerificationLevel type defination +type VerificationLevel int + +// Constants for VerificationLevel levels from 0 to 3 inclusive +const ( + VerificationLevelNone VerificationLevel = iota + VerificationLevelLow + VerificationLevelMedium + VerificationLevelHigh +) + +// A Guild holds all data related to a specific Discord Guild. Guilds are also +// sometimes referred to as Servers in the Discord client. +type Guild struct { + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Region string `json:"region"` + AfkChannelID string `json:"afk_channel_id"` + EmbedChannelID string `json:"embed_channel_id"` + OwnerID string `json:"owner_id"` + JoinedAt string `json:"joined_at"` // make this a timestamp + Splash string `json:"splash"` + AfkTimeout int `json:"afk_timeout"` + VerificationLevel VerificationLevel `json:"verification_level"` + EmbedEnabled bool `json:"embed_enabled"` + Large bool `json:"large"` // ?? + DefaultMessageNotifications int `json:"default_message_notifications"` + Roles []*Role `json:"roles"` + Emojis []*Emoji `json:"emojis"` + Members []*Member `json:"members"` + Presences []*Presence `json:"presences"` + Channels []*Channel `json:"channels"` + VoiceStates []*VoiceState `json:"voice_states"` + Unavailable *bool `json:"unavailable"` +} + +// A GuildParams stores all the data needed to update discord guild settings +type GuildParams struct { + Name string `json:"name"` + Region string `json:"region"` + VerificationLevel *VerificationLevel `json:"verification_level"` +} + +// A Role stores information about Discord guild member roles. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` + Managed bool `json:"managed"` + Hoist bool `json:"hoist"` + Color int `json:"color"` + Position int `json:"position"` + Permissions int `json:"permissions"` +} + +// A VoiceState stores the voice states of Guilds +type VoiceState struct { + UserID string `json:"user_id"` + SessionID string `json:"session_id"` + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` + Suppress bool `json:"suppress"` + SelfMute bool `json:"self_mute"` + SelfDeaf bool `json:"self_deaf"` + Mute bool `json:"mute"` + Deaf bool `json:"deaf"` +} + +// A Presence stores the online, offline, or idle and game status of Guild members. +type Presence struct { + User *User `json:"user"` + Status string `json:"status"` + Game *Game `json:"game"` +} + +// A Game struct holds the name of the "playing .." game for a user +type Game struct { + Name string `json:"name"` + Type int `json:"type"` + URL string `json:"url"` +} + +// A Member stores user information for Guild members. +type Member struct { + GuildID string `json:"guild_id"` + JoinedAt string `json:"joined_at"` + Nick string `json:"nick"` + Deaf bool `json:"deaf"` + Mute bool `json:"mute"` + User *User `json:"user"` + Roles []string `json:"roles"` +} + +// A User stores all data for an individual Discord user. +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Avatar string `json:"Avatar"` + Discriminator string `json:"discriminator"` + Token string `json:"token"` + Verified bool `json:"verified"` + MFAEnabled bool `json:"mfa_enabled"` + Bot bool `json:"bot"` +} + +// A Settings stores data for a specific users Discord client settings. +type Settings struct { + RenderEmbeds bool `json:"render_embeds"` + InlineEmbedMedia bool `json:"inline_embed_media"` + InlineAttachmentMedia bool `json:"inline_attachment_media"` + EnableTtsCommand bool `json:"enable_tts_command"` + MessageDisplayCompact bool `json:"message_display_compact"` + ShowCurrentGame bool `json:"show_current_game"` + AllowEmailFriendRequest bool `json:"allow_email_friend_request"` + ConvertEmoticons bool `json:"convert_emoticons"` + Locale string `json:"locale"` + Theme string `json:"theme"` + GuildPositions []string `json:"guild_positions"` + RestrictedGuilds []string `json:"restricted_guilds"` + FriendSourceFlags *FriendSourceFlags `json:"friend_source_flags"` +} + +// FriendSourceFlags stores ... TODO :) +type FriendSourceFlags struct { + All bool `json:"all"` + MutualGuilds bool `json:"mutual_guilds"` + MutualFriends bool `json:"mutual_friends"` +} + +// An Event provides a basic initial struct for all websocket event. +type Event struct { + Operation int `json:"op"` + Sequence int `json:"s"` + Type string `json:"t"` + RawData json.RawMessage `json:"d"` + Struct interface{} `json:"-"` +} + +// A Ready stores all data for the websocket READY event. +type Ready struct { + Version int `json:"v"` + SessionID string `json:"session_id"` + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + User *User `json:"user"` + ReadState []*ReadState `json:"read_state"` + PrivateChannels []*Channel `json:"private_channels"` + Guilds []*Guild `json:"guilds"` + + // Undocumented fields + Settings *Settings `json:"user_settings"` + UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` + Relationships []*Relationship `json:"relationships"` + Presences []*Presence `json:"presences"` +} + +// A Relationship between the logged in user and Relationship.User +type Relationship struct { + User *User `json:"user"` + Type int `json:"type"` // 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req + ID string `json:"id"` +} + +// A TooManyRequests struct holds information received from Discord +// when receiving a HTTP 429 response. +type TooManyRequests struct { + Bucket string `json:"bucket"` + Message string `json:"message"` + RetryAfter time.Duration `json:"retry_after"` +} + +// A ReadState stores data on the read state of channels. +type ReadState struct { + MentionCount int `json:"mention_count"` + LastMessageID string `json:"last_message_id"` + ID string `json:"id"` +} + +// A TypingStart stores data for the typing start websocket event. +type TypingStart struct { + UserID string `json:"user_id"` + ChannelID string `json:"channel_id"` + Timestamp int `json:"timestamp"` +} + +// A PresenceUpdate stores data for the presence update websocket event. +type PresenceUpdate struct { + Presence + GuildID string `json:"guild_id"` + Roles []string `json:"roles"` +} + +// A MessageAck stores data for the message ack websocket event. +type MessageAck struct { + MessageID string `json:"message_id"` + ChannelID string `json:"channel_id"` +} + +// A GuildIntegrationsUpdate stores data for the guild integrations update +// websocket event. +type GuildIntegrationsUpdate struct { + GuildID string `json:"guild_id"` +} + +// A GuildRole stores data for guild role websocket events. +type GuildRole struct { + Role *Role `json:"role"` + GuildID string `json:"guild_id"` +} + +// A GuildRoleDelete stores data for the guild role delete websocket event. +type GuildRoleDelete struct { + RoleID string `json:"role_id"` + GuildID string `json:"guild_id"` +} + +// A GuildBan stores data for a guild ban. +type GuildBan struct { + User *User `json:"user"` + GuildID string `json:"guild_id"` +} + +// A GuildEmojisUpdate stores data for a guild emoji update event. +type GuildEmojisUpdate struct { + GuildID string `json:"guild_id"` + Emojis []*Emoji `json:"emojis"` +} + +// A GuildIntegration stores data for a guild integration. +type GuildIntegration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account *GuildIntegrationAccount `json:"account"` + SyncedAt int `json:"synced_at"` +} + +// A GuildIntegrationAccount stores data for a guild integration account. +type GuildIntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// A GuildEmbed stores data for a guild embed. +type GuildEmbed struct { + Enabled bool `json:"enabled"` + ChannelID string `json:"channel_id"` +} + +// A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. +type UserGuildSettingsChannelOverride struct { + Muted bool `json:"muted"` + MessageNotifications int `json:"message_notifications"` + ChannelID string `json:"channel_id"` +} + +// A UserGuildSettings stores data for a users guild settings. +type UserGuildSettings struct { + SupressEveryone bool `json:"suppress_everyone"` + Muted bool `json:"muted"` + MobilePush bool `json:"mobile_push"` + MessageNotifications int `json:"message_notifications"` + GuildID string `json:"guild_id"` + ChannelOverrides []*UserGuildSettingsChannelOverride `json:"channel_overrides"` +} + +// A UserGuildSettingsEdit stores data for editing UserGuildSettings +type UserGuildSettingsEdit struct { + SupressEveryone bool `json:"suppress_everyone"` + Muted bool `json:"muted"` + MobilePush bool `json:"mobile_push"` + MessageNotifications int `json:"message_notifications"` + ChannelOverrides map[string]*UserGuildSettingsChannelOverride `json:"channel_overrides"` +} + +// Constants for the different bit offsets of text channel permissions +const ( + PermissionReadMessages = 1 << (iota + 10) + PermissionSendMessages + PermissionSendTTSMessages + PermissionManageMessages + PermissionEmbedLinks + PermissionAttachFiles + PermissionReadMessageHistory + PermissionMentionEveryone +) + +// Constants for the different bit offsets of voice permissions +const ( + PermissionVoiceConnect = 1 << (iota + 20) + PermissionVoiceSpeak + PermissionVoiceMuteMembers + PermissionVoiceDeafenMembers + PermissionVoiceMoveMembers + PermissionVoiceUseVAD +) + +// Constants for the different bit offsets of general permissions +const ( + PermissionCreateInstantInvite = 1 << iota + PermissionKickMembers + PermissionBanMembers + PermissionManageRoles + PermissionManageChannels + PermissionManageServer + + PermissionAllText = PermissionReadMessages | + PermissionSendMessages | + PermissionSendTTSMessages | + PermissionManageMessages | + PermissionEmbedLinks | + PermissionAttachFiles | + PermissionReadMessageHistory | + PermissionMentionEveryone + PermissionAllVoice = PermissionVoiceConnect | + PermissionVoiceSpeak | + PermissionVoiceMuteMembers | + PermissionVoiceDeafenMembers | + PermissionVoiceMoveMembers | + PermissionVoiceUseVAD + PermissionAllChannel = PermissionAllText | + PermissionAllVoice | + PermissionCreateInstantInvite | + PermissionManageRoles | + PermissionManageChannels + PermissionAll = PermissionAllChannel | + PermissionKickMembers | + PermissionBanMembers | + PermissionManageServer +) diff --git a/vendor/github.com/bwmarrin/discordgo/voice.go b/vendor/github.com/bwmarrin/discordgo/voice.go new file mode 100644 index 00000000..094aa59e --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/voice.go @@ -0,0 +1,853 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to Discord voice suppport + +package discordgo + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "log" + "net" + "runtime" + "strings" + "sync" + "time" + + "github.com/gorilla/websocket" + "golang.org/x/crypto/nacl/secretbox" +) + +// ------------------------------------------------------------------------------------------------ +// Code related to both VoiceConnection Websocket and UDP connections. +// ------------------------------------------------------------------------------------------------ + +// A VoiceConnection struct holds all the data and functions related to a Discord Voice Connection. +type VoiceConnection struct { + sync.RWMutex + + Debug bool // If true, print extra logging -- DEPRECATED + LogLevel int + Ready bool // If true, voice is ready to send/receive audio + UserID string + GuildID string + ChannelID string + deaf bool + mute bool + speaking bool + reconnecting bool // If true, voice connection is trying to reconnect + + OpusSend chan []byte // Chan for sending opus audio + OpusRecv chan *Packet // Chan for receiving opus audio + + wsConn *websocket.Conn + wsMutex sync.Mutex + udpConn *net.UDPConn + session *Session + + sessionID string + token string + endpoint string + + // Used to send a close signal to goroutines + close chan struct{} + + // Used to allow blocking until connected + connected chan bool + + // Used to pass the sessionid from onVoiceStateUpdate + // sessionRecv chan string UNUSED ATM + + op4 voiceOP4 + op2 voiceOP2 + + voiceSpeakingUpdateHandlers []VoiceSpeakingUpdateHandler +} + +// VoiceSpeakingUpdateHandler type provides a function defination for the +// VoiceSpeakingUpdate event +type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdate) + +// Speaking sends a speaking notification to Discord over the voice websocket. +// This must be sent as true prior to sending audio and should be set to false +// once finished sending audio. +// b : Send true if speaking, false if not. +func (v *VoiceConnection) Speaking(b bool) (err error) { + + v.log(LogDebug, "called (%t)", b) + + type voiceSpeakingData struct { + Speaking bool `json:"speaking"` + Delay int `json:"delay"` + } + + type voiceSpeakingOp struct { + Op int `json:"op"` // Always 5 + Data voiceSpeakingData `json:"d"` + } + + if v.wsConn == nil { + return fmt.Errorf("No VoiceConnection websocket.") + } + + data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} + v.wsMutex.Lock() + err = v.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + if err != nil { + v.speaking = false + log.Println("Speaking() write json error:", err) + return + } + v.speaking = b + + return +} + +// ChangeChannel sends Discord a request to change channels within a Guild +// !!! NOTE !!! This function may be removed in favour of just using ChannelVoiceJoin +func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err error) { + + v.log(LogInformational, "called") + + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, mute, deaf}} + v.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + if err != nil { + return + } + v.ChannelID = channelID + v.deaf = deaf + v.mute = mute + v.speaking = false + + return +} + +// Disconnect disconnects from this voice channel and closes the websocket +// and udp connections to Discord. +// !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave +func (v *VoiceConnection) Disconnect() (err error) { + + // Send a OP4 with a nil channel to disconnect + if v.sessionID != "" { + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} + v.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + v.sessionID = "" + } + + // Close websocket and udp connections + v.Close() + + v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) + delete(v.session.VoiceConnections, v.GuildID) + + return +} + +// Close closes the voice ws and udp connections +func (v *VoiceConnection) Close() { + + v.log(LogInformational, "called") + + v.Lock() + defer v.Unlock() + + v.Ready = false + v.speaking = false + + if v.close != nil { + v.log(LogInformational, "closing v.close") + close(v.close) + v.close = nil + } + + if v.udpConn != nil { + v.log(LogInformational, "closing udp") + err := v.udpConn.Close() + if err != nil { + log.Println("error closing udp connection: ", err) + } + v.udpConn = nil + } + + if v.wsConn != nil { + v.log(LogInformational, "sending close frame") + + // To cleanly close a connection, a client should send a close + // frame and wait for the server to close the connection. + err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + v.log(LogError, "error closing websocket, %s", err) + } + + // TODO: Wait for Discord to actually close the connection. + time.Sleep(1 * time.Second) + + v.log(LogInformational, "closing websocket") + err = v.wsConn.Close() + if err != nil { + v.log(LogError, "error closing websocket, %s", err) + } + + v.wsConn = nil + } +} + +// AddHandler adds a Handler for VoiceSpeakingUpdate events. +func (v *VoiceConnection) AddHandler(h VoiceSpeakingUpdateHandler) { + v.Lock() + defer v.Unlock() + + v.voiceSpeakingUpdateHandlers = append(v.voiceSpeakingUpdateHandlers, h) +} + +// VoiceSpeakingUpdate is a struct for a VoiceSpeakingUpdate event. +type VoiceSpeakingUpdate struct { + UserID string `json:"user_id"` + SSRC int `json:"ssrc"` + Speaking bool `json:"speaking"` +} + +// ------------------------------------------------------------------------------------------------ +// Unexported Internal Functions Below. +// ------------------------------------------------------------------------------------------------ + +// A voiceOP4 stores the data for the voice operation 4 websocket event +// which provides us with the NaCl SecretBox encryption key +type voiceOP4 struct { + SecretKey [32]byte `json:"secret_key"` + Mode string `json:"mode"` +} + +// A voiceOP2 stores the data for the voice operation 2 websocket event +// which is sort of like the voice READY packet +type voiceOP2 struct { + SSRC uint32 `json:"ssrc"` + Port int `json:"port"` + Modes []string `json:"modes"` + HeartbeatInterval time.Duration `json:"heartbeat_interval"` +} + +// WaitUntilConnected waits for the Voice Connection to +// become ready, if it does not become ready it retuns an err +func (v *VoiceConnection) waitUntilConnected() error { + + v.log(LogInformational, "called") + + i := 0 + for { + if v.Ready { + return nil + } + + if i > 10 { + return fmt.Errorf("Timeout waiting for voice.") + } + + time.Sleep(1 * time.Second) + i++ + } +} + +// Open opens a voice connection. This should be called +// after VoiceChannelJoin is used and the data VOICE websocket events +// are captured. +func (v *VoiceConnection) open() (err error) { + + v.log(LogInformational, "called") + + v.Lock() + defer v.Unlock() + + // Don't open a websocket if one is already open + if v.wsConn != nil { + v.log(LogWarning, "refusing to overwrite non-nil websocket") + return + } + + // TODO temp? loop to wait for the SessionID + i := 0 + for { + if v.sessionID != "" { + break + } + if i > 20 { // only loop for up to 1 second total + return fmt.Errorf("Did not receive voice Session ID in time.") + } + time.Sleep(50 * time.Millisecond) + i++ + } + + // Connect to VoiceConnection Websocket + vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) + v.log(LogInformational, "connecting to voice endpoint %s", vg) + v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) + if err != nil { + v.log(LogWarning, "error connecting to voice endpoint %s, %s", vg, err) + v.log(LogDebug, "voice struct: %#v\n", v) + return + } + + type voiceHandshakeData struct { + ServerID string `json:"server_id"` + UserID string `json:"user_id"` + SessionID string `json:"session_id"` + Token string `json:"token"` + } + type voiceHandshakeOp struct { + Op int `json:"op"` // Always 0 + Data voiceHandshakeData `json:"d"` + } + data := voiceHandshakeOp{0, voiceHandshakeData{v.GuildID, v.UserID, v.sessionID, v.token}} + + err = v.wsConn.WriteJSON(data) + if err != nil { + v.log(LogWarning, "error sending init packet, %s", err) + return + } + + v.close = make(chan struct{}) + go v.wsListen(v.wsConn, v.close) + + // add loop/check for Ready bool here? + // then return false if not ready? + // but then wsListen will also err. + + return +} + +// wsListen listens on the voice websocket for messages and passes them +// to the voice event handler. This is automatically called by the Open func +func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { + + v.log(LogInformational, "called") + + for { + _, message, err := v.wsConn.ReadMessage() + if err != nil { + // Detect if we have been closed manually. If a Close() has already + // happened, the websocket we are listening on will be different to the + // current session. + v.RLock() + sameConnection := v.wsConn == wsConn + v.RUnlock() + if sameConnection { + + v.log(LogError, "voice endpoint %s websocket closed unexpectantly, %s", v.endpoint, err) + + // Start reconnect goroutine then exit. + go v.reconnect() + } + return + } + + // Pass received message to voice event handler + select { + case <-close: + return + default: + go v.onEvent(message) + } + } +} + +// wsEvent handles any voice websocket events. This is only called by the +// wsListen() function. +func (v *VoiceConnection) onEvent(message []byte) { + + v.log(LogDebug, "received: %s", string(message)) + + var e Event + if err := json.Unmarshal(message, &e); err != nil { + v.log(LogError, "unmarshall error, %s", err) + return + } + + switch e.Operation { + + case 2: // READY + + if err := json.Unmarshal(e.RawData, &v.op2); err != nil { + v.log(LogError, "OP2 unmarshall error, %s, %s", err, string(e.RawData)) + return + } + + // Start the voice websocket heartbeat to keep the connection alive + go v.wsHeartbeat(v.wsConn, v.close, v.op2.HeartbeatInterval) + // TODO monitor a chan/bool to verify this was successful + + // Start the UDP connection + err := v.udpOpen() + if err != nil { + v.log(LogError, "error opening udp connection, %s", err) + return + } + + // Start the opusSender. + // TODO: Should we allow 48000/960 values to be user defined? + if v.OpusSend == nil { + v.OpusSend = make(chan []byte, 2) + } + go v.opusSender(v.udpConn, v.close, v.OpusSend, 48000, 960) + + // Start the opusReceiver + if !v.deaf { + if v.OpusRecv == nil { + v.OpusRecv = make(chan *Packet, 2) + } + + go v.opusReceiver(v.udpConn, v.close, v.OpusRecv) + } + + // Send the ready event + v.connected <- true + return + + case 3: // HEARTBEAT response + // add code to use this to track latency? + return + + case 4: // udp encryption secret key + v.op4 = voiceOP4{} + if err := json.Unmarshal(e.RawData, &v.op4); err != nil { + v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData)) + return + } + return + + case 5: + if len(v.voiceSpeakingUpdateHandlers) == 0 { + return + } + + voiceSpeakingUpdate := &VoiceSpeakingUpdate{} + if err := json.Unmarshal(e.RawData, voiceSpeakingUpdate); err != nil { + v.log(LogError, "OP5 unmarshall error, %s, %s", err, string(e.RawData)) + return + } + + for _, h := range v.voiceSpeakingUpdateHandlers { + h(v, voiceSpeakingUpdate) + } + + default: + v.log(LogError, "unknown voice operation, %d, %s", e.Operation, string(e.RawData)) + } + + return +} + +type voiceHeartbeatOp struct { + Op int `json:"op"` // Always 3 + Data int `json:"d"` +} + +// NOTE :: When a guild voice server changes how do we shut this down +// properly, so a new connection can be setup without fuss? +// +// wsHeartbeat sends regular heartbeats to voice Discord so it knows the client +// is still connected. If you do not send these heartbeats Discord will +// disconnect the websocket connection after a few seconds. +func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { + + if close == nil || wsConn == nil { + return + } + + var err error + ticker := time.NewTicker(i * time.Millisecond) + for { + v.log(LogDebug, "sending heartbeat packet") + v.wsMutex.Lock() + err = wsConn.WriteJSON(voiceHeartbeatOp{3, int(time.Now().Unix())}) + v.wsMutex.Unlock() + if err != nil { + v.log(LogError, "error sending heartbeat to voice endpoint %s, %s", v.endpoint, err) + return + } + + select { + case <-ticker.C: + // continue loop and send heartbeat + case <-close: + return + } + } +} + +// ------------------------------------------------------------------------------------------------ +// Code related to the VoiceConnection UDP connection +// ------------------------------------------------------------------------------------------------ + +type voiceUDPData struct { + Address string `json:"address"` // Public IP of machine running this code + Port uint16 `json:"port"` // UDP Port of machine running this code + Mode string `json:"mode"` // always "xsalsa20_poly1305" +} + +type voiceUDPD struct { + Protocol string `json:"protocol"` // Always "udp" ? + Data voiceUDPData `json:"data"` +} + +type voiceUDPOp struct { + Op int `json:"op"` // Always 1 + Data voiceUDPD `json:"d"` +} + +// udpOpen opens a UDP connection to the voice server and completes the +// initial required handshake. This connection is left open in the session +// and can be used to send or receive audio. This should only be called +// from voice.wsEvent OP2 +func (v *VoiceConnection) udpOpen() (err error) { + + v.Lock() + defer v.Unlock() + + if v.wsConn == nil { + return fmt.Errorf("nil voice websocket") + } + + if v.udpConn != nil { + return fmt.Errorf("udp connection already open") + } + + if v.close == nil { + return fmt.Errorf("nil close channel") + } + + if v.endpoint == "" { + return fmt.Errorf("empty endpoint") + } + + host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) + addr, err := net.ResolveUDPAddr("udp", host) + if err != nil { + v.log(LogWarning, "error resolving udp host %s, %s", host, err) + return + } + + v.log(LogInformational, "connecting to udp addr %s", addr.String()) + v.udpConn, err = net.DialUDP("udp", nil, addr) + if err != nil { + v.log(LogWarning, "error connecting to udp addr %s, %s", addr.String(), err) + return + } + + // Create a 70 byte array and put the SSRC code from the Op 2 VoiceConnection event + // into it. Then send that over the UDP connection to Discord + sb := make([]byte, 70) + binary.BigEndian.PutUint32(sb, v.op2.SSRC) + _, err = v.udpConn.Write(sb) + if err != nil { + v.log(LogWarning, "udp write error to %s, %s", addr.String(), err) + return + } + + // Create a 70 byte array and listen for the initial handshake response + // from Discord. Once we get it parse the IP and PORT information out + // of the response. This should be our public IP and PORT as Discord + // saw us. + rb := make([]byte, 70) + rlen, _, err := v.udpConn.ReadFromUDP(rb) + if err != nil { + v.log(LogWarning, "udp read error, %s, %s", addr.String(), err) + return + } + + if rlen < 70 { + v.log(LogWarning, "received udp packet too small") + return fmt.Errorf("received udp packet too small") + } + + // Loop over position 4 though 20 to grab the IP address + // Should never be beyond position 20. + var ip string + for i := 4; i < 20; i++ { + if rb[i] == 0 { + break + } + ip += string(rb[i]) + } + + // Grab port from position 68 and 69 + port := binary.LittleEndian.Uint16(rb[68:70]) + + // Take the data from above and send it back to Discord to finalize + // the UDP connection handshake. + data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "xsalsa20_poly1305"}}} + + v.wsMutex.Lock() + err = v.wsConn.WriteJSON(data) + v.wsMutex.Unlock() + if err != nil { + v.log(LogWarning, "udp write error, %#v, %s", data, err) + return + } + + // start udpKeepAlive + go v.udpKeepAlive(v.udpConn, v.close, 5*time.Second) + // TODO: find a way to check that it fired off okay + + return +} + +// udpKeepAlive sends a udp packet to keep the udp connection open +// This is still a bit of a "proof of concept" +func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct{}, i time.Duration) { + + if udpConn == nil || close == nil { + return + } + + var err error + var sequence uint64 + + packet := make([]byte, 8) + + ticker := time.NewTicker(i) + for { + + binary.LittleEndian.PutUint64(packet, sequence) + sequence++ + + _, err = udpConn.Write(packet) + if err != nil { + v.log(LogError, "write error, %s", err) + return + } + + select { + case <-ticker.C: + // continue loop and send keepalive + case <-close: + return + } + } +} + +// opusSender will listen on the given channel and send any +// pre-encoded opus audio to Discord. Supposedly. +func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { + + if udpConn == nil || close == nil { + return + } + + runtime.LockOSThread() + + // VoiceConnection is now ready to receive audio packets + // TODO: this needs reviewed as I think there must be a better way. + v.Ready = true + defer func() { v.Ready = false }() + + var sequence uint16 + var timestamp uint32 + var recvbuf []byte + var ok bool + udpHeader := make([]byte, 12) + var nonce [24]byte + + // build the parts that don't change in the udpHeader + udpHeader[0] = 0x80 + udpHeader[1] = 0x78 + binary.BigEndian.PutUint32(udpHeader[8:], v.op2.SSRC) + + // start a send loop that loops until buf chan is closed + ticker := time.NewTicker(time.Millisecond * time.Duration(size/(rate/1000))) + for { + + // Get data from chan. If chan is closed, return. + select { + case <-close: + return + case recvbuf, ok = <-opus: + if !ok { + return + } + // else, continue loop + } + + if !v.speaking { + err := v.Speaking(true) + if err != nil { + v.log(LogError, "error sending speaking packet, %s", err) + } + } + + // Add sequence and timestamp to udpPacket + binary.BigEndian.PutUint16(udpHeader[2:], sequence) + binary.BigEndian.PutUint32(udpHeader[4:], timestamp) + + // encrypt the opus data + copy(nonce[:], udpHeader) + sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey) + + // block here until we're exactly at the right time :) + // Then send rtp audio packet to Discord over UDP + select { + case <-close: + return + case <-ticker.C: + // continue + } + _, err := udpConn.Write(sendbuf) + + if err != nil { + v.log(LogError, "udp write error, %s", err) + v.log(LogDebug, "voice struct: %#v\n", v) + return + } + + if (sequence) == 0xFFFF { + sequence = 0 + } else { + sequence++ + } + + if (timestamp + uint32(size)) >= 0xFFFFFFFF { + timestamp = 0 + } else { + timestamp += uint32(size) + } + } +} + +// A Packet contains the headers and content of a received voice packet. +type Packet struct { + SSRC uint32 + Sequence uint16 + Timestamp uint32 + Type []byte + Opus []byte + PCM []int16 +} + +// opusReceiver listens on the UDP socket for incoming packets +// and sends them across the given channel +// NOTE :: This function may change names later. +func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { + + if udpConn == nil || close == nil { + return + } + + p := Packet{} + recvbuf := make([]byte, 1024) + var nonce [24]byte + + for { + rlen, err := udpConn.Read(recvbuf) + if err != nil { + // Detect if we have been closed manually. If a Close() has already + // happened, the udp connection we are listening on will be different + // to the current session. + v.RLock() + sameConnection := v.udpConn == udpConn + v.RUnlock() + if sameConnection { + + v.log(LogError, "udp read error, %s, %s", v.endpoint, err) + v.log(LogDebug, "voice struct: %#v\n", v) + + go v.reconnect() + } + return + } + + select { + case <-close: + return + default: + // continue loop + } + + // For now, skip anything except audio. + if rlen < 12 || recvbuf[0] != 0x80 { + continue + } + + // build a audio packet struct + p.Type = recvbuf[0:2] + p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4]) + p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8]) + p.SSRC = binary.BigEndian.Uint32(recvbuf[8:12]) + // decrypt opus data + copy(nonce[:], recvbuf[0:12]) + p.Opus, _ = secretbox.Open(nil, recvbuf[12:rlen], &nonce, &v.op4.SecretKey) + + if c != nil { + c <- &p + } + } +} + +// Reconnect will close down a voice connection then immediately try to +// reconnect to that session. +// NOTE : This func is messy and a WIP while I find what works. +// It will be cleaned up once a proven stable option is flushed out. +// aka: this is ugly shit code, please don't judge too harshly. +func (v *VoiceConnection) reconnect() { + + v.log(LogInformational, "called") + + v.Lock() + if v.reconnecting { + v.log(LogInformational, "already reconnecting to channel %s, exiting", v.ChannelID) + v.Unlock() + return + } + v.reconnecting = true + v.Unlock() + + defer func() { v.reconnecting = false }() + + // Close any currently open connections + v.Close() + + wait := time.Duration(1) + for { + + <-time.After(wait * time.Second) + wait *= 2 + if wait > 600 { + wait = 600 + } + + if v.session.DataReady == false || v.session.wsConn == nil { + v.log(LogInformational, "cannot reconenct to channel %s with unready session", v.ChannelID) + continue + } + + v.log(LogInformational, "trying to reconnect to channel %s", v.ChannelID) + + _, err := v.session.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf) + if err == nil { + v.log(LogInformational, "successfully reconnected to channel %s", v.ChannelID) + return + } + + // if the reconnect above didn't work lets just send a disconnect + // packet to reset things. + // Send a OP4 with a nil channel to disconnect + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} + v.session.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.session.wsMutex.Unlock() + if err != nil { + v.log(LogError, "error sending disconnect packet, %s", err) + } + + v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) + } +} diff --git a/vendor/github.com/bwmarrin/discordgo/wsapi.go b/vendor/github.com/bwmarrin/discordgo/wsapi.go new file mode 100644 index 00000000..a19c3842 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/wsapi.go @@ -0,0 +1,679 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains low level functions for interacting with the Discord +// data websocket interface. + +package discordgo + +import ( + "bytes" + "compress/zlib" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "reflect" + "runtime" + "time" + + "github.com/gorilla/websocket" +) + +type resumePacket struct { + Op int `json:"op"` + Data struct { + Token string `json:"token"` + SessionID string `json:"session_id"` + Sequence int `json:"seq"` + } `json:"d"` +} + +// Open opens a websocket connection to Discord. +func (s *Session) Open() (err error) { + + s.log(LogInformational, "called") + + s.Lock() + defer func() { + if err != nil { + s.Unlock() + } + }() + + if s.wsConn != nil { + err = errors.New("Web socket already opened.") + return + } + + if s.VoiceConnections == nil { + s.log(LogInformational, "creating new VoiceConnections map") + s.VoiceConnections = make(map[string]*VoiceConnection) + } + + // Get the gateway to use for the Websocket connection + if s.gateway == "" { + s.gateway, err = s.Gateway() + if err != nil { + return + } + + // Add the version and encoding to the URL + s.gateway = fmt.Sprintf("%s?v=4&encoding=json", s.gateway) + } + + header := http.Header{} + header.Add("accept-encoding", "zlib") + + s.log(LogInformational, "connecting to gateway %s", s.gateway) + s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header) + if err != nil { + s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err) + s.gateway = "" // clear cached gateway + // TODO: should we add a retry block here? + return + } + + if s.sessionID != "" && s.sequence > 0 { + + p := resumePacket{} + p.Op = 6 + p.Data.Token = s.Token + p.Data.SessionID = s.sessionID + p.Data.Sequence = s.sequence + + s.log(LogInformational, "sending resume packet to gateway") + err = s.wsConn.WriteJSON(p) + if err != nil { + s.log(LogWarning, "error sending gateway resume packet, %s, %s", s.gateway, err) + return + } + + } else { + + err = s.identify() + if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) + return + } + } + + // Create listening outside of listen, as it needs to happen inside the mutex + // lock. + s.listening = make(chan interface{}) + go s.listen(s.wsConn, s.listening) + + s.Unlock() + + s.initialize() + s.log(LogInformational, "emit connect event") + s.handle(&Connect{}) + + s.log(LogInformational, "exiting") + return +} + +// listen polls the websocket connection for events, it will stop when the +// listening channel is closed, or an error occurs. +func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { + + s.log(LogInformational, "called") + + for { + + messageType, message, err := wsConn.ReadMessage() + + if err != nil { + + // Detect if we have been closed manually. If a Close() has already + // happened, the websocket we are listening on will be different to + // the current session. + s.RLock() + sameConnection := s.wsConn == wsConn + s.RUnlock() + + if sameConnection { + + s.log(LogWarning, "error reading from gateway %s websocket, %s", s.gateway, err) + // There has been an error reading, close the websocket so that + // OnDisconnect event is emitted. + err := s.Close() + if err != nil { + s.log(LogWarning, "error closing session connection, %s", err) + } + + s.log(LogInformational, "calling reconnect() now") + s.reconnect() + } + + return + } + + select { + + case <-listening: + return + + default: + s.onEvent(messageType, message) + + } + } +} + +type heartbeatOp struct { + Op int `json:"op"` + Data int `json:"d"` +} + +// heartbeat sends regular heartbeats to Discord so it knows the client +// is still connected. If you do not send these heartbeats Discord will +// disconnect the websocket connection after a few seconds. +func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, i time.Duration) { + + s.log(LogInformational, "called") + + if listening == nil || wsConn == nil { + return + } + + var err error + ticker := time.NewTicker(i * time.Millisecond) + + for { + + s.log(LogInformational, "sending gateway websocket heartbeat seq %d", s.sequence) + s.wsMutex.Lock() + err = wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + s.wsMutex.Unlock() + if err != nil { + s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) + s.Lock() + s.DataReady = false + s.Unlock() + return + } + s.Lock() + s.DataReady = true + s.Unlock() + + select { + case <-ticker.C: + // continue loop and send heartbeat + case <-listening: + return + } + } +} + +type updateStatusData struct { + IdleSince *int `json:"idle_since"` + Game *Game `json:"game"` +} + +type updateStatusOp struct { + Op int `json:"op"` + Data updateStatusData `json:"d"` +} + +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// If game!="" and url!="" then set the status type to streaming with the URL set. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { + + s.log(LogInformational, "called") + + s.RLock() + defer s.RUnlock() + if s.wsConn == nil { + return errors.New("no websocket connection exists") + } + + var usd updateStatusData + if idle > 0 { + usd.IdleSince = &idle + } + + if game != "" { + gameType := 0 + if url != "" { + gameType = 1 + } + usd.Game = &Game{ + Name: game, + Type: gameType, + URL: url, + } + } + + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) + s.wsMutex.Unlock() + + return +} + +// UpdateStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStatus(idle int, game string) (err error) { + return s.UpdateStreamingStatus(idle, game, "") +} + +// onEvent is the "event handler" for all messages received on the +// Discord Gateway API websocket connection. +// +// If you use the AddHandler() function to register a handler for a +// specific event this function will pass the event along to that handler. +// +// If you use the AddHandler() function to register a handler for the +// "OnEvent" event then all events will be passed to that handler. +// +// TODO: You may also register a custom event handler entirely using... +func (s *Session) onEvent(messageType int, message []byte) { + + var err error + var reader io.Reader + reader = bytes.NewBuffer(message) + + // If this is a compressed message, uncompress it. + if messageType == websocket.BinaryMessage { + + z, err2 := zlib.NewReader(reader) + if err2 != nil { + s.log(LogError, "error uncompressing websocket message, %s", err) + return + } + + defer func() { + err3 := z.Close() + if err3 != nil { + s.log(LogWarning, "error closing zlib, %s", err) + } + }() + + reader = z + } + + // Decode the event into an Event struct. + var e *Event + decoder := json.NewDecoder(reader) + if err = decoder.Decode(&e); err != nil { + s.log(LogError, "error decoding websocket message, %s", err) + return + } + + s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) + + // Ping request. + // Must respond with a heartbeat packet within 5 seconds + if e.Operation == 1 { + s.log(LogInformational, "sending heartbeat in response to Op1") + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(heartbeatOp{1, s.sequence}) + s.wsMutex.Unlock() + if err != nil { + s.log(LogError, "error sending heartbeat in response to Op1") + return + } + + return + } + + // Reconnect + // Must immediately disconnect from gateway and reconnect to new gateway. + if e.Operation == 7 { + // TODO + } + + // Invalid Session + // Must respond with a Identify packet. + if e.Operation == 9 { + + s.log(LogInformational, "sending identify packet to gateway in response to Op9") + + err = s.identify() + if err != nil { + s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) + return + } + + return + } + + // Do not try to Dispatch a non-Dispatch Message + if e.Operation != 0 { + // But we probably should be doing something with them. + // TEMP + s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) + return + } + + // Store the message sequence + s.sequence = e.Sequence + + // Map event to registered event handlers and pass it along + // to any registered functions + i := eventToInterface[e.Type] + if i != nil { + + // Create a new instance of the event type. + i = reflect.New(reflect.TypeOf(i)).Interface() + + // Attempt to unmarshal our event. + if err = json.Unmarshal(e.RawData, i); err != nil { + s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) + } + + // Send event to any registered event handlers for it's type. + // Because the above doesn't cancel this, in case of an error + // the struct could be partially populated or at default values. + // However, most errors are due to a single field and I feel + // it's better to pass along what we received than nothing at all. + // TODO: Think about that decision :) + // Either way, READY events must fire, even with errors. + go s.handle(i) + + } else { + s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) + } + + // Emit event to the OnEvent handler + e.Struct = i + go s.handle(e) +} + +// ------------------------------------------------------------------------------------------------ +// Code related to voice connections that initiate over the data websocket +// ------------------------------------------------------------------------------------------------ + +// A VoiceServerUpdate stores the data received during the Voice Server Update +// data websocket event. This data is used during the initial Voice Channel +// join handshaking. +type VoiceServerUpdate struct { + Token string `json:"token"` + GuildID string `json:"guild_id"` + Endpoint string `json:"endpoint"` +} + +type voiceChannelJoinData struct { + GuildID *string `json:"guild_id"` + ChannelID *string `json:"channel_id"` + SelfMute bool `json:"self_mute"` + SelfDeaf bool `json:"self_deaf"` +} + +type voiceChannelJoinOp struct { + Op int `json:"op"` + Data voiceChannelJoinData `json:"d"` +} + +// ChannelVoiceJoin joins the session user to a voice channel. +// +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { + + s.log(LogInformational, "called") + + voice, _ = s.VoiceConnections[gID] + + if voice == nil { + voice = &VoiceConnection{} + s.VoiceConnections[gID] = voice + } + + voice.GuildID = gID + voice.ChannelID = cID + voice.deaf = deaf + voice.mute = mute + voice.session = s + + // Send the request to Discord that we want to join the voice channel + data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(data) + s.wsMutex.Unlock() + if err != nil { + return + } + + // doesn't exactly work perfect yet.. TODO + err = voice.waitUntilConnected() + if err != nil { + s.log(LogWarning, "error waiting for voice to connect, %s", err) + voice.Close() + return + } + + return +} + +// onVoiceStateUpdate handles Voice State Update events on the data websocket. +func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { + + // If we don't have a connection for the channel, don't bother + if st.ChannelID == "" { + return + } + + // Check if we have a voice connection to update + voice, exists := s.VoiceConnections[st.GuildID] + if !exists { + return + } + + // Need to have this happen at login and store it in the Session + // TODO : This should be done upon connecting to Discord, or + // be moved to a small helper function + self, err := s.User("@me") // TODO: move to Login/New + if err != nil { + log.Println(err) + return + } + + // We only care about events that are about us + if st.UserID != self.ID { + return + } + + // Store the SessionID for later use. + voice.UserID = self.ID // TODO: Review + voice.sessionID = st.SessionID +} + +// onVoiceServerUpdate handles the Voice Server Update data websocket event. +// +// This is also fired if the Guild's voice region changes while connected +// to a voice channel. In that case, need to re-establish connection to +// the new region endpoint. +func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { + + s.log(LogInformational, "called") + + voice, exists := s.VoiceConnections[st.GuildID] + + // If no VoiceConnection exists, just skip this + if !exists { + return + } + + // If currently connected to voice ws/udp, then disconnect. + // Has no effect if not connected. + voice.Close() + + // Store values for later use + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.GuildID = st.GuildID + + // Open a conenction to the voice server + err := voice.open() + if err != nil { + s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) + } +} + +type identifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` + Referer string `json:"$referer"` + ReferringDomain string `json:"$referring_domain"` +} + +type identifyData struct { + Token string `json:"token"` + Properties identifyProperties `json:"properties"` + LargeThreshold int `json:"large_threshold"` + Compress bool `json:"compress"` + Shard *[2]int `json:"shard,omitempty"` +} + +type identifyOp struct { + Op int `json:"op"` + Data identifyData `json:"d"` +} + +// identify sends the identify packet to the gateway +func (s *Session) identify() error { + + properties := identifyProperties{runtime.GOOS, + "Discordgo v" + VERSION, + "", + "", + "", + } + + data := identifyData{s.Token, + properties, + 250, + s.Compress, + nil, + } + + if s.ShardCount > 1 { + + if s.ShardID >= s.ShardCount { + return errors.New("ShardID must be less than ShardCount") + } + + data.Shard = &[2]int{s.ShardID, s.ShardCount} + } + + op := identifyOp{2, data} + + s.wsMutex.Lock() + err := s.wsConn.WriteJSON(op) + s.wsMutex.Unlock() + if err != nil { + return err + } + + return nil +} + +func (s *Session) reconnect() { + + s.log(LogInformational, "called") + + var err error + + if s.ShouldReconnectOnError { + + wait := time.Duration(1) + + for { + s.log(LogInformational, "trying to reconnect to gateway") + + err = s.Open() + if err == nil { + s.log(LogInformational, "successfully reconnected to gateway") + + // I'm not sure if this is actually needed. + // if the gw reconnect works properly, voice should stay alive + // However, there seems to be cases where something "weird" + // happens. So we're doing this for now just to improve + // stability in those edge cases. + for _, v := range s.VoiceConnections { + + s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) + go v.reconnect() + + // This is here just to prevent violently spamming the + // voice reconnects + time.Sleep(1 * time.Second) + + } + return + } + + s.log(LogError, "error reconnecting to gateway, %s", err) + + <-time.After(wait * time.Second) + wait *= 2 + if wait > 600 { + wait = 600 + } + } + } +} + +// Close closes a websocket and stops all listening/heartbeat goroutines. +// TODO: Add support for Voice WS/UDP connections +func (s *Session) Close() (err error) { + + s.log(LogInformational, "called") + s.Lock() + + s.DataReady = false + + if s.listening != nil { + s.log(LogInformational, "closing listening channel") + close(s.listening) + s.listening = nil + } + + // TODO: Close all active Voice Connections too + // this should force stop any reconnecting voice channels too + + if s.wsConn != nil { + + s.log(LogInformational, "sending close frame") + // To cleanly close a connection, a client should send a close + // frame and wait for the server to close the connection. + err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + if err != nil { + s.log(LogError, "error closing websocket, %s", err) + } + + // TODO: Wait for Discord to actually close the connection. + time.Sleep(1 * time.Second) + + s.log(LogInformational, "closing gateway websocket") + err = s.wsConn.Close() + if err != nil { + s.log(LogError, "error closing websocket, %s", err) + } + + s.wsConn = nil + } + + s.Unlock() + + s.log(LogInformational, "emit disconnect event") + s.handle(&Disconnect{}) + + return +} |