From 7f3cbcedc0d748108d5421cf4a8e28876e7b4c29 Mon Sep 17 00:00:00 2001 From: Wim Date: Mon, 6 Aug 2018 21:10:54 +0200 Subject: Use own forks for logrus-prefixed-formatter and discordgo --- vendor/github.com/matterbridge/discordgo/LICENSE | 28 + .../github.com/matterbridge/discordgo/discord.go | 146 ++ .../github.com/matterbridge/discordgo/endpoints.go | 136 ++ vendor/github.com/matterbridge/discordgo/event.go | 238 +++ .../matterbridge/discordgo/eventhandlers.go | 1030 ++++++++++ vendor/github.com/matterbridge/discordgo/events.go | 253 +++ .../discordgo/examples/airhorn/main.go | 211 ++ .../discordgo/examples/appmaker/main.go | 103 + .../matterbridge/discordgo/examples/avatar/main.go | 89 + .../discordgo/examples/mytoken/main.go | 40 + .../discordgo/examples/pingpong/main.go | 71 + .../github.com/matterbridge/discordgo/logging.go | 103 + .../github.com/matterbridge/discordgo/message.go | 252 +++ vendor/github.com/matterbridge/discordgo/oauth2.go | 126 ++ .../github.com/matterbridge/discordgo/ratelimit.go | 194 ++ .../github.com/matterbridge/discordgo/restapi.go | 2049 ++++++++++++++++++++ vendor/github.com/matterbridge/discordgo/state.go | 990 ++++++++++ .../github.com/matterbridge/discordgo/structs.go | 686 +++++++ .../discordgo/tools/cmd/eventhandlers/main.go | 124 ++ vendor/github.com/matterbridge/discordgo/types.go | 58 + vendor/github.com/matterbridge/discordgo/user.go | 47 + vendor/github.com/matterbridge/discordgo/voice.go | 886 +++++++++ vendor/github.com/matterbridge/discordgo/wsapi.go | 845 ++++++++ .../matterbridge/logrus-prefixed-formatter/LICENSE | 21 + .../logrus-prefixed-formatter/examples/basic.go | 59 + .../logrus-prefixed-formatter/examples/themes.go | 48 + .../logrus-prefixed-formatter/formatter.go | 382 ++++ 27 files changed, 9215 insertions(+) create mode 100644 vendor/github.com/matterbridge/discordgo/LICENSE create mode 100644 vendor/github.com/matterbridge/discordgo/discord.go create mode 100644 vendor/github.com/matterbridge/discordgo/endpoints.go create mode 100644 vendor/github.com/matterbridge/discordgo/event.go create mode 100644 vendor/github.com/matterbridge/discordgo/eventhandlers.go create mode 100644 vendor/github.com/matterbridge/discordgo/events.go create mode 100644 vendor/github.com/matterbridge/discordgo/examples/airhorn/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/examples/appmaker/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/examples/avatar/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/examples/mytoken/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/examples/pingpong/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/logging.go create mode 100644 vendor/github.com/matterbridge/discordgo/message.go create mode 100644 vendor/github.com/matterbridge/discordgo/oauth2.go create mode 100644 vendor/github.com/matterbridge/discordgo/ratelimit.go create mode 100644 vendor/github.com/matterbridge/discordgo/restapi.go create mode 100644 vendor/github.com/matterbridge/discordgo/state.go create mode 100644 vendor/github.com/matterbridge/discordgo/structs.go create mode 100644 vendor/github.com/matterbridge/discordgo/tools/cmd/eventhandlers/main.go create mode 100644 vendor/github.com/matterbridge/discordgo/types.go create mode 100644 vendor/github.com/matterbridge/discordgo/user.go create mode 100644 vendor/github.com/matterbridge/discordgo/voice.go create mode 100644 vendor/github.com/matterbridge/discordgo/wsapi.go create mode 100644 vendor/github.com/matterbridge/logrus-prefixed-formatter/LICENSE create mode 100644 vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/basic.go create mode 100644 vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/themes.go create mode 100644 vendor/github.com/matterbridge/logrus-prefixed-formatter/formatter.go (limited to 'vendor/github.com/matterbridge') diff --git a/vendor/github.com/matterbridge/discordgo/LICENSE b/vendor/github.com/matterbridge/discordgo/LICENSE new file mode 100644 index 00000000..8d062ea5 --- /dev/null +++ b/vendor/github.com/matterbridge/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/matterbridge/discordgo/discord.go b/vendor/github.com/matterbridge/discordgo/discord.go new file mode 100644 index 00000000..99fda30b --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/discord.go @@ -0,0 +1,146 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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 ( + "errors" + "fmt" + "net/http" + "time" +) + +// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) +const VERSION = "0.18.0" + +// ErrMFA will be risen by New when the user has 2FA. +var ErrMFA = errors.New("account has 2FA enabled") + +// New creates a new Discord session and will automate some startup +// tasks if given enough information to do so. Currently you can pass zero +// 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. +// IF THE TOKEN IS FOR A BOT, IT MUST BE PREFIXED WITH `BOT ` +// eg: `"Bot "` +// 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. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. +func New(args ...interface{}) (s *Session, err error) { + + // Create an empty Session interface. + s = &Session{ + State: NewState(), + Ratelimiter: NewRatelimiter(), + StateEnabled: true, + Compress: true, + ShouldReconnectOnError: true, + ShardID: 0, + ShardCount: 1, + MaxRestRetries: 3, + Client: &http.Client{Timeout: (20 * time.Second)}, + sequence: new(int64), + LastHeartbeatAck: time.Now().UTC(), + } + + // If no arguments are passed return the empty Session interface. + 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 == "" { + if s.MFA { + err = ErrMFA + } else { + 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 +} diff --git a/vendor/github.com/matterbridge/discordgo/endpoints.go b/vendor/github.com/matterbridge/discordgo/endpoints.go new file mode 100644 index 00000000..335e224d --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/endpoints.go @@ -0,0 +1,136 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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 + +// APIVersion is the Discord API version used for the REST and Websocket API. +var APIVersion = "6" + +// 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/v" + APIVersion + "/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + EndpointGatewayBot = EndpointGateway + "/bot" + EndpointWebhooks = EndpointAPI + "webhooks/" + + EndpointCDN = "https://cdn.discordapp.com/" + EndpointCDNAttachments = EndpointCDN + "attachments/" + EndpointCDNAvatars = EndpointCDN + "avatars/" + EndpointCDNIcons = EndpointCDN + "icons/" + EndpointCDNSplashes = EndpointCDN + "splashes/" + EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" + + 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 EndpointCDNAvatars + uID + "/" + aID + ".png" } + EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } + + EndpointGuild = func(gID string) string { return EndpointGuilds + gID } + 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 } + EndpointGuildMemberRole = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID } + 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 EndpointCDNIcons + gID + "/" + hash + ".png" } + EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } + EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } + + EndpointChannel = func(cID string) string { return EndpointChannels + cID } + 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 } + + EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } + + EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } + EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } + EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } + + EndpointMessageReactionsAll = func(cID, mID string) string { + return EndpointChannelMessage(cID, mID) + "/reactions" + } + EndpointMessageReactions = func(cID, mID, eID string) string { + return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + } + EndpointMessageReaction = func(cID, mID, eID, uID string) string { + return EndpointMessageReactions(cID, mID, eID) + "/" + uID + } + + EndpointRelationships = func() string { return EndpointUsers + "@me" + "/relationships" } + EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } + EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } + + EndpointGuildCreate = EndpointAPI + "guilds" + + 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/matterbridge/discordgo/event.go b/vendor/github.com/matterbridge/discordgo/event.go new file mode 100644 index 00000000..bba396cb --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/event.go @@ -0,0 +1,238 @@ +package discordgo + +// EventHandler is an interface for Discord events. +type EventHandler interface { + // Type returns the type of event this handler belongs to. + Type() string + + // Handle is called whenever an event of Type() happens. + // It is the receivers responsibility to type assert that the interface + // is the expected struct. + Handle(*Session, interface{}) +} + +// EventInterfaceProvider is an interface for providing empty interfaces for +// Discord events. +type EventInterfaceProvider interface { + // Type is the type of event this handler belongs to. + Type() string + + // New returns a new instance of the struct this event handler handles. + // This is called once per event. + // The struct is provided to all handlers of the same Type(). + New() interface{} +} + +// interfaceEventType is the event handler type for interface{} events. +const interfaceEventType = "__INTERFACE__" + +// interfaceEventHandler is an event handler for interface{} events. +type interfaceEventHandler func(*Session, interface{}) + +// Type returns the event type for interface{} events. +func (eh interfaceEventHandler) Type() string { + return interfaceEventType +} + +// Handle is the handler for an interface{} event. +func (eh interfaceEventHandler) Handle(s *Session, i interface{}) { + eh(s, i) +} + +var registeredInterfaceProviders = map[string]EventInterfaceProvider{} + +// registerInterfaceProvider registers a provider so that DiscordGo can +// access it's New() method. +func registerInterfaceProvider(eh EventInterfaceProvider) { + if _, ok := registeredInterfaceProviders[eh.Type()]; ok { + return + // XXX: + // if we should error here, we need to do something with it. + // fmt.Errorf("event %s already registered", eh.Type()) + } + registeredInterfaceProviders[eh.Type()] = eh + return +} + +// eventHandlerInstance is a wrapper around an event handler, as functions +// cannot be compared directly. +type eventHandlerInstance struct { + eventHandler EventHandler +} + +// addEventHandler adds an event handler that will be fired anytime +// the Discord WSAPI matching eventHandler.Type() fires. +func (s *Session) addEventHandler(eventHandler EventHandler) func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + if s.handlers == nil { + s.handlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + s.handlers[eventHandler.Type()] = append(s.handlers[eventHandler.Type()], ehi) + + return func() { + s.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// addEventHandler adds an event handler that will be fired the next time +// the Discord WSAPI matching eventHandler.Type() fires. +func (s *Session) addEventHandlerOnce(eventHandler EventHandler) func() { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + if s.onceHandlers == nil { + s.onceHandlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + s.onceHandlers[eventHandler.Type()] = append(s.onceHandlers[eventHandler.Type()], ehi) + + return func() { + s.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// AddHandler allows you to add an event handler that will be fired anytime +// the Discord WSAPI event that matches the function fires. +// events.go contains all the Discord WSAPI events that can be fired. +// 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() { + eh := handlerForInterface(handler) + + if eh == nil { + s.log(LogError, "Invalid handler type, handler will never be called") + return func() {} + } + + return s.addEventHandler(eh) +} + +// AddHandlerOnce allows you to add an event handler that will be fired the next time +// the Discord WSAPI event that matches the function fires. +// See AddHandler for more details. +func (s *Session) AddHandlerOnce(handler interface{}) func() { + eh := handlerForInterface(handler) + + if eh == nil { + s.log(LogError, "Invalid handler type, handler will never be called") + return func() {} + } + + return s.addEventHandlerOnce(eh) +} + +// removeEventHandler instance removes an event handler instance. +func (s *Session) removeEventHandlerInstance(t string, ehi *eventHandlerInstance) { + s.handlersMu.Lock() + defer s.handlersMu.Unlock() + + handlers := s.handlers[t] + for i := range handlers { + if handlers[i] == ehi { + s.handlers[t] = append(handlers[:i], handlers[i+1:]...) + } + } + + onceHandlers := s.onceHandlers[t] + for i := range onceHandlers { + if onceHandlers[i] == ehi { + s.onceHandlers[t] = append(onceHandlers[:i], handlers[i+1:]...) + } + } +} + +// Handles calling permanent and once handlers for an event type. +func (s *Session) handle(t string, i interface{}) { + for _, eh := range s.handlers[t] { + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } + } + + if len(s.onceHandlers[t]) > 0 { + for _, eh := range s.onceHandlers[t] { + if s.SyncEvents { + eh.eventHandler.Handle(s, i) + } else { + go eh.eventHandler.Handle(s, i) + } + } + s.onceHandlers[t] = nil + } +} + +// Handles an event type by calling internal methods, firing handlers and firing the +// interface{} event. +func (s *Session) handleEvent(t string, i interface{}) { + s.handlersMu.RLock() + defer s.handlersMu.RUnlock() + + // All events are dispatched internally first. + s.onInterface(i) + + // Then they are dispatched to anyone handling interface{} events. + s.handle(interfaceEventType, i) + + // Finally they are dispatched to any typed handlers. + s.handle(t, i) +} + +// setGuildIds will set the GuildID on all the members of a guild. +// This is done as event data does not have it set. +func setGuildIds(g *Guild) { + for _, c := range g.Channels { + c.GuildID = g.ID + } + + for _, m := range g.Members { + m.GuildID = g.ID + } + + for _, vs := range g.VoiceStates { + vs.GuildID = g.ID + } +} + +// onInterface handles all internal events and routes them to the appropriate internal handler. +func (s *Session) onInterface(i interface{}) { + switch t := i.(type) { + case *Ready: + for _, g := range t.Guilds { + setGuildIds(g) + } + s.onReady(t) + case *GuildCreate: + setGuildIds(t.Guild) + case *GuildUpdate: + setGuildIds(t.Guild) + case *VoiceServerUpdate: + go s.onVoiceServerUpdate(t) + case *VoiceStateUpdate: + go s.onVoiceStateUpdate(t) + } + err := s.State.OnInterface(s, i) + if err != nil { + s.log(LogDebug, "error dispatching internal event, %s", err) + } +} + +// onReady handles the ready event. +func (s *Session) onReady(r *Ready) { + + // Store the SessionID within the Session struct. + s.sessionID = r.SessionID +} diff --git a/vendor/github.com/matterbridge/discordgo/eventhandlers.go b/vendor/github.com/matterbridge/discordgo/eventhandlers.go new file mode 100644 index 00000000..5cc157de --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/eventhandlers.go @@ -0,0 +1,1030 @@ +// Code generated by \"eventhandlers\"; DO NOT EDIT +// See events.go + +package discordgo + +// Following are all the event types. +// Event type values are used to match the events returned by Discord. +// EventTypes surrounded by __ are synthetic and are internal to DiscordGo. +const ( + channelCreateEventType = "CHANNEL_CREATE" + channelDeleteEventType = "CHANNEL_DELETE" + channelPinsUpdateEventType = "CHANNEL_PINS_UPDATE" + channelUpdateEventType = "CHANNEL_UPDATE" + connectEventType = "__CONNECT__" + disconnectEventType = "__DISCONNECT__" + eventEventType = "__EVENT__" + guildBanAddEventType = "GUILD_BAN_ADD" + guildBanRemoveEventType = "GUILD_BAN_REMOVE" + guildCreateEventType = "GUILD_CREATE" + guildDeleteEventType = "GUILD_DELETE" + guildEmojisUpdateEventType = "GUILD_EMOJIS_UPDATE" + guildIntegrationsUpdateEventType = "GUILD_INTEGRATIONS_UPDATE" + guildMemberAddEventType = "GUILD_MEMBER_ADD" + guildMemberRemoveEventType = "GUILD_MEMBER_REMOVE" + guildMemberUpdateEventType = "GUILD_MEMBER_UPDATE" + guildMembersChunkEventType = "GUILD_MEMBERS_CHUNK" + guildRoleCreateEventType = "GUILD_ROLE_CREATE" + guildRoleDeleteEventType = "GUILD_ROLE_DELETE" + guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" + guildUpdateEventType = "GUILD_UPDATE" + messageAckEventType = "MESSAGE_ACK" + messageCreateEventType = "MESSAGE_CREATE" + messageDeleteEventType = "MESSAGE_DELETE" + messageDeleteBulkEventType = "MESSAGE_DELETE_BULK" + messageReactionAddEventType = "MESSAGE_REACTION_ADD" + messageReactionRemoveEventType = "MESSAGE_REACTION_REMOVE" + messageReactionRemoveAllEventType = "MESSAGE_REACTION_REMOVE_ALL" + messageUpdateEventType = "MESSAGE_UPDATE" + presenceUpdateEventType = "PRESENCE_UPDATE" + presencesReplaceEventType = "PRESENCES_REPLACE" + rateLimitEventType = "__RATE_LIMIT__" + readyEventType = "READY" + relationshipAddEventType = "RELATIONSHIP_ADD" + relationshipRemoveEventType = "RELATIONSHIP_REMOVE" + resumedEventType = "RESUMED" + typingStartEventType = "TYPING_START" + userGuildSettingsUpdateEventType = "USER_GUILD_SETTINGS_UPDATE" + userNoteUpdateEventType = "USER_NOTE_UPDATE" + userSettingsUpdateEventType = "USER_SETTINGS_UPDATE" + userUpdateEventType = "USER_UPDATE" + voiceServerUpdateEventType = "VOICE_SERVER_UPDATE" + voiceStateUpdateEventType = "VOICE_STATE_UPDATE" +) + +// channelCreateEventHandler is an event handler for ChannelCreate events. +type channelCreateEventHandler func(*Session, *ChannelCreate) + +// Type returns the event type for ChannelCreate events. +func (eh channelCreateEventHandler) Type() string { + return channelCreateEventType +} + +// New returns a new instance of ChannelCreate. +func (eh channelCreateEventHandler) New() interface{} { + return &ChannelCreate{} +} + +// Handle is the handler for ChannelCreate events. +func (eh channelCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelCreate); ok { + eh(s, t) + } +} + +// channelDeleteEventHandler is an event handler for ChannelDelete events. +type channelDeleteEventHandler func(*Session, *ChannelDelete) + +// Type returns the event type for ChannelDelete events. +func (eh channelDeleteEventHandler) Type() string { + return channelDeleteEventType +} + +// New returns a new instance of ChannelDelete. +func (eh channelDeleteEventHandler) New() interface{} { + return &ChannelDelete{} +} + +// Handle is the handler for ChannelDelete events. +func (eh channelDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelDelete); ok { + eh(s, t) + } +} + +// channelPinsUpdateEventHandler is an event handler for ChannelPinsUpdate events. +type channelPinsUpdateEventHandler func(*Session, *ChannelPinsUpdate) + +// Type returns the event type for ChannelPinsUpdate events. +func (eh channelPinsUpdateEventHandler) Type() string { + return channelPinsUpdateEventType +} + +// New returns a new instance of ChannelPinsUpdate. +func (eh channelPinsUpdateEventHandler) New() interface{} { + return &ChannelPinsUpdate{} +} + +// Handle is the handler for ChannelPinsUpdate events. +func (eh channelPinsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelPinsUpdate); ok { + eh(s, t) + } +} + +// channelUpdateEventHandler is an event handler for ChannelUpdate events. +type channelUpdateEventHandler func(*Session, *ChannelUpdate) + +// Type returns the event type for ChannelUpdate events. +func (eh channelUpdateEventHandler) Type() string { + return channelUpdateEventType +} + +// New returns a new instance of ChannelUpdate. +func (eh channelUpdateEventHandler) New() interface{} { + return &ChannelUpdate{} +} + +// Handle is the handler for ChannelUpdate events. +func (eh channelUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ChannelUpdate); ok { + eh(s, t) + } +} + +// connectEventHandler is an event handler for Connect events. +type connectEventHandler func(*Session, *Connect) + +// Type returns the event type for Connect events. +func (eh connectEventHandler) Type() string { + return connectEventType +} + +// Handle is the handler for Connect events. +func (eh connectEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Connect); ok { + eh(s, t) + } +} + +// disconnectEventHandler is an event handler for Disconnect events. +type disconnectEventHandler func(*Session, *Disconnect) + +// Type returns the event type for Disconnect events. +func (eh disconnectEventHandler) Type() string { + return disconnectEventType +} + +// Handle is the handler for Disconnect events. +func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Disconnect); ok { + eh(s, t) + } +} + +// eventEventHandler is an event handler for Event events. +type eventEventHandler func(*Session, *Event) + +// Type returns the event type for Event events. +func (eh eventEventHandler) Type() string { + return eventEventType +} + +// Handle is the handler for Event events. +func (eh eventEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Event); ok { + eh(s, t) + } +} + +// guildBanAddEventHandler is an event handler for GuildBanAdd events. +type guildBanAddEventHandler func(*Session, *GuildBanAdd) + +// Type returns the event type for GuildBanAdd events. +func (eh guildBanAddEventHandler) Type() string { + return guildBanAddEventType +} + +// New returns a new instance of GuildBanAdd. +func (eh guildBanAddEventHandler) New() interface{} { + return &GuildBanAdd{} +} + +// Handle is the handler for GuildBanAdd events. +func (eh guildBanAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildBanAdd); ok { + eh(s, t) + } +} + +// guildBanRemoveEventHandler is an event handler for GuildBanRemove events. +type guildBanRemoveEventHandler func(*Session, *GuildBanRemove) + +// Type returns the event type for GuildBanRemove events. +func (eh guildBanRemoveEventHandler) Type() string { + return guildBanRemoveEventType +} + +// New returns a new instance of GuildBanRemove. +func (eh guildBanRemoveEventHandler) New() interface{} { + return &GuildBanRemove{} +} + +// Handle is the handler for GuildBanRemove events. +func (eh guildBanRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildBanRemove); ok { + eh(s, t) + } +} + +// guildCreateEventHandler is an event handler for GuildCreate events. +type guildCreateEventHandler func(*Session, *GuildCreate) + +// Type returns the event type for GuildCreate events. +func (eh guildCreateEventHandler) Type() string { + return guildCreateEventType +} + +// New returns a new instance of GuildCreate. +func (eh guildCreateEventHandler) New() interface{} { + return &GuildCreate{} +} + +// Handle is the handler for GuildCreate events. +func (eh guildCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildCreate); ok { + eh(s, t) + } +} + +// guildDeleteEventHandler is an event handler for GuildDelete events. +type guildDeleteEventHandler func(*Session, *GuildDelete) + +// Type returns the event type for GuildDelete events. +func (eh guildDeleteEventHandler) Type() string { + return guildDeleteEventType +} + +// New returns a new instance of GuildDelete. +func (eh guildDeleteEventHandler) New() interface{} { + return &GuildDelete{} +} + +// Handle is the handler for GuildDelete events. +func (eh guildDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildDelete); ok { + eh(s, t) + } +} + +// guildEmojisUpdateEventHandler is an event handler for GuildEmojisUpdate events. +type guildEmojisUpdateEventHandler func(*Session, *GuildEmojisUpdate) + +// Type returns the event type for GuildEmojisUpdate events. +func (eh guildEmojisUpdateEventHandler) Type() string { + return guildEmojisUpdateEventType +} + +// New returns a new instance of GuildEmojisUpdate. +func (eh guildEmojisUpdateEventHandler) New() interface{} { + return &GuildEmojisUpdate{} +} + +// Handle is the handler for GuildEmojisUpdate events. +func (eh guildEmojisUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildEmojisUpdate); ok { + eh(s, t) + } +} + +// guildIntegrationsUpdateEventHandler is an event handler for GuildIntegrationsUpdate events. +type guildIntegrationsUpdateEventHandler func(*Session, *GuildIntegrationsUpdate) + +// Type returns the event type for GuildIntegrationsUpdate events. +func (eh guildIntegrationsUpdateEventHandler) Type() string { + return guildIntegrationsUpdateEventType +} + +// New returns a new instance of GuildIntegrationsUpdate. +func (eh guildIntegrationsUpdateEventHandler) New() interface{} { + return &GuildIntegrationsUpdate{} +} + +// Handle is the handler for GuildIntegrationsUpdate events. +func (eh guildIntegrationsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildIntegrationsUpdate); ok { + eh(s, t) + } +} + +// guildMemberAddEventHandler is an event handler for GuildMemberAdd events. +type guildMemberAddEventHandler func(*Session, *GuildMemberAdd) + +// Type returns the event type for GuildMemberAdd events. +func (eh guildMemberAddEventHandler) Type() string { + return guildMemberAddEventType +} + +// New returns a new instance of GuildMemberAdd. +func (eh guildMemberAddEventHandler) New() interface{} { + return &GuildMemberAdd{} +} + +// Handle is the handler for GuildMemberAdd events. +func (eh guildMemberAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberAdd); ok { + eh(s, t) + } +} + +// guildMemberRemoveEventHandler is an event handler for GuildMemberRemove events. +type guildMemberRemoveEventHandler func(*Session, *GuildMemberRemove) + +// Type returns the event type for GuildMemberRemove events. +func (eh guildMemberRemoveEventHandler) Type() string { + return guildMemberRemoveEventType +} + +// New returns a new instance of GuildMemberRemove. +func (eh guildMemberRemoveEventHandler) New() interface{} { + return &GuildMemberRemove{} +} + +// Handle is the handler for GuildMemberRemove events. +func (eh guildMemberRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberRemove); ok { + eh(s, t) + } +} + +// guildMemberUpdateEventHandler is an event handler for GuildMemberUpdate events. +type guildMemberUpdateEventHandler func(*Session, *GuildMemberUpdate) + +// Type returns the event type for GuildMemberUpdate events. +func (eh guildMemberUpdateEventHandler) Type() string { + return guildMemberUpdateEventType +} + +// New returns a new instance of GuildMemberUpdate. +func (eh guildMemberUpdateEventHandler) New() interface{} { + return &GuildMemberUpdate{} +} + +// Handle is the handler for GuildMemberUpdate events. +func (eh guildMemberUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMemberUpdate); ok { + eh(s, t) + } +} + +// guildMembersChunkEventHandler is an event handler for GuildMembersChunk events. +type guildMembersChunkEventHandler func(*Session, *GuildMembersChunk) + +// Type returns the event type for GuildMembersChunk events. +func (eh guildMembersChunkEventHandler) Type() string { + return guildMembersChunkEventType +} + +// New returns a new instance of GuildMembersChunk. +func (eh guildMembersChunkEventHandler) New() interface{} { + return &GuildMembersChunk{} +} + +// Handle is the handler for GuildMembersChunk events. +func (eh guildMembersChunkEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildMembersChunk); ok { + eh(s, t) + } +} + +// guildRoleCreateEventHandler is an event handler for GuildRoleCreate events. +type guildRoleCreateEventHandler func(*Session, *GuildRoleCreate) + +// Type returns the event type for GuildRoleCreate events. +func (eh guildRoleCreateEventHandler) Type() string { + return guildRoleCreateEventType +} + +// New returns a new instance of GuildRoleCreate. +func (eh guildRoleCreateEventHandler) New() interface{} { + return &GuildRoleCreate{} +} + +// Handle is the handler for GuildRoleCreate events. +func (eh guildRoleCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleCreate); ok { + eh(s, t) + } +} + +// guildRoleDeleteEventHandler is an event handler for GuildRoleDelete events. +type guildRoleDeleteEventHandler func(*Session, *GuildRoleDelete) + +// Type returns the event type for GuildRoleDelete events. +func (eh guildRoleDeleteEventHandler) Type() string { + return guildRoleDeleteEventType +} + +// New returns a new instance of GuildRoleDelete. +func (eh guildRoleDeleteEventHandler) New() interface{} { + return &GuildRoleDelete{} +} + +// Handle is the handler for GuildRoleDelete events. +func (eh guildRoleDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleDelete); ok { + eh(s, t) + } +} + +// guildRoleUpdateEventHandler is an event handler for GuildRoleUpdate events. +type guildRoleUpdateEventHandler func(*Session, *GuildRoleUpdate) + +// Type returns the event type for GuildRoleUpdate events. +func (eh guildRoleUpdateEventHandler) Type() string { + return guildRoleUpdateEventType +} + +// New returns a new instance of GuildRoleUpdate. +func (eh guildRoleUpdateEventHandler) New() interface{} { + return &GuildRoleUpdate{} +} + +// Handle is the handler for GuildRoleUpdate events. +func (eh guildRoleUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildRoleUpdate); ok { + eh(s, t) + } +} + +// guildUpdateEventHandler is an event handler for GuildUpdate events. +type guildUpdateEventHandler func(*Session, *GuildUpdate) + +// Type returns the event type for GuildUpdate events. +func (eh guildUpdateEventHandler) Type() string { + return guildUpdateEventType +} + +// New returns a new instance of GuildUpdate. +func (eh guildUpdateEventHandler) New() interface{} { + return &GuildUpdate{} +} + +// Handle is the handler for GuildUpdate events. +func (eh guildUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildUpdate); ok { + eh(s, t) + } +} + +// messageAckEventHandler is an event handler for MessageAck events. +type messageAckEventHandler func(*Session, *MessageAck) + +// Type returns the event type for MessageAck events. +func (eh messageAckEventHandler) Type() string { + return messageAckEventType +} + +// New returns a new instance of MessageAck. +func (eh messageAckEventHandler) New() interface{} { + return &MessageAck{} +} + +// Handle is the handler for MessageAck events. +func (eh messageAckEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageAck); ok { + eh(s, t) + } +} + +// messageCreateEventHandler is an event handler for MessageCreate events. +type messageCreateEventHandler func(*Session, *MessageCreate) + +// Type returns the event type for MessageCreate events. +func (eh messageCreateEventHandler) Type() string { + return messageCreateEventType +} + +// New returns a new instance of MessageCreate. +func (eh messageCreateEventHandler) New() interface{} { + return &MessageCreate{} +} + +// Handle is the handler for MessageCreate events. +func (eh messageCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageCreate); ok { + eh(s, t) + } +} + +// messageDeleteEventHandler is an event handler for MessageDelete events. +type messageDeleteEventHandler func(*Session, *MessageDelete) + +// Type returns the event type for MessageDelete events. +func (eh messageDeleteEventHandler) Type() string { + return messageDeleteEventType +} + +// New returns a new instance of MessageDelete. +func (eh messageDeleteEventHandler) New() interface{} { + return &MessageDelete{} +} + +// Handle is the handler for MessageDelete events. +func (eh messageDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageDelete); ok { + eh(s, t) + } +} + +// messageDeleteBulkEventHandler is an event handler for MessageDeleteBulk events. +type messageDeleteBulkEventHandler func(*Session, *MessageDeleteBulk) + +// Type returns the event type for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Type() string { + return messageDeleteBulkEventType +} + +// New returns a new instance of MessageDeleteBulk. +func (eh messageDeleteBulkEventHandler) New() interface{} { + return &MessageDeleteBulk{} +} + +// Handle is the handler for MessageDeleteBulk events. +func (eh messageDeleteBulkEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageDeleteBulk); ok { + eh(s, t) + } +} + +// messageReactionAddEventHandler is an event handler for MessageReactionAdd events. +type messageReactionAddEventHandler func(*Session, *MessageReactionAdd) + +// Type returns the event type for MessageReactionAdd events. +func (eh messageReactionAddEventHandler) Type() string { + return messageReactionAddEventType +} + +// New returns a new instance of MessageReactionAdd. +func (eh messageReactionAddEventHandler) New() interface{} { + return &MessageReactionAdd{} +} + +// Handle is the handler for MessageReactionAdd events. +func (eh messageReactionAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionAdd); ok { + eh(s, t) + } +} + +// messageReactionRemoveEventHandler is an event handler for MessageReactionRemove events. +type messageReactionRemoveEventHandler func(*Session, *MessageReactionRemove) + +// Type returns the event type for MessageReactionRemove events. +func (eh messageReactionRemoveEventHandler) Type() string { + return messageReactionRemoveEventType +} + +// New returns a new instance of MessageReactionRemove. +func (eh messageReactionRemoveEventHandler) New() interface{} { + return &MessageReactionRemove{} +} + +// Handle is the handler for MessageReactionRemove events. +func (eh messageReactionRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionRemove); ok { + eh(s, t) + } +} + +// messageReactionRemoveAllEventHandler is an event handler for MessageReactionRemoveAll events. +type messageReactionRemoveAllEventHandler func(*Session, *MessageReactionRemoveAll) + +// Type returns the event type for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Type() string { + return messageReactionRemoveAllEventType +} + +// New returns a new instance of MessageReactionRemoveAll. +func (eh messageReactionRemoveAllEventHandler) New() interface{} { + return &MessageReactionRemoveAll{} +} + +// Handle is the handler for MessageReactionRemoveAll events. +func (eh messageReactionRemoveAllEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageReactionRemoveAll); ok { + eh(s, t) + } +} + +// messageUpdateEventHandler is an event handler for MessageUpdate events. +type messageUpdateEventHandler func(*Session, *MessageUpdate) + +// Type returns the event type for MessageUpdate events. +func (eh messageUpdateEventHandler) Type() string { + return messageUpdateEventType +} + +// New returns a new instance of MessageUpdate. +func (eh messageUpdateEventHandler) New() interface{} { + return &MessageUpdate{} +} + +// Handle is the handler for MessageUpdate events. +func (eh messageUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*MessageUpdate); ok { + eh(s, t) + } +} + +// presenceUpdateEventHandler is an event handler for PresenceUpdate events. +type presenceUpdateEventHandler func(*Session, *PresenceUpdate) + +// Type returns the event type for PresenceUpdate events. +func (eh presenceUpdateEventHandler) Type() string { + return presenceUpdateEventType +} + +// New returns a new instance of PresenceUpdate. +func (eh presenceUpdateEventHandler) New() interface{} { + return &PresenceUpdate{} +} + +// Handle is the handler for PresenceUpdate events. +func (eh presenceUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*PresenceUpdate); ok { + eh(s, t) + } +} + +// presencesReplaceEventHandler is an event handler for PresencesReplace events. +type presencesReplaceEventHandler func(*Session, *PresencesReplace) + +// Type returns the event type for PresencesReplace events. +func (eh presencesReplaceEventHandler) Type() string { + return presencesReplaceEventType +} + +// New returns a new instance of PresencesReplace. +func (eh presencesReplaceEventHandler) New() interface{} { + return &PresencesReplace{} +} + +// Handle is the handler for PresencesReplace events. +func (eh presencesReplaceEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*PresencesReplace); ok { + eh(s, t) + } +} + +// rateLimitEventHandler is an event handler for RateLimit events. +type rateLimitEventHandler func(*Session, *RateLimit) + +// Type returns the event type for RateLimit events. +func (eh rateLimitEventHandler) Type() string { + return rateLimitEventType +} + +// Handle is the handler for RateLimit events. +func (eh rateLimitEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RateLimit); ok { + eh(s, t) + } +} + +// readyEventHandler is an event handler for Ready events. +type readyEventHandler func(*Session, *Ready) + +// Type returns the event type for Ready events. +func (eh readyEventHandler) Type() string { + return readyEventType +} + +// New returns a new instance of Ready. +func (eh readyEventHandler) New() interface{} { + return &Ready{} +} + +// Handle is the handler for Ready events. +func (eh readyEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Ready); ok { + eh(s, t) + } +} + +// relationshipAddEventHandler is an event handler for RelationshipAdd events. +type relationshipAddEventHandler func(*Session, *RelationshipAdd) + +// Type returns the event type for RelationshipAdd events. +func (eh relationshipAddEventHandler) Type() string { + return relationshipAddEventType +} + +// New returns a new instance of RelationshipAdd. +func (eh relationshipAddEventHandler) New() interface{} { + return &RelationshipAdd{} +} + +// Handle is the handler for RelationshipAdd events. +func (eh relationshipAddEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RelationshipAdd); ok { + eh(s, t) + } +} + +// relationshipRemoveEventHandler is an event handler for RelationshipRemove events. +type relationshipRemoveEventHandler func(*Session, *RelationshipRemove) + +// Type returns the event type for RelationshipRemove events. +func (eh relationshipRemoveEventHandler) Type() string { + return relationshipRemoveEventType +} + +// New returns a new instance of RelationshipRemove. +func (eh relationshipRemoveEventHandler) New() interface{} { + return &RelationshipRemove{} +} + +// Handle is the handler for RelationshipRemove events. +func (eh relationshipRemoveEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*RelationshipRemove); ok { + eh(s, t) + } +} + +// resumedEventHandler is an event handler for Resumed events. +type resumedEventHandler func(*Session, *Resumed) + +// Type returns the event type for Resumed events. +func (eh resumedEventHandler) Type() string { + return resumedEventType +} + +// New returns a new instance of Resumed. +func (eh resumedEventHandler) New() interface{} { + return &Resumed{} +} + +// Handle is the handler for Resumed events. +func (eh resumedEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*Resumed); ok { + eh(s, t) + } +} + +// typingStartEventHandler is an event handler for TypingStart events. +type typingStartEventHandler func(*Session, *TypingStart) + +// Type returns the event type for TypingStart events. +func (eh typingStartEventHandler) Type() string { + return typingStartEventType +} + +// New returns a new instance of TypingStart. +func (eh typingStartEventHandler) New() interface{} { + return &TypingStart{} +} + +// Handle is the handler for TypingStart events. +func (eh typingStartEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*TypingStart); ok { + eh(s, t) + } +} + +// userGuildSettingsUpdateEventHandler is an event handler for UserGuildSettingsUpdate events. +type userGuildSettingsUpdateEventHandler func(*Session, *UserGuildSettingsUpdate) + +// Type returns the event type for UserGuildSettingsUpdate events. +func (eh userGuildSettingsUpdateEventHandler) Type() string { + return userGuildSettingsUpdateEventType +} + +// New returns a new instance of UserGuildSettingsUpdate. +func (eh userGuildSettingsUpdateEventHandler) New() interface{} { + return &UserGuildSettingsUpdate{} +} + +// Handle is the handler for UserGuildSettingsUpdate events. +func (eh userGuildSettingsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserGuildSettingsUpdate); ok { + eh(s, t) + } +} + +// userNoteUpdateEventHandler is an event handler for UserNoteUpdate events. +type userNoteUpdateEventHandler func(*Session, *UserNoteUpdate) + +// Type returns the event type for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Type() string { + return userNoteUpdateEventType +} + +// New returns a new instance of UserNoteUpdate. +func (eh userNoteUpdateEventHandler) New() interface{} { + return &UserNoteUpdate{} +} + +// Handle is the handler for UserNoteUpdate events. +func (eh userNoteUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserNoteUpdate); ok { + eh(s, t) + } +} + +// userSettingsUpdateEventHandler is an event handler for UserSettingsUpdate events. +type userSettingsUpdateEventHandler func(*Session, *UserSettingsUpdate) + +// Type returns the event type for UserSettingsUpdate events. +func (eh userSettingsUpdateEventHandler) Type() string { + return userSettingsUpdateEventType +} + +// New returns a new instance of UserSettingsUpdate. +func (eh userSettingsUpdateEventHandler) New() interface{} { + return &UserSettingsUpdate{} +} + +// Handle is the handler for UserSettingsUpdate events. +func (eh userSettingsUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserSettingsUpdate); ok { + eh(s, t) + } +} + +// userUpdateEventHandler is an event handler for UserUpdate events. +type userUpdateEventHandler func(*Session, *UserUpdate) + +// Type returns the event type for UserUpdate events. +func (eh userUpdateEventHandler) Type() string { + return userUpdateEventType +} + +// New returns a new instance of UserUpdate. +func (eh userUpdateEventHandler) New() interface{} { + return &UserUpdate{} +} + +// Handle is the handler for UserUpdate events. +func (eh userUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*UserUpdate); ok { + eh(s, t) + } +} + +// voiceServerUpdateEventHandler is an event handler for VoiceServerUpdate events. +type voiceServerUpdateEventHandler func(*Session, *VoiceServerUpdate) + +// Type returns the event type for VoiceServerUpdate events. +func (eh voiceServerUpdateEventHandler) Type() string { + return voiceServerUpdateEventType +} + +// New returns a new instance of VoiceServerUpdate. +func (eh voiceServerUpdateEventHandler) New() interface{} { + return &VoiceServerUpdate{} +} + +// Handle is the handler for VoiceServerUpdate events. +func (eh voiceServerUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*VoiceServerUpdate); ok { + eh(s, t) + } +} + +// voiceStateUpdateEventHandler is an event handler for VoiceStateUpdate events. +type voiceStateUpdateEventHandler func(*Session, *VoiceStateUpdate) + +// Type returns the event type for VoiceStateUpdate events. +func (eh voiceStateUpdateEventHandler) Type() string { + return voiceStateUpdateEventType +} + +// New returns a new instance of VoiceStateUpdate. +func (eh voiceStateUpdateEventHandler) New() interface{} { + return &VoiceStateUpdate{} +} + +// Handle is the handler for VoiceStateUpdate events. +func (eh voiceStateUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*VoiceStateUpdate); ok { + eh(s, t) + } +} + +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Session, interface{}): + return interfaceEventHandler(v) + case func(*Session, *ChannelCreate): + return channelCreateEventHandler(v) + case func(*Session, *ChannelDelete): + return channelDeleteEventHandler(v) + case func(*Session, *ChannelPinsUpdate): + return channelPinsUpdateEventHandler(v) + case func(*Session, *ChannelUpdate): + return channelUpdateEventHandler(v) + case func(*Session, *Connect): + return connectEventHandler(v) + case func(*Session, *Disconnect): + return disconnectEventHandler(v) + case func(*Session, *Event): + return eventEventHandler(v) + case func(*Session, *GuildBanAdd): + return guildBanAddEventHandler(v) + case func(*Session, *GuildBanRemove): + return guildBanRemoveEventHandler(v) + case func(*Session, *GuildCreate): + return guildCreateEventHandler(v) + case func(*Session, *GuildDelete): + return guildDeleteEventHandler(v) + case func(*Session, *GuildEmojisUpdate): + return guildEmojisUpdateEventHandler(v) + case func(*Session, *GuildIntegrationsUpdate): + return guildIntegrationsUpdateEventHandler(v) + case func(*Session, *GuildMemberAdd): + return guildMemberAddEventHandler(v) + case func(*Session, *GuildMemberRemove): + return guildMemberRemoveEventHandler(v) + case func(*Session, *GuildMemberUpdate): + return guildMemberUpdateEventHandler(v) + case func(*Session, *GuildMembersChunk): + return guildMembersChunkEventHandler(v) + case func(*Session, *GuildRoleCreate): + return guildRoleCreateEventHandler(v) + case func(*Session, *GuildRoleDelete): + return guildRoleDeleteEventHandler(v) + case func(*Session, *GuildRoleUpdate): + return guildRoleUpdateEventHandler(v) + case func(*Session, *GuildUpdate): + return guildUpdateEventHandler(v) + case func(*Session, *MessageAck): + return messageAckEventHandler(v) + case func(*Session, *MessageCreate): + return messageCreateEventHandler(v) + case func(*Session, *MessageDelete): + return messageDeleteEventHandler(v) + case func(*Session, *MessageDeleteBulk): + return messageDeleteBulkEventHandler(v) + case func(*Session, *MessageReactionAdd): + return messageReactionAddEventHandler(v) + case func(*Session, *MessageReactionRemove): + return messageReactionRemoveEventHandler(v) + case func(*Session, *MessageReactionRemoveAll): + return messageReactionRemoveAllEventHandler(v) + case func(*Session, *MessageUpdate): + return messageUpdateEventHandler(v) + case func(*Session, *PresenceUpdate): + return presenceUpdateEventHandler(v) + case func(*Session, *PresencesReplace): + return presencesReplaceEventHandler(v) + case func(*Session, *RateLimit): + return rateLimitEventHandler(v) + case func(*Session, *Ready): + return readyEventHandler(v) + case func(*Session, *RelationshipAdd): + return relationshipAddEventHandler(v) + case func(*Session, *RelationshipRemove): + return relationshipRemoveEventHandler(v) + case func(*Session, *Resumed): + return resumedEventHandler(v) + case func(*Session, *TypingStart): + return typingStartEventHandler(v) + case func(*Session, *UserGuildSettingsUpdate): + return userGuildSettingsUpdateEventHandler(v) + case func(*Session, *UserNoteUpdate): + return userNoteUpdateEventHandler(v) + case func(*Session, *UserSettingsUpdate): + return userSettingsUpdateEventHandler(v) + case func(*Session, *UserUpdate): + return userUpdateEventHandler(v) + case func(*Session, *VoiceServerUpdate): + return voiceServerUpdateEventHandler(v) + case func(*Session, *VoiceStateUpdate): + return voiceStateUpdateEventHandler(v) + } + + return nil +} + +func init() { + registerInterfaceProvider(channelCreateEventHandler(nil)) + registerInterfaceProvider(channelDeleteEventHandler(nil)) + registerInterfaceProvider(channelPinsUpdateEventHandler(nil)) + registerInterfaceProvider(channelUpdateEventHandler(nil)) + registerInterfaceProvider(guildBanAddEventHandler(nil)) + registerInterfaceProvider(guildBanRemoveEventHandler(nil)) + registerInterfaceProvider(guildCreateEventHandler(nil)) + registerInterfaceProvider(guildDeleteEventHandler(nil)) + registerInterfaceProvider(guildEmojisUpdateEventHandler(nil)) + registerInterfaceProvider(guildIntegrationsUpdateEventHandler(nil)) + registerInterfaceProvider(guildMemberAddEventHandler(nil)) + registerInterfaceProvider(guildMemberRemoveEventHandler(nil)) + registerInterfaceProvider(guildMemberUpdateEventHandler(nil)) + registerInterfaceProvider(guildMembersChunkEventHandler(nil)) + registerInterfaceProvider(guildRoleCreateEventHandler(nil)) + registerInterfaceProvider(guildRoleDeleteEventHandler(nil)) + registerInterfaceProvider(guildRoleUpdateEventHandler(nil)) + registerInterfaceProvider(guildUpdateEventHandler(nil)) + registerInterfaceProvider(messageAckEventHandler(nil)) + registerInterfaceProvider(messageCreateEventHandler(nil)) + registerInterfaceProvider(messageDeleteEventHandler(nil)) + registerInterfaceProvider(messageDeleteBulkEventHandler(nil)) + registerInterfaceProvider(messageReactionAddEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveEventHandler(nil)) + registerInterfaceProvider(messageReactionRemoveAllEventHandler(nil)) + registerInterfaceProvider(messageUpdateEventHandler(nil)) + registerInterfaceProvider(presenceUpdateEventHandler(nil)) + registerInterfaceProvider(presencesReplaceEventHandler(nil)) + registerInterfaceProvider(readyEventHandler(nil)) + registerInterfaceProvider(relationshipAddEventHandler(nil)) + registerInterfaceProvider(relationshipRemoveEventHandler(nil)) + registerInterfaceProvider(resumedEventHandler(nil)) + registerInterfaceProvider(typingStartEventHandler(nil)) + registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userNoteUpdateEventHandler(nil)) + registerInterfaceProvider(userSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userUpdateEventHandler(nil)) + registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) + registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) +} diff --git a/vendor/github.com/matterbridge/discordgo/events.go b/vendor/github.com/matterbridge/discordgo/events.go new file mode 100644 index 00000000..c78fbdd2 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/events.go @@ -0,0 +1,253 @@ +package discordgo + +import ( + "encoding/json" +) + +// This file contains all the possible structs that can be +// handled by AddHandler/EventHandler. +// DO NOT ADD ANYTHING BUT EVENT HANDLER STRUCTS TO THIS FILE. +//go:generate go run tools/cmd/eventhandlers/main.go + +// Connect is the data for a Connect event. +// This is a sythetic event and is not dispatched by Discord. +type Connect struct{} + +// Disconnect is the data for a Disconnect event. +// This is a sythetic event and is not dispatched by Discord. +type Disconnect struct{} + +// RateLimit is the data for a RateLimit event. +// This is a sythetic event and is not dispatched by Discord. +type RateLimit struct { + *TooManyRequests + URL string +} + +// Event provides a basic initial struct for all websocket events. +type Event struct { + Operation int `json:"op"` + Sequence int64 `json:"s"` + Type string `json:"t"` + RawData json.RawMessage `json:"d"` + // Struct contains one of the other types in this file. + 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"` + User *User `json:"user"` + ReadState []*ReadState `json:"read_state"` + PrivateChannels []*Channel `json:"private_channels"` + Guilds []*Guild `json:"guilds"` + + // Undocumented fields + Settings *Settings `json:"user_settings"` + UserGuildSettings []*UserGuildSettings `json:"user_guild_settings"` + Relationships []*Relationship `json:"relationships"` + Presences []*Presence `json:"presences"` + Notes map[string]string `json:"notes"` +} + +// ChannelCreate is the data for a ChannelCreate event. +type ChannelCreate struct { + *Channel +} + +// ChannelUpdate is the data for a ChannelUpdate event. +type ChannelUpdate struct { + *Channel +} + +// ChannelDelete is the data for a ChannelDelete event. +type ChannelDelete struct { + *Channel +} + +// ChannelPinsUpdate stores data for a ChannelPinsUpdate event. +type ChannelPinsUpdate struct { + LastPinTimestamp string `json:"last_pin_timestamp"` + ChannelID string `json:"channel_id"` +} + +// GuildCreate is the data for a GuildCreate event. +type GuildCreate struct { + *Guild +} + +// GuildUpdate is the data for a GuildUpdate event. +type GuildUpdate struct { + *Guild +} + +// GuildDelete is the data for a GuildDelete event. +type GuildDelete struct { + *Guild +} + +// GuildBanAdd is the data for a GuildBanAdd event. +type GuildBanAdd struct { + User *User `json:"user"` + GuildID string `json:"guild_id"` +} + +// GuildBanRemove is the data for a GuildBanRemove event. +type GuildBanRemove struct { + User *User `json:"user"` + GuildID string `json:"guild_id"` +} + +// GuildMemberAdd is the data for a GuildMemberAdd event. +type GuildMemberAdd struct { + *Member +} + +// GuildMemberUpdate is the data for a GuildMemberUpdate event. +type GuildMemberUpdate struct { + *Member +} + +// GuildMemberRemove is the data for a GuildMemberRemove event. +type GuildMemberRemove struct { + *Member +} + +// GuildRoleCreate is the data for a GuildRoleCreate event. +type GuildRoleCreate struct { + *GuildRole +} + +// GuildRoleUpdate is the data for a GuildRoleUpdate event. +type GuildRoleUpdate struct { + *GuildRole +} + +// A GuildRoleDelete is the data for a GuildRoleDelete event. +type GuildRoleDelete struct { + RoleID string `json:"role_id"` + GuildID string `json:"guild_id"` +} + +// A GuildEmojisUpdate is the data for a guild emoji update event. +type GuildEmojisUpdate struct { + GuildID string `json:"guild_id"` + Emojis []*Emoji `json:"emojis"` +} + +// A GuildMembersChunk is the data for a GuildMembersChunk event. +type GuildMembersChunk struct { + GuildID string `json:"guild_id"` + Members []*Member `json:"members"` +} + +// GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event. +type GuildIntegrationsUpdate struct { + GuildID string `json:"guild_id"` +} + +// MessageAck is the data for a MessageAck event. +type MessageAck struct { + MessageID string `json:"message_id"` + ChannelID string `json:"channel_id"` +} + +// MessageCreate is the data for a MessageCreate event. +type MessageCreate struct { + *Message +} + +// MessageUpdate is the data for a MessageUpdate event. +type MessageUpdate struct { + *Message +} + +// MessageDelete is the data for a MessageDelete event. +type MessageDelete struct { + *Message +} + +// MessageReactionAdd is the data for a MessageReactionAdd event. +type MessageReactionAdd struct { + *MessageReaction +} + +// MessageReactionRemove is the data for a MessageReactionRemove event. +type MessageReactionRemove struct { + *MessageReaction +} + +// MessageReactionRemoveAll is the data for a MessageReactionRemoveAll event. +type MessageReactionRemoveAll struct { + *MessageReaction +} + +// PresencesReplace is the data for a PresencesReplace event. +type PresencesReplace []*Presence + +// PresenceUpdate is the data for a PresenceUpdate event. +type PresenceUpdate struct { + Presence + GuildID string `json:"guild_id"` + Roles []string `json:"roles"` +} + +// Resumed is the data for a Resumed event. +type Resumed struct { + Trace []string `json:"_trace"` +} + +// RelationshipAdd is the data for a RelationshipAdd event. +type RelationshipAdd struct { + *Relationship +} + +// RelationshipRemove is the data for a RelationshipRemove event. +type RelationshipRemove struct { + *Relationship +} + +// TypingStart is the data for a TypingStart event. +type TypingStart struct { + UserID string `json:"user_id"` + ChannelID string `json:"channel_id"` + Timestamp int `json:"timestamp"` +} + +// UserUpdate is the data for a UserUpdate event. +type UserUpdate struct { + *User +} + +// UserSettingsUpdate is the data for a UserSettingsUpdate event. +type UserSettingsUpdate map[string]interface{} + +// UserGuildSettingsUpdate is the data for a UserGuildSettingsUpdate event. +type UserGuildSettingsUpdate struct { + *UserGuildSettings +} + +// UserNoteUpdate is the data for a UserNoteUpdate event. +type UserNoteUpdate struct { + ID string `json:"id"` + Note string `json:"note"` +} + +// VoiceServerUpdate is the data for a VoiceServerUpdate event. +type VoiceServerUpdate struct { + Token string `json:"token"` + GuildID string `json:"guild_id"` + Endpoint string `json:"endpoint"` +} + +// VoiceStateUpdate is the data for a VoiceStateUpdate event. +type VoiceStateUpdate struct { + *VoiceState +} + +// MessageDeleteBulk is the data for a MessageDeleteBulk event +type MessageDeleteBulk struct { + Messages []string `json:"ids"` + ChannelID string `json:"channel_id"` +} diff --git a/vendor/github.com/matterbridge/discordgo/examples/airhorn/main.go b/vendor/github.com/matterbridge/discordgo/examples/airhorn/main.go new file mode 100644 index 00000000..21ceb76b --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/examples/airhorn/main.go @@ -0,0 +1,211 @@ +package main + +import ( + "encoding/binary" + "flag" + "fmt" + "io" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/bwmarrin/discordgo" +) + +func init() { + flag.StringVar(&token, "t", "", "Bot Token") + flag.Parse() +} + +var token string +var buffer = make([][]byte, 0) + +func main() { + + if token == "" { + fmt.Println("No token provided. Please run: airhorn -t ") + 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 bot token. + dg, err := discordgo.New("Bot " + 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) + } + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Airhorn is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() +} + +// This function will be called (due to AddHandler above) when the bot receives +// the "ready" event from Discord. +func ready(s *discordgo.Session, event *discordgo.Ready) { + + // Set the playing status. + s.UpdateStatus(0, "!airhorn") +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + + // check if the message is "!airhorn" + if strings.HasPrefix(m.Content, "!airhorn") { + + // Find the channel that the message came from. + c, err := s.State.Channel(m.ChannelID) + if err != nil { + // 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 guild's 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 { + 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 { + err := file.Close() + if err != nil { + return err + } + 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/matterbridge/discordgo/examples/appmaker/main.go b/vendor/github.com/matterbridge/discordgo/examples/appmaker/main.go new file mode 100644 index 00000000..5581dd93 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/examples/appmaker/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line options +var ( + Token string + Name string + DeleteID string + ListOnly bool +) + +func init() { + + flag.StringVar(&Token, "t", "", "Owner Account Token") + flag.StringVar(&Name, "n", "", "Name to give App/Bot") + flag.StringVar(&DeleteID, "d", "", "Application ID to delete") + flag.BoolVar(&ListOnly, "l", false, "List Applications Only") + flag.Parse() + + if Token == "" { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + var err error + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New(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, err := dg.Applications() + if err != nil { + fmt.Println("error fetching applications,", err) + return + } + + for _, v := range aps { + fmt.Println("-----------------------------------------------------") + b, _ := json.MarshalIndent(v, "", " ") + fmt.Println(string(b)) + } + return + } + + // if -d set, delete the given Application + if DeleteID != "" { + err = dg.ApplicationDelete(DeleteID) + if err != nil { + fmt.Println("error deleting application,", err) + } + return + } + + if Name == "" { + flag.Usage() + os.Exit(1) + } + + // Create a new application. + ap := &discordgo.Application{} + ap.Name = Name + ap, err = dg.ApplicationCreate(ap) + if err != nil { + fmt.Println("error creating new application,", err) + return + } + + fmt.Printf("Application created successfully:\n") + b, _ := json.MarshalIndent(ap, "", " ") + fmt.Println(string(b)) + + // 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") + b, _ = json.MarshalIndent(bot, "", " ") + fmt.Println(string(b)) + + fmt.Println("Please save the above posted info in a secure place.") + fmt.Println("You will need that information to login with your bot account.") +} diff --git a/vendor/github.com/matterbridge/discordgo/examples/avatar/main.go b/vendor/github.com/matterbridge/discordgo/examples/avatar/main.go new file mode 100644 index 00000000..e0a9c880 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/examples/avatar/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "encoding/base64" + "flag" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Token string + AvatarFile string + AvatarURL string +) + +func init() { + + flag.StringVar(&Token, "t", "", "Bot Token") + flag.StringVar(&AvatarFile, "f", "", "Avatar File Name") + flag.StringVar(&AvatarURL, "u", "", "URL to the avatar image") + flag.Parse() + + if Token == "" || (AvatarFile == "" && AvatarURL == "") { + flag.Usage() + os.Exit(1) + } +} + +func main() { + + // Create a new Discord session using the provided login information. + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Declare these here so they can be used in the below two if blocks and + // still carry over to the end of this function. + var base64img string + var contentType string + + // If we're using a URL link for the Avatar + if AvatarURL != "" { + + resp, err := http.Get(AvatarURL) + if err != nil { + fmt.Println("Error retrieving the file, ", err) + return + } + + defer func() { + _ = resp.Body.Close() + }() + + img, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading the response, ", err) + return + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // If we're using a local file for the Avatar + if AvatarFile != "" { + img, err := ioutil.ReadFile(AvatarFile) + if err != nil { + fmt.Println(err) + } + + contentType = http.DetectContentType(img) + base64img = base64.StdEncoding.EncodeToString(img) + } + + // Now lets format our base64 image into the proper format Discord wants + // and then call UserUpdate to set it as our user's Avatar. + avatar := fmt.Sprintf("data:%s;base64,%s", contentType, base64img) + _, err = dg.UserUpdate("", "", "", avatar, "") + if err != nil { + fmt.Println(err) + } +} diff --git a/vendor/github.com/matterbridge/discordgo/examples/mytoken/main.go b/vendor/github.com/matterbridge/discordgo/examples/mytoken/main.go new file mode 100644 index 00000000..9375eadc --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/examples/mytoken/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "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() + + if Email == "" || Password == "" { + flag.Usage() + os.Exit(1) + } +} + +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 + } + + // Print out your token. + fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) +} diff --git a/vendor/github.com/matterbridge/discordgo/examples/pingpong/main.go b/vendor/github.com/matterbridge/discordgo/examples/pingpong/main.go new file mode 100644 index 00000000..155e782f --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/examples/pingpong/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/bwmarrin/discordgo" +) + +// Variables used for command line parameters +var ( + Token string +) + +func init() { + + flag.StringVar(&Token, "t", "", "Bot Token") + flag.Parse() +} + +func main() { + + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + Token) + if err != nil { + fmt.Println("error creating Discord session,", err) + return + } + + // Register the messageCreate func as a callback for MessageCreate events. + dg.AddHandler(messageCreate) + + // Open a websocket connection to Discord and begin listening. + err = dg.Open() + if err != nil { + fmt.Println("error opening connection,", err) + return + } + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + // Cleanly close down the Discord session. + dg.Close() +} + +// This function will be called (due to AddHandler above) every time a new +// message is created on any channel that the autenticated bot has access to. +func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { + + // Ignore all messages created by the bot itself + // This isn't required in this specific example but it's a good practice. + if m.Author.ID == s.State.User.ID { + return + } + // 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/matterbridge/discordgo/logging.go b/vendor/github.com/matterbridge/discordgo/logging.go new file mode 100644 index 00000000..6460b35b --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/logging.go @@ -0,0 +1,103 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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 returned 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 +) + +// Logger can be used to replace the standard logging for discordgo +var Logger func(msgL, caller int, format string, a ...interface{}) + +// 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 separated list of values to pass +func msglog(msgL, caller int, format string, a ...interface{}) { + + if Logger != nil { + Logger(msgL, caller, format, a...) + } else { + + 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/matterbridge/discordgo/message.go b/vendor/github.com/matterbridge/discordgo/message.go new file mode 100644 index 00000000..4fd468fd --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/message.go @@ -0,0 +1,252 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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 ( + "io" + "regexp" + "strings" +) + +// MessageType is the type of Message +type MessageType int + +// Block contains the valid known MessageType values +const ( + MessageTypeDefault MessageType = iota + MessageTypeRecipientAdd + MessageTypeRecipientRemove + MessageTypeCall + MessageTypeChannelNameChange + MessageTypeChannelIconChange + MessageTypeChannelPinnedMessage + MessageTypeGuildMemberJoin +) + +// A Message stores all data related to a specific Discord message. +type Message struct { + ID string `json:"id"` + ChannelID string `json:"channel_id"` + Content string `json:"content"` + Timestamp Timestamp `json:"timestamp"` + EditedTimestamp Timestamp `json:"edited_timestamp"` + MentionRoles []string `json:"mention_roles"` + Tts bool `json:"tts"` + MentionEveryone bool `json:"mention_everyone"` + Author *User `json:"author"` + Attachments []*MessageAttachment `json:"attachments"` + Embeds []*MessageEmbed `json:"embeds"` + Mentions []*User `json:"mentions"` + Reactions []*MessageReactions `json:"reactions"` + Type MessageType `json:"type"` +} + +// File stores info about files you e.g. send in messages. +type File struct { + Name string + ContentType string + Reader io.Reader +} + +// MessageSend stores all parameters you can send with ChannelMessageSendComplex. +type MessageSend struct { + Content string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + Tts bool `json:"tts"` + Files []*File `json:"-"` + + // TODO: Remove this when compatibility is not required. + File *File `json:"-"` +} + +// MessageEdit is used to chain parameters via ChannelMessageEditComplex, which +// is also where you should get the instance from. +type MessageEdit struct { + Content *string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + + ID string + Channel string +} + +// NewMessageEdit returns a MessageEdit struct, initialized +// with the Channel and ID. +func NewMessageEdit(channelID string, messageID string) *MessageEdit { + return &MessageEdit{ + Channel: channelID, + ID: messageID, + } +} + +// SetContent is the same as setting the variable Content, +// except it doesn't take a pointer. +func (m *MessageEdit) SetContent(str string) *MessageEdit { + m.Content = &str + return m +} + +// SetEmbed is a convenience function for setting the embed, +// so you can chain commands. +func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit { + m.Embed = embed + return m +} + +// A MessageAttachment stores data for message attachments. +type MessageAttachment struct { + ID string `json:"id"` + 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"` +} + +// MessageEmbedFooter is a part of a MessageEmbed struct. +type MessageEmbedFooter struct { + Text string `json:"text,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedImage is a part of a MessageEmbed struct. +type MessageEmbedImage struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedThumbnail is a part of a MessageEmbed struct. +type MessageEmbedThumbnail struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedVideo is a part of a MessageEmbed struct. +type MessageEmbedVideo struct { + URL string `json:"url,omitempty"` + ProxyURL string `json:"proxy_url,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` +} + +// MessageEmbedProvider is a part of a MessageEmbed struct. +type MessageEmbedProvider struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` +} + +// MessageEmbedAuthor is a part of a MessageEmbed struct. +type MessageEmbedAuthor struct { + URL string `json:"url,omitempty"` + Name string `json:"name,omitempty"` + IconURL string `json:"icon_url,omitempty"` + ProxyIconURL string `json:"proxy_icon_url,omitempty"` +} + +// MessageEmbedField is a part of a MessageEmbed struct. +type MessageEmbedField struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` + Inline bool `json:"inline,omitempty"` +} + +// An MessageEmbed stores data for message embeds. +type MessageEmbed struct { + URL string `json:"url,omitempty"` + Type string `json:"type,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Color int `json:"color,omitempty"` + Footer *MessageEmbedFooter `json:"footer,omitempty"` + Image *MessageEmbedImage `json:"image,omitempty"` + Thumbnail *MessageEmbedThumbnail `json:"thumbnail,omitempty"` + Video *MessageEmbedVideo `json:"video,omitempty"` + Provider *MessageEmbedProvider `json:"provider,omitempty"` + Author *MessageEmbedAuthor `json:"author,omitempty"` + Fields []*MessageEmbedField `json:"fields,omitempty"` +} + +// MessageReactions holds a reactions object for a message. +type MessageReactions struct { + Count int `json:"count"` + Me bool `json:"me"` + Emoji *Emoji `json:"emoji"` +} + +// ContentWithMentionsReplaced will replace all @ mentions with the +// username of the mention. +func (m *Message) ContentWithMentionsReplaced() (content string) { + content = m.Content + + for _, user := range m.Mentions { + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+user.Username, + ).Replace(content) + } + return +} + +var patternChannels = regexp.MustCompile("<#[^>]*>") + +// ContentWithMoreMentionsReplaced will replace all @ mentions with the +// username of the mention, but also role IDs and more. +func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, err error) { + content = m.Content + + if !s.StateEnabled { + content = m.ContentWithMentionsReplaced() + return + } + + channel, err := s.State.Channel(m.ChannelID) + if err != nil { + content = m.ContentWithMentionsReplaced() + return + } + + for _, user := range m.Mentions { + nick := user.Username + + member, err := s.State.Member(channel.GuildID, user.ID) + if err == nil && member.Nick != "" { + nick = member.Nick + } + + content = strings.NewReplacer( + "<@"+user.ID+">", "@"+user.Username, + "<@!"+user.ID+">", "@"+nick, + ).Replace(content) + } + for _, roleID := range m.MentionRoles { + role, err := s.State.Role(channel.GuildID, roleID) + if err != nil || !role.Mentionable { + continue + } + + content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1) + } + + content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { + channel, err := s.State.Channel(mention[2 : len(mention)-1]) + if err != nil || channel.Type == ChannelTypeGuildVoice { + return mention + } + + return "#" + channel.Name + }) + return +} diff --git a/vendor/github.com/matterbridge/discordgo/oauth2.go b/vendor/github.com/matterbridge/discordgo/oauth2.go new file mode 100644 index 00000000..108b32fe --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/oauth2.go @@ -0,0 +1,126 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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"` + BotRequireCodeGrant bool `json:"bot_require_code_grant,omitempty"` + BotPublic bool `json:"bot_public,omitempty"` + RPCApplicationState int `json:"rpc_application_state,omitempty"` + Flags int `json:"flags,omitempty"` + Owner *User `json:"owner"` + Bot *User `json:"bot"` +} + +// Application returns an Application structure of a specific Application +// appID : The ID of an Application +func (s *Session) Application(appID string) (st *Application, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication("")) + 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.RequestWithBucketID("GET", EndpointApplications, nil, EndpointApplications) + 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.RequestWithBucketID("POST", EndpointApplications, data, EndpointApplications) + 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.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication("")) + 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.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication("")) + 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.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/vendor/github.com/matterbridge/discordgo/ratelimit.go b/vendor/github.com/matterbridge/discordgo/ratelimit.go new file mode 100644 index 00000000..dc48c924 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/ratelimit.go @@ -0,0 +1,194 @@ +package discordgo + +import ( + "net/http" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" +) + +// customRateLimit holds information for defining a custom rate limit +type customRateLimit struct { + suffix string + requests int + reset time.Duration +} + +// RateLimiter holds all ratelimit buckets +type RateLimiter struct { + sync.Mutex + global *int64 + buckets map[string]*Bucket + globalRateLimit time.Duration + customRateLimits []*customRateLimit +} + +// NewRatelimiter returns a new RateLimiter +func NewRatelimiter() *RateLimiter { + + return &RateLimiter{ + buckets: make(map[string]*Bucket), + global: new(int64), + customRateLimits: []*customRateLimit{ + &customRateLimit{ + suffix: "//reactions//", + requests: 1, + reset: 200 * time.Millisecond, + }, + }, + } +} + +// GetBucket retrieves or creates a bucket +func (r *RateLimiter) GetBucket(key string) *Bucket { + r.Lock() + defer r.Unlock() + + if bucket, ok := r.buckets[key]; ok { + return bucket + } + + b := &Bucket{ + Remaining: 1, + Key: key, + global: r.global, + } + + // Check if there is a custom ratelimit set for this bucket ID. + for _, rl := range r.customRateLimits { + if strings.HasSuffix(b.Key, rl.suffix) { + b.customRateLimit = rl + break + } + } + + r.buckets[key] = b + return b +} + +// GetWaitTime returns the duration you should wait for a Bucket +func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration { + // If we ran out of calls and the reset time is still ahead of us + // then we need to take it easy and relax a little + if b.Remaining < minRemaining && b.reset.After(time.Now()) { + return b.reset.Sub(time.Now()) + } + + // Check for global ratelimits + sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) + if now := time.Now(); now.Before(sleepTo) { + return sleepTo.Sub(now) + } + + return 0 +} + +// LockBucket Locks until a request can be made +func (r *RateLimiter) LockBucket(bucketID string) *Bucket { + return r.LockBucketObject(r.GetBucket(bucketID)) +} + +// LockBucketObject Locks an already resolved bucket until a request can be made +func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket { + b.Lock() + + if wait := r.GetWaitTime(b, 1); wait > 0 { + time.Sleep(wait) + } + + b.Remaining-- + return b +} + +// Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) +type Bucket struct { + sync.Mutex + Key string + Remaining int + limit int + reset time.Time + global *int64 + + lastReset time.Time + customRateLimit *customRateLimit + Userdata interface{} +} + +// Release unlocks the bucket and reads the headers to update the buckets ratelimit info +// and locks up the whole thing in case if there's a global ratelimit. +func (b *Bucket) Release(headers http.Header) error { + defer b.Unlock() + + // Check if the bucket uses a custom ratelimiter + if rl := b.customRateLimit; rl != nil { + if time.Now().Sub(b.lastReset) >= rl.reset { + b.Remaining = rl.requests - 1 + b.lastReset = time.Now() + } + if b.Remaining < 1 { + b.reset = time.Now().Add(rl.reset) + } + return nil + } + + if headers == nil { + return nil + } + + remaining := headers.Get("X-RateLimit-Remaining") + reset := headers.Get("X-RateLimit-Reset") + global := headers.Get("X-RateLimit-Global") + retryAfter := headers.Get("Retry-After") + + // Update global and per bucket reset time if the proper headers are available + // If global is set, then it will block all buckets until after Retry-After + // If Retry-After without global is provided it will use that for the new reset + // time since it's more accurate than X-RateLimit-Reset. + // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset + if retryAfter != "" { + parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) + if err != nil { + return err + } + + resetAt := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) + + // Lock either this single bucket or all buckets + if global != "" { + atomic.StoreInt64(b.global, resetAt.UnixNano()) + } else { + b.reset = resetAt + } + } else if reset != "" { + // Calculate the reset time by using the date header returned from discord + discordTime, err := http.ParseTime(headers.Get("Date")) + if err != nil { + return err + } + + unix, err := strconv.ParseInt(reset, 10, 64) + if err != nil { + return err + } + + // Calculate the time until reset and add it to the current local time + // some extra time is added because without it i still encountered 429's. + // The added amount is the lowest amount that gave no 429's + // in 1k requests + delta := time.Unix(unix, 0).Sub(discordTime) + time.Millisecond*250 + b.reset = time.Now().Add(delta) + } + + // Udpate remaining if header is present + if remaining != "" { + parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) + if err != nil { + return err + } + b.Remaining = int(parsedRemaining) + } + + return nil +} diff --git a/vendor/github.com/matterbridge/discordgo/restapi.go b/vendor/github.com/matterbridge/discordgo/restapi.go new file mode 100644 index 00000000..5dc0467f --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/restapi.go @@ -0,0 +1,2049 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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/textproto" + "net/url" + "strconv" + "strings" + "time" +) + +// All error constants +var ( + ErrJSONUnmarshal = errors.New("json unmarshal") + ErrStatusOffline = errors.New("You can't set your Status to offline") + ErrVerificationLevelBounds = errors.New("VerificationLevel out of bounds, should be between 0 and 3") + ErrPruneDaysBounds = errors.New("the number of days should be more than or equal to 1") + ErrGuildNoIcon = errors.New("guild does not have an icon set") + ErrGuildNoSplash = errors.New("guild does not have a splash set") +) + +// Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr +func (s *Session) Request(method, urlStr string, data interface{}) (response []byte, err error) { + return s.RequestWithBucketID(method, urlStr, data, strings.SplitN(urlStr, "?", 2)[0]) +} + +// RequestWithBucketID makes a (GET/POST/...) Requests to Discord REST API with JSON data. +func (s *Session) RequestWithBucketID(method, urlStr string, data interface{}, bucketID string) (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, bucketID, 0) +} + +// request makes a (GET/POST/...) Requests to Discord REST API. +// Sequence is the sequence number, if it fails with a 502 it will +// retry with sequence+1 until it either succeeds or sequence >= session.MaxRestRetries +func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID string, sequence int) (response []byte, err error) { + if bucketID == "" { + bucketID = strings.SplitN(urlStr, "?", 2)[0] + } + return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence) +} + +// RequestWithLockedBucket makes a request using a bucket that's already been locked +func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) { + 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 { + bucket.Release(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) + } + } + + resp, err := s.Client.Do(req) + if err != nil { + bucket.Release(nil) + return + } + defer func() { + err2 := resp.Body.Close() + if err2 != nil { + log.Println("error closing resp body") + } + }() + + err = bucket.Release(resp.Header) + if err != nil { + return + } + + 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 http.StatusBadGateway: + // Retry sending request if possible + if sequence < s.MaxRestRetries { + + s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) + response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1) + } else { + err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) + } + + case 429: // TOO MANY REQUESTS - Rate limiting + rl := TooManyRequests{} + err = json.Unmarshal(response, &rl) + if err != nil { + s.log(LogError, "rate limit unmarshal error, %s", err) + return + } + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) + s.handleEvent(rateLimitEventType, RateLimit{TooManyRequests: &rl, URL: urlStr}) + + time.Sleep(rl.RetryAfter * time.Millisecond) + // we can make the above smarter + // this method can cause longer delays than required + + response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence) + + default: // Error condition + err = newRestError(req, resp, 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. +// +// NOTE: While email/pass authentication is supported by DiscordGo it is +// HIGHLY DISCOURAGED by Discord. Please only use email/pass to obtain a token +// and then use that authentication token for all future connections. +// Also, doing any form of automation with a user (non Bot) account may result +// in that account being permanently banned from Discord. +func (s *Session) Login(email, password string) (err error) { + + data := struct { + Email string `json:"email"` + Password string `json:"password"` + }{email, password} + + response, err := s.RequestWithBucketID("POST", EndpointLogin, data, EndpointLogin) + if err != nil { + return + } + + temp := struct { + Token string `json:"token"` + MFA bool `json:"mfa"` + }{} + + err = unmarshal(response, &temp) + if err != nil { + return + } + + s.Token = temp.Token + s.MFA = temp.MFA + 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.RequestWithBucketID("POST", EndpointRegister, data, EndpointRegister) + 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.RequestWithBucketID("POST", EndpointLogout, data, EndpointLogout) + 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.RequestWithBucketID("GET", EndpointUser(userID), nil, EndpointUsers) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserAvatar is deprecated. Please use UserAvatarDecode +// userID : A user ID or "@me" which is a shortcut of current user ID +func (s *Session) UserAvatar(userID string) (img image.Image, err error) { + u, err := s.User(userID) + if err != nil { + return + } + img, err = s.UserAvatarDecode(u) + return +} + +// UserAvatarDecode returns an image.Image of a user's Avatar +// user : The user which avatar should be retrieved +func (s *Session) UserAvatarDecode(u *User) (img image.Image, err error) { + body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(u.ID, u.Avatar), nil, EndpointUserAvatar("", "")) + if err != nil { + return + } + + 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,omitempty"` + Password string `json:"password,omitempty"` + Username string `json:"username,omitempty"` + Avatar string `json:"avatar,omitempty"` + NewPassword string `json:"new_password,omitempty"` + }{email, password, username, avatar, newPassword} + + body, err := s.RequestWithBucketID("PATCH", EndpointUser("@me"), data, EndpointUsers) + 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.RequestWithBucketID("GET", EndpointUserSettings("@me"), nil, EndpointUserSettings("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserUpdateStatus update the user status +// status : The new status (Actual valid status are 'online','idle','dnd','invisible') +func (s *Session) UserUpdateStatus(status Status) (st *Settings, err error) { + if status == StatusOffline { + err = ErrStatusOffline + return + } + + data := struct { + Status Status `json:"status"` + }{status} + + body, err := s.RequestWithBucketID("PATCH", EndpointUserSettings("@me"), data, EndpointUserSettings("")) + 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.RequestWithBucketID("GET", EndpointUserChannels("@me"), nil, EndpointUserChannels("")) + 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.RequestWithBucketID("POST", EndpointUserChannels("@me"), data, EndpointUserChannels("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserGuilds returns an array of UserGuild structures for all guilds. +// limit : The number guilds that can be returned. (max 100) +// beforeID : If provided all guilds returned will be before given ID. +// afterID : If provided all guilds returned will be after given ID. +func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGuild, err error) { + + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if afterID != "" { + v.Set("after", afterID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + + uri := EndpointUserGuilds("@me") + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) + if err != nil { + return + } + + 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.RequestWithBucketID("PATCH", EndpointUserGuildSettings("@me", guildID), settings, EndpointUserGuildSettings("", guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + 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. +// +// NOTE: This function is now deprecated and will be removed in the future. +// Please see the same function inside state.go +func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { + // Try to just get permissions from state. + apermissions, err = s.State.UserChannelPermissions(userID, channelID) + if err == nil { + return + } + + // Otherwise try get as much data from state as possible, falling back to the network. + channel, err := s.State.Channel(channelID) + if err != nil || channel == nil { + channel, err = s.Channel(channelID) + 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 + } + } + + return memberPermissions(guild, channel, member), nil +} + +// Calculates the permissions for a member. +// https://support.discordapp.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured- +func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) { + userID := member.User.ID + + if userID == guild.OwnerID { + apermissions = PermissionAll + return + } + + for _, role := range guild.Roles { + if role.ID == guild.ID { + apermissions |= role.Permissions + break + } + } + + for _, role := range guild.Roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + apermissions |= role.Permissions + break + } + } + } + + if apermissions&PermissionAdministrator == PermissionAdministrator { + apermissions |= PermissionAll + } + + // Apply @everyone overrides from the channel. + for _, overwrite := range channel.PermissionOverwrites { + if guild.ID == overwrite.ID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } + } + + denies := 0 + allows := 0 + + // Member overwrites can override role overrides, so do two passes + for _, overwrite := range channel.PermissionOverwrites { + for _, roleID := range member.Roles { + if overwrite.Type == "role" && roleID == overwrite.ID { + denies |= overwrite.Deny + allows |= overwrite.Allow + break + } + } + } + + apermissions &= ^denies + apermissions |= allows + + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.Type == "member" && overwrite.ID == userID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } + } + + if apermissions&PermissionAdministrator == PermissionAdministrator { + apermissions |= PermissionAllChannel + } + + return apermissions +} + +// ------------------------------------------------------------------------------------------------ +// 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.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) + 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.RequestWithBucketID("POST", EndpointGuildCreate, data, EndpointGuildCreate) + 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 = ErrVerificationLevelBounds + 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 + } + } + + body, err := s.RequestWithBucketID("PATCH", EndpointGuild(guildID), g, EndpointGuild(guildID)) + 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.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID)) + 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.RequestWithBucketID("DELETE", EndpointUserGuild("@me", guildID), nil, EndpointUserGuild("", guildID)) + 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 []*GuildBan, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildBans(guildID), nil, EndpointGuildBans(guildID)) + 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) { + return s.GuildBanCreateWithReason(guildID, userID, "", days) +} + +// GuildBanCreateWithReason bans the given user from the given guild also providing a reaso. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for this ban +// days : The number of days of previous comments to delete. +func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days int) (err error) { + + uri := EndpointGuildBan(guildID, userID) + + queryParams := url.Values{} + if days > 0 { + queryParams.Set("delete-message-days", strconv.Itoa(days)) + } + if reason != "" { + queryParams.Set("reason", reason) + } + + if len(queryParams) > 0 { + uri += "?" + queryParams.Encode() + } + + _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) + 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.RequestWithBucketID("DELETE", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, "")) + return +} + +// GuildMembers returns a list of members for a guild. +// guildID : The ID of a Guild. +// after : The id of the member to return members after +// limit : max number of members to return (max 1000) +func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*Member, err error) { + + uri := EndpointGuildMembers(guildID) + + v := url.Values{} + + if after != "" { + v.Set("after", after) + } + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildMembers(guildID)) + 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.RequestWithBucketID("GET", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) + 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) { + + return s.GuildMemberDeleteWithReason(guildID, userID, "") +} + +// GuildMemberDeleteWithReason removes the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +// reason : The reason for the kick +func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) { + + uri := EndpointGuildMember(guildID, userID) + if reason != "" { + uri += "?reason=" + url.QueryEscape(reason) + } + + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, "")) + return +} + +// 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.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + 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.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + 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 +// userID : The ID of a user or "@me" which is a shortcut of the current user ID +func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) { + + data := struct { + Nick string `json:"nick"` + }{nickname} + + if userID == "@me" { + userID += "/nick" + } + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + return +} + +// GuildMemberRoleAdd adds the specified role to a given member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// roleID : The ID of a Role to be assigned to the user. +func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) { + + _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + + return +} + +// GuildMemberRoleRemove removes the specified role to a given member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// roleID : The ID of a Role to be removed from the user. +func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + + 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, EndpointGuildChannels(guildID), 0) + 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.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) + 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.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), channels, EndpointGuildChannels(guildID)) + 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.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID)) + 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.RequestWithBucketID("GET", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) + 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.RequestWithBucketID("POST", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) + 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. +// mention : Whether this role is mentionable +func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int, mention bool) (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") + return nil, err + } + + data := struct { + Name string `json:"name"` // The role's name (overwrites existing) + Color int `json:"color"` // The color the role should have (as a decimal, not hex) + Hoist bool `json:"hoist"` // Whether to display the role's users separately + Permissions int `json:"permissions"` // The overall permissions number of the role (overwrites existing) + Mentionable bool `json:"mentionable"` // Whether this role is mentionable + }{name, color, hoist, perm, mention} + + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRole(guildID, roleID), data, EndpointGuildRole(guildID, "")) + 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.RequestWithBucketID("PATCH", EndpointGuildRoles(guildID), roles, EndpointGuildRoles(guildID)) + 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.RequestWithBucketID("DELETE", EndpointGuildRole(guildID, roleID), nil, EndpointGuildRole(guildID, "")) + + return +} + +// GuildPruneCount Returns the number of members that would be removed in a prune operation. +// Requires 'KICK_MEMBER' permission. +// guildID : The ID of a Guild. +// days : The number of days to count prune for (1 or more). +func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, err error) { + count = 0 + + if days <= 0 { + err = ErrPruneDaysBounds + return + } + + p := struct { + Pruned uint32 `json:"pruned"` + }{} + + uri := EndpointGuildPrune(guildID) + fmt.Sprintf("?days=%d", days) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildPrune(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &p) + if err != nil { + return + } + + count = p.Pruned + + return +} + +// GuildPrune Begin as prune operation. Requires the 'KICK_MEMBERS' permission. +// Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. +// guildID : The ID of a Guild. +// days : The number of days to count prune for (1 or more). +func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err error) { + + count = 0 + + if days <= 0 { + err = ErrPruneDaysBounds + return + } + + data := struct { + days uint32 + }{days} + + p := struct { + Pruned uint32 `json:"pruned"` + }{} + + body, err := s.RequestWithBucketID("POST", EndpointGuildPrune(guildID), data, EndpointGuildPrune(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &p) + if err != nil { + return + } + + count = p.Pruned + + 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.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) + 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.RequestWithBucketID("POST", EndpointGuildIntegrations(guildID), data, EndpointGuildIntegrations(guildID)) + 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.RequestWithBucketID("PATCH", EndpointGuildIntegration(guildID, integrationID), data, EndpointGuildIntegration(guildID, "")) + 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.RequestWithBucketID("DELETE", EndpointGuildIntegration(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) + 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.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) + 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 = ErrGuildNoIcon + return + } + + body, err := s.RequestWithBucketID("GET", EndpointGuildIcon(guildID, g.Icon), nil, EndpointGuildIcon(guildID, "")) + 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 = ErrGuildNoSplash + return + } + + body, err := s.RequestWithBucketID("GET", EndpointGuildSplash(guildID, g.Splash), nil, EndpointGuildSplash(guildID, "")) + 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.RequestWithBucketID("GET", EndpointGuildEmbed(guildID), nil, EndpointGuildEmbed(guildID)) + 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.RequestWithBucketID("PATCH", EndpointGuildEmbed(guildID), data, EndpointGuildEmbed(guildID)) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Channels +// ------------------------------------------------------------------------------------------------ + +// Channel returns a Channel structure 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.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) + 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) (*Channel, error) { + return s.ChannelEditComplex(channelID, &ChannelEdit{ + Name: name, + }) +} + +// ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct +// channelID : The ID of a Channel +// data : The channel struct to send +func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) { + body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) + 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.RequestWithBucketID("DELETE", EndpointChannel(channelID), nil, EndpointChannel(channelID)) + 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.RequestWithBucketID("POST", EndpointChannelTyping(channelID), nil, EndpointChannelTyping(channelID)) + 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. +// aroundID : If provided all messages returned will be around given ID. +func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID, aroundID string) (st []*Message, err error) { + + uri := EndpointChannelMessages(channelID) + + 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 aroundID != "" { + v.Set("around", aroundID) + } + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointChannelMessages(channelID)) + 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.RequestWithBucketID("GET", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) + 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 +// lastToken : token returned by last ack +func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) { + + body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, "")) + if err != nil { + return + } + + err = unmarshal(body, &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) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + }) +} + +var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") + +// ChannelMessageSendComplex sends a message to the given channel. +// channelID : The ID of a Channel. +// data : The message struct to send. +func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) (st *Message, err error) { + if data.Embed != nil && data.Embed.Type == "" { + data.Embed.Type = "rich" + } + + endpoint := EndpointChannelMessages(channelID) + + // TODO: Remove this when compatibility is not required. + files := data.Files + if data.File != nil { + if files == nil { + files = []*File{data.File} + } else { + err = fmt.Errorf("cannot specify both File and Files") + return + } + } + + var response []byte + if len(files) > 0 { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + var payload []byte + payload, err = json.Marshal(data) + if err != nil { + return + } + + var p io.Writer + + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="payload_json"`) + h.Set("Content-Type", "application/json") + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = p.Write(payload); err != nil { + return + } + + for i, file := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="file%d"; filename="%s"`, i, quoteEscaper.Replace(file.Name))) + contentType := file.ContentType + if contentType == "" { + contentType = "application/octet-stream" + } + h.Set("Content-Type", contentType) + + p, err = bodywriter.CreatePart(h) + if err != nil { + return + } + + if _, err = io.Copy(p, file.Reader); err != nil { + return + } + } + + err = bodywriter.Close() + if err != nil { + return + } + + response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + } else { + response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + } + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// 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) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + Tts: true, + }) +} + +// ChannelMessageSendEmbed sends a message to the given channel with embedded data. +// channelID : The ID of a Channel. +// embed : The embed data to send. +func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Embed: embed, + }) +} + +// ChannelMessageEdit edits an existing message, replacing it entirely with +// the given content. +// channelID : The ID of a Channel +// messageID : The ID of a Message +// content : The contents of the message +func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetContent(content)) +} + +// ChannelMessageEditComplex edits an existing message, replacing it entirely with +// the given MessageEdit struct +func (s *Session) ChannelMessageEditComplex(m *MessageEdit) (st *Message, err error) { + if m.Embed != nil && m.Embed.Type == "" { + m.Embed.Type = "rich" + } + + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(m.Channel, m.ID), m, EndpointChannelMessage(m.Channel, "")) + if err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// ChannelMessageEditEmbed edits an existing message with embedded data. +// channelID : The ID of a Channel +// messageID : The ID of a Message +// embed : The embed data to send +func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbed(embed)) +} + +// ChannelMessageDelete deletes a message from the Channel. +func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) + return +} + +// ChannelMessagesBulkDelete bulk deletes the messages from the channel for the provided messageIDs. +// If only one messageID is in the slice call channelMessageDelete function. +// 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.RequestWithBucketID("POST", EndpointChannelMessagesBulkDelete(channelID), data, EndpointChannelMessagesBulkDelete(channelID)) + 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.RequestWithBucketID("PUT", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + 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.RequestWithBucketID("DELETE", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + 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.RequestWithBucketID("GET", EndpointChannelMessagesPins(channelID), nil, EndpointChannelMessagesPins(channelID)) + + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelFileSend sends a file to the given channel. +// channelID : The ID of a Channel. +// name: The name of the file. +// io.Reader : A reader for the file contents. +func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}}) +} + +// ChannelFileSendWithMessage sends a file to the given channel with an message. +// DEPRECATED. Use ChannelMessageSendComplex instead. +// channelID : The ID of a Channel. +// content: Optional Message content. +// name: The name of the file. +// io.Reader : A reader for the file contents. +func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}, Content: content}) +} + +// ChannelInvites returns an array of Invite structures for the given channel +// channelID : The ID of a Channel +func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointChannelInvites(channelID), nil, EndpointChannelInvites(channelID)) + 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 and Temporary 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"` + }{i.MaxAge, i.MaxUses, i.Temporary} + + body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) + 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.RequestWithBucketID("PUT", EndpointChannelPermission(channelID, targetID), data, EndpointChannelPermission(channelID, "")) + 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.RequestWithBucketID("DELETE", EndpointChannelPermission(channelID, targetID), nil, EndpointChannelPermission(channelID, "")) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Invites +// ------------------------------------------------------------------------------------------------ + +// Invite returns an Invite structure of the given invite +// inviteID : The invite code +func (s *Session) Invite(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID), nil, EndpointInvite("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// InviteDelete deletes an existing invite +// inviteID : the code of an invite +func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("DELETE", EndpointInvite(inviteID), nil, EndpointInvite("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// InviteAccept accepts an Invite to a Guild or Channel +// inviteID : The invite code +func (s *Session) InviteAccept(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("POST", EndpointInvite(inviteID), nil, EndpointInvite("")) + 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.RequestWithBucketID("GET", EndpointVoiceRegions, nil, EndpointVoiceRegions) + 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.RequestWithBucketID("GET", EndpointVoiceIce, nil, EndpointVoiceIce) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Websockets +// ------------------------------------------------------------------------------------------------ + +// Gateway returns the websocket Gateway address +func (s *Session) Gateway() (gateway string, err error) { + + response, err := s.RequestWithBucketID("GET", EndpointGateway, nil, EndpointGateway) + if err != nil { + return + } + + temp := struct { + URL string `json:"url"` + }{} + + err = unmarshal(response, &temp) + if err != nil { + return + } + + gateway = temp.URL + + // Ensure the gateway always has a trailing slash. + // MacOS will fail to connect if we add query params without a trailing slash on the base domain. + if !strings.HasSuffix(gateway, "/") { + gateway += "/" + } + + return +} + +// GatewayBot returns the websocket Gateway address and the recommended number of shards +func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) { + + response, err := s.RequestWithBucketID("GET", EndpointGatewayBot, nil, EndpointGatewayBot) + if err != nil { + return + } + + err = unmarshal(response, &st) + if err != nil { + return + } + + // Ensure the gateway always has a trailing slash. + // MacOS will fail to connect if we add query params without a trailing slash on the base domain. + if !strings.HasSuffix(st.URL, "/") { + st.URL += "/" + } + + return +} + +// Functions specific to Webhooks + +// WebhookCreate returns a new Webhook. +// channelID: The ID of a Channel. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, err error) { + + data := struct { + Name string `json:"name"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("POST", EndpointChannelWebhooks(channelID), data, EndpointChannelWebhooks(channelID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// ChannelWebhooks returns all webhooks for a given channel. +// channelID: The ID of a channel. +func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointChannelWebhooks(channelID), nil, EndpointChannelWebhooks(channelID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// GuildWebhooks returns all webhooks for a given guild. +// guildID: The ID of a Guild. +func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildWebhooks(guildID), nil, EndpointGuildWebhooks(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// Webhook returns a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointWebhook(webhookID), nil, EndpointWebhooks) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookWithToken returns a webhook for a given ID +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEdit updates an existing Webhook. +// webhookID: The ID of a webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEdit(webhookID, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookEditWithToken updates an existing Webhook with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +// name : The name of the webhook. +// avatar : The avatar of the webhook. +func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{name, avatar} + + body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookDelete deletes a webhook for a given ID +// webhookID: The ID of a webhook. +func (s *Session) WebhookDelete(webhookID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) + + return +} + +// WebhookDeleteWithToken deletes a webhook for a given ID with an auth token. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook. +func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, err error) { + + body, err := s.RequestWithBucketID("DELETE", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// WebhookExecute executes a webhook. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook +func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { + uri := EndpointWebhookToken(webhookID, token) + + if wait { + uri += "?wait=true" + } + + _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", "")) + + return +} + +// MessageReactionAdd creates an emoji reaction to a message. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error { + + _, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, emojiID, "@me"), nil, EndpointMessageReaction(channelID, "", "", "")) + + return err +} + +// MessageReactionRemove deletes an emoji reaction to a message. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +// userID : @me or ID of the user to delete the reaction for. +func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, emojiID, userID), nil, EndpointMessageReaction(channelID, "", "", "")) + + return err +} + +// MessageReactionsRemoveAll deletes all reactions from a message +// channelID : The channel ID +// messageID : The message ID. +func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactionsAll(channelID, messageID), nil, EndpointMessageReactionsAll(channelID, messageID)) + + return err +} + +// MessageReactions gets all the users reactions for a specific emoji. +// channelID : The channel ID. +// messageID : The message ID. +// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. +// limit : max number of users to return (max 100) +func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int) (st []*User, err error) { + uri := EndpointMessageReactions(channelID, messageID, emojiID) + + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to user notes +// ------------------------------------------------------------------------------------------------ + +// UserNoteSet sets the note for a specific user. +func (s *Session) UserNoteSet(userID string, message string) (err error) { + data := struct { + Note string `json:"note"` + }{message} + + _, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes("")) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to Discord Relationships (Friends list) +// ------------------------------------------------------------------------------------------------ + +// RelationshipsGet returns an array of all the relationships of the user. +func (s *Session) RelationshipsGet() (r []*Relationship, err error) { + body, err := s.RequestWithBucketID("GET", EndpointRelationships(), nil, EndpointRelationships()) + if err != nil { + return + } + + err = unmarshal(body, &r) + return +} + +// relationshipCreate creates a new relationship. (I.e. send or accept a friend request, block a user.) +// relationshipType : 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req +func (s *Session) relationshipCreate(userID string, relationshipType int) (err error) { + data := struct { + Type int `json:"type"` + }{relationshipType} + + _, err = s.RequestWithBucketID("PUT", EndpointRelationship(userID), data, EndpointRelationships()) + return +} + +// RelationshipFriendRequestSend sends a friend request to a user. +// userID: ID of the user. +func (s *Session) RelationshipFriendRequestSend(userID string) (err error) { + err = s.relationshipCreate(userID, 4) + return +} + +// RelationshipFriendRequestAccept accepts a friend request from a user. +// userID: ID of the user. +func (s *Session) RelationshipFriendRequestAccept(userID string) (err error) { + err = s.relationshipCreate(userID, 1) + return +} + +// RelationshipUserBlock blocks a user. +// userID: ID of the user. +func (s *Session) RelationshipUserBlock(userID string) (err error) { + err = s.relationshipCreate(userID, 2) + return +} + +// RelationshipDelete removes the relationship with a user. +// userID: ID of the user. +func (s *Session) RelationshipDelete(userID string) (err error) { + _, err = s.RequestWithBucketID("DELETE", EndpointRelationship(userID), nil, EndpointRelationships()) + return +} + +// RelationshipsMutualGet returns an array of all the users both @me and the given user is friends with. +// userID: ID of the user. +func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) { + body, err := s.RequestWithBucketID("GET", EndpointRelationshipsMutual(userID), nil, EndpointRelationshipsMutual(userID)) + if err != nil { + return + } + + err = unmarshal(body, &mf) + return +} diff --git a/vendor/github.com/matterbridge/discordgo/state.go b/vendor/github.com/matterbridge/discordgo/state.go new file mode 100644 index 00000000..8158708b --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/state.go @@ -0,0 +1,990 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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" + "sort" + "sync" +) + +// ErrNilState is returned when the state is nil. +var ErrNilState = errors.New("state not instantiated, please use discordgo.New() or assign Session.State") + +// ErrStateNotFound is returned when the state cache +// requested is not found +var ErrStateNotFound = errors.New("state cache not found") + +// A State contains the current known state. +// As discord sends this in a READY blob, it seems reasonable to simply +// use that struct as the data store. +type State struct { + sync.RWMutex + Ready + + MaxMessageCount int + TrackChannels bool + TrackEmojis bool + TrackMembers bool + TrackRoles bool + TrackVoice bool + TrackPresences bool + + guildMap map[string]*Guild + channelMap map[string]*Channel + memberMap map[string]map[string]*Member +} + +// 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, + TrackPresences: true, + guildMap: make(map[string]*Guild), + channelMap: make(map[string]*Channel), + memberMap: make(map[string]map[string]*Member), + } +} + +func (s *State) createMemberMap(guild *Guild) { + members := make(map[string]*Member) + for _, m := range guild.Members { + members[m.User.ID] = m + } + s.memberMap[guild.ID] = members +} + +// 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 { + s.channelMap[c.ID] = c + } + + // If this guild contains a new member slice, we must regenerate the member map so the pointers stay valid + if guild.Members != nil { + s.createMemberMap(guild) + } else if _, ok := s.memberMap[guild.ID]; !ok { + // Even if we have no new member slice, we still initialize the member map for this guild if it doesn't exist + s.memberMap[guild.ID] = make(map[string]*Member) + } + + if g, ok := s.guildMap[guild.ID]; ok { + // We are about to replace `g` in the state with `guild`, but first we need to + // make sure we preserve any fields that the `guild` doesn't contain from `g`. + if guild.Roles == nil { + guild.Roles = g.Roles + } + if guild.Emojis == nil { + guild.Emojis = g.Emojis + } + if guild.Members == nil { + guild.Members = g.Members + } + if guild.Presences == nil { + guild.Presences = g.Presences + } + if guild.Channels == nil { + guild.Channels = g.Channels + } + if guild.VoiceStates == nil { + 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, ErrStateNotFound +} + +// PresenceAdd adds a presence to the current world state, or +// updates it if it already exists. +func (s *State) PresenceAdd(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + //guild.Presences[i] = presence + + //Update status + guild.Presences[i].Game = presence.Game + guild.Presences[i].Roles = presence.Roles + if presence.Status != "" { + guild.Presences[i].Status = presence.Status + } + if presence.Nick != "" { + guild.Presences[i].Nick = presence.Nick + } + + //Update the optionally sent user information + //ID Is a mandatory field so you should not need to check if it is empty + guild.Presences[i].User.ID = presence.User.ID + + if presence.User.Avatar != "" { + guild.Presences[i].User.Avatar = presence.User.Avatar + } + if presence.User.Discriminator != "" { + guild.Presences[i].User.Discriminator = presence.User.Discriminator + } + if presence.User.Email != "" { + guild.Presences[i].User.Email = presence.User.Email + } + if presence.User.Token != "" { + guild.Presences[i].User.Token = presence.User.Token + } + if presence.User.Username != "" { + guild.Presences[i].User.Username = presence.User.Username + } + + return nil + } + } + + guild.Presences = append(guild.Presences, presence) + return nil +} + +// PresenceRemove removes a presence from the current world state. +func (s *State) PresenceRemove(guildID string, presence *Presence) error { + if s == nil { + return ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + guild.Presences = append(guild.Presences[:i], guild.Presences[i+1:]...) + return nil + } + } + + return ErrStateNotFound +} + +// Presence gets a presence by ID from a guild. +func (s *State) Presence(guildID, userID string) (*Presence, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + for _, p := range guild.Presences { + if p.User.ID == userID { + return p, nil + } + } + + return nil, ErrStateNotFound +} + +// TODO: Consider moving Guild state update methods onto *Guild. + +// 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() + + members, ok := s.memberMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + m, ok := members[member.User.ID] + if !ok { + members[member.User.ID] = member + guild.Members = append(guild.Members, member) + } else { + *m = *member // Update the actual data, which will also update the member pointer in the slice + } + + 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() + + members, ok := s.memberMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + _, ok = members[member.User.ID] + if !ok { + return ErrStateNotFound + } + delete(members, member.User.ID) + + 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 ErrStateNotFound +} + +// Member gets a member by ID from a guild. +func (s *State) Member(guildID, userID string) (*Member, error) { + if s == nil { + return nil, ErrNilState + } + + s.RLock() + defer s.RUnlock() + + members, ok := s.memberMap[guildID] + if !ok { + return nil, ErrStateNotFound + } + + m, ok := members[userID] + if ok { + return m, nil + } + + return nil, ErrStateNotFound +} + +// 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 ErrStateNotFound +} + +// 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, ErrStateNotFound +} + +// ChannelAdd adds a channel to the current world state, or +// updates it if it already exists. +// Channels may exist either as PrivateChannels or inside +// a guild. +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 { + if channel.Messages == nil { + channel.Messages = c.Messages + } + if channel.PermissionOverwrites == nil { + channel.PermissionOverwrites = c.PermissionOverwrites + } + + *c = *channel + return nil + } + + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { + s.PrivateChannels = append(s.PrivateChannels, channel) + } else { + guild, ok := s.guildMap[channel.GuildID] + if !ok { + return ErrStateNotFound + } + + 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.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { + 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 and 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, ErrStateNotFound +} + +// 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, ErrStateNotFound +} + +// 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 + } + if message.Timestamp != "" { + m.Timestamp = message.Timestamp + } + if message.Author != nil { + m.Author = message.Author + } + + 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 + } + + return s.messageRemoveByID(message.ChannelID, message.ID) +} + +// messageRemoveByID removes a message by channelID and messageID from the world state. +func (s *State) messageRemoveByID(channelID, messageID string) error { + c, err := s.Channel(channelID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + for i, m := range c.Messages { + if m.ID == messageID { + c.Messages = append(c.Messages[:i], c.Messages[i+1:]...) + return nil + } + } + + return ErrStateNotFound +} + +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, ErrStateNotFound +} + +// OnReady takes a Ready event and updates all internal state. +func (s *State) onReady(se *Session, r *Ready) (err error) { + if s == nil { + return ErrNilState + } + + s.Lock() + defer s.Unlock() + + // We must track at least the current user for Voice, even + // if state is disabled, store the bare essentials. + if !se.StateEnabled { + ready := Ready{ + Version: r.Version, + SessionID: r.SessionID, + User: r.User, + } + + s.Ready = ready + + return nil + } + + s.Ready = *r + + for _, g := range s.Guilds { + s.guildMap[g.ID] = g + s.createMemberMap(g) + + for _, c := range g.Channels { + s.channelMap[c.ID] = c + } + } + + for _, c := range s.PrivateChannels { + s.channelMap[c.ID] = c + } + + return nil +} + +// OnInterface handles all events related to states. +func (s *State) OnInterface(se *Session, i interface{}) (err error) { + if s == nil { + return ErrNilState + } + + r, ok := i.(*Ready) + if ok { + return s.onReady(se, r) + } + + if !se.StateEnabled { + return nil + } + + switch t := i.(type) { + 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 *GuildMembersChunk: + if s.TrackMembers { + for i := range t.Members { + t.Members[i].GuildID = t.GuildID + err = s.MemberAdd(t.Members[i]) + } + } + 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 *MessageDeleteBulk: + if s.MaxMessageCount != 0 { + for _, mID := range t.Messages { + s.messageRemoveByID(t.ChannelID, mID) + } + } + case *VoiceStateUpdate: + if s.TrackVoice { + err = s.voiceStateUpdate(t) + } + case *PresenceUpdate: + if s.TrackPresences { + s.PresenceAdd(t.GuildID, &t.Presence) + } + if s.TrackMembers { + if t.Status == StatusOffline { + return + } + + var m *Member + m, err = s.Member(t.GuildID, t.User.ID) + + if err != nil { + // Member not found; this is a user coming online + m = &Member{ + GuildID: t.GuildID, + Nick: t.Nick, + User: t.User, + Roles: t.Roles, + } + + } else { + + if t.Nick != "" { + m.Nick = t.Nick + } + + if t.User.Username != "" { + m.User.Username = t.User.Username + } + + // PresenceUpdates always contain a list of roles, so there's no need to check for an empty list here + m.Roles = t.Roles + + } + + err = s.MemberAdd(m) + } + + } + + return +} + +// 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) { + if s == nil { + return 0, ErrNilState + } + + 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 + } + + return memberPermissions(guild, channel, member), nil +} + +// UserColor returns the color of a user in a channel. +// While colors are defined at a Guild level, determining for a channel is more useful in message handlers. +// 0 is returned in cases of error, which is the color of @everyone. +// userID : The ID of the user to calculate the color for. +// channelID : The ID of the channel to calculate the color for. +func (s *State) UserColor(userID, channelID string) int { + if s == nil { + return 0 + } + + channel, err := s.Channel(channelID) + if err != nil { + return 0 + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return 0 + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return 0 + } + + roles := Roles(guild.Roles) + sort.Sort(roles) + + for _, role := range roles { + for _, roleID := range member.Roles { + if role.ID == roleID { + if role.Color != 0 { + return role.Color + } + } + } + } + + return 0 +} diff --git a/vendor/github.com/matterbridge/discordgo/structs.go b/vendor/github.com/matterbridge/discordgo/structs.go new file mode 100644 index 00000000..19d2bad7 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/structs.go @@ -0,0 +1,686 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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" + "net/http" + "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 + MFA bool + + // 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 + + // Whether or not to call event handlers synchronously. + // e.g false = launch event handlers in their own goroutines. + SyncEvents bool + + // Exposed but should not be modified by User. + + // Whether the Data Websocket is ready + DataReady bool // NOTE: Maye be deprecated soon + + // Max number of REST API retries + MaxRestRetries int + + // 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 + + // The http client used for REST requests + Client *http.Client + + // Stores the last HeartbeatAck that was recieved (in UTC) + LastHeartbeatAck time.Time + + // used to deal with rate limits + Ratelimiter *RateLimiter + + // Event handlers + handlersMu sync.RWMutex + handlers map[string][]*eventHandlerInstance + onceHandlers map[string][]*eventHandlerInstance + + // The websocket connection. + wsConn *websocket.Conn + + // When nil, the session is not listening. + listening chan interface{} + + // sequence tracks the current gateway api websocket sequence number + sequence *int64 + + // 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 +} + +// 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 Timestamp `json:"created_at"` + MaxAge int `json:"max_age"` + Uses int `json:"uses"` + MaxUses int `json:"max_uses"` + Revoked bool `json:"revoked"` + Temporary bool `json:"temporary"` + Unique bool `json:"unique"` +} + +// ChannelType is the type of a Channel +type ChannelType int + +// Block contains known ChannelType values +const ( + ChannelTypeGuildText ChannelType = iota + ChannelTypeDM + ChannelTypeGuildVoice + ChannelTypeGroupDM + ChannelTypeGuildCategory +) + +// A Channel holds all data related to an individual Discord channel. +type Channel struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + Name string `json:"name"` + Topic string `json:"topic"` + Type ChannelType `json:"type"` + LastMessageID string `json:"last_message_id"` + NSFW bool `json:"nsfw"` + Position int `json:"position"` + Bitrate int `json:"bitrate"` + Recipients []*User `json:"recipients"` + Messages []*Message `json:"-"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` + ParentID string `json:"parent_id"` +} + +// A ChannelEdit holds Channel Feild data for a channel edit. +type ChannelEdit struct { + Name string `json:"name,omitempty"` + Topic string `json:"topic,omitempty"` + NSFW bool `json:"nsfw,omitempty"` + Position int `json:"position"` + Bitrate int `json:"bitrate,omitempty"` + UserLimit int `json:"user_limit,omitempty"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID string `json:"parent_id,omitempty"` +} + +// 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"` + Animated bool `json:"animated"` +} + +// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. +func (e *Emoji) APIName() string { + if e.ID != "" && e.Name != "" { + return e.Name + ":" + e.ID + } + if e.Name != "" { + return e.Name + } + return e.ID +} + +// VerificationLevel type definition +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 Timestamp `json:"joined_at"` + Splash string `json:"splash"` + AfkTimeout int `json:"afk_timeout"` + MemberCount int `json:"member_count"` + VerificationLevel VerificationLevel `json:"verification_level"` + EmbedEnabled bool `json:"embed_enabled"` + Large bool `json:"large"` // ?? + DefaultMessageNotifications int `json:"default_message_notifications"` + Roles []*Role `json:"roles"` + Emojis []*Emoji `json:"emojis"` + Members []*Member `json:"members"` + Presences []*Presence `json:"presences"` + Channels []*Channel `json:"channels"` + VoiceStates []*VoiceState `json:"voice_states"` + Unavailable bool `json:"unavailable"` +} + +// A UserGuild holds a brief version of a Guild +type UserGuild struct { + ID string `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + Owner bool `json:"owner"` + Permissions int `json:"permissions"` +} + +// A GuildParams stores all the data needed to update discord guild settings +type GuildParams struct { + Name string `json:"name,omitempty"` + Region string `json:"region,omitempty"` + VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` + DefaultMessageNotifications int `json:"default_message_notifications,omitempty"` // TODO: Separate type? + AfkChannelID string `json:"afk_channel_id,omitempty"` + AfkTimeout int `json:"afk_timeout,omitempty"` + Icon string `json:"icon,omitempty"` + OwnerID string `json:"owner_id,omitempty"` + Splash string `json:"splash,omitempty"` +} + +// A Role stores information about Discord guild member roles. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` + Managed bool `json:"managed"` + Mentionable bool `json:"mentionable"` + Hoist bool `json:"hoist"` + Color int `json:"color"` + Position int `json:"position"` + Permissions int `json:"permissions"` +} + +// Roles are a collection of Role +type Roles []*Role + +func (r Roles) Len() int { + return len(r) +} + +func (r Roles) Less(i, j int) bool { + return r[i].Position > r[j].Position +} + +func (r Roles) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +// A VoiceState stores the voice states of Guilds +type VoiceState struct { + UserID string `json:"user_id"` + 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 Status `json:"status"` + Game *Game `json:"game"` + Nick string `json:"nick"` + Roles []string `json:"roles"` + Since *int `json:"since"` +} + +// GameType is the type of "game" (see GameType* consts) in the Game struct +type GameType int + +// Valid GameType values +const ( + GameTypeGame GameType = iota + GameTypeStreaming +) + +// A Game struct holds the name of the "playing .." game for a user +type Game struct { + Name string `json:"name"` + Type GameType `json:"type"` + URL string `json:"url,omitempty"` + Details string `json:"details,omitempty"` + State string `json:"state,omitempty"` + TimeStamps TimeStamps `json:"timestamps,omitempty"` + Assets Assets `json:"assets,omitempty"` + ApplicationID string `json:"application_id,omitempty"` + Instance int8 `json:"instance,omitempty"` + // TODO: Party and Secrets (unknown structure) +} + +// A TimeStamps struct contains start and end times used in the rich presence "playing .." Game +type TimeStamps struct { + EndTimestamp int64 `json:"end,omitempty"` + StartTimestamp int64 `json:"start,omitempty"` +} + +// UnmarshalJSON unmarshals JSON into TimeStamps struct +func (t *TimeStamps) UnmarshalJSON(b []byte) error { + temp := struct { + End float64 `json:"end,omitempty"` + Start float64 `json:"start,omitempty"` + }{} + err := json.Unmarshal(b, &temp) + if err != nil { + return err + } + t.EndTimestamp = int64(temp.End) + t.StartTimestamp = int64(temp.Start) + return nil +} + +// An Assets struct contains assets and labels used in the rich presence "playing .." Game +type Assets struct { + LargeImageID string `json:"large_image,omitempty"` + SmallImageID string `json:"small_image,omitempty"` + LargeText string `json:"large_text,omitempty"` + SmallText string `json:"small_text,omitempty"` +} + +// 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 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"` + 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"` + Status Status `json:"status"` + DetectPlatformAccounts bool `json:"detect_platform_accounts"` + DeveloperMode bool `json:"developer_mode"` +} + +// Status type definition +type Status string + +// Constants for Status with the different current available status +const ( + StatusOnline Status = "online" + StatusIdle Status = "idle" + StatusDoNotDisturb Status = "dnd" + StatusInvisible Status = "invisible" + StatusOffline Status = "offline" +) + +// FriendSourceFlags stores ... TODO :) +type FriendSourceFlags struct { + All bool `json:"all"` + MutualGuilds bool `json:"mutual_guilds"` + MutualFriends bool `json:"mutual_friends"` +} + +// 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"` +} + +// An Ack is used to ack messages +type Ack struct { + Token string `json:"token"` +} + +// A GuildRole stores data for guild roles. +type GuildRole struct { + Role *Role `json:"role"` + GuildID string `json:"guild_id"` +} + +// A GuildBan stores data for a guild ban. +type GuildBan struct { + Reason string `json:"reason"` + User *User `json:"user"` +} + +// A GuildIntegration stores data for a guild integration. +type GuildIntegration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + ExpireBehavior int `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account *GuildIntegrationAccount `json:"account"` + SyncedAt int `json:"synced_at"` +} + +// A GuildIntegrationAccount stores data for a guild integration account. +type GuildIntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// A GuildEmbed stores data for a guild embed. +type GuildEmbed struct { + Enabled bool `json:"enabled"` + ChannelID string `json:"channel_id"` +} + +// A 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"` +} + +// An APIErrorMessage is an api error message returned from discord +type APIErrorMessage struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// Webhook stores the data for a webhook. +type Webhook struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + User *User `json:"user"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Token string `json:"token"` +} + +// WebhookParams is a struct for webhook params, used in the WebhookExecute command. +type WebhookParams struct { + Content string `json:"content,omitempty"` + Username string `json:"username,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + TTS bool `json:"tts,omitempty"` + File string `json:"file,omitempty"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` +} + +// MessageReaction stores the data for a message reaction. +type MessageReaction struct { + UserID string `json:"user_id"` + MessageID string `json:"message_id"` + Emoji Emoji `json:"emoji"` + ChannelID string `json:"channel_id"` +} + +// GatewayBotResponse stores the data for the gateway/bot response +type GatewayBotResponse struct { + URL string `json:"url"` + Shards int `json:"shards"` +} + +// Constants for the different bit offsets of text channel permissions +const ( + PermissionReadMessages = 1 << (iota + 10) + PermissionSendMessages + PermissionSendTTSMessages + PermissionManageMessages + PermissionEmbedLinks + PermissionAttachFiles + PermissionReadMessageHistory + PermissionMentionEveryone + PermissionUseExternalEmojis +) + +// Constants for the different bit offsets of voice permissions +const ( + PermissionVoiceConnect = 1 << (iota + 20) + PermissionVoiceSpeak + PermissionVoiceMuteMembers + PermissionVoiceDeafenMembers + PermissionVoiceMoveMembers + PermissionVoiceUseVAD +) + +// Constants for general management. +const ( + PermissionChangeNickname = 1 << (iota + 26) + PermissionManageNicknames + PermissionManageRoles + PermissionManageWebhooks + PermissionManageEmojis +) + +// Constants for the different bit offsets of general permissions +const ( + PermissionCreateInstantInvite = 1 << iota + PermissionKickMembers + PermissionBanMembers + PermissionAdministrator + PermissionManageChannels + PermissionManageServer + PermissionAddReactions + PermissionViewAuditLogs + + PermissionAllText = PermissionReadMessages | + PermissionSendMessages | + PermissionSendTTSMessages | + PermissionManageMessages | + PermissionEmbedLinks | + PermissionAttachFiles | + PermissionReadMessageHistory | + PermissionMentionEveryone + PermissionAllVoice = PermissionVoiceConnect | + PermissionVoiceSpeak | + PermissionVoiceMuteMembers | + PermissionVoiceDeafenMembers | + PermissionVoiceMoveMembers | + PermissionVoiceUseVAD + PermissionAllChannel = PermissionAllText | + PermissionAllVoice | + PermissionCreateInstantInvite | + PermissionManageRoles | + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs + PermissionAll = PermissionAllChannel | + PermissionKickMembers | + PermissionBanMembers | + PermissionManageServer | + PermissionAdministrator +) + +// Block contains Discord JSON Error Response codes +const ( + ErrCodeUnknownAccount = 10001 + ErrCodeUnknownApplication = 10002 + ErrCodeUnknownChannel = 10003 + ErrCodeUnknownGuild = 10004 + ErrCodeUnknownIntegration = 10005 + ErrCodeUnknownInvite = 10006 + ErrCodeUnknownMember = 10007 + ErrCodeUnknownMessage = 10008 + ErrCodeUnknownOverwrite = 10009 + ErrCodeUnknownProvider = 10010 + ErrCodeUnknownRole = 10011 + ErrCodeUnknownToken = 10012 + ErrCodeUnknownUser = 10013 + ErrCodeUnknownEmoji = 10014 + + ErrCodeBotsCannotUseEndpoint = 20001 + ErrCodeOnlyBotsCanUseEndpoint = 20002 + + ErrCodeMaximumGuildsReached = 30001 + ErrCodeMaximumFriendsReached = 30002 + ErrCodeMaximumPinsReached = 30003 + ErrCodeMaximumGuildRolesReached = 30005 + ErrCodeTooManyReactions = 30010 + + ErrCodeUnauthorized = 40001 + + ErrCodeMissingAccess = 50001 + ErrCodeInvalidAccountType = 50002 + ErrCodeCannotExecuteActionOnDMChannel = 50003 + ErrCodeEmbedCisabled = 50004 + ErrCodeCannotEditFromAnotherUser = 50005 + ErrCodeCannotSendEmptyMessage = 50006 + ErrCodeCannotSendMessagesToThisUser = 50007 + ErrCodeCannotSendMessagesInVoiceChannel = 50008 + ErrCodeChannelVerificationLevelTooHigh = 50009 + ErrCodeOAuth2ApplicationDoesNotHaveBot = 50010 + ErrCodeOAuth2ApplicationLimitReached = 50011 + ErrCodeInvalidOAuthState = 50012 + ErrCodeMissingPermissions = 50013 + ErrCodeInvalidAuthenticationToken = 50014 + ErrCodeNoteTooLong = 50015 + ErrCodeTooFewOrTooManyMessagesToDelete = 50016 + ErrCodeCanOnlyPinMessageToOriginatingChannel = 50019 + ErrCodeCannotExecuteActionOnSystemMessage = 50021 + ErrCodeMessageProvidedTooOldForBulkDelete = 50034 + ErrCodeInvalidFormBody = 50035 + ErrCodeInviteAcceptedToGuildApplicationsBotNotIn = 50036 + + ErrCodeReactionBlocked = 90001 +) diff --git a/vendor/github.com/matterbridge/discordgo/tools/cmd/eventhandlers/main.go b/vendor/github.com/matterbridge/discordgo/tools/cmd/eventhandlers/main.go new file mode 100644 index 00000000..839f009d --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/tools/cmd/eventhandlers/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "log" + "path/filepath" + "regexp" + "sort" + "strings" + "text/template" +) + +var eventHandlerTmpl = template.Must(template.New("eventHandler").Funcs(template.FuncMap{ + "constName": constName, + "isDiscordEvent": isDiscordEvent, + "privateName": privateName, +}).Parse(`// Code generated by \"eventhandlers\"; DO NOT EDIT +// See events.go + +package discordgo + +// Following are all the event types. +// Event type values are used to match the events returned by Discord. +// EventTypes surrounded by __ are synthetic and are internal to DiscordGo. +const ({{range .}} + {{privateName .}}EventType = "{{constName .}}"{{end}} +) +{{range .}} +// {{privateName .}}EventHandler is an event handler for {{.}} events. +type {{privateName .}}EventHandler func(*Session, *{{.}}) + +// Type returns the event type for {{.}} events. +func (eh {{privateName .}}EventHandler) Type() string { + return {{privateName .}}EventType +} +{{if isDiscordEvent .}} +// New returns a new instance of {{.}}. +func (eh {{privateName .}}EventHandler) New() interface{} { + return &{{.}}{} +}{{end}} +// Handle is the handler for {{.}} events. +func (eh {{privateName .}}EventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*{{.}}); ok { + eh(s, t) + } +} + +{{end}} +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Session, interface{}): + return interfaceEventHandler(v){{range .}} + case func(*Session, *{{.}}): + return {{privateName .}}EventHandler(v){{end}} + } + + return nil +} + +func init() { {{range .}}{{if isDiscordEvent .}} + registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}}{{end}} +} +`)) + +func main() { + var buf bytes.Buffer + dir := filepath.Dir(".") + + fs := token.NewFileSet() + parsedFile, err := parser.ParseFile(fs, "events.go", nil, 0) + if err != nil { + log.Fatalf("warning: internal error: could not parse events.go: %s", err) + return + } + + names := []string{} + for object := range parsedFile.Scope.Objects { + names = append(names, object) + } + sort.Strings(names) + eventHandlerTmpl.Execute(&buf, names) + + src, err := format.Source(buf.Bytes()) + if err != nil { + log.Println("warning: internal error: invalid Go generated:", err) + src = buf.Bytes() + } + + err = ioutil.WriteFile(filepath.Join(dir, strings.ToLower("eventhandlers.go")), src, 0644) + if err != nil { + log.Fatal(buf, "writing output: %s", err) + } +} + +var constRegexp = regexp.MustCompile("([a-z])([A-Z])") + +func constCase(name string) string { + return strings.ToUpper(constRegexp.ReplaceAllString(name, "${1}_${2}")) +} + +func isDiscordEvent(name string) bool { + switch { + case name == "Connect", name == "Disconnect", name == "Event", name == "RateLimit", name == "Interface": + return false + default: + return true + } +} + +func constName(name string) string { + if !isDiscordEvent(name) { + return "__" + constCase(name) + "__" + } + + return constCase(name) +} + +func privateName(name string) string { + return strings.ToLower(string(name[0])) + name[1:] +} diff --git a/vendor/github.com/matterbridge/discordgo/types.go b/vendor/github.com/matterbridge/discordgo/types.go new file mode 100644 index 00000000..780b6bb9 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/types.go @@ -0,0 +1,58 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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 custom types, currently only a timestamp wrapper. + +package discordgo + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +// Timestamp stores a timestamp, as sent by the Discord API. +type Timestamp string + +// Parse parses a timestamp string into a time.Time object. +// The only time this can fail is if Discord changes their timestamp format. +func (t Timestamp) Parse() (time.Time, error) { + return time.Parse(time.RFC3339, string(t)) +} + +// RESTError stores error information about a request with a bad response code. +// Message is not always present, there are cases where api calls can fail +// without returning a json message. +type RESTError struct { + Request *http.Request + Response *http.Response + ResponseBody []byte + + Message *APIErrorMessage // Message may be nil. +} + +func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTError { + restErr := &RESTError{ + Request: req, + Response: resp, + ResponseBody: body, + } + + // Attempt to decode the error and assume no message was provided if it fails + var msg *APIErrorMessage + err := json.Unmarshal(body, &msg) + if err == nil { + restErr.Message = msg + } + + return restErr +} + +func (r RESTError) Error() string { + return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) +} diff --git a/vendor/github.com/matterbridge/discordgo/user.go b/vendor/github.com/matterbridge/discordgo/user.go new file mode 100644 index 00000000..a710f286 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/user.go @@ -0,0 +1,47 @@ +package discordgo + +import ( + "fmt" + "strings" +) + +// A User stores all data for an individual Discord user. +type User struct { + ID string `json:"id"` + Email string `json:"email"` + Username string `json:"username"` + Avatar string `json:"avatar"` + Discriminator string `json:"discriminator"` + Token string `json:"token"` + Verified bool `json:"verified"` + MFAEnabled bool `json:"mfa_enabled"` + Bot bool `json:"bot"` +} + +// String returns a unique identifier of the form username#discriminator +func (u *User) String() string { + return fmt.Sprintf("%s#%s", u.Username, u.Discriminator) +} + +// Mention return a string which mentions the user +func (u *User) Mention() string { + return fmt.Sprintf("<@%s>", u.ID) +} + +// AvatarURL returns a URL to the user's avatar. +// size: The size of the user's avatar as a power of two +// if size is an empty string, no size parameter will +// be added to the URL. +func (u *User) AvatarURL(size string) string { + var URL string + if strings.HasPrefix(u.Avatar, "a_") { + URL = EndpointUserAvatarAnimated(u.ID, u.Avatar) + } else { + URL = EndpointUserAvatar(u.ID, u.Avatar) + } + + if size != "" { + return URL + "?size=" + size + } + return URL +} diff --git a/vendor/github.com/matterbridge/discordgo/voice.go b/vendor/github.com/matterbridge/discordgo/voice.go new file mode 100644 index 00000000..3bbf6212 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/voice.go @@ -0,0 +1,886 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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" + "net" + "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 definition 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() + + v.Lock() + defer v.Unlock() + if err != nil { + v.speaking = false + v.log(LogError, "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.session.wsMutex.Lock() + err = v.session.wsConn.WriteJSON(data) + v.session.wsMutex.Unlock() + v.sessionID = "" + } + + // Close websocket and udp connections + v.Close() + + v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) + + v.session.Lock() + delete(v.session.VoiceConnections, v.GuildID) + v.session.Unlock() + + 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 { + v.log(LogError, "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. + v.wsMutex.Lock() + err := v.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + v.wsMutex.Unlock() + if err != nil { + v.log(LogError, "error closing websocket, %s", err) + } + + // 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 returns an err +func (v *VoiceConnection) waitUntilConnected() error { + + v.log(LogInformational, "called") + + i := 0 + for { + v.RLock() + ready := v.Ready + v.RUnlock() + if 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) + } + + return + + case 3: // HEARTBEAT response + // add code to use this to track latency? + return + + case 4: // udp encryption secret key + v.Lock() + defer v.Unlock() + + v.op4 = voiceOP4{} + if err := json.Unmarshal(e.RawData, &v.op4); err != nil { + v.log(LogError, "OP4 unmarshall error, %s, %s", err, string(e.RawData)) + 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(LogDebug, "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) + defer ticker.Stop() + 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 through 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) + defer ticker.Stop() + 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 + } + + // VoiceConnection is now ready to receive audio packets + // TODO: this needs reviewed as I think there must be a better way. + v.Lock() + v.Ready = true + v.Unlock() + defer func() { + v.Lock() + v.Ready = false + v.Unlock() + }() + + 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))) + defer ticker.Stop() + for { + + // Get data from chan. If chan is closed, return. + select { + case <-close: + return + case recvbuf, ok = <-opus: + if !ok { + return + } + // else, continue loop + } + + v.RLock() + speaking := v.speaking + v.RUnlock() + if !speaking { + err := v.Speaking(true) + if err != nil { + v.log(LogError, "error sending speaking packet, %s", err) + } + } + + // 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) + v.RLock() + sendbuf := secretbox.Seal(udpHeader, recvbuf, &nonce, &v.op4.SecretKey) + v.RUnlock() + + // block here until we're exactly at the right time :) + // Then send rtp audio packet to Discord over UDP + 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 + } + + 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 && recvbuf[0] != 0x90) { + continue + } + + // build a audio packet struct + p := Packet{} + p.Type = recvbuf[0:2] + p.Sequence = binary.BigEndian.Uint16(recvbuf[2:4]) + p.Timestamp = binary.BigEndian.Uint32(recvbuf[4:8]) + 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 len(p.Opus) > 8 && recvbuf[0] == 0x90 { + // Extension bit is set, first 8 bytes is the extended header + p.Opus = p.Opus[8:] + } + + if c != nil { + select { + case c <- &p: + case <-close: + return + } + } + } +} + +// 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 reconnect 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 + } + + v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) + + // if the reconnect above didn't work lets just send a disconnect + // packet to reset things. + // Send a OP4 with a nil channel to disconnect + 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) + } + + } +} diff --git a/vendor/github.com/matterbridge/discordgo/wsapi.go b/vendor/github.com/matterbridge/discordgo/wsapi.go new file mode 100644 index 00000000..de66f693 --- /dev/null +++ b/vendor/github.com/matterbridge/discordgo/wsapi.go @@ -0,0 +1,845 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . 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" + "net/http" + "runtime" + "sync/atomic" + "time" + + "github.com/gorilla/websocket" +) + +// ErrWSAlreadyOpen is thrown when you attempt to open +// a websocket that already is open. +var ErrWSAlreadyOpen = errors.New("web socket already opened") + +// ErrWSNotFound is thrown when you attempt to use a websocket +// that doesn't exist +var ErrWSNotFound = errors.New("no websocket connection exists") + +// ErrWSShardBounds is thrown when you try to use a shard ID that is +// less than the total shard count +var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount") + +type resumePacket struct { + Op int `json:"op"` + Data struct { + Token string `json:"token"` + SessionID string `json:"session_id"` + Sequence int64 `json:"seq"` + } `json:"d"` +} + +// Open creates a websocket connection to Discord. +// See: https://discordapp.com/developers/docs/topics/gateway#connecting +func (s *Session) Open() error { + s.log(LogInformational, "called") + + var err error + + // Prevent Open or other major Session functions from + // being called while Open is still running. + s.Lock() + defer s.Unlock() + + // If the websock is already open, bail out here. + if s.wsConn != nil { + return ErrWSAlreadyOpen + } + + // Get the gateway to use for the Websocket connection + if s.gateway == "" { + s.gateway, err = s.Gateway() + if err != nil { + return err + } + + // Add the version and encoding to the URL + s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json" + } + + // Connect to the Gateway + s.log(LogInformational, "connecting to gateway %s", s.gateway) + header := http.Header{} + header.Add("accept-encoding", "zlib") + 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 + s.wsConn = nil // Just to be safe. + return err + } + + defer func() { + // because of this, all code below must set err to the error + // when exiting with an error :) Maybe someone has a better + // way :) + if err != nil { + s.wsConn.Close() + s.wsConn = nil + } + }() + + // The first response from Discord should be an Op 10 (Hello) Packet. + // When processed by onEvent the heartbeat goroutine will be started. + mt, m, err := s.wsConn.ReadMessage() + if err != nil { + return err + } + e, err := s.onEvent(mt, m) + if err != nil { + return err + } + if e.Operation != 10 { + err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation) + return err + } + s.log(LogInformational, "Op 10 Hello Packet received from Discord") + s.LastHeartbeatAck = time.Now().UTC() + var h helloOp + if err = json.Unmarshal(e.RawData, &h); err != nil { + err = fmt.Errorf("error unmarshalling helloOp, %s", err) + return err + } + + // Now we send either an Op 2 Identity if this is a brand new + // connection or Op 6 Resume if we are resuming an existing connection. + sequence := atomic.LoadInt64(s.sequence) + if s.sessionID == "" && sequence == 0 { + + // Send Op 2 Identity Packet + err = s.identify() + if err != nil { + err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err) + return err + } + + } else { + + // Send Op 6 Resume Packet + p := resumePacket{} + p.Op = 6 + p.Data.Token = s.Token + p.Data.SessionID = s.sessionID + p.Data.Sequence = sequence + + s.log(LogInformational, "sending resume packet to gateway") + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(p) + s.wsMutex.Unlock() + if err != nil { + err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err) + return err + } + + } + + // A basic state is a hard requirement for Voice. + // We create it here so the below READY/RESUMED packet can populate + // the state :) + // XXX: Move to New() func? + if s.State == nil { + state := NewState() + state.TrackChannels = false + state.TrackEmojis = false + state.TrackMembers = false + state.TrackRoles = false + state.TrackVoice = false + s.State = state + } + + // Now Discord should send us a READY or RESUMED packet. + mt, m, err = s.wsConn.ReadMessage() + if err != nil { + return err + } + e, err = s.onEvent(mt, m) + if err != nil { + return err + } + if e.Type != `READY` && e.Type != `RESUMED` { + // This is not fatal, but it does not follow their API documentation. + s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e) + } + s.log(LogInformational, "First Packet:\n%#v\n", e) + + s.log(LogInformational, "We are now connected to Discord, emitting connect event") + s.handleEvent(connectEventType, &Connect{}) + + // A VoiceConnections map is a hard requirement for Voice. + // XXX: can this be moved to when opening a voice connection? + if s.VoiceConnections == nil { + s.log(LogInformational, "creating new VoiceConnections map") + s.VoiceConnections = make(map[string]*VoiceConnection) + } + + // Create listening chan outside of listen, as it needs to happen inside the + // mutex lock and needs to exist before calling heartbeat and listen + // go rountines. + s.listening = make(chan interface{}) + + // Start sending heartbeats and reading messages from Discord. + go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval) + go s.listen(s.wsConn, s.listening) + + s.log(LogInformational, "exiting") + return nil +} + +// 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 int64 `json:"d"` +} + +type helloOp struct { + HeartbeatInterval time.Duration `json:"heartbeat_interval"` + Trace []string `json:"_trace"` +} + +// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. +const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond + +// heartbeat sends regular heartbeats to Discord so it knows the client +// is still connected. If you do not send these heartbeats Discord will +// disconnect the websocket connection after a few seconds. +func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) { + + s.log(LogInformational, "called") + + if listening == nil || wsConn == nil { + return + } + + var err error + ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) + defer ticker.Stop() + + for { + s.RLock() + last := s.LastHeartbeatAck + s.RUnlock() + sequence := atomic.LoadInt64(s.sequence) + s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) + s.wsMutex.Lock() + err = wsConn.WriteJSON(heartbeatOp{1, sequence}) + s.wsMutex.Unlock() + if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { + if err != nil { + s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) + } else { + s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) + } + s.Close() + s.reconnect() + return + } + s.Lock() + s.DataReady = true + s.Unlock() + + select { + case <-ticker.C: + // continue loop and send heartbeat + case <-listening: + return + } + } +} + +// UpdateStatusData ia provided to UpdateStatusComplex() +type UpdateStatusData struct { + IdleSince *int `json:"since"` + Game *Game `json:"game"` + AFK bool `json:"afk"` + Status string `json:"status"` +} + +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") + + usd := UpdateStatusData{ + Status: "online", + } + + if idle > 0 { + usd.IdleSince = &idle + } + + if game != "" { + gameType := GameTypeGame + if url != "" { + gameType = GameTypeStreaming + } + usd.Game = &Game{ + Name: game, + Type: gameType, + URL: url, + } + } + + return s.UpdateStatusComplex(usd) +} + +// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. +func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { + + s.RLock() + defer s.RUnlock() + if s.wsConn == nil { + return ErrWSNotFound + } + + 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, "") +} + +type requestGuildMembersData struct { + GuildID string `json:"guild_id"` + Query string `json:"query"` + Limit int `json:"limit"` +} + +type requestGuildMembersOp struct { + Op int `json:"op"` + Data requestGuildMembersData `json:"d"` +} + +// RequestGuildMembers requests guild members from the gateway +// The gateway responds with GuildMembersChunk events +// guildID : The ID of the guild to request members of +// query : String that username starts with, leave empty to return all members +// limit : Max number of items to return, or 0 to request all members matched +func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err error) { + s.log(LogInformational, "called") + + s.RLock() + defer s.RUnlock() + if s.wsConn == nil { + return ErrWSNotFound + } + + data := requestGuildMembersData{ + GuildID: guildID, + Query: query, + Limit: limit, + } + + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data}) + s.wsMutex.Unlock() + + return +} + +// 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. +func (s *Session) onEvent(messageType int, message []byte) (*Event, error) { + + 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 nil, err2 + } + + 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 e, err + } + + 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, atomic.LoadInt64(s.sequence)}) + s.wsMutex.Unlock() + if err != nil { + s.log(LogError, "error sending heartbeat in response to Op1") + return e, err + } + + return e, nil + } + + // Reconnect + // Must immediately disconnect from gateway and reconnect to new gateway. + if e.Operation == 7 { + s.log(LogInformational, "Closing and reconnecting in response to Op7") + s.Close() + s.reconnect() + return e, nil + } + + // 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 e, err + } + + return e, nil + } + + if e.Operation == 10 { + // Op10 is handled by Open() + return e, nil + } + + if e.Operation == 11 { + s.Lock() + s.LastHeartbeatAck = time.Now().UTC() + s.Unlock() + s.log(LogInformational, "got heartbeat ACK") + return e, nil + } + + // 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 e, nil + } + + // Store the message sequence + atomic.StoreInt64(s.sequence, e.Sequence) + + // Map event to registered event handlers and pass it along to any registered handlers. + if eh, ok := registeredInterfaceProviders[e.Type]; ok { + e.Struct = eh.New() + + // Attempt to unmarshal our event. + if err = json.Unmarshal(e.RawData, e.Struct); 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. + s.handleEvent(e.Type, e.Struct) + } else { + s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) + } + + // For legacy reasons, we send the raw event also, this could be useful for handling unknown events. + s.handleEvent(eventEventType, e) + + return e, nil +} + +// ------------------------------------------------------------------------------------------------ +// Code related to voice connections that initiate over the data websocket +// ------------------------------------------------------------------------------------------------ + +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") + + s.RLock() + voice, _ = s.VoiceConnections[gID] + s.RUnlock() + + if voice == nil { + voice = &VoiceConnection{} + s.Lock() + s.VoiceConnections[gID] = voice + s.Unlock() + } + + voice.Lock() + voice.GuildID = gID + voice.ChannelID = cID + voice.deaf = deaf + voice.mute = mute + voice.session = s + voice.Unlock() + + // Send the request to Discord that we want to join the voice channel + data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} + 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(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 + s.RLock() + voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() + if !exists { + return + } + + // We only care about events that are about us. + if s.State.User.ID != st.UserID { + return + } + + // Store the SessionID for later use. + voice.Lock() + voice.UserID = st.UserID + voice.sessionID = st.SessionID + voice.ChannelID = st.ChannelID + voice.Unlock() +} + +// onVoiceServerUpdate handles the Voice Server Update data websocket event. +// +// 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(st *VoiceServerUpdate) { + + s.log(LogInformational, "called") + + s.RLock() + voice, exists := s.VoiceConnections[st.GuildID] + s.RUnlock() + + // If no VoiceConnection exists, just skip this + if !exists { + return + } + + // If currently connected to voice ws/udp, then disconnect. + // Has no effect if not connected. + voice.Close() + + // Store values for later use + voice.Lock() + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.GuildID = st.GuildID + voice.Unlock() + + // Open a connection 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 ErrWSShardBounds + } + + 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. + s.RLock() + defer s.RUnlock() + 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 + } + + // Certain race conditions can call reconnect() twice. If this happens, we + // just break out of the reconnect loop + if err == ErrWSAlreadyOpen { + s.log(LogInformational, "Websocket already exists, no need to reconnect") + return + } + + s.log(LogError, "error reconnecting to gateway, %s", err) + + <-time.After(wait * time.Second) + 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. + s.wsMutex.Lock() + err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + s.wsMutex.Unlock() + if err != nil { + s.log(LogInformational, "error closing websocket, %s", err) + } + + // 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(LogInformational, "error closing websocket, %s", err) + } + + s.wsConn = nil + } + + s.Unlock() + + s.log(LogInformational, "emit disconnect event") + s.handleEvent(disconnectEventType, &Disconnect{}) + + return +} diff --git a/vendor/github.com/matterbridge/logrus-prefixed-formatter/LICENSE b/vendor/github.com/matterbridge/logrus-prefixed-formatter/LICENSE new file mode 100644 index 00000000..b41cb238 --- /dev/null +++ b/vendor/github.com/matterbridge/logrus-prefixed-formatter/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Denis Parchenko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/basic.go b/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/basic.go new file mode 100644 index 00000000..2a42cb43 --- /dev/null +++ b/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/basic.go @@ -0,0 +1,59 @@ +package main + +import ( + "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" +) + +var log = logrus.New() + +func init() { + formatter := new(prefixed.TextFormatter) + log.Formatter = formatter + log.Level = logrus.DebugLevel +} + +func main() { + defer func() { + err := recover() + if err != nil { + // Fatal message + log.WithFields(logrus.Fields{ + "omg": true, + "number": 100, + }).Fatal("[main] The ice breaks!") + } + }() + + // You could either provide a map key called `prefix` to add prefix + log.WithFields(logrus.Fields{ + "prefix": "main", + "animal": "walrus", + "number": 8, + }).Debug("Started observing beach") + + // Or you can simply add prefix in square brackets within message itself + log.WithFields(logrus.Fields{ + "animal": "walrus", + "size": 10, + }).Debug("[main] A group of walrus emerges from the ocean") + + // Warning message + log.WithFields(logrus.Fields{ + "omg": true, + "number": 122, + }).Warn("[main] The group's number increased tremendously!") + + // Information message + log.WithFields(logrus.Fields{ + "prefix": "sensor", + "temperature": -4, + }).Info("Temperature changes") + + // Panic message + log.WithFields(logrus.Fields{ + "prefix": "sensor", + "animal": "orca", + "size": 9009, + }).Panic("It's over 9000!") +} diff --git a/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/themes.go b/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/themes.go new file mode 100644 index 00000000..6c911aea --- /dev/null +++ b/vendor/github.com/matterbridge/logrus-prefixed-formatter/examples/themes.go @@ -0,0 +1,48 @@ +package main + +import ( + "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" +) + +var log = logrus.New() + +func init() { + formatter := new(prefixed.TextFormatter) + formatter.FullTimestamp = true + + // Set specific colors for prefix and timestamp + formatter.SetColorScheme(&prefixed.ColorScheme{ + PrefixStyle: "blue+b", + TimestampStyle: "white+h", + }) + + log.Formatter = formatter + log.Level = logrus.DebugLevel +} + +func main() { + log.WithFields(logrus.Fields{ + "prefix": "main", + "animal": "walrus", + "number": 8, + }).Debug("Started observing beach") + + // Or you can simply add prefix in square brackets within message itself + log.WithFields(logrus.Fields{ + "animal": "walrus", + "size": 10, + }).Debug("[main] A group of walrus emerges from the ocean") + + // Warning message + log.WithFields(logrus.Fields{ + "omg": true, + "number": 122, + }).Warn("[main] The group's number increased tremendously!") + + // Information message + log.WithFields(logrus.Fields{ + "prefix": "sensor", + "temperature": -4, + }).Info("Temperature changes") +} diff --git a/vendor/github.com/matterbridge/logrus-prefixed-formatter/formatter.go b/vendor/github.com/matterbridge/logrus-prefixed-formatter/formatter.go new file mode 100644 index 00000000..7cb807da --- /dev/null +++ b/vendor/github.com/matterbridge/logrus-prefixed-formatter/formatter.go @@ -0,0 +1,382 @@ +package prefixed + +import ( + "bytes" + "fmt" + "io" + "os" + "regexp" + "sort" + "strings" + "sync" + "time" + + "github.com/mgutz/ansi" + "github.com/sirupsen/logrus" + "golang.org/x/crypto/ssh/terminal" +) + +const defaultTimestampFormat = time.RFC3339 + +var ( + baseTimestamp time.Time = time.Now() + defaultColorScheme *ColorScheme = &ColorScheme{ + InfoLevelStyle: "green", + WarnLevelStyle: "yellow", + ErrorLevelStyle: "red", + FatalLevelStyle: "red", + PanicLevelStyle: "red", + DebugLevelStyle: "blue", + PrefixStyle: "cyan", + TimestampStyle: "black+h", + } + noColorsColorScheme *compiledColorScheme = &compiledColorScheme{ + InfoLevelColor: ansi.ColorFunc(""), + WarnLevelColor: ansi.ColorFunc(""), + ErrorLevelColor: ansi.ColorFunc(""), + FatalLevelColor: ansi.ColorFunc(""), + PanicLevelColor: ansi.ColorFunc(""), + DebugLevelColor: ansi.ColorFunc(""), + PrefixColor: ansi.ColorFunc(""), + TimestampColor: ansi.ColorFunc(""), + } + defaultCompiledColorScheme *compiledColorScheme = compileColorScheme(defaultColorScheme) +) + +func miniTS() int { + return int(time.Since(baseTimestamp) / time.Second) +} + +type ColorScheme struct { + InfoLevelStyle string + WarnLevelStyle string + ErrorLevelStyle string + FatalLevelStyle string + PanicLevelStyle string + DebugLevelStyle string + PrefixStyle string + TimestampStyle string +} + +type compiledColorScheme struct { + InfoLevelColor func(string) string + WarnLevelColor func(string) string + ErrorLevelColor func(string) string + FatalLevelColor func(string) string + PanicLevelColor func(string) string + DebugLevelColor func(string) string + PrefixColor func(string) string + TimestampColor func(string) string +} + +type TextFormatter struct { + // Set to true to bypass checking for a TTY before outputting colors. + ForceColors bool + + // Force disabling colors. For a TTY colors are enabled by default. + DisableColors bool + + // Force formatted layout, even for non-TTY output. + ForceFormatting bool + + // Disable timestamp logging. useful when output is redirected to logging + // system that already adds timestamps. + DisableTimestamp bool + + // Disable the conversion of the log levels to uppercase + DisableUppercase bool + + // Enable logging the full timestamp when a TTY is attached instead of just + // the time passed since beginning of execution. + FullTimestamp bool + + // Timestamp format to use for display when a full timestamp is printed. + TimestampFormat string + + // The fields are sorted by default for a consistent output. For applications + // that log extremely frequently and don't use the JSON formatter this may not + // be desired. + DisableSorting bool + + // Wrap empty fields in quotes if true. + QuoteEmptyFields bool + + // Can be set to the override the default quoting character " + // with something else. For example: ', or `. + QuoteCharacter string + + // Pad msg field with spaces on the right for display. + // The value for this parameter will be the size of padding. + // Its default value is zero, which means no padding will be applied for msg. + SpacePadding int + + // Pad prefix field with spaces on the right for display. + // The value for this parameter will be the size of padding. + // Its default value is zero, which means no padding will be applied for prefix. + PrefixPadding int + + // Color scheme to use. + colorScheme *compiledColorScheme + + // Whether the logger's out is to a terminal. + isTerminal bool + + sync.Once +} + +func getCompiledColor(main string, fallback string) func(string) string { + var style string + if main != "" { + style = main + } else { + style = fallback + } + return ansi.ColorFunc(style) +} + +func compileColorScheme(s *ColorScheme) *compiledColorScheme { + return &compiledColorScheme{ + InfoLevelColor: getCompiledColor(s.InfoLevelStyle, defaultColorScheme.InfoLevelStyle), + WarnLevelColor: getCompiledColor(s.WarnLevelStyle, defaultColorScheme.WarnLevelStyle), + ErrorLevelColor: getCompiledColor(s.ErrorLevelStyle, defaultColorScheme.ErrorLevelStyle), + FatalLevelColor: getCompiledColor(s.FatalLevelStyle, defaultColorScheme.FatalLevelStyle), + PanicLevelColor: getCompiledColor(s.PanicLevelStyle, defaultColorScheme.PanicLevelStyle), + DebugLevelColor: getCompiledColor(s.DebugLevelStyle, defaultColorScheme.DebugLevelStyle), + PrefixColor: getCompiledColor(s.PrefixStyle, defaultColorScheme.PrefixStyle), + TimestampColor: getCompiledColor(s.TimestampStyle, defaultColorScheme.TimestampStyle), + } +} + +func (f *TextFormatter) init(entry *logrus.Entry) { + if len(f.QuoteCharacter) == 0 { + f.QuoteCharacter = "\"" + } + if entry.Logger != nil { + f.isTerminal = f.checkIfTerminal(entry.Logger.Out) + } +} + +func (f *TextFormatter) checkIfTerminal(w io.Writer) bool { + switch v := w.(type) { + case *os.File: + return terminal.IsTerminal(int(v.Fd())) + default: + return false + } +} + +func (f *TextFormatter) SetColorScheme(colorScheme *ColorScheme) { + f.colorScheme = compileColorScheme(colorScheme) +} + +func (f *TextFormatter) Format(entry *logrus.Entry) ([]byte, error) { + var b *bytes.Buffer + var keys []string = make([]string, 0, len(entry.Data)) + for k := range entry.Data { + keys = append(keys, k) + } + lastKeyIdx := len(keys) - 1 + + if !f.DisableSorting { + sort.Strings(keys) + } + if entry.Buffer != nil { + b = entry.Buffer + } else { + b = &bytes.Buffer{} + } + + prefixFieldClashes(entry.Data) + + f.Do(func() { f.init(entry) }) + + isFormatted := f.ForceFormatting || f.isTerminal + + timestampFormat := f.TimestampFormat + if timestampFormat == "" { + timestampFormat = defaultTimestampFormat + } + if isFormatted { + isColored := (f.ForceColors || f.isTerminal) && !f.DisableColors + var colorScheme *compiledColorScheme + if isColored { + if f.colorScheme == nil { + colorScheme = defaultCompiledColorScheme + } else { + colorScheme = f.colorScheme + } + } else { + colorScheme = noColorsColorScheme + } + f.printColored(b, entry, keys, timestampFormat, colorScheme) + } else { + if !f.DisableTimestamp { + f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat), true) + } + f.appendKeyValue(b, "level", entry.Level.String(), true) + if entry.Message != "" { + f.appendKeyValue(b, "msg", entry.Message, lastKeyIdx >= 0) + } + for i, key := range keys { + f.appendKeyValue(b, key, entry.Data[key], lastKeyIdx != i) + } + } + + b.WriteByte('\n') + return b.Bytes(), nil +} + +func (f *TextFormatter) printColored(b *bytes.Buffer, entry *logrus.Entry, keys []string, timestampFormat string, colorScheme *compiledColorScheme) { + var levelColor func(string) string + var levelText string + switch entry.Level { + case logrus.InfoLevel: + levelColor = colorScheme.InfoLevelColor + case logrus.WarnLevel: + levelColor = colorScheme.WarnLevelColor + case logrus.ErrorLevel: + levelColor = colorScheme.ErrorLevelColor + case logrus.FatalLevel: + levelColor = colorScheme.FatalLevelColor + case logrus.PanicLevel: + levelColor = colorScheme.PanicLevelColor + default: + levelColor = colorScheme.DebugLevelColor + } + + if entry.Level != logrus.WarnLevel { + levelText = entry.Level.String() + } else { + levelText = "warn" + } + + if !f.DisableUppercase { + levelText = strings.ToUpper(levelText) + } + + level := levelColor(fmt.Sprintf("%5s", levelText)) + prefix := "" + message := entry.Message + + adjustedPrefixPadding := f.PrefixPadding //compensate for ANSI color sequences + + if prefixValue, ok := entry.Data["prefix"]; ok { + rawPrefixLength := len(prefixValue.(string)) + prefix = colorScheme.PrefixColor(" " + prefixValue.(string) + ":") + adjustedPrefixPadding = f.PrefixPadding + (len(prefix) - rawPrefixLength - 1) + } else { + prefixValue, trimmedMsg := extractPrefix(entry.Message) + rawPrefixLength := len(prefixValue) + if len(prefixValue) > 0 { + prefix = colorScheme.PrefixColor(" " + prefixValue + ":") + message = trimmedMsg + } + adjustedPrefixPadding = f.PrefixPadding + (len(prefix) - rawPrefixLength - 1) + } + + prefixFormat := "%s" + if f.PrefixPadding != 0 { + prefixFormat = fmt.Sprintf("%%-%ds", adjustedPrefixPadding) + } + + messageFormat := "%s" + if f.SpacePadding != 0 { + messageFormat = fmt.Sprintf("%%-%ds", f.SpacePadding) + } + + if f.DisableTimestamp { + fmt.Fprintf(b, "%s"+prefixFormat+" "+messageFormat, level, prefix, message) + } else { + var timestamp string + if !f.FullTimestamp { + timestamp = fmt.Sprintf("[%04d]", miniTS()) + } else { + timestamp = fmt.Sprintf("[%s]", entry.Time.Format(timestampFormat)) + } + fmt.Fprintf(b, "%s %s"+prefixFormat+" "+messageFormat, colorScheme.TimestampColor(timestamp), level, prefix, message) + } + for _, k := range keys { + if k != "prefix" { + v := entry.Data[k] + fmt.Fprintf(b, " %s=%+v", levelColor(k), v) + } + } +} + +func (f *TextFormatter) needsQuoting(text string) bool { + if f.QuoteEmptyFields && len(text) == 0 { + return true + } + for _, ch := range text { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '-' || ch == '.') { + return true + } + } + return false +} + +func extractPrefix(msg string) (string, string) { + prefix := "" + regex := regexp.MustCompile("^\\[(.*?)\\]") + if regex.MatchString(msg) { + match := regex.FindString(msg) + prefix, msg = match[1:len(match)-1], strings.TrimSpace(msg[len(match):]) + } + return prefix, msg +} + +func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}, appendSpace bool) { + b.WriteString(key) + b.WriteByte('=') + f.appendValue(b, value) + + if appendSpace { + b.WriteByte(' ') + } +} + +func (f *TextFormatter) appendValue(b *bytes.Buffer, value interface{}) { + switch value := value.(type) { + case string: + if !f.needsQuoting(value) { + b.WriteString(value) + } else { + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, value, f.QuoteCharacter) + } + case error: + errmsg := value.Error() + if !f.needsQuoting(errmsg) { + b.WriteString(errmsg) + } else { + fmt.Fprintf(b, "%s%v%s", f.QuoteCharacter, errmsg, f.QuoteCharacter) + } + default: + fmt.Fprint(b, value) + } +} + +// This is to not silently overwrite `time`, `msg` and `level` fields when +// dumping it. If this code wasn't there doing: +// +// logrus.WithField("level", 1).Info("hello") +// +// would just silently drop the user provided level. Instead with this code +// it'll be logged as: +// +// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."} +func prefixFieldClashes(data logrus.Fields) { + if t, ok := data["time"]; ok { + data["fields.time"] = t + } + + if m, ok := data["msg"]; ok { + data["fields.msg"] = m + } + + if l, ok := data["level"]; ok { + data["fields.level"] = l + } +} -- cgit v1.2.3