diff options
Diffstat (limited to 'vendor/github.com/bwmarrin/discordgo')
27 files changed, 12634 insertions, 0 deletions
diff --git a/vendor/github.com/bwmarrin/discordgo/.gitignore b/vendor/github.com/bwmarrin/discordgo/.gitignore new file mode 100644 index 00000000..681a96b1 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/.gitignore @@ -0,0 +1,5 @@ +# IDE-specific metadata +.idea/ + +# Environment variables. Useful for examples. +.env diff --git a/vendor/github.com/bwmarrin/discordgo/.golangci.yml b/vendor/github.com/bwmarrin/discordgo/.golangci.yml new file mode 100644 index 00000000..dd9d2e5b --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/.golangci.yml @@ -0,0 +1,19 @@ +linters: + disable-all: true + enable: + # - staticcheck + # - unused + - golint + +linters-settings: + staticcheck: + go: "1.13" + + checks: ["all"] + + unused: + go: "1.13" + +issues: + include: + - EXC0002 diff --git a/vendor/github.com/bwmarrin/discordgo/.travis.yml b/vendor/github.com/bwmarrin/discordgo/.travis.yml new file mode 100644 index 00000000..5d9cea3e --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/.travis.yml @@ -0,0 +1,17 @@ +language: go +go: + - 1.13.x + - 1.14.x + - 1.15.x + - 1.16.x +env: + - GO111MODULE=on +install: + - go get github.com/bwmarrin/discordgo + - go get -v . + - go get -v golang.org/x/lint/golint +script: + - diff <(gofmt -d .) <(echo -n) + - go vet -x ./... + - golint -set_exit_status ./... + - go test -v -race ./... diff --git a/vendor/github.com/bwmarrin/discordgo/LICENSE b/vendor/github.com/bwmarrin/discordgo/LICENSE new file mode 100644 index 00000000..8d062ea5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2015, Bruce Marriner +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of discordgo nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/vendor/github.com/bwmarrin/discordgo/README.md b/vendor/github.com/bwmarrin/discordgo/README.md new file mode 100644 index 00000000..2bcd43b8 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/README.md @@ -0,0 +1,105 @@ +# DiscordGo + +[![Go Reference](https://pkg.go.dev/badge/github.com/bwmarrin/discordgo.svg)](https://pkg.go.dev/github.com/bwmarrin/discordgo) [![Go Report Card](https://goreportcard.com/badge/github.com/bwmarrin/discordgo)](https://goreportcard.com/report/github.com/bwmarrin/discordgo) [![Build Status](https://travis-ci.com/bwmarrin/discordgo.svg?branch=master)](https://travis-ci.com/bwmarrin/discordgo) [![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23discordgo-blue.svg)](https://discord.gg/golang) [![Discord API](https://img.shields.io/badge/Discord%20API-%23go_discordgo-blue.svg)](https://discord.com/invite/discord-api) + +<img align="right" alt="DiscordGo logo" src="docs/img/discordgo.svg" width="400"> + +DiscordGo is a [Go](https://golang.org/) package that provides low level +bindings to the [Discord](https://discord.com/) chat client API. DiscordGo +has nearly complete support for all of the Discord API endpoints, websocket +interface, and voice interface. + +If you would like to help the DiscordGo package please use +[this link](https://discord.com/oauth2/authorize?client_id=173113690092994561&scope=bot) +to add the official DiscordGo test bot **dgo** to your server. This provides +indispensable help to this project. + +* See [dgVoice](https://github.com/bwmarrin/dgvoice) package for an example of +additional voice helper functions and features for DiscordGo. + +* See [dca](https://github.com/bwmarrin/dca) for an **experimental** stand alone +tool that wraps `ffmpeg` to create opus encoded audio appropriate for use with +Discord (and DiscordGo). + +**For help with this package or general Go discussion, please join the [Discord +Gophers](https://discord.gg/golang) chat server.** + +## Getting Started + +### Installing + +This assumes you already have a working Go environment, if not please see +[this page](https://golang.org/doc/install) first. + +`go get` *will always pull the latest tagged release from the master branch.* + +```sh +go get github.com/bwmarrin/discordgo +``` + +### Usage + +Import the package into your project. + +```go +import "github.com/bwmarrin/discordgo" +``` + +Construct a new Discord client which can be used to access the variety of +Discord API functions and to set callback functions for Discord events. + +```go +discord, err := discordgo.New("Bot " + "authentication token") +``` + +See Documentation and Examples below for more detailed information. + + +## Documentation + +**NOTICE**: This library and the Discord API are unfinished. +Because of that there may be major changes to library in the future. + +The DiscordGo code is fairly well documented at this point and is currently +the only documentation available. Both GoDoc and GoWalker (below) present +that information in a nice format. + +- [![Go Reference](https://pkg.go.dev/badge/github.com/bwmarrin/discordgo.svg)](https://pkg.go.dev/github.com/bwmarrin/discordgo) +- [![Go Walker](https://gowalker.org/api/v1/badge)](https://gowalker.org/github.com/bwmarrin/discordgo) +- Hand crafted documentation coming eventually. + + +## Examples + +Below is a list of examples and other projects using DiscordGo. Please submit +an issue if you would like your project added or removed from this list. + +- [DiscordGo Examples](https://github.com/bwmarrin/discordgo/tree/master/examples) - A collection of example programs written with DiscordGo +- [Awesome DiscordGo](https://github.com/bwmarrin/discordgo/wiki/Awesome-DiscordGo) - A curated list of high quality projects using DiscordGo + +## Troubleshooting +For help with common problems please reference the +[Troubleshooting](https://github.com/bwmarrin/discordgo/wiki/Troubleshooting) +section of the project wiki. + + +## Contributing +Contributions are very welcomed, however please follow the below guidelines. + +- First open an issue describing the bug or enhancement so it can be +discussed. +- Try to match current naming conventions as closely as possible. +- This package is intended to be a low level direct mapping of the Discord API, +so please avoid adding enhancements outside of that scope without first +discussing it. +- Create a Pull Request with your changes against the master branch. + + +## List of Discord APIs + +See [this chart](https://abal.moe/Discord/Libraries.html) for a feature +comparison and list of other Discord API libraries. + +## Special Thanks + +[Chris Rhodes](https://github.com/iopred) - For the DiscordGo logo and tons of PRs. diff --git a/vendor/github.com/bwmarrin/discordgo/components.go b/vendor/github.com/bwmarrin/discordgo/components.go new file mode 100644 index 00000000..00cbbf19 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/components.go @@ -0,0 +1,241 @@ +package discordgo + +import ( + "encoding/json" + "fmt" +) + +// ComponentType is type of component. +type ComponentType uint + +// MessageComponent types. +const ( + ActionsRowComponent ComponentType = 1 + ButtonComponent ComponentType = 2 + SelectMenuComponent ComponentType = 3 + TextInputComponent ComponentType = 4 +) + +// MessageComponent is a base interface for all message components. +type MessageComponent interface { + json.Marshaler + Type() ComponentType +} + +type unmarshalableMessageComponent struct { + MessageComponent +} + +// UnmarshalJSON is a helper function to unmarshal MessageComponent object. +func (umc *unmarshalableMessageComponent) UnmarshalJSON(src []byte) error { + var v struct { + Type ComponentType `json:"type"` + } + err := json.Unmarshal(src, &v) + if err != nil { + return err + } + + switch v.Type { + case ActionsRowComponent: + umc.MessageComponent = &ActionsRow{} + case ButtonComponent: + umc.MessageComponent = &Button{} + case SelectMenuComponent: + umc.MessageComponent = &SelectMenu{} + case TextInputComponent: + umc.MessageComponent = &TextInput{} + default: + return fmt.Errorf("unknown component type: %d", v.Type) + } + return json.Unmarshal(src, umc.MessageComponent) +} + +// MessageComponentFromJSON is a helper function for unmarshaling message components +func MessageComponentFromJSON(b []byte) (MessageComponent, error) { + var u unmarshalableMessageComponent + err := u.UnmarshalJSON(b) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal into MessageComponent: %w", err) + } + return u.MessageComponent, nil +} + +// ActionsRow is a container for components within one row. +type ActionsRow struct { + Components []MessageComponent `json:"components"` +} + +// MarshalJSON is a method for marshaling ActionsRow to a JSON object. +func (r ActionsRow) MarshalJSON() ([]byte, error) { + type actionsRow ActionsRow + + return json.Marshal(struct { + actionsRow + Type ComponentType `json:"type"` + }{ + actionsRow: actionsRow(r), + Type: r.Type(), + }) +} + +// UnmarshalJSON is a helper function to unmarshal Actions Row. +func (r *ActionsRow) UnmarshalJSON(data []byte) error { + var v struct { + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + r.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + r.Components[i] = v.MessageComponent + } + + return err +} + +// Type is a method to get the type of a component. +func (r ActionsRow) Type() ComponentType { + return ActionsRowComponent +} + +// ButtonStyle is style of button. +type ButtonStyle uint + +// Button styles. +const ( + // PrimaryButton is a button with blurple color. + PrimaryButton ButtonStyle = 1 + // SecondaryButton is a button with grey color. + SecondaryButton ButtonStyle = 2 + // SuccessButton is a button with green color. + SuccessButton ButtonStyle = 3 + // DangerButton is a button with red color. + DangerButton ButtonStyle = 4 + // LinkButton is a special type of button which navigates to a URL. Has grey color. + LinkButton ButtonStyle = 5 +) + +// ComponentEmoji represents button emoji, if it does have one. +type ComponentEmoji struct { + Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` + Animated bool `json:"animated,omitempty"` +} + +// Button represents button component. +type Button struct { + Label string `json:"label"` + Style ButtonStyle `json:"style"` + Disabled bool `json:"disabled"` + Emoji ComponentEmoji `json:"emoji"` + + // NOTE: Only button with LinkButton style can have link. Also, URL is mutually exclusive with CustomID. + URL string `json:"url,omitempty"` + CustomID string `json:"custom_id,omitempty"` +} + +// MarshalJSON is a method for marshaling Button to a JSON object. +func (b Button) MarshalJSON() ([]byte, error) { + type button Button + + if b.Style == 0 { + b.Style = PrimaryButton + } + + return json.Marshal(struct { + button + Type ComponentType `json:"type"` + }{ + button: button(b), + Type: b.Type(), + }) +} + +// Type is a method to get the type of a component. +func (Button) Type() ComponentType { + return ButtonComponent +} + +// SelectMenuOption represents an option for a select menu. +type SelectMenuOption struct { + Label string `json:"label,omitempty"` + Value string `json:"value"` + Description string `json:"description"` + Emoji ComponentEmoji `json:"emoji"` + // Determines whenever option is selected by default or not. + Default bool `json:"default"` +} + +// SelectMenu represents select menu component. +type SelectMenu struct { + CustomID string `json:"custom_id,omitempty"` + // The text which will be shown in the menu if there's no default options or all options was deselected and component was closed. + Placeholder string `json:"placeholder"` + // This value determines the minimal amount of selected items in the menu. + MinValues *int `json:"min_values,omitempty"` + // This value determines the maximal amount of selected items in the menu. + // If MaxValues or MinValues are greater than one then the user can select multiple items in the component. + MaxValues int `json:"max_values,omitempty"` + Options []SelectMenuOption `json:"options"` + Disabled bool `json:"disabled"` +} + +// Type is a method to get the type of a component. +func (SelectMenu) Type() ComponentType { + return SelectMenuComponent +} + +// MarshalJSON is a method for marshaling SelectMenu to a JSON object. +func (m SelectMenu) MarshalJSON() ([]byte, error) { + type selectMenu SelectMenu + + return json.Marshal(struct { + selectMenu + Type ComponentType `json:"type"` + }{ + selectMenu: selectMenu(m), + Type: m.Type(), + }) +} + +// TextInput represents text input component. +type TextInput struct { + CustomID string `json:"custom_id"` + Label string `json:"label"` + Style TextInputStyle `json:"style"` + Placeholder string `json:"placeholder,omitempty"` + Value string `json:"value,omitempty"` + Required bool `json:"required,omitempty"` + MinLength int `json:"min_length,omitempty"` + MaxLength int `json:"max_length,omitempty"` +} + +// Type is a method to get the type of a component. +func (TextInput) Type() ComponentType { + return TextInputComponent +} + +// MarshalJSON is a method for marshaling TextInput to a JSON object. +func (m TextInput) MarshalJSON() ([]byte, error) { + type inputText TextInput + + return json.Marshal(struct { + inputText + Type ComponentType `json:"type"` + }{ + inputText: inputText(m), + Type: m.Type(), + }) +} + +// TextInputStyle is style of text in TextInput component. +type TextInputStyle uint + +// Text styles +const ( + TextInputShort TextInputStyle = 1 + TextInputParagraph TextInputStyle = 2 +) diff --git a/vendor/github.com/bwmarrin/discordgo/discord.go b/vendor/github.com/bwmarrin/discordgo/discord.go new file mode 100644 index 00000000..74f4190a --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/discord.go @@ -0,0 +1,60 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains high level helper functions and easy entry points for the +// entire discordgo package. These functions are being developed and are very +// experimental at this point. They will most likely change so please use the +// low level functions if that's a problem. + +// Package discordgo provides Discord binding for Go +package discordgo + +import ( + "net/http" + "runtime" + "time" +) + +// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) +const VERSION = "0.24.0" + +// New creates a new Discord session with provided token. +// If the token is for a bot, it must be prefixed with "Bot " +// e.g. "Bot ..." +// Or if it is an OAuth2 token, it must be prefixed with "Bearer " +// e.g. "Bearer ..." +func New(token string) (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)}, + UserAgent: "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")", + sequence: new(int64), + LastHeartbeatAck: time.Now().UTC(), + } + + // Initilize the Identify Package with defaults + // These can be modified prior to calling Open() + s.Identify.Compress = true + s.Identify.LargeThreshold = 250 + s.Identify.GuildSubscriptions = true + s.Identify.Properties.OS = runtime.GOOS + s.Identify.Properties.Browser = "DiscordGo v" + VERSION + s.Identify.Intents = IntentsAllWithoutPrivileged + s.Identify.Token = token + s.Token = token + + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/endpoints.go b/vendor/github.com/bwmarrin/discordgo/endpoints.go new file mode 100644 index 00000000..d39a175b --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/endpoints.go @@ -0,0 +1,210 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains variables for all known Discord end points. All functions +// throughout the Discordgo package use these variables for all connections +// to Discord. These are all exported and you may modify them if needed. + +package discordgo + +import "strconv" + +// APIVersion is the Discord API version used for the REST and Websocket API. +var APIVersion = "9" + +// Known Discord API Endpoints. +var ( + EndpointStatus = "https://status.discord.com/api/v2/" + EndpointSm = EndpointStatus + "scheduled-maintenances/" + EndpointSmActive = EndpointSm + "active.json" + EndpointSmUpcoming = EndpointSm + "upcoming.json" + + EndpointDiscord = "https://discord.com/" + EndpointAPI = EndpointDiscord + "api/v" + APIVersion + "/" + EndpointGuilds = EndpointAPI + "guilds/" + EndpointChannels = EndpointAPI + "channels/" + EndpointUsers = EndpointAPI + "users/" + EndpointGateway = EndpointAPI + "gateway" + EndpointGatewayBot = EndpointGateway + "/bot" + EndpointWebhooks = EndpointAPI + "webhooks/" + EndpointStickers = EndpointAPI + "stickers/" + + EndpointCDN = "https://cdn.discordapp.com/" + EndpointCDNAttachments = EndpointCDN + "attachments/" + EndpointCDNAvatars = EndpointCDN + "avatars/" + EndpointCDNIcons = EndpointCDN + "icons/" + EndpointCDNSplashes = EndpointCDN + "splashes/" + EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" + EndpointCDNBanners = EndpointCDN + "banners/" + EndpointCDNGuilds = EndpointCDN + "guilds/" + + EndpointVoice = EndpointAPI + "/voice/" + EndpointVoiceRegions = EndpointVoice + "regions" + + // TODO: EndpointUserGuildMember + + 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" } + EndpointDefaultUserAvatar = func(uDiscriminator string) string { + uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator) + return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png" + } + EndpointUserBanner = func(uID, cID string) string { + return EndpointCDNBanners + uID + "/" + cID + ".png" + } + EndpointUserBannerAnimated = func(uID, cID string) string { + return EndpointCDNBanners + uID + "/" + cID + ".gif" + } + + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + + EndpointGuild = func(gID string) string { return EndpointGuilds + gID } + EndpointGuildThreads = func(gID string) string { return EndpointGuild(gID) + "/threads" } + EndpointGuildActiveThreads = func(gID string) string { return EndpointGuildThreads(gID) + "/active" } + EndpointGuildPreview = func(gID string) string { return EndpointGuilds + gID + "/preview" } + 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 } + 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" } + EndpointGuildWidget = func(gID string) string { return EndpointGuilds + gID + "/widget" } + EndpointGuildEmbed = EndpointGuildWidget + EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } + EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } + EndpointGuildIconAnimated = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".gif" } + EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } + EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } + EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } + EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" } + EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } + EndpointGuildBanner = func(gID, hash string) string { return EndpointCDNBanners + gID + "/" + hash + ".png" } + EndpointGuildStickers = func(gID string) string { return EndpointGuilds + gID + "/stickers" } + EndpointGuildSticker = func(gID, sID string) string { return EndpointGuilds + gID + "/stickers/" + sID } + EndpointGuildScheduledEvents = func(gID string) string { return EndpointGuilds + gID + "/scheduled-events" } + EndpointGuildScheduledEvent = func(gID, eID string) string { return EndpointGuilds + gID + "/scheduled-events/" + eID } + EndpointGuildScheduledEventUsers = func(gID, eID string) string { return EndpointGuildScheduledEvent(gID, eID) + "/users" } + EndpointGuildTemplate = func(tID string) string { return EndpointGuilds + "/templates/" + tID } + EndpointGuildTemplates = func(gID string) string { return EndpointGuilds + gID + "/templates" } + EndpointGuildTemplateSync = func(gID, tID string) string { return EndpointGuilds + gID + "/templates/" + tID } + EndpointGuildMemberAvatar = func(gId, uID, aID string) string { + return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".png" + } + EndpointGuildMemberAvatarAnimated = func(gId, uID, aID string) string { + return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".gif" + } + + EndpointChannel = func(cID string) string { return EndpointChannels + cID } + EndpointChannelThreads = func(cID string) string { return EndpointChannel(cID) + "/threads" } + EndpointChannelActiveThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/active" } + EndpointChannelPublicArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/public" } + EndpointChannelPrivateArchivedThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/archived/private" } + EndpointChannelJoinedPrivateArchivedThreads = func(cID string) string { return EndpointChannel(cID) + "/users/@me/threads/archived/private" } + 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 } + EndpointChannelMessageThread = func(cID, mID string) string { return EndpointChannelMessage(cID, mID) + "/threads" } + 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 } + EndpointChannelMessageCrosspost = func(cID, mID string) string { return EndpointChannel(cID) + "/messages/" + mID + "/crosspost" } + EndpointChannelFollow = func(cID string) string { return EndpointChannel(cID) + "/followers" } + EndpointThreadMembers = func(tID string) string { return EndpointChannel(tID) + "/thread-members" } + EndpointThreadMember = func(tID, mID string) string { return EndpointThreadMembers(tID) + "/" + mID } + + EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } + + EndpointSticker = func(sID string) string { return EndpointStickers + sID } + EndpointNitroStickersPacks = EndpointAPI + "/sticker-packs" + + 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 } + EndpointWebhookMessage = func(wID, token, messageID string) string { + return EndpointWebhookToken(wID, token) + "/messages/" + messageID + } + + 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 + } + + EndpointApplicationGlobalCommands = func(aID string) string { + return EndpointApplication(aID) + "/commands" + } + EndpointApplicationGlobalCommand = func(aID, cID string) string { + return EndpointApplicationGlobalCommands(aID) + "/" + cID + } + + EndpointApplicationGuildCommands = func(aID, gID string) string { + return EndpointApplication(aID) + "/guilds/" + gID + "/commands" + } + EndpointApplicationGuildCommand = func(aID, gID, cID string) string { + return EndpointApplicationGuildCommands(aID, gID) + "/" + cID + } + EndpointApplicationCommandPermissions = func(aID, gID, cID string) string { + return EndpointApplicationGuildCommand(aID, gID, cID) + "/permissions" + } + EndpointApplicationCommandsGuildPermissions = func(aID, gID string) string { + return EndpointApplicationGuildCommands(aID, gID) + "/permissions" + } + EndpointInteraction = func(aID, iToken string) string { + return EndpointAPI + "interactions/" + aID + "/" + iToken + } + EndpointInteractionResponse = func(iID, iToken string) string { + return EndpointInteraction(iID, iToken) + "/callback" + } + EndpointInteractionResponseActions = func(aID, iToken string) string { + return EndpointWebhookMessage(aID, iToken, "@original") + } + EndpointFollowupMessage = func(aID, iToken string) string { + return EndpointWebhookToken(aID, iToken) + } + EndpointFollowupMessageActions = func(aID, iToken, mID string) string { + return EndpointWebhookMessage(aID, iToken, mID) + } + + EndpointGuildCreate = EndpointAPI + "guilds" + + EndpointInvite = func(iID string) string { return EndpointAPI + "invites/" + iID } + + EndpointEmoji = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".png" } + EndpointEmojiAnimated = func(eID string) string { return EndpointCDN + "emojis/" + eID + ".gif" } + + EndpointApplications = EndpointAPI + "applications" + EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } + + EndpointOAuth2 = EndpointAPI + "oauth2/" + EndpointOAuth2Applications = EndpointOAuth2 + "applications" + EndpointOAuth2Application = func(aID string) string { return EndpointOAuth2Applications + "/" + aID } + EndpointOAuth2ApplicationsBot = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/bot" } + EndpointOAuth2ApplicationAssets = func(aID string) string { return EndpointOAuth2Applications + "/" + aID + "/assets" } + + // TODO: Deprecated, remove in the next release + EndpointOauth2 = EndpointOAuth2 + EndpointOauth2Applications = EndpointOAuth2Applications + EndpointOauth2Application = EndpointOAuth2Application + EndpointOauth2ApplicationsBot = EndpointOAuth2ApplicationsBot + EndpointOauth2ApplicationAssets = EndpointOAuth2ApplicationAssets +) diff --git a/vendor/github.com/bwmarrin/discordgo/event.go b/vendor/github.com/bwmarrin/discordgo/event.go new file mode 100644 index 00000000..84dbdc7f --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/event.go @@ -0,0 +1,247 @@ +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. +// The first parameter is a *Session, and the second parameter is a pointer +// to a struct corresponding to the event for which you want to listen. +// +// eg: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { +// }) +// +// or: +// Session.AddHandler(func(s *discordgo.Session, m *discordgo.PresenceUpdate) { +// }) +// +// List of events can be found at this page, with corresponding names in the +// library for each event: https://discord.com/developers/docs/topics/gateway#event-names +// There are also synthetic events fired by the library internally which are +// available for handling, like Connect, Disconnect, and RateLimit. +// events.go contains all of the Discord WSAPI and synthetic events that can be handled. +// +// The return value of this method is a function, that when called will remove the +// event handler. +func (s *Session) AddHandler(handler interface{}) func() { + 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], onceHandlers[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/bwmarrin/discordgo/eventhandlers.go b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go new file mode 100644 index 00000000..18d6248a --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/eventhandlers.go @@ -0,0 +1,1342 @@ +// 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" + guildScheduledEventCreateEventType = "GUILD_SCHEDULED_EVENT_CREATE" + guildScheduledEventUpdateEventType = "GUILD_SCHEDULED_EVENT_UPDATE" + guildScheduledEventDeleteEventType = "GUILD_SCHEDULED_EVENT_DELETE" + interactionCreateEventType = "INTERACTION_CREATE" + inviteCreateEventType = "INVITE_CREATE" + inviteDeleteEventType = "INVITE_DELETE" + 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" + threadCreateEventType = "THREAD_CREATE" + threadDeleteEventType = "THREAD_DELETE" + threadListSyncEventType = "THREAD_LIST_SYNC" + threadMemberUpdateEventType = "THREAD_MEMBER_UPDATE" + threadMembersUpdateEventType = "THREAD_MEMBERS_UPDATE" + threadUpdateEventType = "THREAD_UPDATE" + 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" + webhooksUpdateEventType = "WEBHOOKS_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) + } +} + +// guildScheduledEventCreateEventHandler is an event handler for GuildScheduledEventCreate events. +type guildScheduledEventCreateEventHandler func(*Session, *GuildScheduledEventCreate) + +// Type returns the event type for GuildScheduledEventCreate events. +func (eh guildScheduledEventCreateEventHandler) Type() string { + return guildScheduledEventCreateEventType +} + +// New returns a new instance of GuildScheduledEventCreate. +func (eh guildScheduledEventCreateEventHandler) New() interface{} { + return &GuildScheduledEventCreate{} +} + +// Handle is the handler for GuildScheduledEventCreate events. +func (eh guildScheduledEventCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildScheduledEventCreate); ok { + eh(s, t) + } +} + +// guildScheduledEventUpdateEventHandler is an event handler for GuildScheduledEventUpdate events. +type guildScheduledEventUpdateEventHandler func(*Session, *GuildScheduledEventUpdate) + +// Type returns the event type for GuildScheduledEventUpdate events. +func (eh guildScheduledEventUpdateEventHandler) Type() string { + return guildScheduledEventUpdateEventType +} + +// New returns a new instance of GuildScheduledEventUpdate. +func (eh guildScheduledEventUpdateEventHandler) New() interface{} { + return &GuildScheduledEventUpdate{} +} + +// Handle is the handler for GuildScheduledEventUpdate events. +func (eh guildScheduledEventUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildScheduledEventUpdate); ok { + eh(s, t) + } +} + +// guildScheduledEventDeleteEventHandler is an event handler for GuildScheduledEventDelete events. +type guildScheduledEventDeleteEventHandler func(*Session, *GuildScheduledEventDelete) + +// Type returns the event type for GuildScheduledEventDelete events. +func (eh guildScheduledEventDeleteEventHandler) Type() string { + return guildScheduledEventDeleteEventType +} + +// New returns a new instance of GuildScheduledEventDelete. +func (eh guildScheduledEventDeleteEventHandler) New() interface{} { + return &GuildScheduledEventDelete{} +} + +// Handle is the handler for GuildScheduledEventDelete events. +func (eh guildScheduledEventDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*GuildScheduledEventDelete); 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) + } +} + +// interactionCreateEventHandler is an event handler for InteractionCreate events. +type interactionCreateEventHandler func(*Session, *InteractionCreate) + +// Type returns the event type for InteractionCreate events. +func (eh interactionCreateEventHandler) Type() string { + return interactionCreateEventType +} + +// New returns a new instance of InteractionCreate. +func (eh interactionCreateEventHandler) New() interface{} { + return &InteractionCreate{} +} + +// Handle is the handler for InteractionCreate events. +func (eh interactionCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InteractionCreate); ok { + eh(s, t) + } +} + +// inviteCreateEventHandler is an event handler for InviteCreate events. +type inviteCreateEventHandler func(*Session, *InviteCreate) + +// Type returns the event type for InviteCreate events. +func (eh inviteCreateEventHandler) Type() string { + return inviteCreateEventType +} + +// New returns a new instance of InviteCreate. +func (eh inviteCreateEventHandler) New() interface{} { + return &InviteCreate{} +} + +// Handle is the handler for InviteCreate events. +func (eh inviteCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InviteCreate); ok { + eh(s, t) + } +} + +// inviteDeleteEventHandler is an event handler for InviteDelete events. +type inviteDeleteEventHandler func(*Session, *InviteDelete) + +// Type returns the event type for InviteDelete events. +func (eh inviteDeleteEventHandler) Type() string { + return inviteDeleteEventType +} + +// New returns a new instance of InviteDelete. +func (eh inviteDeleteEventHandler) New() interface{} { + return &InviteDelete{} +} + +// Handle is the handler for InviteDelete events. +func (eh inviteDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InviteDelete); 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) + } +} + +// threadCreateEventHandler is an event handler for ThreadCreate events. +type threadCreateEventHandler func(*Session, *ThreadCreate) + +// Type returns the event type for ThreadCreate events. +func (eh threadCreateEventHandler) Type() string { + return threadCreateEventType +} + +// New returns a new instance of ThreadCreate. +func (eh threadCreateEventHandler) New() interface{} { + return &ThreadCreate{} +} + +// Handle is the handler for ThreadCreate events. +func (eh threadCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadCreate); ok { + eh(s, t) + } +} + +// threadDeleteEventHandler is an event handler for ThreadDelete events. +type threadDeleteEventHandler func(*Session, *ThreadDelete) + +// Type returns the event type for ThreadDelete events. +func (eh threadDeleteEventHandler) Type() string { + return threadDeleteEventType +} + +// New returns a new instance of ThreadDelete. +func (eh threadDeleteEventHandler) New() interface{} { + return &ThreadDelete{} +} + +// Handle is the handler for ThreadDelete events. +func (eh threadDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadDelete); ok { + eh(s, t) + } +} + +// threadListSyncEventHandler is an event handler for ThreadListSync events. +type threadListSyncEventHandler func(*Session, *ThreadListSync) + +// Type returns the event type for ThreadListSync events. +func (eh threadListSyncEventHandler) Type() string { + return threadListSyncEventType +} + +// New returns a new instance of ThreadListSync. +func (eh threadListSyncEventHandler) New() interface{} { + return &ThreadListSync{} +} + +// Handle is the handler for ThreadListSync events. +func (eh threadListSyncEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadListSync); ok { + eh(s, t) + } +} + +// threadMemberUpdateEventHandler is an event handler for ThreadMemberUpdate events. +type threadMemberUpdateEventHandler func(*Session, *ThreadMemberUpdate) + +// Type returns the event type for ThreadMemberUpdate events. +func (eh threadMemberUpdateEventHandler) Type() string { + return threadMemberUpdateEventType +} + +// New returns a new instance of ThreadMemberUpdate. +func (eh threadMemberUpdateEventHandler) New() interface{} { + return &ThreadMemberUpdate{} +} + +// Handle is the handler for ThreadMemberUpdate events. +func (eh threadMemberUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadMemberUpdate); ok { + eh(s, t) + } +} + +// threadMembersUpdateEventHandler is an event handler for ThreadMembersUpdate events. +type threadMembersUpdateEventHandler func(*Session, *ThreadMembersUpdate) + +// Type returns the event type for ThreadMembersUpdate events. +func (eh threadMembersUpdateEventHandler) Type() string { + return threadMembersUpdateEventType +} + +// New returns a new instance of ThreadMembersUpdate. +func (eh threadMembersUpdateEventHandler) New() interface{} { + return &ThreadMembersUpdate{} +} + +// Handle is the handler for ThreadMembersUpdate events. +func (eh threadMembersUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadMembersUpdate); ok { + eh(s, t) + } +} + +// threadUpdateEventHandler is an event handler for ThreadUpdate events. +type threadUpdateEventHandler func(*Session, *ThreadUpdate) + +// Type returns the event type for ThreadUpdate events. +func (eh threadUpdateEventHandler) Type() string { + return threadUpdateEventType +} + +// New returns a new instance of ThreadUpdate. +func (eh threadUpdateEventHandler) New() interface{} { + return &ThreadUpdate{} +} + +// Handle is the handler for ThreadUpdate events. +func (eh threadUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*ThreadUpdate); 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) + } +} + +// webhooksUpdateEventHandler is an event handler for WebhooksUpdate events. +type webhooksUpdateEventHandler func(*Session, *WebhooksUpdate) + +// Type returns the event type for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Type() string { + return webhooksUpdateEventType +} + +// New returns a new instance of WebhooksUpdate. +func (eh webhooksUpdateEventHandler) New() interface{} { + return &WebhooksUpdate{} +} + +// Handle is the handler for WebhooksUpdate events. +func (eh webhooksUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*WebhooksUpdate); ok { + eh(s, t) + } +} + +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Session, interface{}): + 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, *GuildScheduledEventCreate): + return guildScheduledEventCreateEventHandler(v) + case func(*Session, *GuildScheduledEventUpdate): + return guildScheduledEventUpdateEventHandler(v) + case func(*Session, *GuildScheduledEventDelete): + return guildScheduledEventDeleteEventHandler(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, *InteractionCreate): + return interactionCreateEventHandler(v) + case func(*Session, *InviteCreate): + return inviteCreateEventHandler(v) + case func(*Session, *InviteDelete): + return inviteDeleteEventHandler(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, *ThreadCreate): + return threadCreateEventHandler(v) + case func(*Session, *ThreadDelete): + return threadDeleteEventHandler(v) + case func(*Session, *ThreadListSync): + return threadListSyncEventHandler(v) + case func(*Session, *ThreadMemberUpdate): + return threadMemberUpdateEventHandler(v) + case func(*Session, *ThreadMembersUpdate): + return threadMembersUpdateEventHandler(v) + case func(*Session, *ThreadUpdate): + return threadUpdateEventHandler(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) + case func(*Session, *WebhooksUpdate): + return webhooksUpdateEventHandler(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(guildScheduledEventCreateEventHandler(nil)) + registerInterfaceProvider(guildScheduledEventUpdateEventHandler(nil)) + registerInterfaceProvider(guildScheduledEventDeleteEventHandler(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(interactionCreateEventHandler(nil)) + registerInterfaceProvider(inviteCreateEventHandler(nil)) + registerInterfaceProvider(inviteDeleteEventHandler(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(threadCreateEventHandler(nil)) + registerInterfaceProvider(threadDeleteEventHandler(nil)) + registerInterfaceProvider(threadListSyncEventHandler(nil)) + registerInterfaceProvider(threadMemberUpdateEventHandler(nil)) + registerInterfaceProvider(threadMembersUpdateEventHandler(nil)) + registerInterfaceProvider(threadUpdateEventHandler(nil)) + registerInterfaceProvider(typingStartEventHandler(nil)) + registerInterfaceProvider(userGuildSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userNoteUpdateEventHandler(nil)) + registerInterfaceProvider(userSettingsUpdateEventHandler(nil)) + registerInterfaceProvider(userUpdateEventHandler(nil)) + registerInterfaceProvider(voiceServerUpdateEventHandler(nil)) + registerInterfaceProvider(voiceStateUpdateEventHandler(nil)) + registerInterfaceProvider(webhooksUpdateEventHandler(nil)) +} diff --git a/vendor/github.com/bwmarrin/discordgo/events.go b/vendor/github.com/bwmarrin/discordgo/events.go new file mode 100644 index 00000000..cc5d7116 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/events.go @@ -0,0 +1,372 @@ +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 synthetic event and is not dispatched by Discord. +type Connect struct{} + +// Disconnect is the data for a Disconnect event. +// This is a synthetic event and is not dispatched by Discord. +type Disconnect struct{} + +// RateLimit is the data for a RateLimit event. +// This is a synthetic 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"` + GuildID string `json:"guild_id,omitempty"` +} + +// ThreadCreate is the data for a ThreadCreate event. +type ThreadCreate struct { + *Channel + NewlyCreated bool `json:"newly_created"` +} + +// ThreadUpdate is the data for a ThreadUpdate event. +type ThreadUpdate struct { + *Channel + BeforeUpdate *Channel `json:"-"` +} + +// ThreadDelete is the data for a ThreadDelete event. +type ThreadDelete struct { + *Channel +} + +// ThreadListSync is the data for a ThreadListSync event. +type ThreadListSync struct { + // The id of the guild + GuildID string `json:"guild_id"` + // The parent channel ids whose threads are being synced. + // If omitted, then threads were synced for the entire guild. + // This array may contain channel_ids that have no active threads as well, so you know to clear that data. + ChannelIDs []string `json:"channel_ids"` + // All active threads in the given channels that the current user can access + Threads []*Channel `json:"threads"` + // All thread member objects from the synced threads for the current user, + // indicating which threads the current user has been added to + Members []*ThreadMember `json:"members"` +} + +// ThreadMemberUpdate is the data for a ThreadMemberUpdate event. +type ThreadMemberUpdate struct { + *ThreadMember + GuildID string `json:"guild_id"` +} + +// ThreadMembersUpdate is the data for a ThreadMembersUpdate event. +type ThreadMembersUpdate struct { + ID string `json:"id"` + GuildID string `json:"guild_id"` + MemberCount int `json:"member_count"` + AddedMembers []AddedThreadMember `json:"added_members"` + RemovedMembers []string `json:"removed_member_ids"` +} + +// 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 + BeforeDelete *Guild `json:"-"` +} + +// 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"` + ChunkIndex int `json:"chunk_index"` + ChunkCount int `json:"chunk_count"` + Presences []*Presence `json:"presences,omitempty"` +} + +// GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event. +type GuildIntegrationsUpdate struct { + GuildID string `json:"guild_id"` +} + +// GuildScheduledEventCreate is the data for a GuildScheduledEventCreate event. +type GuildScheduledEventCreate struct { + *GuildScheduledEvent +} + +// GuildScheduledEventUpdate is the data for a GuildScheduledEventUpdate event. +type GuildScheduledEventUpdate struct { + *GuildScheduledEvent +} + +// GuildScheduledEventDelete is the data for a GuildScheduledEventDelete event. +type GuildScheduledEventDelete struct { + *GuildScheduledEvent +} + +// 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 +} + +// UnmarshalJSON is a helper function to unmarshal MessageCreate object. +func (m *MessageCreate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + +// MessageUpdate is the data for a MessageUpdate event. +type MessageUpdate struct { + *Message + // BeforeUpdate will be nil if the Message was not previously cached in the state cache. + BeforeUpdate *Message `json:"-"` +} + +// UnmarshalJSON is a helper function to unmarshal MessageUpdate object. +func (m *MessageUpdate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + +// MessageDelete is the data for a MessageDelete event. +type MessageDelete struct { + *Message + BeforeDelete *Message `json:"-"` +} + +// UnmarshalJSON is a helper function to unmarshal MessageDelete object. +func (m *MessageDelete) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &m.Message) +} + +// MessageReactionAdd is the data for a MessageReactionAdd event. +type MessageReactionAdd struct { + *MessageReaction + Member *Member `json:"member,omitempty"` +} + +// 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"` +} + +// 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"` + GuildID string `json:"guild_id,omitempty"` + 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 + // BeforeUpdate will be nil if the VoiceState was not previously cached in the state cache. + BeforeUpdate *VoiceState `json:"-"` +} + +// MessageDeleteBulk is the data for a MessageDeleteBulk event +type MessageDeleteBulk struct { + Messages []string `json:"ids"` + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` +} + +// WebhooksUpdate is the data for a WebhooksUpdate event +type WebhooksUpdate struct { + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` +} + +// InteractionCreate is the data for a InteractionCreate event +type InteractionCreate struct { + *Interaction +} + +// UnmarshalJSON is a helper function to unmarshal Interaction object. +func (i *InteractionCreate) UnmarshalJSON(b []byte) error { + return json.Unmarshal(b, &i.Interaction) +} + +// InviteCreate is the data for a InviteCreate event +type InviteCreate struct { + *Invite + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` +} + +// InviteDelete is the data for a InviteDelete event +type InviteDelete struct { + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id"` + Code string `json:"code"` +} diff --git a/vendor/github.com/bwmarrin/discordgo/interactions.go b/vendor/github.com/bwmarrin/discordgo/interactions.go new file mode 100644 index 00000000..0e5ae3c4 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/interactions.go @@ -0,0 +1,568 @@ +package discordgo + +import ( + "bytes" + "crypto/ed25519" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "time" +) + +// InteractionDeadline is the time allowed to respond to an interaction. +const InteractionDeadline = time.Second * 3 + +// ApplicationCommandType represents the type of application command. +type ApplicationCommandType uint8 + +// Application command types +const ( + // ChatApplicationCommand is default command type. They are slash commands (i.e. called directly from the chat). + ChatApplicationCommand ApplicationCommandType = 1 + // UserApplicationCommand adds command to user context menu. + UserApplicationCommand ApplicationCommandType = 2 + // MessageApplicationCommand adds command to message context menu. + MessageApplicationCommand ApplicationCommandType = 3 +) + +// ApplicationCommand represents an application's slash command. +type ApplicationCommand struct { + ID string `json:"id,omitempty"` + ApplicationID string `json:"application_id,omitempty"` + Version string `json:"version,omitempty"` + Type ApplicationCommandType `json:"type,omitempty"` + Name string `json:"name"` + DefaultPermission *bool `json:"default_permission,omitempty"` + + // NOTE: Chat commands only. Otherwise it mustn't be set. + + Description string `json:"description,omitempty"` + Options []*ApplicationCommandOption `json:"options"` +} + +// ApplicationCommandOptionType indicates the type of a slash command's option. +type ApplicationCommandOptionType uint8 + +// Application command option types. +const ( + ApplicationCommandOptionSubCommand ApplicationCommandOptionType = 1 + ApplicationCommandOptionSubCommandGroup ApplicationCommandOptionType = 2 + ApplicationCommandOptionString ApplicationCommandOptionType = 3 + ApplicationCommandOptionInteger ApplicationCommandOptionType = 4 + ApplicationCommandOptionBoolean ApplicationCommandOptionType = 5 + ApplicationCommandOptionUser ApplicationCommandOptionType = 6 + ApplicationCommandOptionChannel ApplicationCommandOptionType = 7 + ApplicationCommandOptionRole ApplicationCommandOptionType = 8 + ApplicationCommandOptionMentionable ApplicationCommandOptionType = 9 + ApplicationCommandOptionNumber ApplicationCommandOptionType = 10 + ApplicationCommandOptionAttachment ApplicationCommandOptionType = 11 +) + +func (t ApplicationCommandOptionType) String() string { + switch t { + case ApplicationCommandOptionSubCommand: + return "SubCommand" + case ApplicationCommandOptionSubCommandGroup: + return "SubCommandGroup" + case ApplicationCommandOptionString: + return "String" + case ApplicationCommandOptionInteger: + return "Integer" + case ApplicationCommandOptionBoolean: + return "Boolean" + case ApplicationCommandOptionUser: + return "User" + case ApplicationCommandOptionChannel: + return "Channel" + case ApplicationCommandOptionRole: + return "Role" + case ApplicationCommandOptionMentionable: + return "Mentionable" + case ApplicationCommandOptionNumber: + return "Number" + case ApplicationCommandOptionAttachment: + return "Attachment" + } + return fmt.Sprintf("ApplicationCommandOptionType(%d)", t) +} + +// ApplicationCommandOption represents an option/subcommand/subcommands group. +type ApplicationCommandOption struct { + Type ApplicationCommandOptionType `json:"type"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + // NOTE: This feature was on the API, but at some point developers decided to remove it. + // So I commented it, until it will be officially on the docs. + // Default bool `json:"default"` + + ChannelTypes []ChannelType `json:"channel_types"` + Required bool `json:"required"` + Options []*ApplicationCommandOption `json:"options"` + + // NOTE: mutually exclusive with Choices. + Autocomplete bool `json:"autocomplete"` + Choices []*ApplicationCommandOptionChoice `json:"choices"` + // Minimal value of number/integer option. + MinValue *float64 `json:"min_value,omitempty"` + // Maximum value of number/integer option. + MaxValue float64 `json:"max_value,omitempty"` +} + +// ApplicationCommandOptionChoice represents a slash command option choice. +type ApplicationCommandOptionChoice struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// ApplicationCommandPermissions represents a single user or role permission for a command. +type ApplicationCommandPermissions struct { + ID string `json:"id"` + Type ApplicationCommandPermissionType `json:"type"` + Permission bool `json:"permission"` +} + +// ApplicationCommandPermissionsList represents a list of ApplicationCommandPermissions, needed for serializing to JSON. +type ApplicationCommandPermissionsList struct { + Permissions []*ApplicationCommandPermissions `json:"permissions"` +} + +// GuildApplicationCommandPermissions represents all permissions for a single guild command. +type GuildApplicationCommandPermissions struct { + ID string `json:"id"` + ApplicationID string `json:"application_id"` + GuildID string `json:"guild_id"` + Permissions []*ApplicationCommandPermissions `json:"permissions"` +} + +// ApplicationCommandPermissionType indicates whether a permission is user or role based. +type ApplicationCommandPermissionType uint8 + +// Application command permission types. +const ( + ApplicationCommandPermissionTypeRole ApplicationCommandPermissionType = 1 + ApplicationCommandPermissionTypeUser ApplicationCommandPermissionType = 2 +) + +// InteractionType indicates the type of an interaction event. +type InteractionType uint8 + +// Interaction types +const ( + InteractionPing InteractionType = 1 + InteractionApplicationCommand InteractionType = 2 + InteractionMessageComponent InteractionType = 3 + InteractionApplicationCommandAutocomplete InteractionType = 4 + InteractionModalSubmit InteractionType = 5 +) + +func (t InteractionType) String() string { + switch t { + case InteractionPing: + return "Ping" + case InteractionApplicationCommand: + return "ApplicationCommand" + case InteractionMessageComponent: + return "MessageComponent" + case InteractionModalSubmit: + return "ModalSubmit" + } + return fmt.Sprintf("InteractionType(%d)", t) +} + +// Interaction represents data of an interaction. +type Interaction struct { + ID string `json:"id"` + Type InteractionType `json:"type"` + Data InteractionData `json:"data"` + GuildID string `json:"guild_id"` + ChannelID string `json:"channel_id"` + + // The message on which interaction was used. + // NOTE: this field is only filled when a button click triggered the interaction. Otherwise it will be nil. + Message *Message `json:"message"` + + // The member who invoked this interaction. + // NOTE: this field is only filled when the slash command was invoked in a guild; + // if it was invoked in a DM, the `User` field will be filled instead. + // Make sure to check for `nil` before using this field. + Member *Member `json:"member"` + // The user who invoked this interaction. + // NOTE: this field is only filled when the slash command was invoked in a DM; + // if it was invoked in a guild, the `Member` field will be filled instead. + // Make sure to check for `nil` before using this field. + User *User `json:"user"` + + // The user's discord client locale. + Locale Locale `json:"locale"` + // The guild's locale. This defaults to EnglishUS + // NOTE: this field is only filled when the interaction was invoked in a guild. + GuildLocale *Locale `json:"guild_locale"` + + Token string `json:"token"` + Version int `json:"version"` +} + +type interaction Interaction + +type rawInteraction struct { + interaction + Data json.RawMessage `json:"data"` +} + +// UnmarshalJSON is a method for unmarshalling JSON object to Interaction. +func (i *Interaction) UnmarshalJSON(raw []byte) error { + var tmp rawInteraction + err := json.Unmarshal(raw, &tmp) + if err != nil { + return err + } + + *i = Interaction(tmp.interaction) + + switch tmp.Type { + case InteractionApplicationCommand, InteractionApplicationCommandAutocomplete: + v := ApplicationCommandInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v + case InteractionMessageComponent: + v := MessageComponentInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v + case InteractionModalSubmit: + v := ModalSubmitInteractionData{} + err = json.Unmarshal(tmp.Data, &v) + if err != nil { + return err + } + i.Data = v + } + return nil +} + +// MessageComponentData is helper function to assert the inner InteractionData to MessageComponentInteractionData. +// Make sure to check that the Type of the interaction is InteractionMessageComponent before calling. +func (i Interaction) MessageComponentData() (data MessageComponentInteractionData) { + if i.Type != InteractionMessageComponent { + panic("MessageComponentData called on interaction of type " + i.Type.String()) + } + return i.Data.(MessageComponentInteractionData) +} + +// ApplicationCommandData is helper function to assert the inner InteractionData to ApplicationCommandInteractionData. +// Make sure to check that the Type of the interaction is InteractionApplicationCommand before calling. +func (i Interaction) ApplicationCommandData() (data ApplicationCommandInteractionData) { + if i.Type != InteractionApplicationCommand && i.Type != InteractionApplicationCommandAutocomplete { + panic("ApplicationCommandData called on interaction of type " + i.Type.String()) + } + return i.Data.(ApplicationCommandInteractionData) +} + +// ModalSubmitData is helper function to assert the inner InteractionData to ModalSubmitInteractionData. +// Make sure to check that the Type of the interaction is InteractionModalSubmit before calling. +func (i Interaction) ModalSubmitData() (data ModalSubmitInteractionData) { + if i.Type != InteractionModalSubmit { + panic("ModalSubmitData called on interaction of type " + i.Type.String()) + } + return i.Data.(ModalSubmitInteractionData) +} + +// InteractionData is a common interface for all types of interaction data. +type InteractionData interface { + Type() InteractionType +} + +// ApplicationCommandInteractionData contains the data of application command interaction. +type ApplicationCommandInteractionData struct { + ID string `json:"id"` + Name string `json:"name"` + Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"` + + // Slash command options + Options []*ApplicationCommandInteractionDataOption `json:"options"` + // Target (user/message) id on which context menu command was called. + // The details are stored in Resolved according to command type. + TargetID string `json:"target_id"` +} + +// ApplicationCommandInteractionDataResolved contains resolved data of command execution. +// Partial Member objects are missing user, deaf and mute fields. +// Partial Channel objects only have id, name, type and permissions fields. +type ApplicationCommandInteractionDataResolved struct { + Users map[string]*User `json:"users"` + Members map[string]*Member `json:"members"` + Roles map[string]*Role `json:"roles"` + Channels map[string]*Channel `json:"channels"` + Messages map[string]*Message `json:"messages"` + Attachments map[string]*MessageAttachment `json:"attachments"` +} + +// Type returns the type of interaction data. +func (ApplicationCommandInteractionData) Type() InteractionType { + return InteractionApplicationCommand +} + +// MessageComponentInteractionData contains the data of message component interaction. +type MessageComponentInteractionData struct { + CustomID string `json:"custom_id"` + ComponentType ComponentType `json:"component_type"` + + // NOTE: Only filled when ComponentType is SelectMenuComponent (3). Otherwise is nil. + Values []string `json:"values"` +} + +// Type returns the type of interaction data. +func (MessageComponentInteractionData) Type() InteractionType { + return InteractionMessageComponent +} + +// ModalSubmitInteractionData contains the data of modal submit interaction. +type ModalSubmitInteractionData struct { + CustomID string `json:"custom_id"` + Components []MessageComponent `json:"-"` +} + +// Type returns the type of interaction data. +func (ModalSubmitInteractionData) Type() InteractionType { + return InteractionModalSubmit +} + +// UnmarshalJSON is a helper function to correctly unmarshal Components. +func (d *ModalSubmitInteractionData) UnmarshalJSON(data []byte) error { + type modalSubmitInteractionData ModalSubmitInteractionData + var v struct { + modalSubmitInteractionData + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + *d = ModalSubmitInteractionData(v.modalSubmitInteractionData) + d.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + d.Components[i] = v.MessageComponent + } + return err +} + +// ApplicationCommandInteractionDataOption represents an option of a slash command. +type ApplicationCommandInteractionDataOption struct { + Name string `json:"name"` + Type ApplicationCommandOptionType `json:"type"` + // NOTE: Contains the value specified by Type. + Value interface{} `json:"value,omitempty"` + Options []*ApplicationCommandInteractionDataOption `json:"options,omitempty"` + + // NOTE: autocomplete interaction only. + Focused bool `json:"focused,omitempty"` +} + +// IntValue is a utility function for casting option value to integer +func (o ApplicationCommandInteractionDataOption) IntValue() int64 { + if o.Type != ApplicationCommandOptionInteger { + panic("IntValue called on data option of type " + o.Type.String()) + } + return int64(o.Value.(float64)) +} + +// UintValue is a utility function for casting option value to unsigned integer +func (o ApplicationCommandInteractionDataOption) UintValue() uint64 { + if o.Type != ApplicationCommandOptionInteger { + panic("UintValue called on data option of type " + o.Type.String()) + } + return uint64(o.Value.(float64)) +} + +// FloatValue is a utility function for casting option value to float +func (o ApplicationCommandInteractionDataOption) FloatValue() float64 { + if o.Type != ApplicationCommandOptionNumber { + panic("FloatValue called on data option of type " + o.Type.String()) + } + return o.Value.(float64) +} + +// StringValue is a utility function for casting option value to string +func (o ApplicationCommandInteractionDataOption) StringValue() string { + if o.Type != ApplicationCommandOptionString { + panic("StringValue called on data option of type " + o.Type.String()) + } + return o.Value.(string) +} + +// BoolValue is a utility function for casting option value to bool +func (o ApplicationCommandInteractionDataOption) BoolValue() bool { + if o.Type != ApplicationCommandOptionBoolean { + panic("BoolValue called on data option of type " + o.Type.String()) + } + return o.Value.(bool) +} + +// ChannelValue is a utility function for casting option value to channel object. +// s : Session object, if not nil, function additionally fetches all channel's data +func (o ApplicationCommandInteractionDataOption) ChannelValue(s *Session) *Channel { + if o.Type != ApplicationCommandOptionChannel { + panic("ChannelValue called on data option of type " + o.Type.String()) + } + chanID := o.Value.(string) + + if s == nil { + return &Channel{ID: chanID} + } + + ch, err := s.State.Channel(chanID) + if err != nil { + ch, err = s.Channel(chanID) + if err != nil { + return &Channel{ID: chanID} + } + } + + return ch +} + +// RoleValue is a utility function for casting option value to role object. +// s : Session object, if not nil, function additionally fetches all role's data +func (o ApplicationCommandInteractionDataOption) RoleValue(s *Session, gID string) *Role { + if o.Type != ApplicationCommandOptionRole && o.Type != ApplicationCommandOptionMentionable { + panic("RoleValue called on data option of type " + o.Type.String()) + } + roleID := o.Value.(string) + + if s == nil || gID == "" { + return &Role{ID: roleID} + } + + r, err := s.State.Role(roleID, gID) + if err != nil { + roles, err := s.GuildRoles(gID) + if err == nil { + for _, r = range roles { + if r.ID == roleID { + return r + } + } + } + return &Role{ID: roleID} + } + + return r +} + +// UserValue is a utility function for casting option value to user object. +// s : Session object, if not nil, function additionally fetches all user's data +func (o ApplicationCommandInteractionDataOption) UserValue(s *Session) *User { + if o.Type != ApplicationCommandOptionUser && o.Type != ApplicationCommandOptionMentionable { + panic("UserValue called on data option of type " + o.Type.String()) + } + userID := o.Value.(string) + + if s == nil { + return &User{ID: userID} + } + + u, err := s.User(userID) + if err != nil { + return &User{ID: userID} + } + + return u +} + +// InteractionResponseType is type of interaction response. +type InteractionResponseType uint8 + +// Interaction response types. +const ( + // InteractionResponsePong is for ACK ping event. + InteractionResponsePong InteractionResponseType = 1 + // InteractionResponseChannelMessageWithSource is for responding with a message, showing the user's input. + InteractionResponseChannelMessageWithSource InteractionResponseType = 4 + // InteractionResponseDeferredChannelMessageWithSource acknowledges that the event was received, and that a follow-up will come later. + InteractionResponseDeferredChannelMessageWithSource InteractionResponseType = 5 + // InteractionResponseDeferredMessageUpdate acknowledges that the message component interaction event was received, and message will be updated later. + InteractionResponseDeferredMessageUpdate InteractionResponseType = 6 + // InteractionResponseUpdateMessage is for updating the message to which message component was attached. + InteractionResponseUpdateMessage InteractionResponseType = 7 + // InteractionApplicationCommandAutocompleteResult shows autocompletion results. Autocomplete interaction only. + InteractionApplicationCommandAutocompleteResult InteractionResponseType = 8 + // InteractionResponseModal is for responding to an interaction with a modal window. + InteractionResponseModal InteractionResponseType = 9 +) + +// InteractionResponse represents a response for an interaction event. +type InteractionResponse struct { + Type InteractionResponseType `json:"type,omitempty"` + Data *InteractionResponseData `json:"data,omitempty"` +} + +// InteractionResponseData is response data for an interaction. +type InteractionResponseData struct { + TTS bool `json:"tts"` + Content string `json:"content"` + Components []MessageComponent `json:"components"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + Flags uint64 `json:"flags,omitempty"` + Files []*File `json:"-"` + + // NOTE: autocomplete interaction only. + Choices []*ApplicationCommandOptionChoice `json:"choices,omitempty"` + + // NOTE: modal interaction only. + + CustomID string `json:"custom_id,omitempty"` + Title string `json:"title,omitempty"` +} + +// VerifyInteraction implements message verification of the discord interactions api +// signing algorithm, as documented here: +// https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization +func VerifyInteraction(r *http.Request, key ed25519.PublicKey) bool { + var msg bytes.Buffer + + signature := r.Header.Get("X-Signature-Ed25519") + if signature == "" { + return false + } + + sig, err := hex.DecodeString(signature) + if err != nil { + return false + } + + if len(sig) != ed25519.SignatureSize { + return false + } + + timestamp := r.Header.Get("X-Signature-Timestamp") + if timestamp == "" { + return false + } + + msg.WriteString(timestamp) + + defer r.Body.Close() + var body bytes.Buffer + + // at the end of the function, copy the original body back into the request + defer func() { + r.Body = ioutil.NopCloser(&body) + }() + + // copy body into buffers + _, err = io.Copy(&msg, io.TeeReader(r.Body, &body)) + if err != nil { + return false + } + + return ed25519.Verify(key, msg.Bytes(), sig) +} diff --git a/vendor/github.com/bwmarrin/discordgo/locales.go b/vendor/github.com/bwmarrin/discordgo/locales.go new file mode 100644 index 00000000..a6a1c521 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/locales.go @@ -0,0 +1,83 @@ +package discordgo + +// Locale represents the accepted languages for Discord. +// https://discord.com/developers/docs/reference#locales +type Locale string + +// String returns the human-readable string of the locale +func (l Locale) String() string { + if name, ok := Locales[l]; ok { + return name + } + return Unknown.String() +} + +// All defined locales in Discord +const ( + EnglishUS Locale = "en-US" + EnglishGB Locale = "en-GB" + Bulgarian Locale = "bg" + ChineseCN Locale = "zh-CN" + ChineseTW Locale = "zh-TW" + Croatian Locale = "hr" + Czech Locale = "cs" + Danish Locale = "da" + Dutch Locale = "nl" + Finnish Locale = "fi" + French Locale = "fr" + German Locale = "de" + Greek Locale = "el" + Hindi Locale = "hi" + Hungarian Locale = "hu" + Italian Locale = "it" + Japanese Locale = "ja" + Korean Locale = "ko" + Lithuanian Locale = "lt" + Norwegian Locale = "no" + Polish Locale = "pl" + PortugueseBR Locale = "pt-BR" + Romanian Locale = "ro" + Russian Locale = "ru" + SpanishES Locale = "es-ES" + Swedish Locale = "sv-SE" + Thai Locale = "th" + Turkish Locale = "tr" + Ukrainian Locale = "uk" + Vietnamese Locale = "vi" + Unknown Locale = "" +) + +// Locales is a map of all the languages codes to their names. +var Locales = map[Locale]string{ + EnglishUS: "English (United States)", + EnglishGB: "English (Great Britain)", + Bulgarian: "Bulgarian", + ChineseCN: "Chinese (China)", + ChineseTW: "Chinese (Taiwan)", + Croatian: "Croatian", + Czech: "Czech", + Danish: "Danish", + Dutch: "Dutch", + Finnish: "Finnish", + French: "French", + German: "German", + Greek: "Greek", + Hindi: "Hindi", + Hungarian: "Hungarian", + Italian: "Italian", + Japanese: "Japanese", + Korean: "Korean", + Lithuanian: "Lithuanian", + Norwegian: "Norwegian", + Polish: "Polish", + PortugueseBR: "Portuguese (Brazil)", + Romanian: "Romanian", + Russian: "Russian", + SpanishES: "Spanish (Spain)", + Swedish: "Swedish", + Thai: "Thai", + Turkish: "Turkish", + Ukrainian: "Ukrainian", + Vietnamese: "Vietnamese", + Unknown: "unknown", +} diff --git a/vendor/github.com/bwmarrin/discordgo/logging.go b/vendor/github.com/bwmarrin/discordgo/logging.go new file mode 100644 index 00000000..41f0481f --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/logging.go @@ -0,0 +1,103 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to discordgo package logging + +package discordgo + +import ( + "fmt" + "log" + "runtime" + "strings" +) + +const ( + + // LogError level is used for critical errors that could lead to data loss + // or panic that would not be returned to a calling function. + LogError int = iota + + // LogWarning level is used for very abnormal events and errors that are + // also 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 consistency 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/bwmarrin/discordgo/message.go b/vendor/github.com/bwmarrin/discordgo/message.go new file mode 100644 index 00000000..eb2f4962 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/message.go @@ -0,0 +1,541 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to the Message struct + +package discordgo + +import ( + "encoding/json" + "io" + "regexp" + "strings" + "time" +) + +// MessageType is the type of Message +// https://discord.com/developers/docs/resources/channel#message-object-message-types +type MessageType int + +// Block contains the valid known MessageType values +const ( + MessageTypeDefault MessageType = 0 + MessageTypeRecipientAdd MessageType = 1 + MessageTypeRecipientRemove MessageType = 2 + MessageTypeCall MessageType = 3 + MessageTypeChannelNameChange MessageType = 4 + MessageTypeChannelIconChange MessageType = 5 + MessageTypeChannelPinnedMessage MessageType = 6 + MessageTypeGuildMemberJoin MessageType = 7 + MessageTypeUserPremiumGuildSubscription MessageType = 8 + MessageTypeUserPremiumGuildSubscriptionTierOne MessageType = 9 + MessageTypeUserPremiumGuildSubscriptionTierTwo MessageType = 10 + MessageTypeUserPremiumGuildSubscriptionTierThree MessageType = 11 + MessageTypeChannelFollowAdd MessageType = 12 + MessageTypeGuildDiscoveryDisqualified MessageType = 14 + MessageTypeGuildDiscoveryRequalified MessageType = 15 + MessageTypeThreadCreated MessageType = 18 + MessageTypeReply MessageType = 19 + MessageTypeChatInputCommand MessageType = 20 + MessageTypeThreadStarterMessage MessageType = 21 + MessageTypeContextMenuCommand MessageType = 23 +) + +// A Message stores all data related to a specific Discord message. +type Message struct { + // The ID of the message. + ID string `json:"id"` + + // The ID of the channel in which the message was sent. + ChannelID string `json:"channel_id"` + + // The ID of the guild in which the message was sent. + GuildID string `json:"guild_id,omitempty"` + + // The content of the message. + Content string `json:"content"` + + // The time at which the messsage was sent. + // CAUTION: this field may be removed in a + // future API version; it is safer to calculate + // the creation time via the ID. + Timestamp time.Time `json:"timestamp"` + + // The time at which the last edit of the message + // occurred, if it has been edited. + EditedTimestamp *time.Time `json:"edited_timestamp"` + + // The roles mentioned in the message. + MentionRoles []string `json:"mention_roles"` + + // Whether the message is text-to-speech. + TTS bool `json:"tts"` + + // Whether the message mentions everyone. + MentionEveryone bool `json:"mention_everyone"` + + // The author of the message. This is not guaranteed to be a + // valid user (webhook-sent messages do not possess a full author). + Author *User `json:"author"` + + // A list of attachments present in the message. + Attachments []*MessageAttachment `json:"attachments"` + + // A list of components attached to the message. + Components []MessageComponent `json:"-"` + + // A list of embeds present in the message. + Embeds []*MessageEmbed `json:"embeds"` + + // A list of users mentioned in the message. + Mentions []*User `json:"mentions"` + + // A list of reactions to the message. + Reactions []*MessageReactions `json:"reactions"` + + // Whether the message is pinned or not. + Pinned bool `json:"pinned"` + + // The type of the message. + Type MessageType `json:"type"` + + // The webhook ID of the message, if it was generated by a webhook + WebhookID string `json:"webhook_id"` + + // Member properties for this message's author, + // contains only partial information + Member *Member `json:"member"` + + // Channels specifically mentioned in this message + // Not all channel mentions in a message will appear in mention_channels. + // Only textual channels that are visible to everyone in a lurkable guild will ever be included. + // Only crossposted messages (via Channel Following) currently include mention_channels at all. + // If no mentions in the message meet these requirements, this field will not be sent. + MentionChannels []*Channel `json:"mention_channels"` + + // Is sent with Rich Presence-related chat embeds + Activity *MessageActivity `json:"activity"` + + // Is sent with Rich Presence-related chat embeds + Application *MessageApplication `json:"application"` + + // MessageReference contains reference data sent with crossposted or reply messages. + // This does not contain the reference *to* this message; this is for when *this* message references another. + // To generate a reference to this message, use (*Message).Reference(). + MessageReference *MessageReference `json:"message_reference"` + + // The message associated with the message_reference + // NOTE: This field is only returned for messages with a type of 19 (REPLY) or 21 (THREAD_STARTER_MESSAGE). + // If the message is a reply but the referenced_message field is not present, + // the backend did not attempt to fetch the message that was being replied to, so its state is unknown. + // If the field exists but is null, the referenced message was deleted. + ReferencedMessage *Message `json:"referenced_message"` + + // Is sent when the message is a response to an Interaction, without an existing message. + // This means responses to message component interactions do not include this property, + // instead including a MessageReference, as components exist on preexisting messages. + Interaction *MessageInteraction `json:"interaction"` + + // The flags of the message, which describe extra features of a message. + // This is a combination of bit masks; the presence of a certain permission can + // be checked by performing a bitwise AND between this int and the flag. + Flags MessageFlags `json:"flags"` + + // The thread that was started from this message, includes thread member object + Thread *Channel `json:"thread,omitempty"` + + // An array of Sticker objects, if any were sent. + StickerItems []*Sticker `json:"sticker_items"` +} + +// UnmarshalJSON is a helper function to unmarshal the Message. +func (m *Message) UnmarshalJSON(data []byte) error { + type message Message + var v struct { + message + RawComponents []unmarshalableMessageComponent `json:"components"` + } + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + *m = Message(v.message) + m.Components = make([]MessageComponent, len(v.RawComponents)) + for i, v := range v.RawComponents { + m.Components[i] = v.MessageComponent + } + return err +} + +// GetCustomEmojis pulls out all the custom (Non-unicode) emojis from a message and returns a Slice of the Emoji struct. +func (m *Message) GetCustomEmojis() []*Emoji { + var toReturn []*Emoji + emojis := EmojiRegex.FindAllString(m.Content, -1) + if len(emojis) < 1 { + return toReturn + } + for _, em := range emojis { + parts := strings.Split(em, ":") + toReturn = append(toReturn, &Emoji{ + ID: parts[2][:len(parts[2])-1], + Name: parts[1], + Animated: strings.HasPrefix(em, "<a:"), + }) + } + return toReturn +} + +// MessageFlags is the flags of "message" (see MessageFlags* consts) +// https://discord.com/developers/docs/resources/channel#message-object-message-flags +type MessageFlags int + +// Valid MessageFlags values +const ( + // MessageFlagsCrossPosted This message has been published to subscribed channels (via Channel Following). + MessageFlagsCrossPosted MessageFlags = 1 << 0 + // MessageFlagsIsCrossPosted this message originated from a message in another channel (via Channel Following). + MessageFlagsIsCrossPosted MessageFlags = 1 << 1 + // MessageFlagsSupressEmbeds do not include any embeds when serializing this message. + MessageFlagsSupressEmbeds MessageFlags = 1 << 2 + // MessageFlagsSourceMessageDeleted the source message for this crosspost has been deleted (via Channel Following). + MessageFlagsSourceMessageDeleted MessageFlags = 1 << 3 + // MessageFlagsUrgent this message came from the urgent message system. + MessageFlagsUrgent MessageFlags = 1 << 4 + // MessageFlagsHasThread this message has an associated thread, with the same id as the message. + MessageFlagsHasThread MessageFlags = 1 << 5 + // MessageFlagsEphemeral this message is only visible to the user who invoked the Interaction. + MessageFlagsEphemeral MessageFlags = 1 << 6 + // MessageFlagsLoading this message is an Interaction Response and the bot is "thinking". + MessageFlagsLoading MessageFlags = 1 << 7 + // MessageFlagsFailedToMentionSomeRolesInThread this message failed to mention some roles and add their members to the thread. + MessageFlagsFailedToMentionSomeRolesInThread MessageFlags = 1 << 8 +) + +// 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"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + TTS bool `json:"tts"` + Components []MessageComponent `json:"components"` + Files []*File `json:"-"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + Reference *MessageReference `json:"message_reference,omitempty"` + + // TODO: Remove this when compatibility is not required. + File *File `json:"-"` + + // TODO: Remove this when compatibility is not required. + Embed *MessageEmbed `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"` + Components []MessageComponent `json:"components"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + + ID string + Channel string + + // TODO: Remove this when compatibility is not required. + Embed *MessageEmbed `json:"-"` +} + +// 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.Embeds = []*MessageEmbed{embed} + return m +} + +// SetEmbeds is a convenience function for setting the embeds, +// so you can chain commands. +func (m *MessageEdit) SetEmbeds(embeds []*MessageEmbed) *MessageEdit { + m.Embeds = embeds + return m +} + +// AllowedMentionType describes the types of mentions used +// in the MessageAllowedMentions type. +type AllowedMentionType string + +// The types of mentions used in MessageAllowedMentions. +const ( + AllowedMentionTypeRoles AllowedMentionType = "roles" + AllowedMentionTypeUsers AllowedMentionType = "users" + AllowedMentionTypeEveryone AllowedMentionType = "everyone" +) + +// MessageAllowedMentions allows the user to specify which mentions +// Discord is allowed to parse in this message. This is useful when +// sending user input as a message, as it prevents unwanted mentions. +// If this type is used, all mentions must be explicitly whitelisted, +// either by putting an AllowedMentionType in the Parse slice +// (allowing all mentions of that type) or, in the case of roles and +// users, explicitly allowing those mentions on an ID-by-ID basis. +// For more information on this functionality, see: +// https://discordapp.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mentions-reference +type MessageAllowedMentions struct { + // The mention types that are allowed to be parsed in this message. + // Please note that this is purposely **not** marked as omitempty, + // so if a zero-value MessageAllowedMentions object is provided no + // mentions will be allowed. + Parse []AllowedMentionType `json:"parse"` + + // A list of role IDs to allow. This cannot be used when specifying + // AllowedMentionTypeRoles in the Parse slice. + Roles []string `json:"roles,omitempty"` + + // A list of user IDs to allow. This cannot be used when specifying + // AllowedMentionTypeUsers in the Parse slice. + Users []string `json:"users,omitempty"` +} + +// 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"` + ContentType string `json:"content_type"` + Width int `json:"width"` + Height int `json:"height"` + Size int `json:"size"` + Ephemeral bool `json:"ephemeral"` +} + +// 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"` + 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 EmbedType `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"` +} + +// EmbedType is the type of embed +// https://discord.com/developers/docs/resources/channel#embed-object-embed-types +type EmbedType string + +// Block of valid EmbedTypes +const ( + EmbedTypeRich EmbedType = "rich" + EmbedTypeImage EmbedType = "image" + EmbedTypeVideo EmbedType = "video" + EmbedTypeGifv EmbedType = "gifv" + EmbedTypeArticle EmbedType = "article" + EmbedTypeLink EmbedType = "link" +) + +// MessageReactions holds a reactions object for a message. +type MessageReactions struct { + Count int `json:"count"` + Me bool `json:"me"` + Emoji *Emoji `json:"emoji"` +} + +// MessageActivity is sent with Rich Presence-related chat embeds +type MessageActivity struct { + Type MessageActivityType `json:"type"` + PartyID string `json:"party_id"` +} + +// MessageActivityType is the type of message activity +type MessageActivityType int + +// Constants for the different types of Message Activity +const ( + MessageActivityTypeJoin MessageActivityType = 1 + MessageActivityTypeSpectate MessageActivityType = 2 + MessageActivityTypeListen MessageActivityType = 3 + MessageActivityTypeJoinRequest MessageActivityType = 5 +) + +// MessageApplication is sent with Rich Presence-related chat embeds +type MessageApplication struct { + ID string `json:"id"` + CoverImage string `json:"cover_image"` + Description string `json:"description"` + Icon string `json:"icon"` + Name string `json:"name"` +} + +// MessageReference contains reference data sent with crossposted messages +type MessageReference struct { + MessageID string `json:"message_id"` + ChannelID string `json:"channel_id"` + GuildID string `json:"guild_id,omitempty"` +} + +// Reference returns MessageReference of given message +func (m *Message) Reference() *MessageReference { + return &MessageReference{ + GuildID: m.GuildID, + ChannelID: m.ChannelID, + MessageID: m.ID, + } +} + +// ContentWithMentionsReplaced will replace all @<id> 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 @<id> 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 +} + +// MessageInteraction contains information about the application command interaction which generated the message. +type MessageInteraction struct { + ID string `json:"id"` + Type InteractionType `json:"type"` + Name string `json:"name"` + User *User `json:"user"` + + // Member is only present when the interaction is from a guild. + Member *Member `json:"member"` +} diff --git a/vendor/github.com/bwmarrin/discordgo/mkdocs.yml b/vendor/github.com/bwmarrin/discordgo/mkdocs.yml new file mode 100644 index 00000000..3ee8eb37 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/mkdocs.yml @@ -0,0 +1,17 @@ +site_name: DiscordGo +site_author: Bruce Marriner +site_url: http://bwmarrin.github.io/discordgo/ +repo_url: https://github.com/bwmarrin/discordgo + +dev_addr: 0.0.0.0:8000 +theme: yeti + +markdown_extensions: + - smarty + - toc: + permalink: True + - sane_lists + +pages: + - 'Home': 'index.md' + - 'Getting Started': 'GettingStarted.md' diff --git a/vendor/github.com/bwmarrin/discordgo/oauth2.go b/vendor/github.com/bwmarrin/discordgo/oauth2.go new file mode 100644 index 00000000..2fa2d8d5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/oauth2.go @@ -0,0 +1,154 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains functions related to Discord OAuth2 endpoints + +package discordgo + +// ------------------------------------------------------------------------------------------------ +// Code specific to Discord OAuth2 Applications +// ------------------------------------------------------------------------------------------------ + +// The MembershipState represents whether the user is in the team or has been invited into it +type MembershipState int + +// Constants for the different stages of the MembershipState +const ( + MembershipStateInvited MembershipState = 1 + MembershipStateAccepted MembershipState = 2 +) + +// A TeamMember struct stores values for a single Team Member, extending the normal User data - note that the user field is partial +type TeamMember struct { + User *User `json:"user"` + TeamID string `json:"team_id"` + MembershipState MembershipState `json:"membership_state"` + Permissions []string `json:"permissions"` +} + +// A Team struct stores the members of a Discord Developer Team as well as some metadata about it +type Team struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + OwnerID string `json:"owner_user_id"` + Members []*TeamMember `json:"members"` +} + +// 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", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) + 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", EndpointOAuth2Applications, nil, EndpointOAuth2Applications) + 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"` + }{ap.Name, ap.Description} + + body, err := s.RequestWithBucketID("POST", EndpointOAuth2Applications, data, EndpointOAuth2Applications) + 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"` + }{ap.Name, ap.Description} + + body, err := s.RequestWithBucketID("PUT", EndpointOAuth2Application(appID), data, EndpointOAuth2Application("")) + 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", EndpointOAuth2Application(appID), nil, EndpointOAuth2Application("")) + if err != nil { + return + } + + return +} + +// Asset struct stores values for an asset of an application +type Asset struct { + Type int `json:"type"` + ID string `json:"id"` + Name string `json:"name"` +} + +// ApplicationAssets returns an application's assets +func (s *Session) ApplicationAssets(appID string) (ass []*Asset, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointOAuth2ApplicationAssets(appID), nil, EndpointOAuth2ApplicationAssets("")) + if err != nil { + return + } + + err = unmarshal(body, &ass) + 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", EndpointOAuth2ApplicationsBot(appID), nil, EndpointOAuth2ApplicationsBot("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/ratelimit.go b/vendor/github.com/bwmarrin/discordgo/ratelimit.go new file mode 100644 index 00000000..c992fd45 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/ratelimit.go @@ -0,0 +1,197 @@ +package discordgo + +import ( + "math" + "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{ + { + 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") + resetAfter := headers.Get("X-RateLimit-Reset-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 resetAfter != "" { + parsedAfter, err := strconv.ParseFloat(resetAfter, 64) + if err != nil { + return err + } + + whole, frac := math.Modf(parsedAfter) + resetAt := time.Now().Add(time.Duration(whole) * time.Second).Add(time.Duration(frac*1000) * 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.ParseFloat(reset, 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 + whole, frac := math.Modf(unix) + delta := time.Unix(int64(whole), 0).Add(time.Duration(frac*1000)*time.Millisecond).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/bwmarrin/discordgo/restapi.go b/vendor/github.com/bwmarrin/discordgo/restapi.go new file mode 100644 index 00000000..41796fe2 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/restapi.go @@ -0,0 +1,2871 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains functions for interacting with the Discord REST/JSON API +// at the lowest level. + +package discordgo + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "image" + _ "image/jpeg" // For JPEG decoding + _ "image/png" // For PNG decoding + "io" + "io/ioutil" + "log" + "net/http" + "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") + ErrUnauthorized = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discord.com/developers/docs/reference#authentication-example-bot-token-authorization-header") +) + +// Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr +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) + } + + // Discord's API returns a 400 Bad Request is Content-Type is set, but the + // request body is empty. + if b != nil { + req.Header.Set("Content-Type", contentType) + } + + // TODO: Make a configurable static variable. + req.Header.Set("User-Agent", s.UserAgent) + + 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: + 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 %v", urlStr, rl.RetryAfter) + s.handleEvent(rateLimitEventType, &RateLimit{TooManyRequests: &rl, URL: urlStr}) + + time.Sleep(rl.RetryAfter) + // 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) + case http.StatusUnauthorized: + if strings.Index(s.Token, "Bot ") != 0 { + s.log(LogInformational, ErrUnauthorized.Error()) + err = ErrUnauthorized + } + fallthrough + default: // Error condition + err = newRestError(req, resp, response) + } + + return +} + +func unmarshal(data []byte, v interface{}) error { + err := json.Unmarshal(data, v) + if err != nil { + return fmt.Errorf("%w: %s", ErrJSONUnmarshal, err) + } + + return nil +} + +// ------------------------------------------------------------------------------------------------ +// 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 current user settings. +func (s *Session) UserUpdate(username, avatar 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 { + Username string `json:"username,omitempty"` + Avatar string `json:"avatar,omitempty"` + }{username, avatar} + + body, err := s.RequestWithBucketID("PATCH", EndpointUser("@me"), data, EndpointUsers) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserConnections returns the user's connections +func (s *Session) UserConnections() (conn []*UserConnection, err error) { + response, err := s.RequestWithBucketID("GET", EndpointUserConnections("@me"), nil, EndpointUserConnections("@me")) + if err != nil { + return nil, err + } + + err = unmarshal(response, &conn) + if err != nil { + return + } + + return +} + +// 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 += "?" + v.Encode() + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointUserGuilds("")) + 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 int64, 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, userID, member.Roles), nil +} + +// Calculates the permissions for a member. +// https://support.discord.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured- +func memberPermissions(guild *Guild, channel *Channel, userID string, roles []string) (apermissions int64) { + 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 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 + } + } + + var denies, allows int64 + // Member overwrites can override role overrides, so do two passes + for _, overwrite := range channel.PermissionOverwrites { + for _, roleID := range roles { + if overwrite.Type == PermissionOverwriteTypeRole && roleID == overwrite.ID { + denies |= overwrite.Deny + allows |= overwrite.Allow + break + } + } + } + + apermissions &= ^denies + apermissions |= allows + + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.Type == PermissionOverwriteTypeMember && 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) { + body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildPreview returns a GuildPreview structure of a specific public Guild. +// guildID : The ID of a Guild +func (s *Session) GuildPreview(guildID string) (st *GuildPreview, err error) { + body, err := s.RequestWithBucketID("GET", EndpointGuildPreview(guildID), nil, EndpointGuildPreview(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, 4] + if g.VerificationLevel != nil { + val := *g.VerificationLevel + if val < 0 || val > 4 { + 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 GuildBan 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) +} + +// GuildBan finds ban by given guild and user id and returns GuildBan structure +func (s *Session) GuildBan(guildID, userID string) (st *GuildBan, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, userID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + +// 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 += "?" + 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) + // The returned object doesn't have the GuildID attribute so we will set it here. + st.GuildID = guildID + return +} + +// GuildMemberAdd force joins a user to the guild. +// accessToken : Valid access_token for the user. +// guildID : The ID of a Guild. +// userID : The ID of a User. +// nick : Value to set users nickname to +// roles : A list of role ID's to set on the member. +// mute : If the user is muted. +// deaf : If the user is deafened. +func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) { + + data := struct { + AccessToken string `json:"access_token"` + Nick string `json:"nick,omitempty"` + Roles []string `json:"roles,omitempty"` + Mute bool `json:"mute,omitempty"` + Deaf bool `json:"deaf,omitempty"` + }{accessToken, nick, roles, mute, deaf} + + _, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + if err != nil { + return err + } + + return err +} + +// GuildMemberDelete removes the given user from the given guild. +// guildID : The ID of a Guild. +// userID : The ID of a User +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, "")) + 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 nil to remove from voice channel +// 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 string, userID string, channelID *string) (err error) { + data := struct { + ChannelID *string `json:"channel_id"` + }{channelID} + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + 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 +// nickname : The nickname of the member, "" will reset their nickname +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 +} + +// GuildMemberMute server mutes a guild member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// mute : boolean value for if the user should be muted +func (s *Session) GuildMemberMute(guildID string, userID string, mute bool) (err error) { + data := struct { + Mute bool `json:"mute"` + }{mute} + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + return +} + +// GuildMemberTimeout times out a guild member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// until : The timestamp for how long a member should be timed out. +// Set to nil to remove timeout. +func (s *Session) GuildMemberTimeout(guildID string, userID string, until *time.Time) (err error) { + data := struct { + CommunicationDisabledUntil *time.Time `json:"communication_disabled_until"` + }{until} + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + return +} + +// GuildMemberDeafen server deafens a guild member +// guildID : The ID of a Guild. +// userID : The ID of a User. +// deaf : boolean value for if the user should be deafened +func (s *Session) GuildMemberDeafen(guildID string, userID string, deaf bool) (err error) { + data := struct { + Deaf bool `json:"deaf"` + }{deaf} + + _, 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 +} + +// GuildChannelCreateData is provided to GuildChannelCreateComplex +type GuildChannelCreateData struct { + Name string `json:"name"` + Type ChannelType `json:"type"` + Topic string `json:"topic,omitempty"` + Bitrate int `json:"bitrate,omitempty"` + UserLimit int `json:"user_limit,omitempty"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + Position int `json:"position,omitempty"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` + ParentID string `json:"parent_id,omitempty"` + NSFW bool `json:"nsfw,omitempty"` +} + +// GuildChannelCreateComplex creates a new channel in the given guild +// guildID : The ID of a Guild +// data : A data struct describing the new Channel, Name and Type are mandatory, other fields depending on the type +func (s *Session) GuildChannelCreateComplex(guildID string, data GuildChannelCreateData) (st *Channel, err error) { + body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) + if err != nil { + return + } + + 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 : Type of the channel +func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { + return s.GuildChannelCreateComplex(guildID, GuildChannelCreateData{ + Name: name, + Type: ctype, + }) +} + +// GuildChannelsReorder updates the order of channels in a guild +// guildID : The ID of a Guild. +// channels : Updated channels. +func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { + + data := make([]struct { + ID string `json:"id"` + Position int `json:"position"` + }, len(channels)) + + for i, c := range channels { + data[i].ID = c.ID + data[i].Position = c.Position + } + + _, err = s.RequestWithBucketID("PATCH", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) + return +} + +// 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 int64, 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 int64 `json:"permissions,string"` // 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) + "?days=" + strconv.FormatUint(uint64(days), 10) + 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 []*Integration, 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 +} + +// 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 +} + +// GuildAuditLog returns the audit log for a Guild. +// guildID : The ID of a Guild. +// userID : If provided the log will be filtered for the given ID. +// beforeID : If provided all log entries returned will be before the given ID. +// actionType : If provided the log will be filtered for the given Action Type. +// limit : The number messages that can be returned. (default 50, min 1, max 100) +func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) { + + uri := EndpointGuildAuditLogs(guildID) + + v := url.Values{} + if userID != "" { + v.Set("user_id", userID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + if actionType > 0 { + v.Set("action_type", strconv.Itoa(actionType)) + } + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + if len(v) > 0 { + uri = fmt.Sprintf("%s?%s", uri, v.Encode()) + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildAuditLogs(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildEmojis returns all emoji +// guildID : The ID of a Guild. +func (s *Session) GuildEmojis(guildID string) (emoji []*Emoji, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildEmojis(guildID), nil, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiCreate creates a new emoji +// guildID : The ID of a Guild. +// name : The Name of the Emoji. +// image : The base64 encoded emoji image, has to be smaller than 256KB. +// roles : The roles for which this emoji will be whitelisted, can be nil. +func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Image string `json:"image"` + Roles []string `json:"roles,omitempty"` + }{name, image, roles} + + body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiEdit modifies an emoji +// guildID : The ID of a Guild. +// emojiID : The ID of an Emoji. +// name : The Name of the Emoji. +// roles : The roles for which this emoji will be whitelisted, can be nil. +func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) { + + data := struct { + Name string `json:"name"` + Roles []string `json:"roles,omitempty"` + }{name, roles} + + body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &emoji) + return +} + +// GuildEmojiDelete deletes an Emoji. +// guildID : The ID of a Guild. +// emojiID : The ID of an Emoji. +func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID)) + return +} + +// GuildTemplate returns a GuildTemplate for the given code +// templateCode: The Code of a GuildTemplate +func (s *Session) GuildTemplate(templateCode string) (st *GuildTemplate, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildTemplate(templateCode), nil, EndpointGuildTemplate(templateCode)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildCreateWithTemplate creates a guild based on a GuildTemplate +// templateCode: The Code of a GuildTemplate +// name: The name of the guild (2-100) characters +// icon: base64 encoded 128x128 image for the guild icon +func (s *Session) GuildCreateWithTemplate(templateCode, name, icon string) (st *Guild, err error) { + + data := struct { + Name string `json:"name"` + Icon string `json:"icon"` + }{name, icon} + + body, err := s.RequestWithBucketID("POST", EndpointGuildTemplate(templateCode), data, EndpointGuildTemplate(templateCode)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildTemplates returns all of GuildTemplates +// guildID: The ID of the guild +func (s *Session) GuildTemplates(guildID string) (st []*GuildTemplate, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildTemplates(guildID), nil, EndpointGuildTemplates(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildTemplateCreate creates a template for the guild +// guildID: The ID of the guild +// name: The name of the template (1-100 characters) +// description: The description for the template (0-120 characters) +func (s *Session) GuildTemplateCreate(guildID, name, description string) (st *GuildTemplate) { + + data := struct { + Name string `json:"name"` + Description string `json:"description"` + }{name, description} + + body, err := s.RequestWithBucketID("POST", EndpointGuildTemplates(guildID), data, EndpointGuildTemplates(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildTemplateSync syncs the template to the guild's current state +// guildID: The ID of the guild +// templateCode: The code of the template +func (s *Session) GuildTemplateSync(guildID, templateCode string) (err error) { + + _, err = s.RequestWithBucketID("PUT", EndpointGuildTemplateSync(guildID, templateCode), nil, EndpointGuildTemplateSync(guildID, "")) + return +} + +// GuildTemplateEdit modifies the template's metadata +// guildID: The ID of the guild +// templateCode: The code of the template +// name: The name of the template (1-100 characters) +// description: The description for the template (0-120 characters) +func (s *Session) GuildTemplateEdit(guildID, templateCode, name, description string) (st *GuildTemplate, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + }{name, description} + + body, err := s.RequestWithBucketID("PATCH", EndpointGuildTemplateSync(guildID, templateCode), data, EndpointGuildTemplateSync(guildID, "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildTemplateDelete deletes the template +// guildID: The ID of the guild +// templateCode: The code of the template +func (s *Session) GuildTemplateDelete(guildID, templateCode string) (err error) { + + _, err = s.RequestWithBucketID("DELETE", EndpointGuildTemplateSync(guildID, templateCode), nil, EndpointGuildTemplateSync(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 += "?" + 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 +} + +// 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) { + // TODO: Remove this when compatibility is not required. + if data.Embed != nil { + if data.Embeds == nil { + data.Embeds = []*MessageEmbed{data.Embed} + } else { + err = fmt.Errorf("cannot specify both Embed and Embeds") + return + } + } + + for _, embed := range data.Embeds { + if embed.Type == "" { + 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 { + contentType, body, encodeErr := MultipartBodyWithJSON(data, files) + if encodeErr != nil { + return st, encodeErr + } + + response, err = s.request("POST", endpoint, contentType, body, 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.ChannelMessageSendEmbeds(channelID, []*MessageEmbed{embed}) +} + +// ChannelMessageSendEmbeds sends a message to the given channel with multiple embedded data. +// channelID : The ID of a Channel. +// embeds : The embeds data to send. +func (s *Session) ChannelMessageSendEmbeds(channelID string, embeds []*MessageEmbed) (*Message, error) { + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Embeds: embeds, + }) +} + +// ChannelMessageSendReply sends a message to the given channel with reference data. +// channelID : The ID of a Channel. +// content : The message to send. +// reference : The message reference to send. +func (s *Session) ChannelMessageSendReply(channelID string, content string, reference *MessageReference) (*Message, error) { + if reference == nil { + return nil, fmt.Errorf("reply attempted with nil message reference") + } + return s.ChannelMessageSendComplex(channelID, &MessageSend{ + Content: content, + Reference: reference, + }) +} + +// 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) { + // TODO: Remove this when compatibility is not required. + if m.Embed != nil { + if m.Embeds == nil { + m.Embeds = []*MessageEmbed{m.Embed} + } else { + err = fmt.Errorf("cannot specify both Embed and Embeds") + return + } + } + + for _, embed := range m.Embeds { + if embed.Type == "" { + 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.ChannelMessageEditEmbeds(channelID, messageID, []*MessageEmbed{embed}) +} + +// ChannelMessageEditEmbeds edits an existing message with multiple embedded data. +// channelID : The ID of a Channel +// messageID : The ID of a Message +// embeds : The embeds data to send +func (s *Session) ChannelMessageEditEmbeds(channelID, messageID string, embeds []*MessageEmbed) (*Message, error) { + return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbeds(embeds)) +} + +// 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"` + Unique bool `json:"unique"` + }{i.MaxAge, i.MaxUses, i.Temporary, i.Unique} + + body, err := s.RequestWithBucketID("POST", EndpointChannelInvites(channelID), data, EndpointChannelInvites(channelID)) + if err != nil { + 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 string, targetType PermissionOverwriteType, allow, deny int64) (err error) { + + data := struct { + ID string `json:"id"` + Type PermissionOverwriteType `json:"type"` + Allow int64 `json:"allow,string"` + Deny int64 `json:"deny,string"` + }{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 +} + +// ChannelMessageCrosspost cross posts a message in a news channel to followers +// of the channel +// channelID : The ID of a Channel +// messageID : The ID of a Message +func (s *Session) ChannelMessageCrosspost(channelID, messageID string) (st *Message, err error) { + + endpoint := EndpointChannelMessageCrosspost(channelID, messageID) + + body, err := s.RequestWithBucketID("POST", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ChannelNewsFollow follows a news channel in the targetID +// channelID : The ID of a News Channel +// targetID : The ID of a Channel where the News Channel should post to +func (s *Session) ChannelNewsFollow(channelID, targetID string) (st *ChannelFollow, err error) { + + endpoint := EndpointChannelFollow(channelID) + + data := struct { + WebhookChannelID string `json:"webhook_channel_id"` + }{targetID} + + body, err := s.RequestWithBucketID("POST", endpoint, data, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &st) + 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 +} + +// InviteWithCounts returns an Invite structure of the given invite including approximate member counts +// inviteID : The invite code +func (s *Session) InviteWithCounts(inviteID string) (st *Invite, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointInvite(inviteID)+"?with_counts=true", nil, EndpointInvite("")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// InviteDelete deletes an existing invite +// inviteID : the code of an invite +func (s *Session) InviteDelete(inviteID string) (st *Invite, err error) { + + 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 +} + +// ------------------------------------------------------------------------------------------------ +// 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, channelID string) (st *Role, err error) { + + data := struct { + Name string `json:"name,omitempty"` + Avatar string `json:"avatar,omitempty"` + ChannelID string `json:"channel_id,omitempty"` + }{name, avatar, channelID} + + body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) + if err != nil { + 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 +} + +func (s *Session) webhookExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) { + uri := EndpointWebhookToken(webhookID, token) + + v := url.Values{} + if wait { + v.Set("wait", "true") + } + + if threadID != "" { + v.Set("thread_id", threadID) + } + if len(v) != 0 { + uri += "?" + v.Encode() + } + + var response []byte + if len(data.Files) > 0 { + contentType, body, encodeErr := MultipartBodyWithJSON(data, data.Files) + if encodeErr != nil { + return st, encodeErr + } + + response, err = s.request("POST", uri, contentType, body, uri, 0) + } else { + response, err = s.RequestWithBucketID("POST", uri, data, uri) + } + if !wait || err != nil { + return + } + + err = unmarshal(response, &st) + return +} + +// WebhookExecute executes a webhook. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook +// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) +func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (st *Message, err error) { + return s.webhookExecute(webhookID, token, wait, "", data) +} + +// WebhookThreadExecute executes a webhook in a thread. +// webhookID: The ID of a webhook. +// token : The auth token for the webhook +// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) +// threadID : Sends a message to the specified thread within a webhook's channel. The thread will automatically be unarchived. +func (s *Session) WebhookThreadExecute(webhookID, token string, wait bool, threadID string, data *WebhookParams) (st *Message, err error) { + return s.webhookExecute(webhookID, token, wait, threadID, data) +} + +// WebhookMessage gets a webhook message. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of message to get +func (s *Session) WebhookMessage(webhookID, token, messageID string) (message *Message, err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointWebhookToken("", "")) + if err != nil { + return + } + + err = json.Unmarshal(body, &message) + + return +} + +// WebhookMessageEdit edits a webhook message and returns a new one. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of message to edit +func (s *Session) WebhookMessageEdit(webhookID, token, messageID string, data *WebhookEdit) (st *Message, err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + + var response []byte + if len(data.Files) > 0 { + contentType, body, err := MultipartBodyWithJSON(data, data.Files) + if err != nil { + return nil, err + } + + response, err = s.request("PATCH", uri, contentType, body, uri, 0) + if err != nil { + return nil, err + } + } else { + response, err = s.RequestWithBucketID("PATCH", uri, data, EndpointWebhookToken("", "")) + + if err != nil { + return nil, err + } + } + + err = unmarshal(response, &st) + return +} + +// WebhookMessageDelete deletes a webhook message. +// webhookID : The ID of a webhook +// token : The auth token for the webhook +// messageID : The ID of a message to edit +func (s *Session) WebhookMessageDelete(webhookID, token, messageID string) (err error) { + uri := EndpointWebhookMessage(webhookID, token, messageID) + + _, err = s.RequestWithBucketID("DELETE", uri, nil, 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 { + + // emoji such as #⃣ need to have # escaped + emojiID = strings.Replace(emojiID, "#", "%23", -1) + _, 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 { + + // emoji such as #⃣ need to have # escaped + emojiID = strings.Replace(emojiID, "#", "%23", -1) + _, 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 +} + +// MessageReactionsRemoveEmoji deletes all reactions of a certain emoji from a message +// channelID : The channel ID +// messageID : The message ID +// emojiID : The emoji ID +func (s *Session) MessageReactionsRemoveEmoji(channelID, messageID, emojiID string) error { + + // emoji such as #⃣ need to have # escaped + emojiID = strings.Replace(emojiID, "#", "%23", -1) + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactions(channelID, messageID, emojiID), nil, EndpointMessageReactions(channelID, messageID, emojiID)) + + 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) +// beforeID : If provided all reactions returned will be before given ID. +// afterID : If provided all reactions returned will be after given ID. +func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int, beforeID, afterID string) (st []*User, err error) { + // emoji such as #⃣ need to have # escaped + emojiID = strings.Replace(emojiID, "#", "%23", -1) + uri := EndpointMessageReactions(channelID, messageID, emojiID) + + v := url.Values{} + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if afterID != "" { + v.Set("after", afterID) + } + if beforeID != "" { + v.Set("before", beforeID) + } + + if len(v) > 0 { + uri += "?" + v.Encode() + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to threads +// ------------------------------------------------------------------------------------------------ + +// MessageThreadStartComplex creates a new thread from an existing message. +// channelID : Channel to create thread in +// messageID : Message to start thread from +// data : Parameters of the thread +func (s *Session) MessageThreadStartComplex(channelID, messageID string, data *ThreadStart) (ch *Channel, err error) { + endpoint := EndpointChannelMessageThread(channelID, messageID) + var body []byte + body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &ch) + return +} + +// MessageThreadStart creates a new thread from an existing message. +// channelID : Channel to create thread in +// messageID : Message to start thread from +// name : Name of the thread +// archiveDuration : Auto archive duration (in minutes) +func (s *Session) MessageThreadStart(channelID, messageID string, name string, archiveDuration int) (ch *Channel, err error) { + return s.MessageThreadStartComplex(channelID, messageID, &ThreadStart{ + Name: name, + AutoArchiveDuration: archiveDuration, + }) +} + +// ThreadStartComplex creates a new thread. +// channelID : Channel to create thread in +// data : Parameters of the thread +func (s *Session) ThreadStartComplex(channelID string, data *ThreadStart) (ch *Channel, err error) { + endpoint := EndpointChannelThreads(channelID) + var body []byte + body, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &ch) + return +} + +// ThreadStart creates a new thread. +// channelID : Channel to create thread in +// name : Name of the thread +// archiveDuration : Auto archive duration (in minutes) +func (s *Session) ThreadStart(channelID, name string, typ ChannelType, archiveDuration int) (ch *Channel, err error) { + return s.ThreadStartComplex(channelID, &ThreadStart{ + Name: name, + Type: typ, + AutoArchiveDuration: archiveDuration, + }) +} + +// ThreadJoin adds current user to a thread +func (s *Session) ThreadJoin(id string) error { + endpoint := EndpointThreadMember(id, "@me") + _, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint) + return err +} + +// ThreadLeave removes current user to a thread +func (s *Session) ThreadLeave(id string) error { + endpoint := EndpointThreadMember(id, "@me") + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + return err +} + +// ThreadMemberAdd adds another member to a thread +func (s *Session) ThreadMemberAdd(threadID, memberID string) error { + endpoint := EndpointThreadMember(threadID, memberID) + _, err := s.RequestWithBucketID("PUT", endpoint, nil, endpoint) + return err +} + +// ThreadMemberRemove removes another member from a thread +func (s *Session) ThreadMemberRemove(threadID, memberID string) error { + endpoint := EndpointThreadMember(threadID, memberID) + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + return err +} + +// ThreadMember returns thread member object for the specified member of a thread +func (s *Session) ThreadMember(threadID, memberID string) (member *ThreadMember, err error) { + endpoint := EndpointThreadMember(threadID, memberID) + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + + if err != nil { + return + } + + err = unmarshal(body, &member) + return +} + +// ThreadMembers returns all members of specified thread. +func (s *Session) ThreadMembers(threadID string) (members []*ThreadMember, err error) { + var body []byte + body, err = s.RequestWithBucketID("GET", EndpointThreadMembers(threadID), nil, EndpointThreadMembers(threadID)) + + if err != nil { + return + } + + err = unmarshal(body, &members) + return +} + +// ThreadsActive returns all active threads for specified channel. +func (s *Session) ThreadsActive(channelID string) (threads *ThreadsList, err error) { + var body []byte + body, err = s.RequestWithBucketID("GET", EndpointChannelActiveThreads(channelID), nil, EndpointChannelActiveThreads(channelID)) + if err != nil { + return + } + + err = unmarshal(body, &threads) + return +} + +// GuildThreadsActive returns all active threads for specified guild. +func (s *Session) GuildThreadsActive(guildID string) (threads *ThreadsList, err error) { + var body []byte + body, err = s.RequestWithBucketID("GET", EndpointGuildActiveThreads(guildID), nil, EndpointGuildActiveThreads(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &threads) + return +} + +// ThreadsArchived returns archived threads for specified channel. +// before : If specified returns only threads before the timestamp +// limit : Optional maximum amount of threads to return. +func (s *Session) ThreadsArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelPublicArchivedThreads(channelID) + v := url.Values{} + if before != nil { + v.Set("before", before.Format(time.RFC3339)) + } + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + endpoint += "?" + v.Encode() + } + + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &threads) + return +} + +// ThreadsPrivateArchived returns archived private threads for specified channel. +// before : If specified returns only threads before the timestamp +// limit : Optional maximum amount of threads to return. +func (s *Session) ThreadsPrivateArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelPrivateArchivedThreads(channelID) + v := url.Values{} + if before != nil { + v.Set("before", before.Format(time.RFC3339)) + } + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + endpoint += "?" + v.Encode() + } + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &threads) + return +} + +// ThreadsPrivateJoinedArchived returns archived joined private threads for specified channel. +// before : If specified returns only threads before the timestamp +// limit : Optional maximum amount of threads to return. +func (s *Session) ThreadsPrivateJoinedArchived(channelID string, before *time.Time, limit int) (threads *ThreadsList, err error) { + endpoint := EndpointChannelJoinedPrivateArchivedThreads(channelID) + v := url.Values{} + if before != nil { + v.Set("before", before.Format(time.RFC3339)) + } + + if limit > 0 { + v.Set("limit", strconv.Itoa(limit)) + } + + if len(v) > 0 { + endpoint += "?" + v.Encode() + } + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &threads) + return +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to application (slash) commands +// ------------------------------------------------------------------------------------------------ + +// ApplicationCommandCreate creates a global application command and returns it. +// appID : The application ID. +// guildID : Guild ID to create guild-specific application command. If empty - creates global application command. +// cmd : New application command data. +func (s *Session) ApplicationCommandCreate(appID string, guildID string, cmd *ApplicationCommand) (ccmd *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommands(appID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommands(appID, guildID) + } + + body, err := s.RequestWithBucketID("POST", endpoint, *cmd, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &ccmd) + + return +} + +// ApplicationCommandEdit edits application command and returns new command data. +// appID : The application ID. +// cmdID : Application command ID to edit. +// guildID : Guild ID to edit guild-specific application command. If empty - edits global application command. +// cmd : Updated application command data. +func (s *Session) ApplicationCommandEdit(appID, guildID, cmdID string, cmd *ApplicationCommand) (updated *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + body, err := s.RequestWithBucketID("PATCH", endpoint, *cmd, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &updated) + + return +} + +// ApplicationCommandBulkOverwrite Creates commands overwriting existing commands. Returns a list of commands. +// appID : The application ID. +// commands : The commands to create. +func (s *Session) ApplicationCommandBulkOverwrite(appID string, guildID string, commands []*ApplicationCommand) (createdCommands []*ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommands(appID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommands(appID, guildID) + } + + body, err := s.RequestWithBucketID("PUT", endpoint, commands, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &createdCommands) + + return +} + +// ApplicationCommandDelete deletes application command by ID. +// appID : The application ID. +// cmdID : Application command ID to delete. +// guildID : Guild ID to delete guild-specific application command. If empty - deletes global application command. +func (s *Session) ApplicationCommandDelete(appID, guildID, cmdID string) error { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + + return err +} + +// ApplicationCommand retrieves an application command by given ID. +// appID : The application ID. +// cmdID : Application command ID. +// guildID : Guild ID to retrieve guild-specific application command. If empty - retrieves global application command. +func (s *Session) ApplicationCommand(appID, guildID, cmdID string) (cmd *ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommand(appID, cmdID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommand(appID, guildID, cmdID) + } + + body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &cmd) + + return +} + +// ApplicationCommands retrieves all commands in application. +// appID : The application ID. +// guildID : Guild ID to retrieve all guild-specific application commands. If empty - retrieves global application commands. +func (s *Session) ApplicationCommands(appID, guildID string) (cmd []*ApplicationCommand, err error) { + endpoint := EndpointApplicationGlobalCommands(appID) + if guildID != "" { + endpoint = EndpointApplicationGuildCommands(appID, guildID) + } + + body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &cmd) + + return +} + +// GuildApplicationCommandsPermissions returns permissions for application commands in a guild. +// appID : The application ID +// guildID : Guild ID to retrieve application commands permissions for. +func (s *Session) GuildApplicationCommandsPermissions(appID, guildID string) (permissions []*GuildApplicationCommandPermissions, err error) { + endpoint := EndpointApplicationCommandsGuildPermissions(appID, guildID) + + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &permissions) + return +} + +// ApplicationCommandPermissions returns all permissions of an application command +// appID : The Application ID +// guildID : The guild ID containing the application command +// cmdID : The command ID to retrieve the permissions of +func (s *Session) ApplicationCommandPermissions(appID, guildID, cmdID string) (permissions *GuildApplicationCommandPermissions, err error) { + endpoint := EndpointApplicationCommandPermissions(appID, guildID, cmdID) + + var body []byte + body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &permissions) + return +} + +// ApplicationCommandPermissionsEdit edits the permissions of an application command +// appID : The Application ID +// guildID : The guild ID containing the application command +// cmdID : The command ID to edit the permissions of +// permissions : An object containing a list of permissions for the application command +func (s *Session) ApplicationCommandPermissionsEdit(appID, guildID, cmdID string, permissions *ApplicationCommandPermissionsList) (err error) { + endpoint := EndpointApplicationCommandPermissions(appID, guildID, cmdID) + + _, err = s.RequestWithBucketID("PUT", endpoint, permissions, endpoint) + return +} + +// ApplicationCommandPermissionsBatchEdit edits the permissions of a batch of commands +// appID : The Application ID +// guildID : The guild ID to batch edit commands of +// permissions : A list of permissions paired with a command ID, guild ID, and application ID per application command +func (s *Session) ApplicationCommandPermissionsBatchEdit(appID, guildID string, permissions []*GuildApplicationCommandPermissions) (err error) { + endpoint := EndpointApplicationCommandsGuildPermissions(appID, guildID) + + _, err = s.RequestWithBucketID("PUT", endpoint, permissions, endpoint) + return +} + +// InteractionRespond creates the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// resp : Response message data. +func (s *Session) InteractionRespond(interaction *Interaction, resp *InteractionResponse) (err error) { + endpoint := EndpointInteractionResponse(interaction.ID, interaction.Token) + + if resp.Data != nil && len(resp.Data.Files) > 0 { + contentType, body, err := MultipartBodyWithJSON(resp, resp.Data.Files) + if err != nil { + return err + } + + _, err = s.request("POST", endpoint, contentType, body, endpoint, 0) + } else { + _, err = s.RequestWithBucketID("POST", endpoint, *resp, endpoint) + } + return err +} + +// InteractionResponse gets the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +func (s *Session) InteractionResponse(appID string, interaction *Interaction) (*Message, error) { + return s.WebhookMessage(appID, interaction.Token, "@original") +} + +// InteractionResponseEdit edits the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// newresp : Updated response message data. +func (s *Session) InteractionResponseEdit(appID string, interaction *Interaction, newresp *WebhookEdit) (*Message, error) { + return s.WebhookMessageEdit(appID, interaction.Token, "@original", newresp) +} + +// InteractionResponseDelete deletes the response to an interaction. +// appID : The application ID. +// interaction : Interaction instance. +func (s *Session) InteractionResponseDelete(appID string, interaction *Interaction) error { + endpoint := EndpointInteractionResponseActions(appID, interaction.Token) + + _, err := s.RequestWithBucketID("DELETE", endpoint, nil, endpoint) + + return err +} + +// FollowupMessageCreate creates the followup message for an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// wait : Waits for server confirmation of message send and ensures that the return struct is populated (it is nil otherwise) +// data : Data of the message to send. +func (s *Session) FollowupMessageCreate(appID string, interaction *Interaction, wait bool, data *WebhookParams) (*Message, error) { + return s.WebhookExecute(appID, interaction.Token, wait, data) +} + +// FollowupMessageEdit edits a followup message of an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// messageID : The followup message ID. +// data : Data to update the message +func (s *Session) FollowupMessageEdit(appID string, interaction *Interaction, messageID string, data *WebhookEdit) (*Message, error) { + return s.WebhookMessageEdit(appID, interaction.Token, messageID, data) +} + +// FollowupMessageDelete deletes a followup message of an interaction. +// appID : The application ID. +// interaction : Interaction instance. +// messageID : The followup message ID. +func (s *Session) FollowupMessageDelete(appID string, interaction *Interaction, messageID string) error { + return s.WebhookMessageDelete(appID, interaction.Token, messageID) +} + +// ------------------------------------------------------------------------------------------------ +// Functions specific to guilds scheduled events +// ------------------------------------------------------------------------------------------------ + +// GuildScheduledEvents returns an array of GuildScheduledEvent for a guild +// guildID : The ID of a Guild +// userCount : Whether to include the user count in the response +func (s *Session) GuildScheduledEvents(guildID string, userCount bool) (st []*GuildScheduledEvent, err error) { + uri := EndpointGuildScheduledEvents(guildID) + if userCount { + uri += "?with_user_count=true" + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEvents(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildScheduledEvent returns a specific GuildScheduledEvent in a guild +// guildID : The ID of a Guild +// eventID : The ID of the event +// userCount : Whether to include the user count in the response +func (s *Session) GuildScheduledEvent(guildID, eventID string, userCount bool) (st *GuildScheduledEvent, err error) { + uri := EndpointGuildScheduledEvent(guildID, eventID) + if userCount { + uri += "?with_user_count=true" + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEvent(guildID, eventID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildScheduledEventCreate creates a GuildScheduledEvent for a guild and returns it +// guildID : The ID of a Guild +// eventID : The ID of the event +func (s *Session) GuildScheduledEventCreate(guildID string, event *GuildScheduledEventParams) (st *GuildScheduledEvent, err error) { + body, err := s.RequestWithBucketID("POST", EndpointGuildScheduledEvents(guildID), event, EndpointGuildScheduledEvents(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildScheduledEventEdit updates a specific event for a guild and returns it. +// guildID : The ID of a Guild +// eventID : The ID of the event +func (s *Session) GuildScheduledEventEdit(guildID, eventID string, event *GuildScheduledEventParams) (st *GuildScheduledEvent, err error) { + body, err := s.RequestWithBucketID("PATCH", EndpointGuildScheduledEvent(guildID, eventID), event, EndpointGuildScheduledEvent(guildID, eventID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// GuildScheduledEventDelete deletes a specific GuildScheduledEvent in a guild +// guildID : The ID of a Guild +// eventID : The ID of the event +func (s *Session) GuildScheduledEventDelete(guildID, eventID string) (err error) { + _, err = s.RequestWithBucketID("DELETE", EndpointGuildScheduledEvent(guildID, eventID), nil, EndpointGuildScheduledEvent(guildID, eventID)) + return +} + +// GuildScheduledEventUsers returns an array of GuildScheduledEventUser for a particular event in a guild +// guildID : The ID of a Guild +// eventID : The ID of the event +// limit : The maximum number of users to return (Max 100) +// withMember : Whether to include the member object in the response +// beforeID : If is not empty all returned users entries will be before the given ID +// afterID : If is not empty all returned users entries will be after the given ID +func (s *Session) GuildScheduledEventUsers(guildID, eventID string, limit int, withMember bool, beforeID, afterID string) (st []*GuildScheduledEventUser, err error) { + uri := EndpointGuildScheduledEventUsers(guildID, eventID) + + queryParams := url.Values{} + if withMember { + queryParams.Set("with_member", "true") + } + if limit > 0 { + queryParams.Set("limit", strconv.Itoa(limit)) + } + if beforeID != "" { + queryParams.Set("before", beforeID) + } + if afterID != "" { + queryParams.Set("after", afterID) + } + + if len(queryParams) > 0 { + uri += "?" + queryParams.Encode() + } + + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointGuildScheduledEventUsers(guildID, eventID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} diff --git a/vendor/github.com/bwmarrin/discordgo/state.go b/vendor/github.com/bwmarrin/discordgo/state.go new file mode 100644 index 00000000..e75be895 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/state.go @@ -0,0 +1,1277 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to state tracking. If enabled, state +// tracking will capture the initial READY packet and many other websocket +// events and maintain an in-memory state of of guilds, channels, users, and +// so forth. This information can be accessed through the Session.State struct. + +package discordgo + +import ( + "errors" + "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") + +// ErrMessageIncompletePermissions is returned when the message +// requested for permissions does not contain enough data to +// generate the permissions. +var ErrMessageIncompletePermissions = errors.New("message incomplete, unable to determine permissions") + +// 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 represents how many messages per channel the state will store. + MaxMessageCount int + TrackChannels bool + TrackThreads bool + TrackEmojis bool + TrackMembers bool + TrackThreadMembers 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, + TrackThreads: true, + TrackEmojis: true, + TrackMembers: true, + TrackThreadMembers: 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 + } + + // Add all the threads to the state in case of thread sync list. + for _, t := range guild.Threads { + s.channelMap[t.ID] = t + } + + // 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.MemberCount == 0 { + guild.MemberCount = g.MemberCount + } + 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.Threads == nil { + guild.Threads = g.Threads + } + 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 +} + +func (s *State) presenceAdd(guildID string, presence *Presence) error { + guild, ok := s.guildMap[guildID] + if !ok { + return ErrStateNotFound + } + + for i, p := range guild.Presences { + if p.User.ID == presence.User.ID { + //guild.Presences[i] = presence + + //Update status + guild.Presences[i].Activities = presence.Activities + if presence.Status != "" { + guild.Presences[i].Status = presence.Status + } + + //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 +} + +// 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 + } + + s.Lock() + defer s.Unlock() + + return s.presenceAdd(guildID, presence) +} + +// 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. + +func (s *State) memberAdd(member *Member) error { + guild, ok := s.guildMap[member.GuildID] + if !ok { + return ErrStateNotFound + } + + 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 { + // We are about to replace `m` in the state with `member`, but first we need to + // make sure we preserve any fields that the `member` doesn't contain from `m`. + if member.JoinedAt.IsZero() { + member.JoinedAt = m.JoinedAt + } + *m = *member + } + return nil +} + +// 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 + } + + s.Lock() + defer s.Unlock() + + return s.memberAdd(member) +} + +// 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 + } + if channel.ThreadMetadata == nil { + channel.ThreadMetadata = c.ThreadMetadata + } + + *c = *channel + return nil + } + + if channel.Type == ChannelTypeDM || channel.Type == ChannelTypeGroupDM { + s.PrivateChannels = append(s.PrivateChannels, channel) + s.channelMap[channel.ID] = channel + return nil + } + + guild, ok := s.guildMap[channel.GuildID] + if !ok { + return ErrStateNotFound + } + + if channel.IsThread() { + guild.Threads = append(guild.Threads, channel) + } else { + 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 + } + } + delete(s.channelMap, channel.ID) + return nil + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + if channel.IsThread() { + for i, t := range guild.Threads { + if t.ID == channel.ID { + guild.Threads = append(guild.Threads[:i], guild.Threads[i+1:]...) + break + } + } + } else { + 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 +} + +// ThreadListSync syncs guild threads with provided ones. +func (s *State) ThreadListSync(tls *ThreadListSync) error { + guild, err := s.Guild(tls.GuildID) + if err != nil { + return err + } + + s.Lock() + defer s.Unlock() + + // This algorithm filters out archived or + // threads which are children of channels in channelIDs + // and then it adds all synced threads to guild threads and cache + index := 0 +outer: + for _, t := range guild.Threads { + if !t.ThreadMetadata.Archived && tls.ChannelIDs != nil { + for _, v := range tls.ChannelIDs { + if t.ParentID == v { + delete(s.channelMap, t.ID) + continue outer + } + } + guild.Threads[index] = t + index++ + } else { + delete(s.channelMap, t.ID) + } + } + guild.Threads = guild.Threads[:index] + for _, t := range tls.Threads { + s.channelMap[t.ID] = t + guild.Threads = append(guild.Threads, t) + } + + for _, m := range tls.Members { + if c, ok := s.channelMap[m.ID]; ok { + c.Member = m + } + } + + return nil +} + +// ThreadMembersUpdate updates thread members list +func (s *State) ThreadMembersUpdate(tmu *ThreadMembersUpdate) error { + thread, err := s.Channel(tmu.ID) + if err != nil { + return err + } + s.Lock() + defer s.Unlock() + + for idx, member := range thread.Members { + for _, removedMember := range tmu.RemovedMembers { + if member.ID == removedMember { + thread.Members = append(thread.Members[:idx], thread.Members[idx+1:]...) + break + } + } + } + + for _, addedMember := range tmu.AddedMembers { + thread.Members = append(thread.Members, addedMember.ThreadMember) + if addedMember.Member != nil { + err = s.memberAdd(addedMember.Member) + if err != nil { + return err + } + } + if addedMember.Presence != nil { + err = s.presenceAdd(tmu.GuildID, addedMember.Presence) + if err != nil { + return err + } + } + } + thread.MemberCount = tmu.MemberCount + + return nil +} + +// ThreadMemberUpdate sets or updates member data for the current user. +func (s *State) ThreadMemberUpdate(mu *ThreadMemberUpdate) error { + thread, err := s.Channel(mu.ID) + if err != nil { + return err + } + + thread.Member = mu.ThreadMember + 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 per channel. +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 != nil { + 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.IsZero() { + m.Timestamp = message.Timestamp + } + if message.Author != nil { + m.Author = message.Author + } + if message.Components != nil { + m.Components = message.Components + } + + 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 +} + +// VoiceState gets a VoiceState by guild and user ID. +func (s *State) VoiceState(guildID, userID string) (*VoiceState, error) { + if s == nil { + return nil, ErrNilState + } + + guild, err := s.Guild(guildID) + if err != nil { + return nil, err + } + + for _, state := range guild.VoiceStates { + if state.UserID == userID { + return state, nil + } + } + + return nil, ErrStateNotFound +} + +// 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: + var old *Guild + old, err = s.Guild(t.ID) + if err == nil { + oldCopy := *old + t.BeforeDelete = &oldCopy + } + + err = s.GuildRemove(t.Guild) + case *GuildMemberAdd: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount++ + + // Caches member if tracking is enabled. + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } + case *GuildMemberUpdate: + if s.TrackMembers { + err = s.MemberAdd(t.Member) + } + case *GuildMemberRemove: + // Updates the MemberCount of the guild. + guild, err := s.Guild(t.Member.GuildID) + if err != nil { + return err + } + guild.MemberCount-- + + // Removes member from the cache if tracking is enabled. + if s.TrackMembers { + err = s.MemberRemove(t.Member) + } + case *GuildMembersChunk: + if s.TrackMembers { + for i := range t.Members { + t.Members[i].GuildID = t.GuildID + err = s.MemberAdd(t.Members[i]) + } + } + + if s.TrackPresences { + for _, p := range t.Presences { + err = s.PresenceAdd(t.GuildID, p) + } + } + 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 *ThreadCreate: + if s.TrackThreads { + err = s.ChannelAdd(t.Channel) + } + case *ThreadUpdate: + if s.TrackThreads { + old, err := s.Channel(t.ID) + if err == nil { + oldCopy := *old + t.BeforeUpdate = &oldCopy + } + err = s.ChannelAdd(t.Channel) + } + case *ThreadDelete: + if s.TrackThreads { + err = s.ChannelRemove(t.Channel) + } + case *ThreadMemberUpdate: + if s.TrackThreads { + err = s.ThreadMemberUpdate(t) + } + case *ThreadMembersUpdate: + if s.TrackThreadMembers { + err = s.ThreadMembersUpdate(t) + } + case *ThreadListSync: + if s.TrackThreads { + err = s.ThreadListSync(t) + } + case *MessageCreate: + if s.MaxMessageCount != 0 { + err = s.MessageAdd(t.Message) + } + case *MessageUpdate: + if s.MaxMessageCount != 0 { + var old *Message + old, err = s.Message(t.ChannelID, t.ID) + if err == nil { + oldCopy := *old + t.BeforeUpdate = &oldCopy + } + + err = s.MessageAdd(t.Message) + } + case *MessageDelete: + if s.MaxMessageCount != 0 { + var old *Message + old, err = s.Message(t.ChannelID, t.ID) + if err == nil { + oldCopy := *old + t.BeforeDelete = &oldCopy + } + + 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 { + var old *VoiceState + old, err = s.VoiceState(t.GuildID, t.UserID) + if err == nil { + oldCopy := *old + t.BeforeUpdate = &oldCopy + } + + 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, + User: t.User, + } + } else { + if t.User.Username != "" { + m.User.Username = t.User.Username + } + } + + 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 int64, 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 + } + + member, err := s.Member(guild.ID, userID) + if err != nil { + return + } + + return memberPermissions(guild, channel, userID, member.Roles), nil +} + +// MessagePermissions returns the permissions of the author of the message +// in the channel in which it was sent. +func (s *State) MessagePermissions(message *Message) (apermissions int64, err error) { + if s == nil { + return 0, ErrNilState + } + + if message.Author == nil || message.Member == nil { + return 0, ErrMessageIncompletePermissions + } + + channel, err := s.Channel(message.ChannelID) + if err != nil { + return + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return + } + + return memberPermissions(guild, channel, message.Author.ID, message.Member.Roles), 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 + } + + return firstRoleColorColor(guild, member.Roles) +} + +// MessageColor returns the color of the author's name as displayed +// in the client associated with this message. +func (s *State) MessageColor(message *Message) int { + if s == nil { + return 0 + } + + if message.Member == nil || message.Member.Roles == nil { + return 0 + } + + channel, err := s.Channel(message.ChannelID) + if err != nil { + return 0 + } + + guild, err := s.Guild(channel.GuildID) + if err != nil { + return 0 + } + + return firstRoleColorColor(guild, message.Member.Roles) +} + +func firstRoleColorColor(guild *Guild, memberRoles []string) int { + roles := Roles(guild.Roles) + sort.Sort(roles) + + for _, role := range roles { + for _, roleID := range memberRoles { + if role.ID == roleID { + if role.Color != 0 { + return role.Color + } + } + } + } + + for _, role := range roles { + if role.ID == guild.ID { + return role.Color + } + } + + return 0 +} diff --git a/vendor/github.com/bwmarrin/discordgo/structs.go b/vendor/github.com/bwmarrin/discordgo/structs.go new file mode 100644 index 00000000..3a92c9fd --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/structs.go @@ -0,0 +1,2038 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains all structures for the discordgo package. These +// may be moved about later into separate files but I find it easier to have +// them all located together. + +package discordgo + +import ( + "encoding/json" + "fmt" + "math" + "net/http" + "regexp" + "strings" + "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 + // TODO: Remove Below, Deprecated, Use Identify struct + 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 + + // Identify is sent during initial handshake with the discord gateway. + // https://discord.com/developers/docs/topics/gateway#identify + Identify Identify + + // TODO: Remove Below, Deprecated, Use Identify struct + // 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 + + // The user agent used for REST APIs + UserAgent string + + // Stores the last HeartbeatAck that was received (in UTC) + LastHeartbeatAck time.Time + + // Stores the last Heartbeat sent (in UTC) + LastHeartbeatSent 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 +} + +// Application stores values for a Discord Application +type Application struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Icon string `json:"icon,omitempty"` + Description string `json:"description,omitempty"` + RPCOrigins []string `json:"rpc_origins,omitempty"` + BotPublic bool `json:"bot_public,omitempty"` + BotRequireCodeGrant bool `json:"bot_require_code_grant,omitempty"` + TermsOfServiceURL string `json:"terms_of_service_url"` + PrivacyProxyURL string `json:"privacy_policy_url"` + Owner *User `json:"owner"` + Summary string `json:"summary"` + VerifyKey string `json:"verify_key"` + Team *Team `json:"team"` + GuildID string `json:"guild_id"` + PrimarySKUID string `json:"primary_sku_id"` + Slug string `json:"slug"` + CoverImage string `json:"cover_image"` + Flags int `json:"flags,omitempty"` +} + +// UserConnection is a Connection returned from the UserConnections endpoint +type UserConnection struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Revoked bool `json:"revoked"` + Integrations []*Integration `json:"integrations"` +} + +// Integration stores integration information +type Integration struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Enabled bool `json:"enabled"` + Syncing bool `json:"syncing"` + RoleID string `json:"role_id"` + EnableEmoticons bool `json:"enable_emoticons"` + ExpireBehavior ExpireBehavior `json:"expire_behavior"` + ExpireGracePeriod int `json:"expire_grace_period"` + User *User `json:"user"` + Account IntegrationAccount `json:"account"` + SyncedAt time.Time `json:"synced_at"` +} + +// ExpireBehavior of Integration +// https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors +type ExpireBehavior int + +// Block of valid ExpireBehaviors +const ( + ExpireBehaviorRemoveRole ExpireBehavior = 0 + ExpireBehaviorKick ExpireBehavior = 1 +) + +// IntegrationAccount is integration account information +// sent by the UserConnections endpoint +type IntegrationAccount struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// A VoiceRegion stores data for a specific voice region server. +type VoiceRegion struct { + ID string `json:"id"` + 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"` +} + +// InviteTargetType indicates the type of target of an invite +// https://discord.com/developers/docs/resources/invite#invite-object-invite-target-types +type InviteTargetType uint8 + +// Invite target types +const ( + InviteTargetStream InviteTargetType = 1 + InviteTargetEmbeddedAppliction InviteTargetType = 2 +) + +// 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 time.Time `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"` + TargetUser *User `json:"target_user"` + TargetType InviteTargetType `json:"target_type"` + TargetApplication *Application `json:"target_application"` + + // will only be filled when using InviteWithCounts + ApproximatePresenceCount int `json:"approximate_presence_count"` + ApproximateMemberCount int `json:"approximate_member_count"` +} + +// ChannelType is the type of a Channel +type ChannelType int + +// Block contains known ChannelType values +const ( + ChannelTypeGuildText ChannelType = 0 + ChannelTypeDM ChannelType = 1 + ChannelTypeGuildVoice ChannelType = 2 + ChannelTypeGroupDM ChannelType = 3 + ChannelTypeGuildCategory ChannelType = 4 + ChannelTypeGuildNews ChannelType = 5 + ChannelTypeGuildStore ChannelType = 6 + ChannelTypeGuildNewsThread ChannelType = 10 + ChannelTypeGuildPublicThread ChannelType = 11 + ChannelTypeGuildPrivateThread ChannelType = 12 +) + +// A Channel holds all data related to an individual Discord channel. +type Channel struct { + // The ID of the channel. + ID string `json:"id"` + + // The ID of the guild to which the channel belongs, if it is in a guild. + // Else, this ID is empty (e.g. DM channels). + GuildID string `json:"guild_id"` + + // The name of the channel. + Name string `json:"name"` + + // The topic of the channel. + Topic string `json:"topic"` + + // The type of the channel. + Type ChannelType `json:"type"` + + // The ID of the last message sent in the channel. This is not + // guaranteed to be an ID of a valid message. + LastMessageID string `json:"last_message_id"` + + // The timestamp of the last pinned message in the channel. + // nil if the channel has no pinned messages. + LastPinTimestamp *time.Time `json:"last_pin_timestamp"` + + // An approximate count of messages in a thread, stops counting at 50 + MessageCount int `json:"message_count"` + // An approximate count of users in a thread, stops counting at 50 + MemberCount int `json:"member_count"` + + // Whether the channel is marked as NSFW. + NSFW bool `json:"nsfw"` + + // Icon of the group DM channel. + Icon string `json:"icon"` + + // The position of the channel, used for sorting in client. + Position int `json:"position"` + + // The bitrate of the channel, if it is a voice channel. + Bitrate int `json:"bitrate"` + + // The recipients of the channel. This is only populated in DM channels. + Recipients []*User `json:"recipients"` + + // The messages in the channel. This is only present in state-cached channels, + // and State.MaxMessageCount must be non-zero. + Messages []*Message `json:"-"` + + // A list of permission overwrites present for the channel. + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` + + // The user limit of the voice channel. + UserLimit int `json:"user_limit"` + + // The ID of the parent channel, if the channel is under a category. For threads - id of the channel thread was created in. + ParentID string `json:"parent_id"` + + // Amount of seconds a user has to wait before sending another message or creating another thread (0-21600) + // bots, as well as users with the permission manage_messages or manage_channel, are unaffected + RateLimitPerUser int `json:"rate_limit_per_user"` + + // ID of the creator of the group DM or thread + OwnerID string `json:"owner_id"` + + // ApplicationID of the DM creator Zeroed if guild channel or not a bot user + ApplicationID string `json:"application_id"` + + // Thread-specific fields not needed by other channels + ThreadMetadata *ThreadMetadata `json:"thread_metadata,omitempty"` + // Thread member object for the current user, if they have joined the thread, only included on certain API endpoints + Member *ThreadMember `json:"thread_member"` + + // All thread members. State channels only. + Members []*ThreadMember `json:"-"` +} + +// Mention returns a string which mentions the channel +func (c *Channel) Mention() string { + return fmt.Sprintf("<#%s>", c.ID) +} + +// IsThread is a helper function to determine if channel is a thread or not +func (c *Channel) IsThread() bool { + return c.Type == ChannelTypeGuildPublicThread || c.Type == ChannelTypeGuildPrivateThread || c.Type == ChannelTypeGuildNewsThread +} + +// A ChannelEdit holds Channel Field 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"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` + + // NOTE: threads only + + Archived bool `json:"archived,omitempty"` + AutoArchiveDuration int `json:"auto_archive_duration,omitempty"` + Locked bool `json:"locked,bool"` + Invitable bool `json:"invitable,omitempty"` +} + +// A ChannelFollow holds data returned after following a news channel +type ChannelFollow struct { + ChannelID string `json:"channel_id"` + WebhookID string `json:"webhook_id"` +} + +// PermissionOverwriteType represents the type of resource on which +// a permission overwrite acts. +type PermissionOverwriteType int + +// The possible permission overwrite types. +const ( + PermissionOverwriteTypeRole PermissionOverwriteType = 0 + PermissionOverwriteTypeMember PermissionOverwriteType = 1 +) + +// A PermissionOverwrite holds permission overwrite data for a Channel +type PermissionOverwrite struct { + ID string `json:"id"` + Type PermissionOverwriteType `json:"type"` + Deny int64 `json:"deny,string"` + Allow int64 `json:"allow,string"` +} + +// ThreadStart stores all parameters you can use with MessageThreadStartComplex or ThreadStartComplex +type ThreadStart struct { + Name string `json:"name"` + AutoArchiveDuration int `json:"auto_archive_duration,omitempty"` + Type ChannelType `json:"type,omitempty"` + Invitable bool `json:"invitable"` + RateLimitPerUser int `json:"rate_limit_per_user,omitempty"` +} + +// ThreadMetadata contains a number of thread-specific channel fields that are not needed by other channel types. +type ThreadMetadata struct { + // Whether the thread is archived + Archived bool `json:"archived"` + // Duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 + AutoArchiveDuration int `json:"auto_archive_duration"` + // Timestamp when the thread's archive status was last changed, used for calculating recent activity + ArchiveTimestamp time.Time `json:"archive_timestamp"` + // Whether the thread is locked; when a thread is locked, only users with MANAGE_THREADS can unarchive it + Locked bool `json:"locked"` + // Whether non-moderators can add other non-moderators to a thread; only available on private threads + Invitable bool `json:"invitable"` +} + +// ThreadMember is used to indicate whether a user has joined a thread or not. +// NOTE: ID and UserID are empty (omitted) on the member sent within each thread in the GUILD_CREATE event. +type ThreadMember struct { + // The id of the thread + ID string `json:"id,omitempty"` + // The id of the user + UserID string `json:"user_id,omitempty"` + // The time the current user last joined the thread + JoinTimestamp time.Time `json:"join_timestamp"` + // Any user-thread settings, currently only used for notifications + Flags int +} + +// ThreadsList represents a list of threads alongisde with thread member objects for the current user. +type ThreadsList struct { + Threads []*Channel `json:"threads"` + Members []*ThreadMember `json:"members"` + HasMore bool `json:"has_more"` +} + +// AddedThreadMember holds information about the user who was added to the thread +type AddedThreadMember struct { + *ThreadMember + Member *Member `json:"member"` + Presence *Presence `json:"presence"` +} + +// Emoji struct holds data related to Emoji's +type Emoji struct { + ID string `json:"id"` + Name string `json:"name"` + Roles []string `json:"roles"` + User *User `json:"user"` + RequireColons bool `json:"require_colons"` + Managed bool `json:"managed"` + Animated bool `json:"animated"` + Available bool `json:"available"` +} + +// EmojiRegex is the regex used to find and identify emojis in messages +var ( + EmojiRegex = regexp.MustCompile(`<(a|):[A-z0-9_~]+:[0-9]{18}>`) +) + +// MessageFormat returns a correctly formatted Emoji for use in Message content and embeds +func (e *Emoji) MessageFormat() string { + if e.ID != "" && e.Name != "" { + if e.Animated { + return "<a:" + e.APIName() + ">" + } + + return "<:" + e.APIName() + ">" + } + + return e.APIName() +} + +// APIName returns an correctly formatted API name for use in the MessageReactions endpoints. +func (e *Emoji) APIName() string { + if e.ID != "" && e.Name != "" { + return e.Name + ":" + e.ID + } + if e.Name != "" { + return e.Name + } + return e.ID +} + +// StickerFormat is the file format of the Sticker. +type StickerFormat int + +// Defines all known Sticker types. +const ( + StickerFormatTypePNG StickerFormat = 1 + StickerFormatTypeAPNG StickerFormat = 2 + StickerFormatTypeLottie StickerFormat = 3 +) + +// StickerType is the type of sticker. +type StickerType int + +// Defines Sticker types. +const ( + StickerTypeStandard StickerType = 1 + StickerTypeGuild StickerType = 2 +) + +// Sticker represents a sticker object that can be sent in a Message. +type Sticker struct { + ID string `json:"id"` + PackID string `json:"pack_id"` + Name string `json:"name"` + Description string `json:"description"` + Tags string `json:"tags"` + Type StickerType `json:"type"` + FormatType StickerFormat `json:"format_type"` + Available bool `json:"available"` + GuildID string `json:"guild_id"` + User *User `json:"user"` + SortValue int `json:"sort_value"` +} + +// StickerPack represents a pack of standard stickers. +type StickerPack struct { + ID string `json:"id"` + Stickers []*Sticker `json:"stickers"` + Name string `json:"name"` + SKUID string `json:"sku_id"` + CoverStickerID string `json:"cover_sticker_id"` + Description string `json:"description"` + BannerAssetID string `json:"banner_asset_id"` +} + +// VerificationLevel type definition +type VerificationLevel int + +// Constants for VerificationLevel levels from 0 to 4 inclusive +const ( + VerificationLevelNone VerificationLevel = 0 + VerificationLevelLow VerificationLevel = 1 + VerificationLevelMedium VerificationLevel = 2 + VerificationLevelHigh VerificationLevel = 3 + VerificationLevelVeryHigh VerificationLevel = 4 +) + +// ExplicitContentFilterLevel type definition +type ExplicitContentFilterLevel int + +// Constants for ExplicitContentFilterLevel levels from 0 to 2 inclusive +const ( + ExplicitContentFilterDisabled ExplicitContentFilterLevel = 0 + ExplicitContentFilterMembersWithoutRoles ExplicitContentFilterLevel = 1 + ExplicitContentFilterAllMembers ExplicitContentFilterLevel = 2 +) + +// MfaLevel type definition +type MfaLevel int + +// Constants for MfaLevel levels from 0 to 1 inclusive +const ( + MfaLevelNone MfaLevel = 0 + MfaLevelElevated MfaLevel = 1 +) + +// PremiumTier type definition +type PremiumTier int + +// Constants for PremiumTier levels from 0 to 3 inclusive +const ( + PremiumTierNone PremiumTier = 0 + PremiumTier1 PremiumTier = 1 + PremiumTier2 PremiumTier = 2 + PremiumTier3 PremiumTier = 3 +) + +// 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 { + // The ID of the guild. + ID string `json:"id"` + + // The name of the guild. (2–100 characters) + Name string `json:"name"` + + // The hash of the guild's icon. Use Session.GuildIcon + // to retrieve the icon itself. + Icon string `json:"icon"` + + // The voice region of the guild. + Region string `json:"region"` + + // The ID of the AFK voice channel. + AfkChannelID string `json:"afk_channel_id"` + + // The user ID of the owner of the guild. + OwnerID string `json:"owner_id"` + + // If we are the owner of the guild + Owner bool `json:"owner"` + + // The time at which the current user joined the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + JoinedAt time.Time `json:"joined_at"` + + // The hash of the guild's discovery splash. + DiscoverySplash string `json:"discovery_splash"` + + // The hash of the guild's splash. + Splash string `json:"splash"` + + // The timeout, in seconds, before a user is considered AFK in voice. + AfkTimeout int `json:"afk_timeout"` + + // The number of members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + MemberCount int `json:"member_count"` + + // The verification level required for the guild. + VerificationLevel VerificationLevel `json:"verification_level"` + + // Whether the guild is considered large. This is + // determined by a member threshold in the identify packet, + // and is currently hard-coded at 250 members in the library. + Large bool `json:"large"` + + // The default message notification setting for the guild. + DefaultMessageNotifications MessageNotifications `json:"default_message_notifications"` + + // A list of roles in the guild. + Roles []*Role `json:"roles"` + + // A list of the custom emojis present in the guild. + Emojis []*Emoji `json:"emojis"` + + // A list of the custom stickers present in the guild. + Stickers []*Sticker `json:"stickers"` + + // A list of the members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Members []*Member `json:"members"` + + // A list of partial presence objects for members in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Presences []*Presence `json:"presences"` + + // The maximum number of presences for the guild (the default value, currently 25000, is in effect when null is returned) + MaxPresences int `json:"max_presences"` + + // The maximum number of members for the guild + MaxMembers int `json:"max_members"` + + // A list of channels in the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Channels []*Channel `json:"channels"` + + // A list of all active threads in the guild that current user has permission to view + // This field is only present in GUILD_CREATE events and websocket + // update events and thus is only present in state-cached guilds. + Threads []*Channel `json:"threads"` + + // A list of voice states for the guild. + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + VoiceStates []*VoiceState `json:"voice_states"` + + // Whether this guild is currently unavailable (most likely due to outage). + // This field is only present in GUILD_CREATE events and websocket + // update events, and thus is only present in state-cached guilds. + Unavailable bool `json:"unavailable"` + + // The explicit content filter level + ExplicitContentFilter ExplicitContentFilterLevel `json:"explicit_content_filter"` + + // The list of enabled guild features + Features []string `json:"features"` + + // Required MFA level for the guild + MfaLevel MfaLevel `json:"mfa_level"` + + // The application id of the guild if bot created. + ApplicationID string `json:"application_id"` + + // Whether or not the Server Widget is enabled + WidgetEnabled bool `json:"widget_enabled"` + + // The Channel ID for the Server Widget + WidgetChannelID string `json:"widget_channel_id"` + + // The Channel ID to which system messages are sent (eg join and leave messages) + SystemChannelID string `json:"system_channel_id"` + + // The System channel flags + SystemChannelFlags SystemChannelFlag `json:"system_channel_flags"` + + // The ID of the rules channel ID, used for rules. + RulesChannelID string `json:"rules_channel_id"` + + // the vanity url code for the guild + VanityURLCode string `json:"vanity_url_code"` + + // the description for the guild + Description string `json:"description"` + + // The hash of the guild's banner + Banner string `json:"banner"` + + // The premium tier of the guild + PremiumTier PremiumTier `json:"premium_tier"` + + // The total number of users currently boosting this server + PremiumSubscriptionCount int `json:"premium_subscription_count"` + + // The preferred locale of a guild with the "PUBLIC" feature; used in server discovery and notices from Discord; defaults to "en-US" + PreferredLocale string `json:"preferred_locale"` + + // The id of the channel where admins and moderators of guilds with the "PUBLIC" feature receive notices from Discord + PublicUpdatesChannelID string `json:"public_updates_channel_id"` + + // The maximum amount of users in a video channel + MaxVideoChannelUsers int `json:"max_video_channel_users"` + + // Approximate number of members in this guild, returned from the GET /guild/<id> endpoint when with_counts is true + ApproximateMemberCount int `json:"approximate_member_count"` + + // Approximate number of non-offline members in this guild, returned from the GET /guild/<id> endpoint when with_counts is true + ApproximatePresenceCount int `json:"approximate_presence_count"` + + // Permissions of our user + Permissions int64 `json:"permissions,string"` +} + +// A GuildPreview holds data related to a specific public Discord Guild, even if the user is not in the guild. +type GuildPreview struct { + // The ID of the guild. + ID string `json:"id"` + + // The name of the guild. (2–100 characters) + Name string `json:"name"` + + // The hash of the guild's icon. Use Session.GuildIcon + // to retrieve the icon itself. + Icon string `json:"icon"` + + // The hash of the guild's splash. + Splash string `json:"splash"` + + // The hash of the guild's discovery splash. + DiscoverySplash string `json:"discovery_splash"` + + // A list of the custom emojis present in the guild. + Emojis []*Emoji `json:"emojis"` + + // The list of enabled guild features + Features []string `json:"features"` + + // Approximate number of members in this guild, returned from the GET /guild/<id> endpoint when with_counts is true + ApproximateMemberCount int `json:"approximate_member_count"` + + // Approximate number of non-offline members in this guild, returned from the GET /guild/<id> endpoint when with_counts is true + ApproximatePresenceCount int `json:"approximate_presence_count"` + + // the description for the guild + Description string `json:"description"` +} + +// GuildScheduledEvent is a representation of a scheduled event in a guild. Only for retrieval of the data. +// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event +type GuildScheduledEvent struct { + // The ID of the scheduled event + ID string `json:"id"` + // The guild id which the scheduled event belongs to + GuildID string `json:"guild_id"` + // The channel id in which the scheduled event will be hosted, or null if scheduled entity type is EXTERNAL + ChannelID string `json:"channel_id"` + // The id of the user that created the scheduled event + CreatorID string `json:"creator_id"` + // The name of the scheduled event (1-100 characters) + Name string `json:"name"` + // The description of the scheduled event (1-1000 characters) + Description string `json:"description"` + // The time the scheduled event will start + ScheduledStartTime time.Time `json:"scheduled_start_time"` + // The time the scheduled event will end, required only when entity_type is EXTERNAL + ScheduledEndTime *time.Time `json:"scheduled_end_time"` + // The privacy level of the scheduled event + PrivacyLevel GuildScheduledEventPrivacyLevel `json:"privacy_level"` + // The status of the scheduled event + Status GuildScheduledEventStatus `json:"status"` + // Type of the entity where event would be hosted + // See field requirements + // https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-field-requirements-by-entity-type + EntityType GuildScheduledEventEntityType `json:"entity_type"` + // The id of an entity associated with a guild scheduled event + EntityID string `json:"entity_id"` + // Additional metadata for the guild scheduled event + EntityMetadata GuildScheduledEventEntityMetadata `json:"entity_metadata"` + // The user that created the scheduled event + Creator *User `json:"creator"` + // The number of users subscribed to the scheduled event + UserCount int `json:"user_count"` + // The cover image hash of the scheduled event + // see https://discord.com/developers/docs/reference#image-formatting for more + // information about image formatting + Image string `json:"image"` +} + +// GuildScheduledEventParams are the parameters allowed for creating or updating a scheduled event +// https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event +type GuildScheduledEventParams struct { + // The channel id in which the scheduled event will be hosted, or null if scheduled entity type is EXTERNAL + ChannelID string `json:"channel_id,omitempty"` + // The name of the scheduled event (1-100 characters) + Name string `json:"name,omitempty"` + // The description of the scheduled event (1-1000 characters) + Description string `json:"description,omitempty"` + // The time the scheduled event will start + ScheduledStartTime *time.Time `json:"scheduled_start_time,omitempty"` + // The time the scheduled event will end, required only when entity_type is EXTERNAL + ScheduledEndTime *time.Time `json:"scheduled_end_time,omitempty"` + // The privacy level of the scheduled event + PrivacyLevel GuildScheduledEventPrivacyLevel `json:"privacy_level,omitempty"` + // The status of the scheduled event + Status GuildScheduledEventStatus `json:"status,omitempty"` + // Type of the entity where event would be hosted + // See field requirements + // https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-field-requirements-by-entity-type + EntityType GuildScheduledEventEntityType `json:"entity_type,omitempty"` + // Additional metadata for the guild scheduled event + EntityMetadata *GuildScheduledEventEntityMetadata `json:"entity_metadata,omitempty"` + // The cover image hash of the scheduled event + // see https://discord.com/developers/docs/reference#image-formatting for more + // information about image formatting + Image string `json:"image,omitempty"` +} + +// MarshalJSON is a helper function to marshal GuildScheduledEventParams +func (p GuildScheduledEventParams) MarshalJSON() ([]byte, error) { + type guildScheduledEventParams GuildScheduledEventParams + + if p.EntityType == GuildScheduledEventEntityTypeExternal && p.ChannelID == "" { + return json.Marshal(struct { + guildScheduledEventParams + ChannelID json.RawMessage `json:"channel_id"` + }{ + guildScheduledEventParams: guildScheduledEventParams(p), + ChannelID: json.RawMessage("null"), + }) + } + + return json.Marshal(guildScheduledEventParams(p)) +} + +// GuildScheduledEventEntityMetadata holds additional metadata for guild scheduled event. +type GuildScheduledEventEntityMetadata struct { + // location of the event (1-100 characters) + // required for events with 'entity_type': EXTERNAL + Location string `json:"location"` +} + +// GuildScheduledEventPrivacyLevel is the privacy level of a scheduled event. +// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level +type GuildScheduledEventPrivacyLevel int + +const ( + // GuildScheduledEventPrivacyLevelGuildOnly makes the scheduled + // event is only accessible to guild members + GuildScheduledEventPrivacyLevelGuildOnly GuildScheduledEventPrivacyLevel = 2 +) + +// GuildScheduledEventStatus is the status of a scheduled event +// Valid Guild Scheduled Event Status Transitions : +// SCHEDULED --> ACTIVE --> COMPLETED +// SCHEDULED --> CANCELED +// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status +type GuildScheduledEventStatus int + +const ( + // GuildScheduledEventStatusScheduled represents the current event is in scheduled state + GuildScheduledEventStatusScheduled = 1 + // GuildScheduledEventStatusActive represents the current event is in active state + GuildScheduledEventStatusActive = 2 + // GuildScheduledEventStatusCompleted represents the current event is in completed state + GuildScheduledEventStatusCompleted = 3 + // GuildScheduledEventStatusCanceled represents the current event is in canceled state + GuildScheduledEventStatusCanceled = 4 +) + +// GuildScheduledEventEntityType is the type of entity associated with a guild scheduled event. +// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-types +type GuildScheduledEventEntityType int + +const ( + // GuildScheduledEventEntityTypeStageInstance represents a stage channel + GuildScheduledEventEntityTypeStageInstance = 1 + // GuildScheduledEventEntityTypeVoice represents a voice channel + GuildScheduledEventEntityTypeVoice = 2 + // GuildScheduledEventEntityTypeExternal represents an external event + GuildScheduledEventEntityTypeExternal = 3 +) + +// GuildScheduledEventUser is a user subscribed to a scheduled event. +// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-user-object +type GuildScheduledEventUser struct { + GuildScheduledEventID string `json:"guild_scheduled_event_id"` + User *User `json:"user"` + Member *Member `json:"member"` +} + +// A GuildTemplate represents +type GuildTemplate struct { + // The unique code for the guild template + Code string `json:"code"` + + // The name of the template + Name string `json:"name"` + + // The description for the template + Description string `json:"description"` + + // The number of times this template has been used + UsageCount string `json:"usage_count"` + + // The ID of the user who created the template + CreatorID string `json:"creator_id"` + + // The user who created the template + Creator *User `json:"creator"` + + // The timestamp of when the template was created + CreatedAt time.Time `json:"created_at"` + + // The timestamp of when the template was last synced + UpdatedAt time.Time `json:"updated_at"` + + // The ID of the guild the template was based on + SourceGuildID string `json:"source_guild_id"` + + // The guild 'snapshot' this template contains + SerializedSourceGuild *Guild `json:"serialized_source_guild"` + + // Whether the template has unsynced changes + IsDirty bool `json:"is_dirty"` +} + +// MessageNotifications is the notification level for a guild +// https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level +type MessageNotifications int + +// Block containing known MessageNotifications values +const ( + MessageNotificationsAllMessages MessageNotifications = 0 + MessageNotificationsOnlyMentions MessageNotifications = 1 +) + +// SystemChannelFlag is the type of flags in the system channel (see SystemChannelFlag* consts) +// https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags +type SystemChannelFlag int + +// Block containing known SystemChannelFlag values +const ( + SystemChannelFlagsSuppressJoin SystemChannelFlag = 1 << 0 + SystemChannelFlagsSuppressPremium SystemChannelFlag = 1 << 1 +) + +// IconURL returns a URL to the guild's icon. +func (g *Guild) IconURL() string { + if g.Icon == "" { + return "" + } + + if strings.HasPrefix(g.Icon, "a_") { + return EndpointGuildIconAnimated(g.ID, g.Icon) + } + + return EndpointGuildIcon(g.ID, g.Icon) +} + +// BannerURL returns a URL to the guild's banner. +func (g *Guild) BannerURL() string { + if g.Banner == "" { + return "" + } + return EndpointGuildBanner(g.ID, g.Banner) +} + +// 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 int64 `json:"permissions,string"` +} + +// 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"` + Banner string `json:"banner,omitempty"` +} + +// A Role stores information about Discord guild member roles. +type Role struct { + // The ID of the role. + ID string `json:"id"` + + // The name of the role. + Name string `json:"name"` + + // Whether this role is managed by an integration, and + // thus cannot be manually added to, or taken from, members. + Managed bool `json:"managed"` + + // Whether this role is mentionable. + Mentionable bool `json:"mentionable"` + + // Whether this role is hoisted (shows up separately in member list). + Hoist bool `json:"hoist"` + + // The hex color of this role. + Color int `json:"color"` + + // The position of this role in the guild's role hierarchy. + Position int `json:"position"` + + // The permissions of the role on the guild (doesn't include channel overrides). + // This is a combination of bit masks; the presence of a certain permission can + // be checked by performing a bitwise AND between this int and the permission. + Permissions int64 `json:"permissions,string"` +} + +// Mention returns a string which mentions the role +func (r *Role) Mention() string { + return fmt.Sprintf("<@&%s>", r.ID) +} + +// Roles are a collection of Role +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"` + Activities []*Activity `json:"activities"` + Since *int `json:"since"` +} + +// 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. A guild +// member represents a certain user's presence in a guild. +type Member struct { + // The guild ID on which the member exists. + GuildID string `json:"guild_id"` + + // The time at which the member joined the guild. + JoinedAt time.Time `json:"joined_at"` + + // The nickname of the member, if they have one. + Nick string `json:"nick"` + + // Whether the member is deafened at a guild level. + Deaf bool `json:"deaf"` + + // Whether the member is muted at a guild level. + Mute bool `json:"mute"` + + // The hash of the avatar for the guild member, if any. + Avatar string `json:"avatar"` + + // The underlying user on which the member is based. + User *User `json:"user"` + + // A list of IDs of the roles which are possessed by the member. + Roles []string `json:"roles"` + + // When the user used their Nitro boost on the server + PremiumSince *time.Time `json:"premium_since"` + + // Is true while the member hasn't accepted the membership screen. + Pending bool `json:"pending"` + + // Total permissions of the member in the channel, including overrides, returned when in the interaction object. + Permissions int64 `json:"permissions,string"` + + // The time at which the member's timeout will expire. + // Time in the past or nil if the user is not timed out. + CommunicationDisabledUntil *time.Time `json:"communication_disabled_until"` +} + +// Mention creates a member mention +func (m *Member) Mention() string { + return "<@!" + m.User.ID + ">" +} + +// AvatarURL returns the URL of the member'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 (m *Member) AvatarURL(size string) string { + if m.Avatar == "" { + return m.User.AvatarURL(size) + } + // The default/empty avatar case should be handled by the above condition + return avatarURL(m.Avatar, "", EndpointGuildMemberAvatar(m.GuildID, m.User.ID, m.Avatar), + EndpointGuildMemberAvatarAnimated(m.GuildID, m.User.ID, m.Avatar), size) + +} + +// 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"` +} + +// UnmarshalJSON helps support translation of a milliseconds-based float +// into a time.Duration on TooManyRequests. +func (t *TooManyRequests) UnmarshalJSON(b []byte) error { + u := struct { + Bucket string `json:"bucket"` + Message string `json:"message"` + RetryAfter float64 `json:"retry_after"` + }{} + err := json.Unmarshal(b, &u) + if err != nil { + return err + } + + t.Bucket = u.Bucket + t.Message = u.Message + whole, frac := math.Modf(u.RetryAfter) + t.RetryAfter = time.Duration(whole)*time.Second + time.Duration(frac*1000)*time.Millisecond + return nil +} + +// 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 GuildEmbed stores data for a guild embed. +type GuildEmbed struct { + Enabled bool `json:"enabled"` + ChannelID string `json:"channel_id"` +} + +// A GuildAuditLog stores data for a guild audit log. +// https://discord.com/developers/docs/resources/audit-log#audit-log-object-audit-log-structure +type GuildAuditLog struct { + Webhooks []*Webhook `json:"webhooks,omitempty"` + Users []*User `json:"users,omitempty"` + AuditLogEntries []*AuditLogEntry `json:"audit_log_entries"` + Integrations []*Integration `json:"integrations"` +} + +// AuditLogEntry for a GuildAuditLog +// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-entry-structure +type AuditLogEntry struct { + TargetID string `json:"target_id"` + Changes []*AuditLogChange `json:"changes"` + UserID string `json:"user_id"` + ID string `json:"id"` + ActionType *AuditLogAction `json:"action_type"` + Options *AuditLogOptions `json:"options"` + Reason string `json:"reason"` +} + +// AuditLogChange for an AuditLogEntry +type AuditLogChange struct { + NewValue interface{} `json:"new_value"` + OldValue interface{} `json:"old_value"` + Key *AuditLogChangeKey `json:"key"` +} + +// AuditLogChangeKey value for AuditLogChange +// https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-key +type AuditLogChangeKey string + +// Block of valid AuditLogChangeKey +const ( + // AuditLogChangeKeyAfkChannelID is sent when afk channel changed (snowflake) - guild + AuditLogChangeKeyAfkChannelID AuditLogChangeKey = "afk_channel_id" + // AuditLogChangeKeyAfkTimeout is sent when afk timeout duration changed (int) - guild + AuditLogChangeKeyAfkTimeout AuditLogChangeKey = "afk_timeout" + // AuditLogChangeKeyAllow is sent when a permission on a text or voice channel was allowed for a role (string) - role + AuditLogChangeKeyAllow AuditLogChangeKey = "allow" + // AudirChangeKeyApplicationID is sent when application id of the added or removed webhook or bot (snowflake) - channel + AuditLogChangeKeyApplicationID AuditLogChangeKey = "application_id" + // AuditLogChangeKeyArchived is sent when thread was archived/unarchived (bool) - thread + AuditLogChangeKeyArchived AuditLogChangeKey = "archived" + // AuditLogChangeKeyAsset is sent when asset is changed (string) - sticker + AuditLogChangeKeyAsset AuditLogChangeKey = "asset" + // AuditLogChangeKeyAutoArchiveDuration is sent when auto archive duration changed (int) - thread + AuditLogChangeKeyAutoArchiveDuration AuditLogChangeKey = "auto_archive_duration" + // AuditLogChangeKeyAvailable is sent when availability of sticker changed (bool) - sticker + AuditLogChangeKeyAvailable AuditLogChangeKey = "available" + // AuditLogChangeKeyAvatarHash is sent when user avatar changed (string) - user + AuditLogChangeKeyAvatarHash AuditLogChangeKey = "avatar_hash" + // AuditLogChangeKeyBannerHash is sent when guild banner changed (string) - guild + AuditLogChangeKeyBannerHash AuditLogChangeKey = "banner_hash" + // AuditLogChangeKeyBitrate is sent when voice channel bitrate changed (int) - channel + AuditLogChangeKeyBitrate AuditLogChangeKey = "bitrate" + // AuditLogChangeKeyChannelID is sent when channel for invite code or guild scheduled event changed (snowflake) - invite or guild scheduled event + AuditLogChangeKeyChannelID AuditLogChangeKey = "channel_id" + // AuditLogChangeKeyCode is sent when invite code changed (string) - invite + AuditLogChangeKeyCode AuditLogChangeKey = "code" + // AuditLogChangeKeyColor is sent when role color changed (int) - role + AuditLogChangeKeyColor AuditLogChangeKey = "color" + // AuditLogChangeKeyCommunicationDisabledUntil is sent when member timeout state changed (ISO8601 timestamp) - member + AuditLogChangeKeyCommunicationDisabledUntil AuditLogChangeKey = "communication_disabled_until" + // AuditLogChangeKeyDeaf is sent when user server deafened/undeafened (bool) - member + AuditLogChangeKeyDeaf AuditLogChangeKey = "deaf" + // AuditLogChangeKeyDefaultAutoArchiveDuration is sent when default auto archive duration for newly created threads changed (int) - channel + AuditLogChangeKeyDefaultAutoArchiveDuration AuditLogChangeKey = "default_auto_archive_duration" + // AuditLogChangeKeyDefaultMessageNotification is sent when default message notification level changed (int) - guild + AuditLogChangeKeyDefaultMessageNotification AuditLogChangeKey = "default_message_notifications" + // AuditLogChangeKeyDeny is sent when a permission on a text or voice channel was denied for a role (string) - role + AuditLogChangeKeyDeny AuditLogChangeKey = "deny" + // AuditLogChangeKeyDescription is sent when description changed (string) - guild, sticker, or guild scheduled event + AuditLogChangeKeyDescription AuditLogChangeKey = "description" + // AuditLogChangeKeyDiscoverySplashHash is sent when discovery splash changed (string) - guild + AuditLogChangeKeyDiscoverySplashHash AuditLogChangeKey = "discovery_splash_hash" + // AuditLogChangeKeyEnableEmoticons is sent when integration emoticons enabled/disabled (bool) - integration + AuditLogChangeKeyEnableEmoticons AuditLogChangeKey = "enable_emoticons" + // AuditLogChangeKeyEntityType is sent when entity type of guild scheduled event was changed (int) - guild scheduled event + AuditLogChangeKeyEntityType AuditLogChangeKey = "entity_type" + // AuditLogChangeKeyExpireBehavior is sent when integration expiring subscriber behavior changed (int) - integration + AuditLogChangeKeyExpireBehavior AuditLogChangeKey = "expire_behavior" + // AuditLogChangeKeyExpireGracePeriod is sent when integration expire grace period changed (int) - integration + AuditLogChangeKeyExpireGracePeriod AuditLogChangeKey = "expire_grace_period" + // AuditLogChangeKeyExplicitContentFilter is sent when change in whose messages are scanned and deleted for explicit content in the server is made (int) - guild + AuditLogChangeKeyExplicitContentFilter AuditLogChangeKey = "explicit_content_filter" + // AuditLogChangeKeyFormatType is sent when format type of sticker changed (int - sticker format type) - sticker + AuditLogChangeKeyFormatType AuditLogChangeKey = "format_type" + // AuditLogChangeKeyGuildID is sent when guild sticker is in changed (snowflake) - sticker + AuditLogChangeKeyGuildID AuditLogChangeKey = "guild_id" + // AuditLogChangeKeyHoist is sent when role is now displayed/no longer displayed separate from online users (bool) - role + AuditLogChangeKeyHoist AuditLogChangeKey = "hoist" + // AuditLogChangeKeyIconHash is sent when icon changed (string) - guild or role + AuditLogChangeKeyIconHash AuditLogChangeKey = "icon_hash" + // AuditLogChangeKeyID is sent when the id of the changed entity - sometimes used in conjunction with other keys (snowflake) - any + AuditLogChangeKeyID AuditLogChangeKey = "id" + // AuditLogChangeKeyInvitable is sent when private thread is now invitable/uninvitable (bool) - thread + AuditLogChangeKeyInvitable AuditLogChangeKey = "invitable" + // AuditLogChangeKeyInviterID is sent when person who created invite code changed (snowflake) - invite + AuditLogChangeKeyInviterID AuditLogChangeKey = "inviter_id" + // AuditLogChangeKeyLocation is sent when channel id for guild scheduled event changed (string) - guild scheduled event + AuditLogChangeKeyLocation AuditLogChangeKey = "location" + // AuditLogChangeKeyLocked is sent when thread was locked/unlocked (bool) - thread + AuditLogChangeKeyLocked AuditLogChangeKey = "locked" + // AuditLogChangeKeyMaxAge is sent when invite code expiration time changed (int) - invite + AuditLogChangeKeyMaxAge AuditLogChangeKey = "max_age" + // AuditLogChangeKeyMaxUses is sent when max number of times invite code can be used changed (int) - invite + AuditLogChangeKeyMaxUses AuditLogChangeKey = "max_uses" + // AuditLogChangeKeyMentionable is sent when role is now mentionable/unmentionable (bool) - role + AuditLogChangeKeyMentionable AuditLogChangeKey = "mentionable" + // AuditLogChangeKeyMfaLevel is sent when two-factor auth requirement changed (int - mfa level) - guild + AuditLogChangeKeyMfaLevel AuditLogChangeKey = "mfa_level" + // AuditLogChangeKeyMute is sent when user server muted/unmuted (bool) - member + AuditLogChangeKeyMute AuditLogChangeKey = "mute" + // AuditLogChangeKeyName is sent when name changed (string) - any + AuditLogChangeKeyName AuditLogChangeKey = "name" + // AuditLogChangeKeyNick is sent when user nickname changed (string) - member + AuditLogChangeKeyNick AuditLogChangeKey = "nick" + // AuditLogChangeKeyNSFW is sent when channel nsfw restriction changed (bool) - channel + AuditLogChangeKeyNSFW AuditLogChangeKey = "nsfw" + // AuditLogChangeKeyOwnerID is sent when owner changed (snowflake) - guild + AuditLogChangeKeyOwnerID AuditLogChangeKey = "owner_id" + // AuditLogChangeKeyPermissionOverwrite is sent when permissions on a channel changed (array of channel overwrite objects) - channel + AuditLogChangeKeyPermissionOverwrite AuditLogChangeKey = "permission_overwrites" + // AuditLogChangeKeyPermissions is sent when permissions for a role changed (string) - role + AuditLogChangeKeyPermissions AuditLogChangeKey = "permissions" + // AuditLogChangeKeyPosition is sent when text or voice channel position changed (int) - channel + AuditLogChangeKeyPosition AuditLogChangeKey = "position" + // AuditLogChangeKeyPreferredLocale is sent when preferred locale changed (string) - guild + AuditLogChangeKeyPreferredLocale AuditLogChangeKey = "preferred_locale" + // AuditLogChangeKeyPrivacylevel is sent when privacy level of the stage instance changed (integer - privacy level) - stage instance or guild scheduled event + AuditLogChangeKeyPrivacylevel AuditLogChangeKey = "privacy_level" + // AuditLogChangeKeyPruneDeleteDays is sent when number of days after which inactive and role-unassigned members are kicked changed (int) - guild + AuditLogChangeKeyPruneDeleteDays AuditLogChangeKey = "prune_delete_days" + // AuditLogChangeKeyPulibUpdatesChannelID is sent when id of the public updates channel changed (snowflake) - guild + AuditLogChangeKeyPulibUpdatesChannelID AuditLogChangeKey = "public_updates_channel_id" + // AuditLogChangeKeyRateLimitPerUser is sent when amount of seconds a user has to wait before sending another message changed (int) - channel + AuditLogChangeKeyRateLimitPerUser AuditLogChangeKey = "rate_limit_per_user" + // AuditLogChangeKeyRegion is sent when region changed (string) - guild + AuditLogChangeKeyRegion AuditLogChangeKey = "region" + // AuditLogChangeKeyRulesChannelID is sent when id of the rules channel changed (snowflake) - guild + AuditLogChangeKeyRulesChannelID AuditLogChangeKey = "rules_channel_id" + // AuditLogChangeKeySplashHash is sent when invite splash page artwork changed (string) - guild + AuditLogChangeKeySplashHash AuditLogChangeKey = "splash_hash" + // AuditLogChangeKeyStatus is sent when status of guild scheduled event was changed (int - guild scheduled event status) - guild scheduled event + AuditLogChangeKeyStatus AuditLogChangeKey = "status" + // AuditLogChangeKeySystemChannelID is sent when id of the system channel changed (snowflake) - guild + AuditLogChangeKeySystemChannelID AuditLogChangeKey = "system_channel_id" + // AuditLogChangeKeyTags is sent when related emoji of sticker changed (string) - sticker + AuditLogChangeKeyTags AuditLogChangeKey = "tags" + // AuditLogChangeKeyTemporary is sent when invite code is now temporary or never expires (bool) - invite + AuditLogChangeKeyTemporary AuditLogChangeKey = "temporary" + // TODO: remove when compatibility is not required + AuditLogChangeKeyTempoary = AuditLogChangeKeyTemporary + // AuditLogChangeKeyTopic is sent when text channel topic or stage instance topic changed (string) - channel or stage instance + AuditLogChangeKeyTopic AuditLogChangeKey = "topic" + // AuditLogChangeKeyType is sent when type of entity created (int or string) - any + AuditLogChangeKeyType AuditLogChangeKey = "type" + // AuditLogChangeKeyUnicodeEmoji is sent when role unicode emoji changed (string) - role + AuditLogChangeKeyUnicodeEmoji AuditLogChangeKey = "unicode_emoji" + // AuditLogChangeKeyUserLimit is sent when new user limit in a voice channel set (int) - voice channel + AuditLogChangeKeyUserLimit AuditLogChangeKey = "user_limit" + // AuditLogChangeKeyUses is sent when number of times invite code used changed (int) - invite + AuditLogChangeKeyUses AuditLogChangeKey = "uses" + // AuditLogChangeKeyVanityURLCode is sent when guild invite vanity url changed (string) - guild + AuditLogChangeKeyVanityURLCode AuditLogChangeKey = "vanity_url_code" + // AuditLogChangeKeyVerificationLevel is sent when required verification level changed (int - verification level) - guild + AuditLogChangeKeyVerificationLevel AuditLogChangeKey = "verification_level" + // AuditLogChangeKeyWidgetChannelID is sent when channel id of the server widget changed (snowflake) - guild + AuditLogChangeKeyWidgetChannelID AuditLogChangeKey = "widget_channel_id" + // AuditLogChangeKeyWidgetEnabled is sent when server widget enabled/disabled (bool) - guild + AuditLogChangeKeyWidgetEnabled AuditLogChangeKey = "widget_enabled" + // AuditLogChangeKeyRoleAdd is sent when new role added (array of partial role objects) - guild + AuditLogChangeKeyRoleAdd AuditLogChangeKey = "$add" + // AuditLogChangeKeyRoleRemove is sent when role removed (array of partial role objects) - guild + AuditLogChangeKeyRoleRemove AuditLogChangeKey = "$remove" +) + +// AuditLogOptions optional data for the AuditLog +// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info +type AuditLogOptions struct { + DeleteMemberDays string `json:"delete_member_days"` + MembersRemoved string `json:"members_removed"` + ChannelID string `json:"channel_id"` + MessageID string `json:"message_id"` + Count string `json:"count"` + ID string `json:"id"` + Type *AuditLogOptionsType `json:"type"` + RoleName string `json:"role_name"` +} + +// AuditLogOptionsType of the AuditLogOption +// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info +type AuditLogOptionsType string + +// Valid Types for AuditLogOptionsType +const ( + AuditLogOptionsTypeMember AuditLogOptionsType = "member" + AuditLogOptionsTypeRole AuditLogOptionsType = "role" +) + +// AuditLogAction is the Action of the AuditLog (see AuditLogAction* consts) +// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-audit-log-events +type AuditLogAction int + +// Block contains Discord Audit Log Action Types +const ( + AuditLogActionGuildUpdate AuditLogAction = 1 + + AuditLogActionChannelCreate AuditLogAction = 10 + AuditLogActionChannelUpdate AuditLogAction = 11 + AuditLogActionChannelDelete AuditLogAction = 12 + AuditLogActionChannelOverwriteCreate AuditLogAction = 13 + AuditLogActionChannelOverwriteUpdate AuditLogAction = 14 + AuditLogActionChannelOverwriteDelete AuditLogAction = 15 + + AuditLogActionMemberKick AuditLogAction = 20 + AuditLogActionMemberPrune AuditLogAction = 21 + AuditLogActionMemberBanAdd AuditLogAction = 22 + AuditLogActionMemberBanRemove AuditLogAction = 23 + AuditLogActionMemberUpdate AuditLogAction = 24 + AuditLogActionMemberRoleUpdate AuditLogAction = 25 + AuditLogActionMemberMove AuditLogAction = 26 + AuditLogActionMemberDisconnect AuditLogAction = 27 + AuditLogActionBotAdd AuditLogAction = 28 + + AuditLogActionRoleCreate AuditLogAction = 30 + AuditLogActionRoleUpdate AuditLogAction = 31 + AuditLogActionRoleDelete AuditLogAction = 32 + + AuditLogActionInviteCreate AuditLogAction = 40 + AuditLogActionInviteUpdate AuditLogAction = 41 + AuditLogActionInviteDelete AuditLogAction = 42 + + AuditLogActionWebhookCreate AuditLogAction = 50 + AuditLogActionWebhookUpdate AuditLogAction = 51 + AuditLogActionWebhookDelete AuditLogAction = 52 + + AuditLogActionEmojiCreate AuditLogAction = 60 + AuditLogActionEmojiUpdate AuditLogAction = 61 + AuditLogActionEmojiDelete AuditLogAction = 62 + + AuditLogActionMessageDelete AuditLogAction = 72 + AuditLogActionMessageBulkDelete AuditLogAction = 73 + AuditLogActionMessagePin AuditLogAction = 74 + AuditLogActionMessageUnpin AuditLogAction = 75 + + AuditLogActionIntegrationCreate AuditLogAction = 80 + AuditLogActionIntegrationUpdate AuditLogAction = 81 + AuditLogActionIntegrationDelete AuditLogAction = 82 + AuditLogActionStageInstanceCreate AuditLogAction = 83 + AuditLogActionStageInstanceUpdate AuditLogAction = 84 + AuditLogActionStageInstanceDelete AuditLogAction = 85 + + AuditLogActionStickerCreate AuditLogAction = 90 + AuditLogActionStickerUpdate AuditLogAction = 91 + AuditLogActionStickerDelete AuditLogAction = 92 + + AuditLogGuildScheduledEventCreate AuditLogAction = 100 + AuditLogGuildScheduledEventUpdare AuditLogAction = 101 + AuditLogGuildScheduledEventDelete AuditLogAction = 102 + + AuditLogActionThreadCreate AuditLogAction = 110 + AuditLogActionThreadUpdate AuditLogAction = 111 + AuditLogActionThreadDelete AuditLogAction = 112 +) + +// 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"` +} + +// 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"` + GuildID string `json:"guild_id,omitempty"` +} + +// GatewayBotResponse stores the data for the gateway/bot response +type GatewayBotResponse struct { + URL string `json:"url"` + Shards int `json:"shards"` + SessionStartLimit SessionInformation `json:"session_start_limit"` +} + +// SessionInformation provides the information for max concurrency sharding +type SessionInformation struct { + Total int `json:"total,omitempty"` + Remaining int `json:"remaining,omitempty"` + ResetAfter int `json:"reset_after,omitempty"` + MaxConcurrency int `json:"max_concurrency,omitempty"` +} + +// GatewayStatusUpdate is sent by the client to indicate a presence or status update +// https://discord.com/developers/docs/topics/gateway#update-status-gateway-status-update-structure +type GatewayStatusUpdate struct { + Since int `json:"since"` + Game Activity `json:"game"` + Status string `json:"status"` + AFK bool `json:"afk"` +} + +// Activity defines the Activity sent with GatewayStatusUpdate +// https://discord.com/developers/docs/topics/gateway#activity-object +type Activity struct { + Name string `json:"name"` + Type ActivityType `json:"type"` + URL string `json:"url,omitempty"` + CreatedAt time.Time `json:"created_at"` + ApplicationID string `json:"application_id,omitempty"` + State string `json:"state,omitempty"` + Details string `json:"details,omitempty"` + Timestamps TimeStamps `json:"timestamps,omitempty"` + Emoji Emoji `json:"emoji,omitempty"` + Party Party `json:"party,omitempty"` + Assets Assets `json:"assets,omitempty"` + Secrets Secrets `json:"secrets,omitempty"` + Instance bool `json:"instance,omitempty"` + Flags int `json:"flags,omitempty"` +} + +// UnmarshalJSON is a custom unmarshaljson to make CreatedAt a time.Time instead of an int +func (activity *Activity) UnmarshalJSON(b []byte) error { + temp := struct { + Name string `json:"name"` + Type ActivityType `json:"type"` + URL string `json:"url,omitempty"` + CreatedAt int64 `json:"created_at"` + ApplicationID string `json:"application_id,omitempty"` + State string `json:"state,omitempty"` + Details string `json:"details,omitempty"` + Timestamps TimeStamps `json:"timestamps,omitempty"` + Emoji Emoji `json:"emoji,omitempty"` + Party Party `json:"party,omitempty"` + Assets Assets `json:"assets,omitempty"` + Secrets Secrets `json:"secrets,omitempty"` + Instance bool `json:"instance,omitempty"` + Flags int `json:"flags,omitempty"` + }{} + err := json.Unmarshal(b, &temp) + if err != nil { + return err + } + activity.CreatedAt = time.Unix(0, temp.CreatedAt*1000000) + activity.ApplicationID = temp.ApplicationID + activity.Assets = temp.Assets + activity.Details = temp.Details + activity.Emoji = temp.Emoji + activity.Flags = temp.Flags + activity.Instance = temp.Instance + activity.Name = temp.Name + activity.Party = temp.Party + activity.Secrets = temp.Secrets + activity.State = temp.State + activity.Timestamps = temp.Timestamps + activity.Type = temp.Type + activity.URL = temp.URL + return nil +} + +// Party defines the Party field in the Activity struct +// https://discord.com/developers/docs/topics/gateway#activity-object +type Party struct { + ID string `json:"id,omitempty"` + Size []int `json:"size,omitempty"` +} + +// Secrets defines the Secrets field for the Activity struct +// https://discord.com/developers/docs/topics/gateway#activity-object +type Secrets struct { + Join string `json:"join,omitempty"` + Spectate string `json:"spectate,omitempty"` + Match string `json:"match,omitempty"` +} + +// ActivityType is the type of Activity (see ActivityType* consts) in the Activity struct +// https://discord.com/developers/docs/topics/gateway#activity-object-activity-types +type ActivityType int + +// Valid ActivityType values +const ( + ActivityTypeGame ActivityType = 0 + ActivityTypeStreaming ActivityType = 1 + ActivityTypeListening ActivityType = 2 + ActivityTypeWatching ActivityType = 3 + ActivityTypeCustom ActivityType = 4 + ActivityTypeCompeting ActivityType = 5 +) + +// Identify is sent during initial handshake with the discord gateway. +// https://discord.com/developers/docs/topics/gateway#identify +type Identify struct { + Token string `json:"token"` + Properties IdentifyProperties `json:"properties"` + Compress bool `json:"compress"` + LargeThreshold int `json:"large_threshold"` + Shard *[2]int `json:"shard,omitempty"` + Presence GatewayStatusUpdate `json:"presence,omitempty"` + GuildSubscriptions bool `json:"guild_subscriptions"` + Intents Intent `json:"intents"` +} + +// IdentifyProperties contains the "properties" portion of an Identify packet +// https://discord.com/developers/docs/topics/gateway#identify-identify-connection-properties +type IdentifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` + Referer string `json:"$referer"` + ReferringDomain string `json:"$referring_domain"` +} + +// Constants for the different bit offsets of text channel permissions +const ( + // Deprecated: PermissionReadMessages has been replaced with PermissionViewChannel for text and voice channels + PermissionReadMessages = 0x0000000000000400 + PermissionSendMessages = 0x0000000000000800 + PermissionSendTTSMessages = 0x0000000000001000 + PermissionManageMessages = 0x0000000000002000 + PermissionEmbedLinks = 0x0000000000004000 + PermissionAttachFiles = 0x0000000000008000 + PermissionReadMessageHistory = 0x0000000000010000 + PermissionMentionEveryone = 0x0000000000020000 + PermissionUseExternalEmojis = 0x0000000000040000 + PermissionUseSlashCommands = 0x0000000080000000 + PermissionManageThreads = 0x0000000400000000 + PermissionCreatePublicThreads = 0x0000000800000000 + PermissionCreatePrivateThreads = 0x0000001000000000 + PermissionSendMessagesInThreads = 0x0000004000000000 +) + +// Constants for the different bit offsets of voice permissions +const ( + PermissionVoicePrioritySpeaker = 0x0000000000000100 + PermissionVoiceStreamVideo = 0x0000000000000200 + PermissionVoiceConnect = 0x0000000000100000 + PermissionVoiceSpeak = 0x0000000000200000 + PermissionVoiceMuteMembers = 0x0000000000400000 + PermissionVoiceDeafenMembers = 0x0000000000800000 + PermissionVoiceMoveMembers = 0x0000000001000000 + PermissionVoiceUseVAD = 0x0000000002000000 + PermissionVoiceRequestToSpeak = 0x0000000100000000 +) + +// Constants for general management. +const ( + PermissionChangeNickname = 0x0000000004000000 + PermissionManageNicknames = 0x0000000008000000 + PermissionManageRoles = 0x0000000010000000 + PermissionManageWebhooks = 0x0000000020000000 + PermissionManageEmojis = 0x0000000040000000 +) + +// Constants for the different bit offsets of general permissions +const ( + PermissionCreateInstantInvite = 0x0000000000000001 + PermissionKickMembers = 0x0000000000000002 + PermissionBanMembers = 0x0000000000000004 + PermissionAdministrator = 0x0000000000000008 + PermissionManageChannels = 0x0000000000000010 + PermissionManageServer = 0x0000000000000020 + PermissionAddReactions = 0x0000000000000040 + PermissionViewAuditLogs = 0x0000000000000080 + PermissionViewChannel = 0x0000000000000400 + PermissionViewGuildInsights = 0x0000000000080000 + PermissionModerateMembers = 0x0000010000000000 + + PermissionAllText = PermissionViewChannel | + PermissionSendMessages | + PermissionSendTTSMessages | + PermissionManageMessages | + PermissionEmbedLinks | + PermissionAttachFiles | + PermissionReadMessageHistory | + PermissionMentionEveryone + PermissionAllVoice = PermissionViewChannel | + PermissionVoiceConnect | + PermissionVoiceSpeak | + PermissionVoiceMuteMembers | + PermissionVoiceDeafenMembers | + PermissionVoiceMoveMembers | + PermissionVoiceUseVAD | + PermissionVoicePrioritySpeaker + PermissionAllChannel = PermissionAllText | + PermissionAllVoice | + PermissionCreateInstantInvite | + PermissionManageRoles | + PermissionManageChannels | + PermissionAddReactions | + PermissionViewAuditLogs + PermissionAll = PermissionAllChannel | + PermissionKickMembers | + PermissionBanMembers | + PermissionManageServer | + PermissionAdministrator | + PermissionManageWebhooks | + PermissionManageEmojis +) + +// Block contains Discord JSON Error Response codes +const ( + ErrCodeGeneralError = 0 + + 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 + ErrCodeUnknownWebhook = 10015 + ErrCodeUnknownWebhookService = 10016 + ErrCodeUnknownSession = 10020 + ErrCodeUnknownBan = 10026 + ErrCodeUnknownSKU = 10027 + ErrCodeUnknownStoreListing = 10028 + ErrCodeUnknownEntitlement = 10029 + ErrCodeUnknownBuild = 10030 + ErrCodeUnknownLobby = 10031 + ErrCodeUnknownBranch = 10032 + ErrCodeUnknownStoreDirectoryLayout = 10033 + ErrCodeUnknownRedistributable = 10036 + ErrCodeUnknownGiftCode = 10038 + ErrCodeUnknownStream = 10049 + ErrCodeUnknownPremiumServerSubscribeCooldown = 10050 + ErrCodeUnknownGuildTemplate = 10057 + ErrCodeUnknownDiscoveryCategory = 10059 + ErrCodeUnknownSticker = 10060 + ErrCodeUnknownInteraction = 10062 + ErrCodeUnknownApplicationCommand = 10063 + ErrCodeUnknownApplicationCommandPermissions = 10066 + ErrCodeUnknownStageInstance = 10067 + ErrCodeUnknownGuildMemberVerificationForm = 10068 + ErrCodeUnknownGuildWelcomeScreen = 10069 + ErrCodeUnknownGuildScheduledEvent = 10070 + ErrCodeUnknownGuildScheduledEventUser = 10071 + + ErrCodeBotsCannotUseEndpoint = 20001 + ErrCodeOnlyBotsCanUseEndpoint = 20002 + ErrCodeExplicitContentCannotBeSentToTheDesiredRecipients = 20009 + ErrCodeYouAreNotAuthorizedToPerformThisActionOnThisApplication = 20012 + ErrCodeThisActionCannotBePerformedDueToSlowmodeRateLimit = 20016 + ErrCodeOnlyTheOwnerOfThisAccountCanPerformThisAction = 20018 + ErrCodeMessageCannotBeEditedDueToAnnouncementRateLimits = 20022 + ErrCodeChannelHasHitWriteRateLimit = 20028 + ErrCodeTheWriteActionYouArePerformingOnTheServerHasHitTheWriteRateLimit = 20029 + ErrCodeStageTopicContainsNotAllowedWordsForPublicStages = 20031 + ErrCodeGuildPremiumSubscriptionLevelTooLow = 20035 + + ErrCodeMaximumGuildsReached = 30001 + ErrCodeMaximumPinsReached = 30003 + ErrCodeMaximumNumberOfRecipientsReached = 30004 + ErrCodeMaximumGuildRolesReached = 30005 + ErrCodeMaximumNumberOfWebhooksReached = 30007 + ErrCodeMaximumNumberOfEmojisReached = 30008 + ErrCodeTooManyReactions = 30010 + ErrCodeMaximumNumberOfGuildChannelsReached = 30013 + ErrCodeMaximumNumberOfAttachmentsInAMessageReached = 30015 + ErrCodeMaximumNumberOfInvitesReached = 30016 + ErrCodeMaximumNumberOfAnimatedEmojisReached = 30018 + ErrCodeMaximumNumberOfServerMembersReached = 30019 + ErrCodeMaximumNumberOfGuildDiscoverySubcategoriesReached = 30030 + ErrCodeGuildAlreadyHasATemplate = 30031 + ErrCodeMaximumNumberOfThreadParticipantsReached = 30033 + ErrCodeMaximumNumberOfBansForNonGuildMembersHaveBeenExceeded = 30035 + ErrCodeMaximumNumberOfBansFetchesHasBeenReached = 30037 + ErrCodeMaximumNumberOfUncompletedGuildScheduledEventsReached = 30038 + ErrCodeMaximumNumberOfStickersReached = 30039 + ErrCodeMaximumNumberOfPruneRequestsHasBeenReached = 30040 + ErrCodeMaximumNumberOfGuildWidgetSettingsUpdatesHasBeenReached = 30042 + ErrCodeMaximumNumberOfEditsToMessagesOlderThanOneHourReached = 30046 + + ErrCodeUnauthorized = 40001 + ErrCodeActionRequiredVerifiedAccount = 40002 + ErrCodeOpeningDirectMessagesTooFast = 40003 + ErrCodeSendMessagesHasBeenTemporarilyDisabled = 40004 + ErrCodeRequestEntityTooLarge = 40005 + ErrCodeFeatureTemporarilyDisabledServerSide = 40006 + ErrCodeUserIsBannedFromThisGuild = 40007 + ErrCodeTargetIsNotConnectedToVoice = 40032 + ErrCodeMessageAlreadyCrossposted = 40033 + ErrCodeAnApplicationWithThatNameAlreadyExists = 40041 + ErrCodeInteractionHasAlreadyBeenAcknowledged = 40060 + + ErrCodeMissingAccess = 50001 + ErrCodeInvalidAccountType = 50002 + ErrCodeCannotExecuteActionOnDMChannel = 50003 + ErrCodeEmbedDisabled = 50004 + ErrCodeGuildWidgetDisabled = 50004 + ErrCodeCannotEditFromAnotherUser = 50005 + ErrCodeCannotSendEmptyMessage = 50006 + ErrCodeCannotSendMessagesToThisUser = 50007 + ErrCodeCannotSendMessagesInVoiceChannel = 50008 + ErrCodeChannelVerificationLevelTooHigh = 50009 + ErrCodeOAuth2ApplicationDoesNotHaveBot = 50010 + ErrCodeOAuth2ApplicationLimitReached = 50011 + ErrCodeInvalidOAuthState = 50012 + ErrCodeMissingPermissions = 50013 + ErrCodeInvalidAuthenticationToken = 50014 + ErrCodeTooFewOrTooManyMessagesToDelete = 50016 + ErrCodeCanOnlyPinMessageToOriginatingChannel = 50019 + ErrCodeInviteCodeWasEitherInvalidOrTaken = 50020 + ErrCodeCannotExecuteActionOnSystemMessage = 50021 + ErrCodeCannotExecuteActionOnThisChannelType = 50024 + ErrCodeInvalidOAuth2AccessTokenProvided = 50025 + ErrCodeMissingRequiredOAuth2Scope = 50026 + ErrCodeInvalidWebhookTokenProvided = 50027 + ErrCodeInvalidRole = 50028 + ErrCodeInvalidRecipients = 50033 + ErrCodeMessageProvidedTooOldForBulkDelete = 50034 + ErrCodeInvalidFormBody = 50035 + ErrCodeInviteAcceptedToGuildApplicationsBotNotIn = 50036 + ErrCodeInvalidAPIVersionProvided = 50041 + ErrCodeFileUploadedExceedsTheMaximumSize = 50045 + ErrCodeInvalidFileUploaded = 50046 + ErrCodeInvalidGuild = 50055 + ErrCodeInvalidMessageType = 50068 + ErrCodeCannotDeleteAChannelRequiredForCommunityGuilds = 50074 + ErrCodeInvalidStickerSent = 50081 + ErrCodePerformedOperationOnArchivedThread = 50083 + ErrCodeBeforeValueIsEarlierThanThreadCreationDate = 50085 + ErrCodeCommunityServerChannelsMustBeTextChannels = 50086 + ErrCodeThisServerIsNotAvailableInYourLocation = 50095 + ErrCodeThisServerNeedsMonetizationEnabledInOrderToPerformThisAction = 50097 + ErrCodeThisServerNeedsMoreBoostsToPerformThisAction = 50101 + ErrCodeTheRequestBodyContainsInvalidJSON = 50109 + + ErrCodeNoUsersWithDiscordTagExist = 80004 + + ErrCodeReactionBlocked = 90001 + + ErrCodeAPIResourceIsCurrentlyOverloaded = 130000 + + ErrCodeTheStageIsAlreadyOpen = 150006 + + ErrCodeCannotReplyWithoutPermissionToReadMessageHistory = 160002 + ErrCodeThreadAlreadyCreatedForThisMessage = 160004 + ErrCodeThreadIsLocked = 160005 + ErrCodeMaximumNumberOfActiveThreadsReached = 160006 + ErrCodeMaximumNumberOfActiveAnnouncementThreadsReached = 160007 + + ErrCodeInvalidJSONForUploadedLottieFile = 170001 + ErrCodeUploadedLottiesCannotContainRasterizedImages = 170002 + ErrCodeStickerMaximumFramerateExceeded = 170003 + ErrCodeStickerFrameCountExceedsMaximumOfOneThousandFrames = 170004 + ErrCodeLottieAnimationMaximumDimensionsExceeded = 170005 + ErrCodeStickerFrameRateOutOfRange = 170006 + ErrCodeStickerAnimationDurationExceedsMaximumOfFiveSeconds = 170007 + + ErrCodeCannotUpdateAFinishedEvent = 180000 + ErrCodeFailedToCreateStageNeededForStageEvent = 180002 +) + +// Intent is the type of a Gateway Intent +// https://discord.com/developers/docs/topics/gateway#gateway-intents +type Intent int + +// Constants for the different bit offsets of intents +const ( + IntentGuilds Intent = 1 << 0 + IntentGuildMembers Intent = 1 << 1 + IntentGuildBans Intent = 1 << 2 + IntentGuildEmojis Intent = 1 << 3 + IntentGuildIntegrations Intent = 1 << 4 + IntentGuildWebhooks Intent = 1 << 5 + IntentGuildInvites Intent = 1 << 6 + IntentGuildVoiceStates Intent = 1 << 7 + IntentGuildPresences Intent = 1 << 8 + IntentGuildMessages Intent = 1 << 9 + IntentGuildMessageReactions Intent = 1 << 10 + IntentGuildMessageTyping Intent = 1 << 11 + IntentDirectMessages Intent = 1 << 12 + IntentDirectMessageReactions Intent = 1 << 13 + IntentDirectMessageTyping Intent = 1 << 14 + IntentMessageContent Intent = 1 << 15 + IntentGuildScheduledEvents Intent = 1 << 16 + + // TODO: remove when compatibility is not needed + + IntentsGuilds Intent = 1 << 0 + IntentsGuildMembers Intent = 1 << 1 + IntentsGuildBans Intent = 1 << 2 + IntentsGuildEmojis Intent = 1 << 3 + IntentsGuildIntegrations Intent = 1 << 4 + IntentsGuildWebhooks Intent = 1 << 5 + IntentsGuildInvites Intent = 1 << 6 + IntentsGuildVoiceStates Intent = 1 << 7 + IntentsGuildPresences Intent = 1 << 8 + IntentsGuildMessages Intent = 1 << 9 + IntentsGuildMessageReactions Intent = 1 << 10 + IntentsGuildMessageTyping Intent = 1 << 11 + IntentsDirectMessages Intent = 1 << 12 + IntentsDirectMessageReactions Intent = 1 << 13 + IntentsDirectMessageTyping Intent = 1 << 14 + IntentsMessageContent Intent = 1 << 15 + IntentsGuildScheduledEvents Intent = 1 << 16 + + IntentsAllWithoutPrivileged = IntentGuilds | + IntentGuildBans | + IntentGuildEmojis | + IntentGuildIntegrations | + IntentGuildWebhooks | + IntentGuildInvites | + IntentGuildVoiceStates | + IntentGuildMessages | + IntentGuildMessageReactions | + IntentGuildMessageTyping | + IntentDirectMessages | + IntentDirectMessageReactions | + IntentDirectMessageTyping | + IntentGuildScheduledEvents + + IntentsAll = IntentsAllWithoutPrivileged | + IntentGuildMembers | + IntentGuildPresences | + IntentMessageContent + + IntentsNone Intent = 0 +) + +// MakeIntent used to help convert a gateway intent value for use in the Identify structure; +// this was useful to help support the use of a pointer type when intents were optional. +// This is now a no-op, and is not necessary to use. +func MakeIntent(intents Intent) Intent { + return intents +} diff --git a/vendor/github.com/bwmarrin/discordgo/types.go b/vendor/github.com/bwmarrin/discordgo/types.go new file mode 100644 index 00000000..7f969aef --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/types.go @@ -0,0 +1,47 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains custom types, currently only a timestamp wrapper. + +package discordgo + +import ( + "encoding/json" + "net/http" +) + +// 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 "HTTP " + r.Response.Status + ", " + string(r.ResponseBody) +} diff --git a/vendor/github.com/bwmarrin/discordgo/user.go b/vendor/github.com/bwmarrin/discordgo/user.go new file mode 100644 index 00000000..6c48bf25 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/user.go @@ -0,0 +1,107 @@ +package discordgo + +// UserFlags is the flags of "user" (see UserFlags* consts) +// https://discord.com/developers/docs/resources/user#user-object-user-flags +type UserFlags int + +// Valid UserFlags values +const ( + UserFlagDiscordEmployee UserFlags = 1 << 0 + UserFlagDiscordPartner UserFlags = 1 << 1 + UserFlagHypeSquadEvents UserFlags = 1 << 2 + UserFlagBugHunterLevel1 UserFlags = 1 << 3 + UserFlagHouseBravery UserFlags = 1 << 6 + UserFlagHouseBrilliance UserFlags = 1 << 7 + UserFlagHouseBalance UserFlags = 1 << 8 + UserFlagEarlySupporter UserFlags = 1 << 9 + UserFlagTeamUser UserFlags = 1 << 10 + UserFlagSystem UserFlags = 1 << 12 + UserFlagBugHunterLevel2 UserFlags = 1 << 14 + UserFlagVerifiedBot UserFlags = 1 << 16 + UserFlagVerifiedBotDeveloper UserFlags = 1 << 17 + UserFlagDiscordCertifiedModerator UserFlags = 1 << 18 +) + +// A User stores all data for an individual Discord user. +type User struct { + // The ID of the user. + ID string `json:"id"` + + // The email of the user. This is only present when + // the application possesses the email scope for the user. + Email string `json:"email"` + + // The user's username. + Username string `json:"username"` + + // The hash of the user's avatar. Use Session.UserAvatar + // to retrieve the avatar itself. + Avatar string `json:"avatar"` + + // The user's chosen language option. + Locale string `json:"locale"` + + // The discriminator of the user (4 numbers after name). + Discriminator string `json:"discriminator"` + + // The token of the user. This is only present for + // the user represented by the current session. + Token string `json:"token"` + + // Whether the user's email is verified. + Verified bool `json:"verified"` + + // Whether the user has multi-factor authentication enabled. + MFAEnabled bool `json:"mfa_enabled"` + + // The hash of the user's banner image. + Banner string `json:"banner"` + + // User's banner color, encoded as an integer representation of hexadecimal color code + AccentColor int `json:"accent_color"` + + // Whether the user is a bot. + Bot bool `json:"bot"` + + // The public flags on a user's account. + // This is a combination of bit masks; the presence of a certain flag can + // be checked by performing a bitwise AND between this int and the flag. + PublicFlags UserFlags `json:"public_flags"` + + // The type of Nitro subscription on a user's account. + // Only available when the request is authorized via a Bearer token. + PremiumType int `json:"premium_type"` + + // Whether the user is an Official Discord System user (part of the urgent message system). + System bool `json:"system"` + + // The flags on a user's account. + // Only available when the request is authorized via a Bearer token. + Flags int `json:"flags"` +} + +// String returns a unique identifier of the form username#discriminator +func (u *User) String() string { + return u.Username + "#" + u.Discriminator +} + +// Mention return a string which mentions the user +func (u *User) Mention() string { + return "<@" + 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 { + return avatarURL(u.Avatar, EndpointDefaultUserAvatar(u.Discriminator), + EndpointUserAvatar(u.ID, u.Avatar), EndpointUserAvatarAnimated(u.ID, u.Avatar), size) +} + +// BannerURL returns the URL of the users's banner image. +// size: The size of the desired banner image as a power of two +// Image size can be any power of two between 16 and 4096. +func (u *User) BannerURL(size string) string { + return bannerURL(u.Banner, EndpointUserBanner(u.ID, u.Banner), EndpointUserBannerAnimated(u.ID, u.Banner), size) +} diff --git a/vendor/github.com/bwmarrin/discordgo/util.go b/vendor/github.com/bwmarrin/discordgo/util.go new file mode 100644 index 00000000..62313033 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/util.go @@ -0,0 +1,110 @@ +package discordgo + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/textproto" + "strconv" + "strings" + "time" +) + +// SnowflakeTimestamp returns the creation time of a Snowflake ID relative to the creation of Discord. +func SnowflakeTimestamp(ID string) (t time.Time, err error) { + i, err := strconv.ParseInt(ID, 10, 64) + if err != nil { + return + } + timestamp := (i >> 22) + 1420070400000 + t = time.Unix(0, timestamp*1000000) + return +} + +// MultipartBodyWithJSON returns the contentType and body for a discord request +// data : The object to encode for payload_json in the multipart request +// files : Files to include in the request +func MultipartBodyWithJSON(data interface{}, files []*File) (requestContentType string, requestBody []byte, err error) { + body := &bytes.Buffer{} + bodywriter := multipart.NewWriter(body) + + 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 + } + + return bodywriter.FormDataContentType(), body.Bytes(), nil +} + +func avatarURL(avatarHash, defaultAvatarURL, staticAvatarURL, animatedAvatarURL, size string) string { + var URL string + if avatarHash == "" { + URL = defaultAvatarURL + } else if strings.HasPrefix(avatarHash, "a_") { + URL = animatedAvatarURL + } else { + URL = staticAvatarURL + } + + if size != "" { + return URL + "?size=" + size + } + return URL +} + +func bannerURL(bannerHash, staticBannerURL, animatedBannerURL, size string) string { + var URL string + if bannerHash == "" { + return "" + } else if strings.HasPrefix(bannerHash, "a_") { + URL = animatedBannerURL + } else { + URL = staticBannerURL + } + + if size != "" { + return URL + "?size=" + size + } + return URL +} diff --git a/vendor/github.com/bwmarrin/discordgo/voice.go b/vendor/github.com/bwmarrin/discordgo/voice.go new file mode 100644 index 00000000..aedb8790 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/voice.go @@ -0,0 +1,914 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains code related to Discord voice suppport + +package discordgo + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "net" + "strconv" + "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, %s", 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. +func (v *VoiceConnection) Disconnect() (err error) { + + // Send a OP4 with a nil channel to disconnect + v.Lock() + 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 = "" + } + v.Unlock() + + // 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, %s", 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"` + IP string `json:"ip"` +} + +// 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 := "wss://" + strings.TrimSuffix(v.endpoint, ":80") + v.log(LogInformational, "connecting to voice endpoint %s", vg) + v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) + if err != nil { + 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 { + // 4014 indicates a manual disconnection by someone in the guild; + // we shouldn't reconnect. + if websocket.IsCloseError(err, 4014) { + v.log(LogInformational, "received 4014 manual disconnection") + + // Abandon the voice WS connection + v.Lock() + v.wsConn = nil + v.Unlock() + + v.session.Lock() + delete(v.session.VoiceConnections, v.GuildID) + v.session.Unlock() + + v.Close() + + return + } + + // 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 := v.op2.IP + ":" + strconv.Itoa(v.op2.Port) + addr, err := net.ResolveUDPAddr("udp", host) + if err != nil { + v.log(LogWarning, "error resolving udp host %s, %s", host, err) + 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.BigEndian.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) + + // extension bit set, and not a RTCP packet + if ((recvbuf[0] & 0x10) == 0x10) && ((recvbuf[1] & 0x80) == 0) { + // get extended header length + extlen := binary.BigEndian.Uint16(p.Opus[2:4]) + // 4 bytes (ext header header) + 4*extlen (ext header data) + shift := int(4 + 4*extlen) + if len(p.Opus) > shift { + p.Opus = p.Opus[shift:] + } + } + + 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/bwmarrin/discordgo/webhook.go b/vendor/github.com/bwmarrin/discordgo/webhook.go new file mode 100644 index 00000000..f54a45ce --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/webhook.go @@ -0,0 +1,49 @@ +package discordgo + +// Webhook stores the data for a webhook. +type Webhook struct { + ID string `json:"id"` + Type WebhookType `json:"type"` + 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"` + + // ApplicationID is the bot/OAuth2 application that created this webhook + ApplicationID string `json:"application_id,omitempty"` +} + +// WebhookType is the type of Webhook (see WebhookType* consts) in the Webhook struct +// https://discord.com/developers/docs/resources/webhook#webhook-object-webhook-types +type WebhookType int + +// Valid WebhookType values +const ( + WebhookTypeIncoming WebhookType = 1 + WebhookTypeChannelFollower WebhookType = 2 +) + +// 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"` + Files []*File `json:"-"` + Components []MessageComponent `json:"components"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` + // NOTE: Works only for followup messages. + Flags uint64 `json:"flags,omitempty"` +} + +// WebhookEdit stores data for editing of a webhook message. +type WebhookEdit struct { + Content string `json:"content,omitempty"` + Components []MessageComponent `json:"components"` + Embeds []*MessageEmbed `json:"embeds,omitempty"` + Files []*File `json:"-"` + AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` +} diff --git a/vendor/github.com/bwmarrin/discordgo/wsapi.go b/vendor/github.com/bwmarrin/discordgo/wsapi.go new file mode 100644 index 00000000..f2c228d5 --- /dev/null +++ b/vendor/github.com/bwmarrin/discordgo/wsapi.go @@ -0,0 +1,912 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner <bruce@sqls.net>. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains low level functions for interacting with the Discord +// data websocket interface. + +package discordgo + +import ( + "bytes" + "compress/zlib" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "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 +// more 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://discord.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(LogError, "error connecting to gateway %s, %s", s.gateway, err) + s.gateway = "" // clear cached gateway + s.wsConn = nil // Just to be safe. + return err + } + + s.wsConn.SetCloseHandler(func(code int, text string) error { + return nil + }) + + defer func() { + // because of this, all code below must set err to the error + // when exiting with an error :) Maybe someone has a better + // 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"` +} + +// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. +const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond + +// HeartbeatLatency returns the latency between heartbeat acknowledgement and heartbeat send. +func (s *Session) HeartbeatLatency() time.Duration { + + return s.LastHeartbeatAck.Sub(s.LastHeartbeatSent) + +} + +// heartbeat sends regular heartbeats to Discord so it knows the client +// is still connected. If you do not send these heartbeats Discord will +// disconnect the websocket connection after a few seconds. +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(LogDebug, "sending gateway websocket heartbeat seq %d", sequence) + s.wsMutex.Lock() + s.LastHeartbeatSent = time.Now().UTC() + err = wsConn.WriteJSON(heartbeatOp{1, sequence}) + s.wsMutex.Unlock() + if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { + 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"` + Activities []*Activity `json:"activities"` + AFK bool `json:"afk"` + Status string `json:"status"` +} + +type updateStatusOp struct { + Op int `json:"op"` + Data UpdateStatusData `json:"d"` +} + +func newUpdateStatusData(idle int, activityType ActivityType, name, url string) *UpdateStatusData { + usd := &UpdateStatusData{ + Status: "online", + } + + if idle > 0 { + usd.IdleSince = &idle + } + + if name != "" { + usd.Activities = []*Activity{{ + Name: name, + Type: activityType, + URL: url, + }} + } + + return usd +} + +// UpdateGameStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If name!="" then set game. +// if otherwise, set status to active, and no activity. +func (s *Session) UpdateGameStatus(idle int, name string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(idle, ActivityTypeGame, name, "")) +} + +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If name!="" then set game. +// If name!="" 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, name string, url string) (err error) { + gameType := ActivityTypeGame + if url != "" { + gameType = ActivityTypeStreaming + } + return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, name, url)) +} + +// UpdateListeningStatus is used to set the user to "Listening to..." +// If name!="" then set to what user is listening to +// Else, set user to active and no activity. +func (s *Session) UpdateListeningStatus(name string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(0, ActivityTypeListening, name, "")) +} + +// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. +func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { + // The comment does say "untouched by discordgo", but we might need to lie a bit here. + // The Discord documentation lists `activities` as being nullable, but in practice this + // doesn't seem to be the case. I had filed an issue about this at + // https://github.com/discord/discord-api-docs/issues/2559, but as of writing this + // haven't had any movement on it, so at this point I'm assuming this is an error, + // and am fixing this bug accordingly. Because sending `null` for `activities` instantly + // disconnects us, I think that disallowing it from being sent in `UpdateStatusComplex` + // isn't that big of an issue. + if usd.Activities == nil { + usd.Activities = make([]*Activity, 0) + } + + 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 +} + +type requestGuildMembersData struct { + GuildIDs []string `json:"guild_id"` + Query string `json:"query"` + Limit int `json:"limit"` + Presences bool `json:"presences"` +} + +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 : Single Guild ID 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 +// presences : Whether to request presences of guild members +func (s *Session) RequestGuildMembers(guildID string, query string, limit int, presences bool) (err error) { + data := requestGuildMembersData{ + GuildIDs: []string{guildID}, + Query: query, + Limit: limit, + Presences: presences, + } + err = s.requestGuildMembers(data) + return +} + +// RequestGuildMembersBatch requests guild members from the gateway +// The gateway responds with GuildMembersChunk events +// guildID : Slice of guild IDs 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 +// presences : Whether to request presences of guild members +func (s *Session) RequestGuildMembersBatch(guildIDs []string, query string, limit int, presences bool) (err error) { + data := requestGuildMembersData{ + GuildIDs: guildIDs, + Query: query, + Limit: limit, + Presences: presences, + } + err = s.requestGuildMembers(data) + return +} + +func (s *Session) requestGuildMembers(data requestGuildMembersData) (err error) { + s.log(LogInformational, "called") + + s.RLock() + defer s.RUnlock() + if s.wsConn == nil { + return ErrWSNotFound + } + + 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.CloseWithCode(websocket.CloseServiceRestart) + 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(LogDebug, "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() + + err = s.ChannelVoiceJoinManual(gID, cID, mute, deaf) + 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 +} + +// ChannelVoiceJoinManual initiates a voice session to a voice channel, but does not complete it. +// +// This should only be used when the VoiceServerUpdate will be intercepted and used elsewhere. +// +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join, leave empty to disconnect. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (s *Session) ChannelVoiceJoinManual(gID, cID string, mute, deaf bool) (err error) { + + s.log(LogInformational, "called") + + var channelID *string + if cID == "" { + channelID = nil + } else { + channelID = &cID + } + + // Send the request to Discord that we want to join the voice channel + data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, channelID, mute, deaf}} + s.wsMutex.Lock() + err = s.wsConn.WriteJSON(data) + s.wsMutex.Unlock() + 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 identifyOp struct { + Op int `json:"op"` + Data Identify `json:"d"` +} + +// identify sends the identify packet to the gateway +func (s *Session) identify() error { + s.log(LogDebug, "called") + + // TODO: This is a temporary block of code to help + // maintain backwards compatibility + if s.Compress == false { + s.Identify.Compress = false + } + + // TODO: This is a temporary block of code to help + // maintain backwards compatibility + if s.Token != "" && s.Identify.Token == "" { + s.Identify.Token = s.Token + } + + // TODO: Below block should be refactored so ShardID and ShardCount + // can be deprecated and their usage moved to the Session.Identify + // struct + if s.ShardCount > 1 { + + if s.ShardID >= s.ShardCount { + return ErrWSShardBounds + } + + s.Identify.Shard = &[2]int{s.ShardID, s.ShardCount} + } + + // Send Identify packet to Discord + op := identifyOp{2, s.Identify} + s.log(LogDebug, "Identify Packet: \n%#v", op) + s.wsMutex.Lock() + err := s.wsConn.WriteJSON(op) + s.wsMutex.Unlock() + + return err +} + +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 +func (s *Session) Close() error { + return s.CloseWithCode(websocket.CloseNormalClosure) +} + +// CloseWithCode closes a websocket using the provided closeCode and stops all +// listening/heartbeat goroutines. +// TODO: Add support for Voice WS/UDP connections +func (s *Session) CloseWithCode(closeCode int) (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(closeCode, "")) + 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 +} |