mirror of
https://github.com/cwinfo/matterbridge.git
synced 2025-07-03 22:27:44 +00:00
Switch to discordgo upstream again (#1759)
* Switch to upstream discordgo again * Fix discord api changes
This commit is contained in:
5
vendor/github.com/bwmarrin/discordgo/.gitignore
generated
vendored
Normal file
5
vendor/github.com/bwmarrin/discordgo/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
# IDE-specific metadata
|
||||
.idea/
|
||||
|
||||
# Environment variables. Useful for examples.
|
||||
.env
|
19
vendor/github.com/bwmarrin/discordgo/.golangci.yml
generated
vendored
Normal file
19
vendor/github.com/bwmarrin/discordgo/.golangci.yml
generated
vendored
Normal file
@ -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
|
17
vendor/github.com/bwmarrin/discordgo/.travis.yml
generated
vendored
Normal file
17
vendor/github.com/bwmarrin/discordgo/.travis.yml
generated
vendored
Normal file
@ -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 ./...
|
28
vendor/github.com/bwmarrin/discordgo/LICENSE
generated
vendored
Normal file
28
vendor/github.com/bwmarrin/discordgo/LICENSE
generated
vendored
Normal file
@ -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.
|
||||
|
105
vendor/github.com/bwmarrin/discordgo/README.md
generated
vendored
Normal file
105
vendor/github.com/bwmarrin/discordgo/README.md
generated
vendored
Normal file
@ -0,0 +1,105 @@
|
||||
# DiscordGo
|
||||
|
||||
[](https://pkg.go.dev/github.com/bwmarrin/discordgo) [](https://goreportcard.com/report/github.com/bwmarrin/discordgo) [](https://travis-ci.com/bwmarrin/discordgo) [](https://discord.gg/golang) [](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.
|
||||
|
||||
- [](https://pkg.go.dev/github.com/bwmarrin/discordgo)
|
||||
- [](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.
|
241
vendor/github.com/bwmarrin/discordgo/components.go
generated
vendored
Normal file
241
vendor/github.com/bwmarrin/discordgo/components.go
generated
vendored
Normal file
@ -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
|
||||
)
|
60
vendor/github.com/bwmarrin/discordgo/discord.go
generated
vendored
Normal file
60
vendor/github.com/bwmarrin/discordgo/discord.go
generated
vendored
Normal file
@ -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
|
||||
}
|
210
vendor/github.com/bwmarrin/discordgo/endpoints.go
generated
vendored
Normal file
210
vendor/github.com/bwmarrin/discordgo/endpoints.go
generated
vendored
Normal file
@ -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
|
||||
)
|
247
vendor/github.com/bwmarrin/discordgo/event.go
generated
vendored
Normal file
247
vendor/github.com/bwmarrin/discordgo/event.go
generated
vendored
Normal file
@ -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
|
||||
}
|
1342
vendor/github.com/bwmarrin/discordgo/eventhandlers.go
generated
vendored
Normal file
1342
vendor/github.com/bwmarrin/discordgo/eventhandlers.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
372
vendor/github.com/bwmarrin/discordgo/events.go
generated
vendored
Normal file
372
vendor/github.com/bwmarrin/discordgo/events.go
generated
vendored
Normal file
@ -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"`
|
||||
}
|
568
vendor/github.com/bwmarrin/discordgo/interactions.go
generated
vendored
Normal file
568
vendor/github.com/bwmarrin/discordgo/interactions.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
83
vendor/github.com/bwmarrin/discordgo/locales.go
generated
vendored
Normal file
83
vendor/github.com/bwmarrin/discordgo/locales.go
generated
vendored
Normal file
@ -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",
|
||||
}
|
103
vendor/github.com/bwmarrin/discordgo/logging.go
generated
vendored
Normal file
103
vendor/github.com/bwmarrin/discordgo/logging.go
generated
vendored
Normal file
@ -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()))
|
||||
}
|
||||
*/
|
541
vendor/github.com/bwmarrin/discordgo/message.go
generated
vendored
Normal file
541
vendor/github.com/bwmarrin/discordgo/message.go
generated
vendored
Normal file
@ -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"`
|
||||
}
|
17
vendor/github.com/bwmarrin/discordgo/mkdocs.yml
generated
vendored
Normal file
17
vendor/github.com/bwmarrin/discordgo/mkdocs.yml
generated
vendored
Normal file
@ -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'
|
154
vendor/github.com/bwmarrin/discordgo/oauth2.go
generated
vendored
Normal file
154
vendor/github.com/bwmarrin/discordgo/oauth2.go
generated
vendored
Normal file
@ -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
|
||||
}
|
197
vendor/github.com/bwmarrin/discordgo/ratelimit.go
generated
vendored
Normal file
197
vendor/github.com/bwmarrin/discordgo/ratelimit.go
generated
vendored
Normal file
@ -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
|
||||
}
|
2871
vendor/github.com/bwmarrin/discordgo/restapi.go
generated
vendored
Normal file
2871
vendor/github.com/bwmarrin/discordgo/restapi.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1277
vendor/github.com/bwmarrin/discordgo/state.go
generated
vendored
Normal file
1277
vendor/github.com/bwmarrin/discordgo/state.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2038
vendor/github.com/bwmarrin/discordgo/structs.go
generated
vendored
Normal file
2038
vendor/github.com/bwmarrin/discordgo/structs.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
47
vendor/github.com/bwmarrin/discordgo/types.go
generated
vendored
Normal file
47
vendor/github.com/bwmarrin/discordgo/types.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
107
vendor/github.com/bwmarrin/discordgo/user.go
generated
vendored
Normal file
107
vendor/github.com/bwmarrin/discordgo/user.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
110
vendor/github.com/bwmarrin/discordgo/util.go
generated
vendored
Normal file
110
vendor/github.com/bwmarrin/discordgo/util.go
generated
vendored
Normal file
@ -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
|
||||
}
|
914
vendor/github.com/bwmarrin/discordgo/voice.go
generated
vendored
Normal file
914
vendor/github.com/bwmarrin/discordgo/voice.go
generated
vendored
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
49
vendor/github.com/bwmarrin/discordgo/webhook.go
generated
vendored
Normal file
49
vendor/github.com/bwmarrin/discordgo/webhook.go
generated
vendored
Normal file
@ -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"`
|
||||
}
|
912
vendor/github.com/bwmarrin/discordgo/wsapi.go
generated
vendored
Normal file
912
vendor/github.com/bwmarrin/discordgo/wsapi.go
generated
vendored
Normal file
@ -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
|
||||
}
|
Reference in New Issue
Block a user