Compare commits
No commits in common. "v3.3.0" and "master" have entirely different histories.
48
.build.yml
48
.build.yml
|
@ -1,48 +0,0 @@
|
|||
image: "nixos/latest"
|
||||
packages:
|
||||
- nixos.go
|
||||
- nixos.git
|
||||
- nixos.gcc
|
||||
sources:
|
||||
- https://github.com/diamondburned/arikawa
|
||||
secrets:
|
||||
# Integration test secrets.
|
||||
- f51d6157-b4be-4697-99d0-6cd129243f63
|
||||
environment:
|
||||
GO111MODULE: "on"
|
||||
CGO_ENABLED: "1"
|
||||
# Integration test variables.
|
||||
SHARD_COUNT: "2"
|
||||
tested: "./api,./gateway,./bot,./discord"
|
||||
cov_file: "/tmp/cov_results"
|
||||
dismock: "github.com/mavolin/dismock/v2/pkg/dismock"
|
||||
dismock_v: "259685b84e4b6ab364b0fd858aac2aa2dfa42502"
|
||||
|
||||
tasks:
|
||||
- generate: |-
|
||||
cd arikawa
|
||||
go generate ./...
|
||||
|
||||
if [[ "$(git status --porcelain)" ]]; then
|
||||
echo "Repository differ after regeneration."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- build: cd arikawa && go build ./...
|
||||
- unit: cd arikawa && go test -tags unitonly -race ./...
|
||||
|
||||
- integration: |-
|
||||
sh -c '
|
||||
test -f ~/.env || {
|
||||
echo "Skipped integration tests."
|
||||
exit 0
|
||||
}
|
||||
|
||||
cd arikawa
|
||||
go get ./...
|
||||
go get $dismock@$dismock_v
|
||||
|
||||
source ~/.env
|
||||
go test -coverpkg $tested -coverprofile $cov_file -race ./... $dismock
|
||||
go tool cover -func $cov_file
|
||||
'
|
|
@ -1,2 +0,0 @@
|
|||
github: diamondburned
|
||||
liberapay: diamondburned
|
|
@ -1,40 +0,0 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Nix packages
|
||||
uses: diamondburned/cache-install@0746911c01dc84bba95d35be82db09435537d8ca
|
||||
|
||||
- name: Generate
|
||||
run: |
|
||||
go generate ./...
|
||||
|
||||
if ! git diff --exit-code; then
|
||||
echo '::error::Repository differ after `go generate`.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
go build ./...
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
go test -coverprofile /tmp/coverage.out -race ./...
|
||||
go tool cover -func /tmp/coverage.out
|
||||
env:
|
||||
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||
|
||||
- name: Upload coverage profile
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage
|
||||
path: /tmp/coverage.out
|
|
@ -1,53 +1,52 @@
|
|||
{
|
||||
"image": "golang:alpine",
|
||||
"variables": {
|
||||
"GO111MODULE": "on",
|
||||
"CGO_ENABLED": "1", # for the race detector
|
||||
"COV": "/tmp/cov_results",
|
||||
"dismock": "github.com/mavolin/dismock/v3/pkg/dismock",
|
||||
"dismock_v": "259685b84e4b6ab364b0fd858aac2aa2dfa42502",
|
||||
# used only in integration_test
|
||||
"tested": "./api,./gateway,./bot,./discord"
|
||||
},
|
||||
"before_script": [
|
||||
"apk add git build-base"
|
||||
"go get ./...",
|
||||
"go get $dismock@$dismock_v",
|
||||
],
|
||||
"stages": [
|
||||
"build",
|
||||
"test"
|
||||
],
|
||||
"build_test": {
|
||||
"stage": "build",
|
||||
"script": [
|
||||
"go build ./..."
|
||||
]
|
||||
},
|
||||
"unit_test": {
|
||||
"stage": "test",
|
||||
"timeout": "4m", # 4 minutes
|
||||
# Don't run the test if we have a $BOT_TOKEN, because
|
||||
# integration_test will run instead.
|
||||
"except": {
|
||||
"variables": [ "$BOT_TOKEN" ]
|
||||
"image": "golang:alpine",
|
||||
"variables": {
|
||||
"GO111MODULE": "on",
|
||||
"CGO_ENABLED": "0",
|
||||
"COV": "/tmp/cov_results",
|
||||
"dismock": "github.com/mavolin/dismock/pkg/dismock",
|
||||
# used only in integration_test
|
||||
"tested": "./api,./gateway,./bot,./discord"
|
||||
},
|
||||
"script": [
|
||||
"go test -coverpkg $tested -coverprofile $COV -tags unitonly -v -race ./... $dismock",
|
||||
"go tool cover -func $COV"
|
||||
]
|
||||
},
|
||||
"integration_test": {
|
||||
"stage": "test",
|
||||
"timeout": "8m", # 8 minutes
|
||||
# Run the test only if we have $BOT_TOKEN, else fallback to unit
|
||||
# tests.
|
||||
"only": {
|
||||
"variables": [ "$BOT_TOKEN", "$CHANNEL_ID", "$VOICE_ID" ]
|
||||
"before_script": [
|
||||
"apk add git"
|
||||
],
|
||||
"stages": [
|
||||
"build",
|
||||
"test"
|
||||
],
|
||||
"build_test": {
|
||||
"stage": "build",
|
||||
"script": [
|
||||
"go build ./..."
|
||||
]
|
||||
},
|
||||
"script": [
|
||||
"go test -coverpkg $tested -coverprofile $COV -v -race ./... $dismock",
|
||||
"go tool cover -func $COV"
|
||||
]
|
||||
}
|
||||
"unit_test": {
|
||||
"stage": "test",
|
||||
"timeout": "2m", # 2 minutes
|
||||
# Don't run the test if we have a $BOT_TOKEN, because
|
||||
# integration_test will run instead.
|
||||
"except": {
|
||||
"variables": [ "$BOT_TOKEN" ]
|
||||
},
|
||||
"script": [
|
||||
"go test -v -coverprofile $COV ./...",
|
||||
"go tool cover -func $COV"
|
||||
]
|
||||
},
|
||||
"integration_test": {
|
||||
"stage": "test",
|
||||
"timeout": "5m", # 5 minutes
|
||||
# Run the test only if we have $BOT_TOKEN, else fallback to unit
|
||||
# tests.
|
||||
"only": {
|
||||
"variables": [ "$BOT_TOKEN" ]
|
||||
},
|
||||
"script": [
|
||||
"go get ./...",
|
||||
# Test this package along with dismock.
|
||||
"go test -coverpkg $tested -coverprofile $COV -tags integration -v ./... $dismock",
|
||||
"go tool cover -func $COV"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
// To run, do `GUILD_ID="GUILD ID" BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
||||
func main() {
|
||||
guildID := discord.GuildID(mustSnowflakeEnv("GUILD_ID"))
|
||||
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
s := state.New("Bot " + token)
|
||||
|
||||
app, err := s.CurrentApplication()
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to get application ID:", err)
|
||||
}
|
||||
|
||||
s.AddHandler(func(e *gateway.InteractionCreateEvent) {
|
||||
var resp api.InteractionResponse
|
||||
switch d := e.Data.(type) {
|
||||
case *discord.CommandInteraction:
|
||||
content := option.NewNullableString("Pong: " + d.Options[0].String() + "!")
|
||||
resp = api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: content,
|
||||
},
|
||||
}
|
||||
case *discord.AutocompleteInteraction:
|
||||
allChoices := api.AutocompleteStringChoices{
|
||||
{Name: "Choice A", Value: "Choice A"},
|
||||
{Name: "Choice B", Value: "Choice B"},
|
||||
{Name: "Choice C", Value: "Choice C"},
|
||||
{Name: "Abc Def", Value: "Abcdef"},
|
||||
{Name: "Ghi Jkl", Value: "Ghijkl"},
|
||||
{Name: "Mno Pqr", Value: "Mnopqr"},
|
||||
{Name: "Stu Vwx", Value: "Stuvwx"},
|
||||
}
|
||||
query := strings.ToLower(d.Options[0].String())
|
||||
var choices api.AutocompleteStringChoices
|
||||
for _, choice := range allChoices {
|
||||
if strings.HasPrefix(strings.ToLower(choice.Name), query) ||
|
||||
strings.HasPrefix(strings.ToLower(choice.Value), query) {
|
||||
choices = append(choices, choice)
|
||||
}
|
||||
}
|
||||
resp = api.InteractionResponse{
|
||||
Type: api.AutocompleteResult,
|
||||
Data: &api.InteractionResponseData{
|
||||
Choices: &choices,
|
||||
},
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.RespondInteraction(e.ID, e.Token, resp); err != nil {
|
||||
log.Println("failed to send interaction callback:", err)
|
||||
}
|
||||
})
|
||||
|
||||
s.AddIntents(gateway.IntentGuilds)
|
||||
s.AddIntents(gateway.IntentGuildMessages)
|
||||
|
||||
if err := s.Open(context.Background()); err != nil {
|
||||
log.Fatalln("failed to open:", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
log.Println("Gateway connected. Getting all guild commands.")
|
||||
|
||||
commands, err := s.GuildCommands(app.ID, guildID)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to get guild commands:", err)
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
log.Println("Existing command", command.Name, "found.")
|
||||
}
|
||||
|
||||
newCommands := []api.CreateCommandData{
|
||||
{
|
||||
Name: "ping",
|
||||
Description: "Basic ping command.",
|
||||
Options: []discord.CommandOption{
|
||||
&discord.StringOption{
|
||||
OptionName: "text",
|
||||
Description: "Text to echo back",
|
||||
Autocomplete: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := s.BulkOverwriteGuildCommands(app.ID, guildID, newCommands); err != nil {
|
||||
log.Fatalln("failed to create guild command:", err)
|
||||
}
|
||||
|
||||
// Block forever.
|
||||
select {}
|
||||
}
|
||||
|
||||
func mustSnowflakeEnv(env string) discord.Snowflake {
|
||||
s, err := discord.ParseSnowflake(os.Getenv(env))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid snowflake for $%s: %v", env, err)
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -1,144 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
// To run, do `GUILD_ID="GUILD ID" BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
||||
func main() {
|
||||
guildID := discord.GuildID(mustSnowflakeEnv("GUILD_ID"))
|
||||
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("no $BOT_TOKEN given")
|
||||
}
|
||||
|
||||
s := session.New("Bot " + token)
|
||||
|
||||
app, err := s.CurrentApplication()
|
||||
if err != nil {
|
||||
log.Fatalln("failed to get application ID:", err)
|
||||
}
|
||||
|
||||
s.AddHandler(func(e *gateway.InteractionCreateEvent) {
|
||||
var resp api.InteractionResponse
|
||||
|
||||
switch data := e.Data.(type) {
|
||||
case *discord.CommandInteraction:
|
||||
if data.Name != "buttons" {
|
||||
resp = api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("Unknown command: " + data.Name),
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
// Send a message with a button back on slash commands.
|
||||
resp = api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("This is a message with a button!"),
|
||||
Components: discord.ComponentsPtr(
|
||||
&discord.ActionRowComponent{
|
||||
&discord.ButtonComponent{
|
||||
Label: "Hello World!",
|
||||
CustomID: "first_button",
|
||||
Emoji: &discord.ComponentEmoji{Name: "👋"},
|
||||
Style: discord.PrimaryButtonStyle(),
|
||||
},
|
||||
&discord.ButtonComponent{
|
||||
Label: "Secondary",
|
||||
CustomID: "second_button",
|
||||
Style: discord.SecondaryButtonStyle(),
|
||||
},
|
||||
&discord.ButtonComponent{
|
||||
Label: "Success",
|
||||
CustomID: "success_button",
|
||||
Style: discord.SuccessButtonStyle(),
|
||||
},
|
||||
&discord.ButtonComponent{
|
||||
Label: "Danger",
|
||||
CustomID: "danger_button",
|
||||
Style: discord.DangerButtonStyle(),
|
||||
},
|
||||
},
|
||||
// This is automatically put into its own row.
|
||||
&discord.ButtonComponent{
|
||||
Label: "Link",
|
||||
Style: discord.LinkButtonStyle("https://google.com"),
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
case discord.ComponentInteraction:
|
||||
resp = api.InteractionResponse{
|
||||
Type: api.UpdateMessage,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("Custom ID: " + string(data.ID())),
|
||||
},
|
||||
}
|
||||
default:
|
||||
log.Printf("unknown interaction type %T", e.Data)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.RespondInteraction(e.ID, e.Token, resp); err != nil {
|
||||
log.Println("failed to send interaction callback:", err)
|
||||
}
|
||||
})
|
||||
|
||||
s.AddIntents(gateway.IntentGuilds)
|
||||
s.AddIntents(gateway.IntentGuildMessages)
|
||||
|
||||
if err := s.Open(context.Background()); err != nil {
|
||||
log.Fatalln("failed to open:", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
log.Println("Gateway connected. Getting all guild commands.")
|
||||
|
||||
commands, err := s.GuildCommands(app.ID, guildID)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to get guild commands:", err)
|
||||
}
|
||||
|
||||
for _, command := range commands {
|
||||
log.Println("Existing command", command.Name, "found.")
|
||||
}
|
||||
|
||||
newCommands := []api.CreateCommandData{
|
||||
{
|
||||
Name: "buttons",
|
||||
Description: "Send an interactable message.",
|
||||
},
|
||||
}
|
||||
|
||||
log.Println("Creating guild commands...")
|
||||
|
||||
if _, err := s.BulkOverwriteGuildCommands(app.ID, guildID, newCommands); err != nil {
|
||||
log.Fatalln("failed to create guild command:", err)
|
||||
}
|
||||
|
||||
log.Println("Guild commands created. Bot is ready.")
|
||||
|
||||
// Block forever.
|
||||
select {}
|
||||
}
|
||||
|
||||
func mustSnowflakeEnv(env string) discord.Snowflake {
|
||||
s, err := discord.ParseSnowflake(os.Getenv(env))
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid snowflake for $%s: %v", env, err)
|
||||
}
|
||||
return s
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
# commands-hybrid
|
||||
|
||||
commands-hybrid is an alternative variant of commands, where the program permits
|
||||
being hosted either as a Gateway-based daemon or as a web server using the
|
||||
Interactions Webhook API.
|
||||
|
||||
## Usage
|
||||
|
||||
### Gateway Mode
|
||||
|
||||
```sh
|
||||
BOT_TOKEN="<token here>" go run .
|
||||
```
|
||||
|
||||
### Interactions Webhook Mode
|
||||
|
||||
```sh
|
||||
BOT_TOKEN="<token here>" WEBHOOK_ADDR="localhost:29485" WEBHOOK_PUBKEY="<hex app pubkey>" go run .
|
||||
```
|
||||
|
||||
The endpoint will be `http://localhost:29485/`. I recommend using something like
|
||||
[srv.us](https://srv.us) to expose this endpoint as a public one, which can then
|
||||
be used by Discord.
|
|
@ -1,146 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/api/webhook"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
var commands = []api.CreateCommandData{
|
||||
{
|
||||
Name: "ping",
|
||||
Description: "ping pong!",
|
||||
},
|
||||
{
|
||||
Name: "echo",
|
||||
Description: "echo back the argument",
|
||||
Options: []discord.CommandOption{
|
||||
&discord.StringOption{
|
||||
OptionName: "argument",
|
||||
Description: "what's echoed back",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "thonk",
|
||||
Description: "biiiig thonk",
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
var (
|
||||
webhookAddr = os.Getenv("WEBHOOK_ADDR")
|
||||
webhookPubkey = os.Getenv("WEBHOOK_PUBKEY")
|
||||
)
|
||||
|
||||
if webhookAddr != "" {
|
||||
state := state.NewAPIOnlyState(token, nil)
|
||||
|
||||
h := newHandler(state)
|
||||
|
||||
if err := overwriteCommands(state); err != nil {
|
||||
log.Fatalln("cannot update commands:", err)
|
||||
}
|
||||
|
||||
srv, err := webhook.NewInteractionServer(webhookPubkey, h)
|
||||
if err != nil {
|
||||
log.Fatalln("cannot create interaction server:", err)
|
||||
}
|
||||
|
||||
log.Println("listening and serving at", webhookAddr+"/")
|
||||
log.Fatalln(http.ListenAndServe(webhookAddr, srv))
|
||||
} else {
|
||||
state := state.New("Bot " + token)
|
||||
state.AddIntents(gateway.IntentGuilds)
|
||||
state.AddHandler(func(*gateway.ReadyEvent) {
|
||||
me, _ := state.Me()
|
||||
log.Println("connected to the gateway as", me.Tag())
|
||||
})
|
||||
|
||||
h := newHandler(state)
|
||||
state.AddInteractionHandler(h)
|
||||
|
||||
if err := overwriteCommands(state); err != nil {
|
||||
log.Fatalln("cannot update commands:", err)
|
||||
}
|
||||
|
||||
if err := h.s.Connect(context.Background()); err != nil {
|
||||
log.Fatalln("cannot connect:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func overwriteCommands(s *state.State) error {
|
||||
return cmdroute.OverwriteCommands(s, commands)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
*cmdroute.Router
|
||||
s *state.State
|
||||
}
|
||||
|
||||
func newHandler(s *state.State) *handler {
|
||||
h := &handler{s: s}
|
||||
|
||||
h.Router = cmdroute.NewRouter()
|
||||
// Automatically defer handles if they're slow.
|
||||
h.Use(cmdroute.Deferrable(s, cmdroute.DeferOpts{}))
|
||||
h.AddFunc("ping", h.cmdPing)
|
||||
h.AddFunc("echo", h.cmdEcho)
|
||||
h.AddFunc("thonk", h.cmdThonk)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *handler) cmdPing(ctx context.Context, cmd cmdroute.CommandData) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("Pong!"),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) cmdEcho(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
var options struct {
|
||||
Arg string `discord:"argument"`
|
||||
}
|
||||
|
||||
if err := data.Options.Unmarshal(&options); err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString(options.Arg),
|
||||
AllowedMentions: &api.AllowedMentions{}, // don't mention anyone
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) cmdThonk(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
time.Sleep(time.Duration(3+rand.Intn(5)) * time.Second)
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("https://tenor.com/view/thonk-thinking-sun-thonk-sun-thinking-sun-gif-14999983"),
|
||||
}
|
||||
}
|
||||
|
||||
func errorResponse(err error) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("**Error:** " + err.Error()),
|
||||
Flags: discord.EphemeralMessage,
|
||||
AllowedMentions: &api.AllowedMentions{ /* none */ },
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
var commands = []api.CreateCommandData{{Name: "ping", Description: "Ping!"}}
|
||||
|
||||
func main() {
|
||||
r := cmdroute.NewRouter()
|
||||
r.AddFunc("ping", func(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{Content: option.NewNullableString("Pong!")}
|
||||
})
|
||||
|
||||
s := state.New("Bot " + os.Getenv("BOT_TOKEN"))
|
||||
s.AddInteractionHandler(r)
|
||||
s.AddIntents(gateway.IntentGuilds)
|
||||
|
||||
if err := cmdroute.OverwriteCommands(s, commands); err != nil {
|
||||
log.Fatalln("cannot update commands:", err)
|
||||
}
|
||||
|
||||
if err := s.Connect(context.TODO()); err != nil {
|
||||
log.Println("cannot connect:", err)
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
// To run, do `BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
||||
var commands = []api.CreateCommandData{
|
||||
{
|
||||
Name: "ping",
|
||||
Description: "ping pong!",
|
||||
},
|
||||
{
|
||||
Name: "echo",
|
||||
Description: "echo back the argument",
|
||||
Options: []discord.CommandOption{
|
||||
&discord.StringOption{
|
||||
OptionName: "argument",
|
||||
Description: "what's echoed back",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "thonk",
|
||||
Description: "biiiig thonk",
|
||||
},
|
||||
}
|
||||
|
||||
func main() {
|
||||
token := os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
h := newHandler(state.New("Bot " + token))
|
||||
h.s.AddInteractionHandler(h)
|
||||
h.s.AddIntents(gateway.IntentGuilds)
|
||||
h.s.AddHandler(func(*gateway.ReadyEvent) {
|
||||
me, _ := h.s.Me()
|
||||
log.Println("connected to the gateway as", me.Tag())
|
||||
})
|
||||
|
||||
if err := overwriteCommands(h.s); err != nil {
|
||||
log.Fatalln("cannot update commands:", err)
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
if err := h.s.Connect(ctx); err != nil {
|
||||
log.Fatalln("cannot connect:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func overwriteCommands(s *state.State) error {
|
||||
return cmdroute.OverwriteCommands(s, commands)
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
*cmdroute.Router
|
||||
s *state.State
|
||||
}
|
||||
|
||||
func newHandler(s *state.State) *handler {
|
||||
h := &handler{s: s}
|
||||
|
||||
h.Router = cmdroute.NewRouter()
|
||||
// Automatically defer handles if they're slow.
|
||||
h.Use(cmdroute.Deferrable(s, cmdroute.DeferOpts{}))
|
||||
h.AddFunc("ping", h.cmdPing)
|
||||
h.AddFunc("echo", h.cmdEcho)
|
||||
h.AddFunc("thonk", h.cmdThonk)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *handler) cmdPing(ctx context.Context, cmd cmdroute.CommandData) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("Pong!"),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) cmdEcho(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
var options struct {
|
||||
Arg string `discord:"argument"`
|
||||
}
|
||||
|
||||
if err := data.Options.Unmarshal(&options); err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString(options.Arg),
|
||||
AllowedMentions: &api.AllowedMentions{}, // don't mention anyone
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) cmdThonk(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
time.Sleep(time.Duration(3+rand.Intn(5)) * time.Second)
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("https://tenor.com/view/thonk-thinking-sun-thonk-sun-thinking-sun-gif-14999983"),
|
||||
}
|
||||
}
|
||||
|
||||
func errorResponse(err error) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("**Error:** " + err.Error()),
|
||||
Flags: discord.EphemeralMessage,
|
||||
AllowedMentions: &api.AllowedMentions{ /* none */ },
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
// Package main demonstrates a bare simple bot without a state cache. It logs
|
||||
// all messages it sees into stderr.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session/shard"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
)
|
||||
|
||||
// To run, do `BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
||||
func main() {
|
||||
var token = os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
newShard := state.NewShardFunc(func(m *shard.Manager, s *state.State) {
|
||||
// Add the needed Gateway intents.
|
||||
s.AddIntents(gateway.IntentGuildMessages)
|
||||
s.AddIntents(gateway.IntentDirectMessages)
|
||||
|
||||
s.AddHandler(func(c *gateway.MessageCreateEvent) {
|
||||
_, shardIx := m.FromGuildID(c.GuildID)
|
||||
log.Println(c.Author.Tag(), "sent", c.Content, "on shard", shardIx)
|
||||
})
|
||||
})
|
||||
|
||||
m, err := shard.NewManager("Bot "+token, newShard)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to create shard manager:", err)
|
||||
}
|
||||
|
||||
if err := m.Open(context.Background()); err != nil {
|
||||
log.Fatalln("failed to connect shards:", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var shardNum int
|
||||
|
||||
m.ForEach(func(s shard.Shard) {
|
||||
state := s.(*state.State)
|
||||
|
||||
u, err := state.Me()
|
||||
if err != nil {
|
||||
log.Fatalln("failed to get myself:", err)
|
||||
}
|
||||
|
||||
log.Printf("Shard %d/%d started as %s", shardNum, m.NumShards()-1, u.Tag())
|
||||
|
||||
shardNum++
|
||||
})
|
||||
|
||||
// Block forever.
|
||||
select {}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
module github.com/diamondburned/arikawa/v3/0-examples/voice
|
||||
|
||||
go 1.17
|
||||
|
||||
replace github.com/diamondburned/arikawa/v3 => ../../
|
||||
|
||||
require (
|
||||
github.com/diamondburned/arikawa/v3 v3.0.0-rc.6
|
||||
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647
|
||||
github.com/pkg/errors v0.9.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/schema v1.2.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
|
||||
)
|
|
@ -1,40 +0,0 @@
|
|||
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647 h1:TJWvffl1cMLzSOvw8Wv3CQicuU9NaKDKXvBfh5T9W00=
|
||||
github.com/diamondburned/oggreader v0.0.0-20201118014549-87df9534b647/go.mod h1:xEJuvlmPx1wBKUWkx+MUp1ULSMQwSM9FS+bnFJhPQkk=
|
||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
|
||||
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
|
||||
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
@ -1,127 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/voice"
|
||||
"github.com/diamondburned/arikawa/v3/voice/udp"
|
||||
"github.com/diamondburned/oggreader"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
file := flag.Arg(0)
|
||||
if file == "" {
|
||||
log.Fatalln("usage:", filepath.Base(os.Args[0]), "<audio file>")
|
||||
}
|
||||
|
||||
voiceID, err := discord.ParseSnowflake(os.Getenv("VOICE_ID"))
|
||||
if err != nil {
|
||||
log.Fatalln("failed to parse $VOICE_ID:", err)
|
||||
}
|
||||
chID := discord.ChannelID(voiceID)
|
||||
|
||||
state := state.New("Bot " + os.Getenv("BOT_TOKEN"))
|
||||
voice.AddIntents(state)
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
if err := state.Open(ctx); err != nil {
|
||||
log.Fatalln("failed to open:", err)
|
||||
}
|
||||
defer state.Close()
|
||||
|
||||
if err := start(ctx, state, chID, file); err != nil {
|
||||
// Ignore context canceled errors as they're often intentional.
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional constants to tweak the Opus stream.
|
||||
const (
|
||||
frameDuration = 60 // ms
|
||||
timeIncrement = 2880
|
||||
)
|
||||
|
||||
func start(ctx context.Context, s *state.State, id discord.ChannelID, file string) error {
|
||||
v, err := voice.NewSession(s)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot make new voice session")
|
||||
}
|
||||
|
||||
// Optimize Opus frame duration. This step is optional, but it is
|
||||
// recommended.
|
||||
v.SetUDPDialer(udp.DialFuncWithFrequency(
|
||||
frameDuration*time.Millisecond, // correspond to -frame_duration
|
||||
timeIncrement,
|
||||
))
|
||||
|
||||
ffmpeg := exec.CommandContext(ctx,
|
||||
"ffmpeg", "-hide_banner", "-loglevel", "error",
|
||||
// Streaming is slow, so a single thread is all we need.
|
||||
"-threads", "1",
|
||||
// Input file.
|
||||
"-i", file,
|
||||
// Output format; leave as "libopus".
|
||||
"-c:a", "libopus",
|
||||
// Bitrate in kilobits. This doesn't matter, but I recommend 96k as the
|
||||
// sweet spot.
|
||||
"-b:a", "96k",
|
||||
// Frame duration should be the same as what's given into
|
||||
// udp.DialFuncWithFrequency.
|
||||
"-frame_duration", strconv.Itoa(frameDuration),
|
||||
// Disable variable bitrate to keep packet sizes consistent. This is
|
||||
// optional.
|
||||
"-vbr", "off",
|
||||
// Output format, which is opus, so we need to unwrap the opus file.
|
||||
"-f", "opus",
|
||||
"-",
|
||||
)
|
||||
|
||||
ffmpeg.Stderr = os.Stderr
|
||||
|
||||
stdout, err := ffmpeg.StdoutPipe()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get stdout pipe")
|
||||
}
|
||||
|
||||
// Kickstart FFmpeg before we join. FFmpeg will wait until we start
|
||||
// consuming the stream to process further.
|
||||
if err := ffmpeg.Start(); err != nil {
|
||||
return errors.Wrap(err, "failed to start ffmpeg")
|
||||
}
|
||||
|
||||
// Join the voice channel.
|
||||
if err := v.JoinChannelAndSpeak(ctx, id, false, true); err != nil {
|
||||
return errors.Wrap(err, "failed to join channel")
|
||||
}
|
||||
defer v.Leave(ctx)
|
||||
|
||||
// Start decoding FFmpeg's OGG-container output and extract the raw Opus
|
||||
// frames into the stream.
|
||||
if err := oggreader.DecodeBuffered(v, stdout); err != nil {
|
||||
return errors.Wrap(err, "failed to decode ogg")
|
||||
}
|
||||
|
||||
// Wait until FFmpeg finishes writing entirely and leave.
|
||||
if err := ffmpeg.Wait(); err != nil {
|
||||
return errors.Wrap(err, "failed to finish ffmpeg")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
148
README.md
148
README.md
|
@ -1,127 +1,67 @@
|
|||
# arikawa
|
||||
|
||||
[![ Pipeline Status ][pipeline_img ]][pipeline ]
|
||||
[![ Report Card ][goreportcard_img]][goreportcard]
|
||||
[![ Godoc Reference ][pkg.go.dev_img ]][pkg.go.dev ]
|
||||
[![ Examples ][examples_img ]][examples ]
|
||||
[![ Discord Gophers ][dgophers_img ]][dgophers ]
|
||||
[![ Hime Arikawa ][himeArikawa_img ]][himeArikawa ]
|
||||
[![Pipeline status](https://gitlab.com/diamondburned/arikawa/badges/master/pipeline.svg?style=flat-square)](https://gitlab.com/diamondburned/arikawa/pipelines )
|
||||
[![ Coverage](https://gitlab.com/diamondburned/arikawa/badges/master/coverage.svg?style=flat-square)](https://gitlab.com/diamondburned/arikawa/commits/master )
|
||||
[![ Report Card](https://goreportcard.com/badge/github.com/diamondburned/arikawa?style=flat-square )](https://goreportcard.com/report/github.com/diamondburned/arikawa)
|
||||
[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue?style=flat-square )](https://pkg.go.dev/github.com/diamondburned/arikawa )
|
||||
[![ Examples](https://img.shields.io/badge/Example-__example%2F-blueviolet?style=flat-square )](https://github.com/diamondburned/arikawa/tree/master/_example )
|
||||
[![Discord Gophers](https://img.shields.io/badge/Discord%20Gophers-%23arikawa-%237289da?style=flat-square)](https://discord.gg/7jSf85J )
|
||||
[![ Hime Arikawa](https://img.shields.io/badge/Hime-Arikawa-ea75a2?style=flat-square )](https://hime-goto.fandom.com/wiki/Hime_Arikawa )
|
||||
|
||||
A Golang library for the Discord API.
|
||||
|
||||
[dgophers]: https://discord.gg/7jSf85J
|
||||
[dgophers_img]: https://img.shields.io/badge/Discord%20Gophers-%23arikawa-%237289da?style=flat-square
|
||||
|
||||
[examples]: https://github.com/diamondburned/arikawa/tree/v3/0-examples
|
||||
[examples_img]: https://img.shields.io/badge/Example-__example%2F-blueviolet?style=flat-square
|
||||
|
||||
[pipeline]: https://builds.sr.ht/~diamondburned/arikawa
|
||||
[pipeline_img]: https://builds.sr.ht/~diamondburned/arikawa.svg?style=flat-square
|
||||
|
||||
[pkg.go.dev]: https://pkg.go.dev/github.com/diamondburned/arikawa/v3
|
||||
[pkg.go.dev_img]: https://pkg.go.dev/badge/github.com/diamondburned/arikawa/v3
|
||||
|
||||
[himeArikawa]: https://hime-goto.fandom.com/wiki/Hime_Arikawa
|
||||
[himeArikawa_img]: https://img.shields.io/badge/Hime-Arikawa-ea75a2?style=flat-square
|
||||
|
||||
[goreportcard]: https://goreportcard.com/report/github.com/diamondburned/arikawa
|
||||
[goreportcard_img]: https://goreportcard.com/badge/github.com/diamondburned/arikawa?style=flat-square
|
||||
|
||||
|
||||
## Library Highlights
|
||||
|
||||
- More modularity with components divided up into independent packages, such as
|
||||
the API client and the Websocket Gateway being fully independent.
|
||||
- Clear separation of models: API and Gateway models are never mixed together so
|
||||
to not be confusing.
|
||||
- Extend and intercept Gateway events, allowing for use cases such as reading
|
||||
deleted messages.
|
||||
- Pluggable Gateway cache allows for custom caching implementations such as
|
||||
Redis, automatically falling back to the API if needed.
|
||||
- Typed Snowflakes make it much harder to accidentally use the wrong ID (e.g.
|
||||
it is impossible to use a channel ID as a message ID).
|
||||
- Working user account support, with much of them in [ningen][ningen]. Please
|
||||
do not use this for self-botting, as that is against Discord's ToS.
|
||||
|
||||
[ningen]: https://github.com/diamondburned/ningen
|
||||
|
||||
|
||||
## Examples
|
||||
|
||||
### [Commands (Hybrid)](https://github.com/diamondburned/arikawa/tree/v3/0-examples/commands-hybrid)
|
||||
|
||||
commands-hybrid is an alternative variant of
|
||||
[commands](https://github.com/diamondburned/arikawa/tree/v3/0-examples/commands),
|
||||
where the program permits being hosted either as a Gateway-based daemon or as a
|
||||
web server using the Interactions Webhook API.
|
||||
|
||||
Both examples demonstrate adding interaction commands into the bot as well as an
|
||||
example of routing those commands to be executed.
|
||||
|
||||
### [Simple](https://github.com/diamondburned/arikawa/tree/v3/0-examples/simple)
|
||||
### [Simple](https://github.com/diamondburned/arikawa/tree/master/_example/simple)
|
||||
|
||||
Simple bot example without any state. All it does is logging messages sent into
|
||||
the console. Run with `BOT_TOKEN="TOKEN" go run .`. This example only
|
||||
demonstrates the most simple needs; in most cases, bots should use the state or
|
||||
the bot router.
|
||||
the console. Run with `BOT_TOKEN="TOKEN" go run .`.
|
||||
|
||||
**Note** that Discord discourages use of bots that do not use the interactions
|
||||
API, meaning that this example should not be used for bots.
|
||||
|
||||
### [Undeleter](https://github.com/diamondburned/arikawa/tree/v3/0-examples/undeleter)
|
||||
### [Undeleter](https://github.com/diamondburned/arikawa/tree/master/_example/undeleter)
|
||||
|
||||
A slightly more complicated example. This bot uses a local state to cache
|
||||
everything, including messages. It detects when someone deletes a message,
|
||||
logging the content into the console.
|
||||
|
||||
This example demonstrates the PreHandler feature of the state library.
|
||||
PreHandler calls all handlers that are registered (separately from the session),
|
||||
calling them before the state is updated.
|
||||
This example demonstrates the PreHandler feature of this library. PreHandler
|
||||
calls all handlers that are registered (separately from the session), calling
|
||||
them before the state is updated.
|
||||
|
||||
**Note** that Discord discourages use of bots that do not use the interactions
|
||||
API, meaning that this example should not be used for bots.
|
||||
### [Advanced Bot](https://github.com/diamondburned/arikawa/tree/master/_example/advanced_bot)
|
||||
|
||||
### Bare Minimum Bot Example
|
||||
A pretty complicated example demonstrating the reflect-based command router
|
||||
that's built-in. The router turns exported struct methods into commands, its
|
||||
arguments into command arguments, and more.
|
||||
|
||||
The least amount of code recommended to have a bot that responds to a /ping.
|
||||
The library has a pretty detailed documentation available in [GoDoc
|
||||
Reference](https://pkg.go.dev/github.com/diamondburned/arikawa/bot).
|
||||
|
||||
```go
|
||||
package main
|
||||
## Comparison: Why not discordgo?
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/cmdroute"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
var commands = []api.CreateCommandData{{Name: "ping", Description: "Ping!"}}
|
||||
|
||||
func main() {
|
||||
r := cmdroute.NewRouter()
|
||||
r.AddFunc("ping", func(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
|
||||
return &api.InteractionResponseData{Content: option.NewNullableString("Pong!")}
|
||||
})
|
||||
|
||||
s := state.New("Bot " + os.Getenv("BOT_TOKEN"))
|
||||
s.AddInteractionHandler(r)
|
||||
s.AddIntents(gateway.IntentGuilds)
|
||||
|
||||
if err := cmdroute.OverwriteCommands(s, commands); err != nil {
|
||||
log.Fatalln("cannot update commands:", err)
|
||||
}
|
||||
|
||||
if err := s.Connect(context.TODO()); err != nil {
|
||||
log.Println("cannot connect:", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
Discordgo is great. It's the first library that I used when I was learning Go.
|
||||
Though there are some things that I disagree on. Here are some ways that this
|
||||
library is different:
|
||||
|
||||
- Better package structure: this library divides the Discord library up into
|
||||
smaller packages.
|
||||
- Cleaner API/Gateway structure separation: this library separates fields that
|
||||
would only appear in Gateway events, so to not cause confusion.
|
||||
- Automatic un-pagination: this library automatically un-paginates endpoints
|
||||
that would otherwise not return everything fully.
|
||||
- Flexible underlying abstractions: this library allows plugging in different
|
||||
JSON and Websocket implementations, as well as direct access to the HTTP
|
||||
client.
|
||||
- Flexible API abstractions: because packages are separated, the developer could
|
||||
choose to use a lower level package (such as `gateway`) or a higher level
|
||||
package (such as `state`).
|
||||
- Pre-handlers in the state: this allows the developers to access items from the
|
||||
state storage before they're removed.
|
||||
- Pluggable state storages: although only having a default state storage in the
|
||||
library, it is abstracted with an interface, making it possible to implement a
|
||||
custom remote or local state storage.
|
||||
- REST-updated state: this library will call the REST API if it can't find
|
||||
things in the state, which is useful for keeping it updated.
|
||||
- No code generation: just so the library is a lot easier to maintain.
|
||||
|
||||
## Testing
|
||||
|
||||
|
@ -130,5 +70,5 @@ tests, do:
|
|||
|
||||
```sh
|
||||
export BOT_TOKEN="<BOT_TOKEN>"
|
||||
go test -tags integration -race ./...
|
||||
go test -tags integration ./...
|
||||
```
|
||||
|
|
|
@ -8,11 +8,11 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot/extras/arguments"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot/extras/middlewares"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
"github.com/diamondburned/arikawa/bot/extras/arguments"
|
||||
"github.com/diamondburned/arikawa/bot/extras/middlewares"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
)
|
||||
|
||||
type Bot struct {
|
||||
|
@ -22,7 +22,7 @@ type Bot struct {
|
|||
|
||||
func (bot *Bot) Setup(sub *bot.Subcommand) {
|
||||
// Only allow people in guilds to run guildInfo.
|
||||
sub.AddMiddleware(bot.GuildInfo, middlewares.GuildOnly(bot.Ctx))
|
||||
sub.AddMiddleware("GuildInfo", middlewares.GuildOnly(bot.Ctx))
|
||||
}
|
||||
|
||||
// Help prints the default help message.
|
||||
|
@ -64,7 +64,7 @@ func (bot *Bot) GuildInfo(m *gateway.MessageCreateEvent) (string, error) {
|
|||
// Repeat tells the bot to wait for the user's response, then repeat what they
|
||||
// said.
|
||||
func (bot *Bot) Repeat(m *gateway.MessageCreateEvent) (string, error) {
|
||||
_, err := bot.Ctx.SendMessage(m.ChannelID, "What do you want me to say?")
|
||||
_, err := bot.Ctx.SendMessage(m.ChannelID, "What do you want me to say?", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
|
@ -6,9 +6,9 @@ import (
|
|||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot/extras/middlewares"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
"github.com/diamondburned/arikawa/bot/extras/middlewares"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
)
|
||||
|
||||
// Flag for administrators only.
|
||||
|
@ -26,14 +26,12 @@ func (d *Debug) Setup(sub *bot.Subcommand) {
|
|||
|
||||
// Manually set the usage for each function.
|
||||
|
||||
// Those methods can take in a regular Go method reference.
|
||||
sub.ChangeCommandInfo(d.GOOS, "GOOS", "Prints the current operating system")
|
||||
sub.ChangeCommandInfo(d.GC, "GC", "Triggers the garbage collector")
|
||||
// They could also take in the raw name.
|
||||
sub.ChangeCommandInfo("GOOS", "GOOS", "Prints the current operating system")
|
||||
sub.ChangeCommandInfo("GC", "GC", "Triggers the garbage collector")
|
||||
sub.ChangeCommandInfo("Goroutines", "", "Prints the current number of Goroutines")
|
||||
|
||||
sub.Hide(d.Die)
|
||||
sub.AddMiddleware(d.Die, middlewares.AdminOnly(d.Context))
|
||||
sub.Hide("Die")
|
||||
sub.AddMiddleware("Die", middlewares.AdminOnly(d.Context))
|
||||
}
|
||||
|
||||
// ~go goroutines
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
)
|
||||
|
||||
// To run, do `BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
||||
func main() {
|
||||
var token = os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
commands := &Bot{}
|
||||
|
||||
wait, err := bot.Start(token, commands, func(ctx *bot.Context) error {
|
||||
ctx.HasPrefix = bot.NewPrefix("!", "~")
|
||||
ctx.EditableCommands = true
|
||||
|
||||
// Subcommand demo, but this can be in another package.
|
||||
ctx.MustRegisterSubcommand(&Debug{})
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Println("Bot started")
|
||||
|
||||
// As of this commit, wait() will block until SIGINT or fatal. The past
|
||||
// versions close on call, but this one will block.
|
||||
// If for some reason you want the Cancel() function, manually make a new
|
||||
// context.
|
||||
if err := wait(); err != nil {
|
||||
log.Fatalln("Gateway fatal error:", err)
|
||||
}
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
// Package main demonstrates a bare simple bot without a state cache. It logs
|
||||
// all messages it sees into stderr.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/session"
|
||||
)
|
||||
|
||||
// To run, do `BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
@ -19,16 +16,16 @@ func main() {
|
|||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
s := session.New("Bot " + token)
|
||||
s, err := session.New("Bot " + token)
|
||||
if err != nil {
|
||||
log.Fatalln("Session failed:", err)
|
||||
}
|
||||
|
||||
s.AddHandler(func(c *gateway.MessageCreateEvent) {
|
||||
log.Println(c.Author.Username, "sent", c.Content)
|
||||
})
|
||||
|
||||
// Add the needed Gateway intents.
|
||||
s.AddIntents(gateway.IntentGuildMessages)
|
||||
s.AddIntents(gateway.IntentDirectMessages)
|
||||
|
||||
if err := s.Open(context.Background()); err != nil {
|
||||
if err := s.Open(); err != nil {
|
||||
log.Fatalln("Failed to connect:", err)
|
||||
}
|
||||
defer s.Close()
|
|
@ -1,14 +1,12 @@
|
|||
// Package main demonstrates the PreHandler API of the State.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/utils/handler"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/arikawa/utils/handler"
|
||||
)
|
||||
|
||||
// To run, do `BOT_TOKEN="TOKEN HERE" go run .`
|
||||
|
@ -19,10 +17,15 @@ func main() {
|
|||
log.Fatalln("No $BOT_TOKEN given.")
|
||||
}
|
||||
|
||||
s := state.New("Bot " + token)
|
||||
s, err := state.New("Bot " + token)
|
||||
if err != nil {
|
||||
log.Fatalln("Session failed:", err)
|
||||
}
|
||||
|
||||
// Make a pre-handler
|
||||
s.PreHandler = handler.New()
|
||||
s.PreHandler.AddSyncHandler(func(c *gateway.MessageDeleteEvent) {
|
||||
s.PreHandler.Synchronous = true
|
||||
s.PreHandler.AddHandler(func(c *gateway.MessageDeleteEvent) {
|
||||
// Grab from the state
|
||||
m, err := s.Message(c.ChannelID, c.ID)
|
||||
if err != nil {
|
||||
|
@ -32,11 +35,7 @@ func main() {
|
|||
}
|
||||
})
|
||||
|
||||
// Add the needed Gateway intents.
|
||||
s.AddIntents(gateway.IntentGuildMessages)
|
||||
s.AddIntents(gateway.IntentDirectMessages)
|
||||
|
||||
if err := s.Open(context.Background()); err != nil {
|
||||
if err := s.Open(); err != nil {
|
||||
log.Fatalln("Failed to connect:", err)
|
||||
}
|
||||
defer s.Close()
|
90
api/api.go
90
api/api.go
|
@ -6,15 +6,14 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api/rate"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/arikawa/api/rate"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||
)
|
||||
|
||||
var (
|
||||
BaseEndpoint = "https://discord.com"
|
||||
Version = "9"
|
||||
Version = "6"
|
||||
Path = "/api/v" + Version
|
||||
|
||||
Endpoint = BaseEndpoint + Path + "/"
|
||||
|
@ -22,12 +21,11 @@ var (
|
|||
EndpointGatewayBot = EndpointGateway + "/bot"
|
||||
)
|
||||
|
||||
var UserAgent = "DiscordBot (https://github.com/diamondburned/arikawa/v3)"
|
||||
var UserAgent = "DiscordBot (https://github.com/diamondburned/arikawa, v0.0.1)"
|
||||
|
||||
type Client struct {
|
||||
*httputil.Client
|
||||
*Session
|
||||
AcquireOptions rate.AcquireOptions
|
||||
Session
|
||||
}
|
||||
|
||||
func NewClient(token string) *Client {
|
||||
|
@ -35,34 +33,19 @@ func NewClient(token string) *Client {
|
|||
}
|
||||
|
||||
func NewCustomClient(token string, httpClient *httputil.Client) *Client {
|
||||
c := &Client{
|
||||
Session: &Session{
|
||||
Limiter: rate.NewLimiter(Path),
|
||||
Token: token,
|
||||
UserAgent: UserAgent,
|
||||
},
|
||||
Client: httpClient.Copy(),
|
||||
ses := Session{
|
||||
Limiter: rate.NewLimiter(Path),
|
||||
Token: token,
|
||||
UserAgent: UserAgent,
|
||||
}
|
||||
|
||||
c.Client.OnRequest = append(c.Client.OnRequest, c.InjectRequest)
|
||||
c.Client.OnResponse = append(c.Client.OnResponse, c.OnResponse)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// WithLocale creates a copy of Client with an explicitly stated language locale
|
||||
// using the X-Discord-Locale HTTP header.
|
||||
func (c *Client) WithLocale(language discord.Language) *Client {
|
||||
client := c.Client.Copy()
|
||||
client.OnRequest = append(client.OnRequest, func(r httpdriver.Request) error {
|
||||
r.AddHeader(http.Header{"X-Discord-Locale": []string{string(language)}})
|
||||
return nil
|
||||
})
|
||||
hcl := httpClient.Copy()
|
||||
hcl.OnRequest = append(hcl.OnRequest, ses.InjectRequest)
|
||||
hcl.OnResponse = append(hcl.OnResponse, ses.OnResponse)
|
||||
|
||||
return &Client{
|
||||
Client: client,
|
||||
Session: c.Session,
|
||||
AcquireOptions: c.AcquireOptions,
|
||||
Client: hcl,
|
||||
Session: ses,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,26 +53,11 @@ func (c *Client) WithLocale(language discord.Language) *Client {
|
|||
// used for method timeouts and such. This method is thread-safe.
|
||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
||||
return &Client{
|
||||
Client: c.Client.WithContext(ctx),
|
||||
Session: c.Session,
|
||||
AcquireOptions: c.AcquireOptions,
|
||||
Client: c.Client.WithContext(ctx),
|
||||
Session: c.Session,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) InjectRequest(r httpdriver.Request) error {
|
||||
r.AddHeader(http.Header{
|
||||
"Authorization": {c.Session.Token},
|
||||
"User-Agent": {c.Session.UserAgent},
|
||||
})
|
||||
|
||||
ctx := c.AcquireOptions.Context(r.GetContext())
|
||||
return c.Session.Limiter.Acquire(ctx, r.GetPath())
|
||||
}
|
||||
|
||||
func (c *Client) OnResponse(r httpdriver.Request, resp httpdriver.Response) error {
|
||||
return c.Session.Limiter.Release(r.GetPath(), httpdriver.OptHeader(resp))
|
||||
}
|
||||
|
||||
// Session keeps a single session. This is typically wrapped around Client.
|
||||
type Session struct {
|
||||
Limiter *rate.Limiter
|
||||
|
@ -98,17 +66,17 @@ type Session struct {
|
|||
UserAgent string
|
||||
}
|
||||
|
||||
// AuditLogReason is the type embedded in data structs when the action
|
||||
// performed by calling that api endpoint supports attaching a custom audit log
|
||||
// reason.
|
||||
type AuditLogReason string
|
||||
func (s *Session) InjectRequest(r httpdriver.Request) error {
|
||||
r.AddHeader(http.Header{
|
||||
"Authorization": {s.Token},
|
||||
"User-Agent": {s.UserAgent},
|
||||
"X-RateLimit-Precision": {"millisecond"},
|
||||
})
|
||||
|
||||
// Header returns a http.Header containing the reason, or nil if the reason is
|
||||
// empty.
|
||||
func (r AuditLogReason) Header() http.Header {
|
||||
if len(r) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return http.Header{"X-Audit-Log-Reason": []string{string(r)}}
|
||||
// Rate limit stuff
|
||||
return s.Limiter.Acquire(r.GetContext(), r.GetPath())
|
||||
}
|
||||
|
||||
func (s *Session) OnResponse(r httpdriver.Request, resp httpdriver.Response) error {
|
||||
return s.Limiter.Release(r.GetPath(), httpdriver.OptHeader(resp))
|
||||
}
|
||||
|
|
|
@ -1,288 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
)
|
||||
|
||||
var EndpointApplications = Endpoint + "applications/"
|
||||
|
||||
// CurrentApplication returns the current bot account's Discord application. It
|
||||
// can be used to get the application ID.
|
||||
func (c *Client) CurrentApplication() (*discord.Application, error) {
|
||||
var app *discord.Application
|
||||
return app, c.RequestJSON(
|
||||
&app, "GET",
|
||||
Endpoint+"/oauth2/applications/@me",
|
||||
)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/interactions/application-commands#create-global-application-command
|
||||
type CreateCommandData struct {
|
||||
Name string `json:"name"`
|
||||
NameLocalizations discord.StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations discord.StringLocales `json:"description_localizations,omitempty"`
|
||||
Options discord.CommandOptions `json:"options,omitempty"`
|
||||
DefaultMemberPermissions *discord.Permissions `json:"default_member_permissions,string,omitempty"`
|
||||
NoDMPermission bool `json:"-"`
|
||||
NoDefaultPermission bool `json:"-"`
|
||||
Type discord.CommandType `json:"type,omitempty"`
|
||||
}
|
||||
|
||||
func (c CreateCommandData) MarshalJSON() ([]byte, error) {
|
||||
type RawCreateCommandData CreateCommandData
|
||||
cmd := struct {
|
||||
RawCreateCommandData
|
||||
DMPermission bool `json:"dm_permission"`
|
||||
DefaultPermission bool `json:"default_permission"`
|
||||
}{RawCreateCommandData: (RawCreateCommandData)(c)}
|
||||
|
||||
// Discord defaults default_permission to true, so we need to invert the
|
||||
// meaning of the field (>No<DefaultPermission) to match Go's default
|
||||
// value, false.
|
||||
cmd.DefaultPermission = !c.NoDefaultPermission
|
||||
cmd.DMPermission = !c.NoDMPermission
|
||||
|
||||
return json.Marshal(cmd)
|
||||
}
|
||||
|
||||
func (c *CreateCommandData) UnmarshalJSON(data []byte) error {
|
||||
type RawCreateCommandData CreateCommandData
|
||||
cmd := struct {
|
||||
*RawCreateCommandData
|
||||
DMPermission bool `json:"dm_permission"`
|
||||
DefaultPermission bool `json:"default_permission"`
|
||||
}{RawCreateCommandData: (*RawCreateCommandData)(c)}
|
||||
if err := json.Unmarshal(data, &cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Discord defaults default_permission to true, so we need to invert the
|
||||
// meaning of the field (>No<DefaultPermission) to match Go's default
|
||||
// value, false.
|
||||
c.NoDefaultPermission = !cmd.DefaultPermission
|
||||
c.NoDMPermission = !cmd.DMPermission
|
||||
|
||||
// Discord defaults type to 1 if omitted.
|
||||
if c.Type == 0 {
|
||||
c.Type = discord.ChatInputCommand
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Commands(appID discord.AppID) ([]discord.Command, error) {
|
||||
var cmds []discord.Command
|
||||
return cmds, c.RequestJSON(
|
||||
&cmds, "GET",
|
||||
EndpointApplications+appID.String()+"/commands",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) Command(
|
||||
appID discord.AppID, commandID discord.CommandID) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "GET",
|
||||
EndpointApplications+appID.String()+"/commands/"+commandID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) CreateCommand(
|
||||
appID discord.AppID, data CreateCommandData) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "POST",
|
||||
EndpointApplications+appID.String()+"/commands",
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) EditCommand(
|
||||
appID discord.AppID,
|
||||
commandID discord.CommandID, data CreateCommandData) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "PATCH",
|
||||
EndpointApplications+appID.String()+"/commands/"+commandID.String(),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteCommand(appID discord.AppID, commandID discord.CommandID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointApplications+appID.String()+"/commands/"+commandID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// BulkOverwriteCommands takes a slice of application commands, overwriting
|
||||
// existing commands that are registered globally for this application. Updates
|
||||
// will be available in all guilds after 1 hour.
|
||||
//
|
||||
// Commands that do not already exist will count toward daily application
|
||||
// command create limits.
|
||||
func (c *Client) BulkOverwriteCommands(
|
||||
appID discord.AppID, commands []CreateCommandData) ([]discord.Command, error) {
|
||||
|
||||
var cmds []discord.Command
|
||||
return cmds, c.RequestJSON(
|
||||
&cmds, "PUT",
|
||||
EndpointApplications+appID.String()+"/commands",
|
||||
httputil.WithJSONBody(commands))
|
||||
}
|
||||
|
||||
func (c *Client) GuildCommands(
|
||||
appID discord.AppID, guildID discord.GuildID) ([]discord.Command, error) {
|
||||
|
||||
var cmds []discord.Command
|
||||
return cmds, c.RequestJSON(
|
||||
&cmds, "GET",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+"/commands",
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) GuildCommand(
|
||||
appID discord.AppID,
|
||||
guildID discord.GuildID, commandID discord.CommandID) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "GET",
|
||||
EndpointApplications+appID.String()+
|
||||
"/guilds/"+guildID.String()+
|
||||
"/commands/"+commandID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) CreateGuildCommand(
|
||||
appID discord.AppID,
|
||||
guildID discord.GuildID, data CreateCommandData) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "POST",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+"/commands",
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) EditGuildCommand(
|
||||
appID discord.AppID, guildID discord.GuildID,
|
||||
commandID discord.CommandID, data CreateCommandData) (*discord.Command, error) {
|
||||
|
||||
var cmd *discord.Command
|
||||
return cmd, c.RequestJSON(
|
||||
&cmd, "PATCH",
|
||||
EndpointApplications+appID.String()+
|
||||
"/guilds/"+guildID.String()+
|
||||
"/commands/"+commandID.String(),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteGuildCommand(
|
||||
appID discord.AppID, guildID discord.GuildID, commandID discord.CommandID) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointApplications+appID.String()+
|
||||
"/guilds/"+guildID.String()+
|
||||
"/commands/"+commandID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// BulkOverwriteGuildCommands takes a slice of application commands,
|
||||
// overwriting existing commands that are registered for the guild.
|
||||
func (c *Client) BulkOverwriteGuildCommands(
|
||||
appID discord.AppID,
|
||||
guildID discord.GuildID, commands []CreateCommandData) ([]discord.Command, error) {
|
||||
|
||||
var cmds []discord.Command
|
||||
return cmds, c.RequestJSON(
|
||||
&cmds, "PUT",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+"/commands",
|
||||
httputil.WithJSONBody(commands))
|
||||
}
|
||||
|
||||
// GuildCommandPermissions fetches command permissions for all commands for the
|
||||
// application in a guild.
|
||||
func (c *Client) GuildCommandPermissions(
|
||||
appID discord.AppID, guildID discord.GuildID) ([]discord.GuildCommandPermissions, error) {
|
||||
|
||||
var perms []discord.GuildCommandPermissions
|
||||
return perms, c.RequestJSON(
|
||||
&perms, "GET",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+"/commands/permissions",
|
||||
)
|
||||
}
|
||||
|
||||
// CommandPermissions fetches command permissions for a specific command for
|
||||
// the application in a guild.
|
||||
func (c *Client) CommandPermissions(
|
||||
appID discord.AppID, guildID discord.GuildID,
|
||||
commandID discord.CommandID) (*discord.GuildCommandPermissions, error) {
|
||||
|
||||
var perms *discord.GuildCommandPermissions
|
||||
return perms, c.RequestJSON(
|
||||
&perms, "GET",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+
|
||||
"/commands/"+commandID.String()+"/permissions",
|
||||
)
|
||||
}
|
||||
|
||||
type editCommandPermissionsData struct {
|
||||
Permissions []discord.CommandPermissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// EditCommandPermissions edits command permissions for a specific command for
|
||||
// the application in a guild. Up to 10 permission overwrites can be added for
|
||||
// a command.
|
||||
//
|
||||
// Existing permissions for the command will be overwritten in that guild.
|
||||
// Deleting or renaming a command will permanently delete all permissions for
|
||||
// that command.
|
||||
func (c *Client) EditCommandPermissions(
|
||||
appID discord.AppID, guildID discord.GuildID, commandID discord.CommandID,
|
||||
permissions []discord.CommandPermissions) (*discord.GuildCommandPermissions, error) {
|
||||
|
||||
data := editCommandPermissionsData{Permissions: permissions}
|
||||
|
||||
var perms *discord.GuildCommandPermissions
|
||||
return perms, c.RequestJSON(
|
||||
&perms, "PUT",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+
|
||||
"/commands/"+commandID.String()+"/permissions",
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/interactions/slash-commands#application-command-permissions-object-guild-application-command-permissions-structure
|
||||
type BatchEditCommandPermissionsData struct {
|
||||
ID discord.CommandID `json:"id"`
|
||||
Permissions []discord.CommandPermissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// BatchEditCommandPermissions batch edits permissions for all commands in a
|
||||
// guild. Up to 10 permission overwrites can be added for a command.
|
||||
//
|
||||
// Existing permissions for the command will be overwritten in that guild.
|
||||
// Deleting or renaming a command will permanently delete all permissions for
|
||||
// that command.
|
||||
func (c *Client) BatchEditCommandPermissions(
|
||||
appID discord.AppID, guildID discord.GuildID,
|
||||
data []BatchEditCommandPermissionsData) ([]discord.GuildCommandPermissions, error) {
|
||||
|
||||
var perms []discord.GuildCommandPermissions
|
||||
return perms, c.RequestJSON(
|
||||
&perms, "PUT",
|
||||
EndpointApplications+appID.String()+"/guilds/"+guildID.String()+"/commands/permissions",
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
39
api/bot.go
39
api/bot.go
|
@ -1,39 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
)
|
||||
|
||||
// BotData contains the GatewayURL as well as extra metadata on how to
|
||||
// shard bots.
|
||||
type BotData struct {
|
||||
URL string `json:"url"`
|
||||
Shards int `json:"shards,omitempty"`
|
||||
StartLimit *SessionStartLimit `json:"session_start_limit"`
|
||||
}
|
||||
|
||||
// SessionStartLimit is the information on the current session start limit. It's
|
||||
// used in BotData.
|
||||
type SessionStartLimit struct {
|
||||
Total int `json:"total"`
|
||||
Remaining int `json:"remaining"`
|
||||
ResetAfter discord.Milliseconds `json:"reset_after"`
|
||||
MaxConcurrency int `json:"max_concurrency"`
|
||||
}
|
||||
|
||||
// BotURL fetches the Gateway URL along with extra metadata. The token
|
||||
// passed in will NOT be prefixed with Bot.
|
||||
func (c *Client) BotURL() (*BotData, error) {
|
||||
var g *BotData
|
||||
return g, c.RequestJSON(&g, "GET", EndpointGatewayBot)
|
||||
}
|
||||
|
||||
// GatewayURL asks Discord for a Websocket URL to the Gateway.
|
||||
func GatewayURL(ctx context.Context) (string, error) {
|
||||
var g BotData
|
||||
err := httputil.NewClient().WithContext(ctx).RequestJSON(&g, "GET", EndpointGateway)
|
||||
return g.URL, err
|
||||
}
|
447
api/channel.go
447
api/channel.go
|
@ -1,9 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
var EndpointChannels = Endpoint + "channels/"
|
||||
|
@ -28,8 +28,6 @@ type CreateChannelData struct {
|
|||
//
|
||||
// Channel Types: Text, News
|
||||
Topic string `json:"topic,omitempty"`
|
||||
// Flags is a bitmask that contains if a thread is pinned for example
|
||||
Flags discord.ChannelFlags `json:"flags,omitempty"`
|
||||
// VoiceBitrate is the bitrate (in bits) of the voice channel.
|
||||
// 8000 to 96000 (128000 for VIP servers)
|
||||
//
|
||||
|
@ -51,85 +49,48 @@ type CreateChannelData struct {
|
|||
//
|
||||
// Channel Types: All
|
||||
Position option.Int `json:"position,omitempty"`
|
||||
// Overwrites are the channel's permission overwrites.
|
||||
// Permissions are the channel's permission overwrites.
|
||||
//
|
||||
// Channel Types: All
|
||||
Overwrites []discord.Overwrite `json:"permission_overwrites,omitempty"`
|
||||
Permissions []discord.Overwrite `json:"permission_overwrites,omitempty"`
|
||||
// CategoryID is the id of the parent category for a channel.
|
||||
//
|
||||
// Channel Types: Text, News, Store, Voice
|
||||
CategoryID discord.ChannelID `json:"parent_id,string,omitempty"`
|
||||
// NSFW specifies whether the channel is nsfw.
|
||||
//
|
||||
// Channel Types: Text, News, Store
|
||||
// Channel Types: Text, News, Store.
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
// RTCRegionID is the channel voice region id. It will be determined
|
||||
// automatically set, if omitted.
|
||||
//
|
||||
// Channel Types: Voice
|
||||
RTCRegionID string `json:"rtc_region,omitempty"`
|
||||
// VideoQualityMode is the camera video quality mode of the voice channel.
|
||||
// This defaults to discord.AutoVideoQuality, if not set.
|
||||
//
|
||||
// ChannelTypes: Voice
|
||||
VoiceQualityMode discord.VideoQualityMode `json:"voice_quality_mode,omitempty"`
|
||||
|
||||
AvailableTags []discord.Tag `json:"available_tags,omitempty"`
|
||||
DefaultReactionEmoji *discord.ForumReaction `json:"default_reaction_emoji,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// CreateChannel creates a new channel object for the guild.
|
||||
//
|
||||
// Requires the MANAGE_CHANNELS permission. If setting permission overwrites,
|
||||
// only permissions your bot has in the guild can be allowed/denied. Setting
|
||||
// MANAGE_ROLES permission in channels is only possible for guild
|
||||
// administrators. Returns the new channel object on success.
|
||||
//
|
||||
// Fires a ChannelCreate Gateway event.
|
||||
// Requires the MANAGE_CHANNELS permission.
|
||||
// Fires a Channel Create Gateway event.
|
||||
func (c *Client) CreateChannel(
|
||||
guildID discord.GuildID, data CreateChannelData) (*discord.Channel, error) {
|
||||
|
||||
var ch *discord.Channel
|
||||
return ch, c.RequestJSON(
|
||||
&ch, "POST",
|
||||
EndpointGuilds+guildID.String()+"/channels",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
type (
|
||||
MoveChannelsData struct {
|
||||
// Channels are the channels to be moved.
|
||||
Channels []MoveChannelData
|
||||
type MoveChannelData struct {
|
||||
// ID is the channel id.
|
||||
ID discord.ChannelID `json:"id"`
|
||||
// Position is the sorting position of the channel
|
||||
Position option.Int `json:"position"`
|
||||
}
|
||||
|
||||
AuditLogReason
|
||||
}
|
||||
|
||||
MoveChannelData struct {
|
||||
// ID is the channel id.
|
||||
ID discord.ChannelID `json:"id"`
|
||||
// Position is the sorting position of the channel.
|
||||
Position option.Int `json:"position"`
|
||||
// LockPermissions syncs the permission overwrites with the new parent,
|
||||
// if moving to a new category.
|
||||
LockPermissions option.Bool `json:"lock_permissions"`
|
||||
// CategoryID is the new parent ID for the channel that is moved.
|
||||
CategoryID discord.ChannelID `json:"parent_id,string,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// MoveChannels modifies the position of channels in the guild.
|
||||
// MoveChannel modifies the position of channels in the guild.
|
||||
//
|
||||
// Requires MANAGE_CHANNELS.
|
||||
//
|
||||
// Fires multiple Channel Update Gateway events.
|
||||
func (c *Client) MoveChannels(guildID discord.GuildID, data MoveChannelsData) error {
|
||||
func (c *Client) MoveChannel(guildID discord.GuildID, data []MoveChannelData) error {
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointGuilds+guildID.String()+"/channels",
|
||||
httputil.WithJSONBody(data.Channels), httputil.WithHeaders(data.Header()),
|
||||
EndpointGuilds+guildID.String()+"/channels", httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -151,26 +112,24 @@ type ModifyChannelData struct {
|
|||
//
|
||||
// Channel Types: Text, News
|
||||
Type *discord.ChannelType `json:"type,omitempty"`
|
||||
// Position is the position of the channel in the left-hand listing.
|
||||
// Postion is the position of the channel in the left-hand listing
|
||||
//
|
||||
// Channel Types: Text, News, Voice, Store, Category
|
||||
// Channel Types: All
|
||||
Position option.NullableInt `json:"position,omitempty"`
|
||||
// Topic is the 0-1024 character channel topic.
|
||||
//
|
||||
// Channel Types: Text, News
|
||||
Topic option.NullableString `json:"topic,omitempty"`
|
||||
// Flags is a bitmask that contains if a thread is pinned for example
|
||||
Flags *discord.ChannelFlags `json:"flags,omitempty"`
|
||||
// NSFW specifies whether the channel is nsfw.
|
||||
//
|
||||
// Channel Types: Text, News, Store
|
||||
// Channel Types: Text, News, Store.
|
||||
NSFW option.NullableBool `json:"nsfw,omitempty"`
|
||||
// UserRateLimit is the amount of seconds a user has to wait before sending
|
||||
// another message (0-21600).
|
||||
// Bots, as well as users with the permission manage_messages or
|
||||
// manage_channel, are unaffected.
|
||||
//
|
||||
// Channel Types: Text, Thread
|
||||
// Channel Types: Text
|
||||
UserRateLimit option.NullableUint `json:"rate_limit_per_user,omitempty"`
|
||||
// VoiceBitrate is the bitrate (in bits) of the voice channel.
|
||||
// 8000 to 96000 (128000 for VIP servers)
|
||||
|
@ -182,63 +141,20 @@ type ModifyChannelData struct {
|
|||
//
|
||||
// Channel Types: Voice
|
||||
VoiceUserLimit option.NullableUint `json:"user_limit,omitempty"`
|
||||
// RTCRegionID is the channel voice region id. It will be determined
|
||||
// automatically set, if omitted.
|
||||
// Permissions are the channel or category-specific permissions.
|
||||
//
|
||||
// Channel Types: Voice
|
||||
RTCRegionID option.NullableString `json:"rtc_region,omitempty"`
|
||||
// Overwrites are the channel or category-specific permissions.
|
||||
//
|
||||
// Channel Types: Text, News, Store, Voice, Category
|
||||
Overwrites *[]discord.Overwrite `json:"permission_overwrites,omitempty"`
|
||||
// Channel Types: All
|
||||
Permissions *[]discord.Overwrite `json:"permission_overwrites,omitempty"`
|
||||
// CategoryID is the id of the new parent category for a channel.
|
||||
//
|
||||
// Channel Types: Text, News, Store, Voice
|
||||
CategoryID discord.ChannelID `json:"parent_id,string,omitempty"`
|
||||
|
||||
// Icon is a base64 encoded icon.
|
||||
//
|
||||
// Channel Types: Group DM
|
||||
Icon string `json:"icon,omitempty"`
|
||||
|
||||
// Archived specifies whether the thread is archived.
|
||||
Archived option.Bool `json:"archived,omitempty"`
|
||||
// AutoArchiveDuration is the duration in minutes to automatically archive
|
||||
// the thread after recent activity.
|
||||
//
|
||||
// Note that the three and seven day archive durations require the server
|
||||
// to be boosted.
|
||||
AutoArchiveDuration discord.ArchiveDuration `json:"auto_archive_duration,omitempty"`
|
||||
// Locked specifies whether the thread is locked. When a thread is locked,
|
||||
// only users with MANAGE_THREADS can unarchive it.
|
||||
Locked option.Bool `json:"locked,omitempty"`
|
||||
// Invitable specifies whether non-moderators can add other
|
||||
// non-moderators to a thread; only available on private threads
|
||||
Invitable option.Bool `json:"invitable,omitempty"`
|
||||
|
||||
AvailableTags *[]discord.Tag `json:"available_tags,omitempty"`
|
||||
AppliedTags *[]discord.TagID `json:"applied_tags,omitempty"`
|
||||
DefaultReactionEmoji **discord.ForumReaction `json:"default_reaction_emoji,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyChannel updates a channel's settings.
|
||||
//
|
||||
// If modifying a guild channel, requires the MANAGE_CHANNELS permission for
|
||||
// that guild. If modifying a thread, requires the MANAGE_THREADS permission.
|
||||
// Furthermore, if modifying permission overwrites, the MANAGE_ROLES permission
|
||||
// is required. Only permissions your bot has in the guild or channel can be
|
||||
// allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the
|
||||
// channel).
|
||||
//
|
||||
// Fires a Channel Update event when modifying a guild channel, and a Thread
|
||||
// Update event when modifying a thread.
|
||||
// Requires the MANAGE_CHANNELS permission for the guild.
|
||||
func (c *Client) ModifyChannel(channelID discord.ChannelID, data ModifyChannelData) error {
|
||||
return c.FastRequest(
|
||||
"PATCH", EndpointChannels+channelID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
return c.FastRequest("PATCH", EndpointChannels+channelID.String(), httputil.WithJSONBody(data))
|
||||
}
|
||||
|
||||
// DeleteChannel deletes a channel, or closes a private message. Requires the
|
||||
|
@ -247,13 +163,8 @@ func (c *Client) ModifyChannel(channelID discord.ChannelID, data ModifyChannelDa
|
|||
// Channel Update Gateway event will fire for each of them.
|
||||
//
|
||||
// Fires a Channel Delete Gateway event.
|
||||
func (c *Client) DeleteChannel(
|
||||
channelID discord.ChannelID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointChannels+channelID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
func (c *Client) DeleteChannel(channelID discord.ChannelID) error {
|
||||
return c.FastRequest("DELETE", EndpointChannels+channelID.String())
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#edit-channel-permissions-json-params
|
||||
|
@ -264,8 +175,6 @@ type EditChannelPermissionData struct {
|
|||
Allow discord.Permissions `json:"allow,string"`
|
||||
// Deny is a permission bit set for denied permissions.
|
||||
Deny discord.Permissions `json:"deny,string"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// EditChannelPermission edits the channel's permission overwrites for a user
|
||||
|
@ -273,12 +182,11 @@ type EditChannelPermissionData struct {
|
|||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
func (c *Client) EditChannelPermission(
|
||||
channelID discord.ChannelID,
|
||||
overwriteID discord.Snowflake, data EditChannelPermissionData) error {
|
||||
channelID discord.ChannelID, overwriteID discord.Snowflake, data EditChannelPermissionData) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"PUT", EndpointChannels+channelID.String()+"/permissions/"+overwriteID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -286,12 +194,10 @@ func (c *Client) EditChannelPermission(
|
|||
// role in a channel. Only usable for guild channels.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
func (c *Client) DeleteChannelPermission(
|
||||
channelID discord.ChannelID, overwriteID discord.Snowflake, reason AuditLogReason) error {
|
||||
|
||||
func (c *Client) DeleteChannelPermission(channelID discord.ChannelID, overwriteID discord.Snowflake) error {
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointChannels+channelID.String()+"/permissions/"+overwriteID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
"DELETE",
|
||||
EndpointChannels+channelID.String()+"/permissions/"+overwriteID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -311,32 +217,21 @@ func (c *Client) PinnedMessages(channelID discord.ChannelID) ([]discord.Message,
|
|||
// PinMessage pins a message in a channel.
|
||||
//
|
||||
// Requires the MANAGE_MESSAGES permission.
|
||||
func (c *Client) PinMessage(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"PUT", EndpointChannels+channelID.String()+"/pins/"+messageID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
func (c *Client) PinMessage(channelID discord.ChannelID, messageID discord.MessageID) error {
|
||||
return c.FastRequest("PUT", EndpointChannels+channelID.String()+"/pins/"+messageID.String())
|
||||
}
|
||||
|
||||
// UnpinMessage deletes a pinned message in a channel.
|
||||
//
|
||||
// Requires the MANAGE_MESSAGES permission.
|
||||
func (c *Client) UnpinMessage(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointChannels+channelID.String()+"/pins/"+messageID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
func (c *Client) UnpinMessage(channelID discord.ChannelID, messageID discord.MessageID) error {
|
||||
return c.FastRequest("DELETE", EndpointChannels+channelID.String()+"/pins/"+messageID.String())
|
||||
}
|
||||
|
||||
// AddRecipient adds a user to a group direct message. As accessToken is
|
||||
// needed, clearly this endpoint should only be used for OAuth. AccessToken can
|
||||
// be obtained with the "gdm.join" scope.
|
||||
func (c *Client) AddRecipient(
|
||||
channelID discord.ChannelID, userID discord.UserID, accessToken, nickname string) error {
|
||||
// AddRecipient adds a user to a group direct message. As accessToken is needed,
|
||||
// clearly this endpoint should only be used for OAuth. AccessToken can be
|
||||
// obtained with the "gdm.join" scope.
|
||||
func (c *Client) AddRecipient(channelID discord.ChannelID, userID discord.UserID, accessToken, nickname string) error {
|
||||
|
||||
var params struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
|
@ -363,7 +258,7 @@ func (c *Client) RemoveRecipient(channelID discord.ChannelID, userID discord.Use
|
|||
|
||||
// Ack is the read state of a channel. This is undocumented.
|
||||
type Ack struct {
|
||||
Token *string `json:"token"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// Ack marks the read state of a channel. This is undocumented. The method will
|
||||
|
@ -376,259 +271,3 @@ func (c *Client) Ack(channelID discord.ChannelID, messageID discord.MessageID, a
|
|||
httputil.WithJSONBody(ack),
|
||||
)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#start-thread-with-message-json-params
|
||||
// and
|
||||
// https://discord.com/developers/docs/resources/channel#start-thread-without-message-json-params
|
||||
type StartThreadData struct {
|
||||
// Name is the 1-100 character channel name.
|
||||
Name string `json:"name"`
|
||||
// AutoArchiveDuration is the duration in minutes to automatically archive
|
||||
// the thread after recent activity.
|
||||
//
|
||||
// Note that the three and seven day archive durations require the server
|
||||
// to be boosted.
|
||||
AutoArchiveDuration discord.ArchiveDuration `json:"auto_archive_duration"`
|
||||
// Type is the type of thread to create.
|
||||
//
|
||||
// This field can only be used when starting a thread without a message
|
||||
Type discord.ChannelType `json:"type,omitempty"` // we can omit, since thread types start at 10
|
||||
// Invitable specifies whether non-moderators can add other
|
||||
// non-moderators to a thread; only available on private threads.
|
||||
//
|
||||
// This field can only be used when starting a thread without a message
|
||||
Invitable bool `json:"invitable,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// StartThreadWithMessage creates a new thread from an existing message.
|
||||
//
|
||||
// When called on a GUILD_TEXT channel, creates a GUILD_PUBLIC_THREAD. When
|
||||
// called on a GUILD_NEWS channel, creates a GUILD_NEWS_THREAD. The id of the
|
||||
// created thread will be the same as the id of the message, and as such a
|
||||
// message can only have a single thread created from it.
|
||||
//
|
||||
// Fires a Thread Create Gateway event.
|
||||
func (c *Client) StartThreadWithMessage(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, data StartThreadData) (*discord.Channel, error) {
|
||||
|
||||
data.Type = 0
|
||||
|
||||
var ch *discord.Channel
|
||||
return ch, c.RequestJSON(
|
||||
&ch, "POST",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+"/threads",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
// StartThreadWithoutMessage creates a new thread that is not connected to an
|
||||
// existing message.
|
||||
//
|
||||
// Fires a Thread Create Gateway event.
|
||||
func (c *Client) StartThreadWithoutMessage(
|
||||
channelID discord.ChannelID, data StartThreadData) (*discord.Channel, error) {
|
||||
|
||||
var ch *discord.Channel
|
||||
return ch, c.RequestJSON(
|
||||
&ch, "POST",
|
||||
EndpointChannels+channelID.String()+"/threads",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
// JoinThread adds the current user to a thread. Also requires the thread is
|
||||
// not archived.
|
||||
//
|
||||
// Fires a Thread Members Update Gateway event.
|
||||
func (c *Client) JoinThread(threadID discord.ChannelID) error {
|
||||
return c.FastRequest("PUT", EndpointChannels+threadID.String()+"/thread-members/@me")
|
||||
}
|
||||
|
||||
// AddThreadMember adds another member to a thread. Requires the ability to
|
||||
// send messages in the thread. Also requires the thread is not archived.
|
||||
//
|
||||
// Fires a Thread Members Update Gateway event.
|
||||
func (c *Client) AddThreadMember(threadID discord.ChannelID, userID discord.UserID) error {
|
||||
return c.FastRequest(
|
||||
"PUT",
|
||||
EndpointChannels+threadID.String()+"/thread-members/"+userID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// LeaveThread removes the current user from a thread. Also requires the thread
|
||||
// is not archived.
|
||||
//
|
||||
// Fires a Thread Members Update Gateway event.
|
||||
func (c *Client) LeaveThread(threadID discord.ChannelID) error {
|
||||
return c.FastRequest("DELETE", EndpointChannels+threadID.String()+"/thread-members/@me")
|
||||
}
|
||||
|
||||
// RemoveThreadMember removes another member from a thread. Requires the
|
||||
// MANAGE_THREADS permission, or the creator of the thread if it is a
|
||||
// discord.GuildPrivateThread. Also requires the thread is not archived.
|
||||
//
|
||||
// Fires a Thread Members Update Gateway event.
|
||||
func (c *Client) RemoveThreadMember(threadID discord.ChannelID, userID discord.UserID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointChannels+threadID.String()+"/thread-members/"+userID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// ThreadMembers list all members of the thread.
|
||||
//
|
||||
// This endpoint is restricted according to whether the GUILD_MEMBERS
|
||||
// Privileged Intent is enabled for your application.
|
||||
func (c *Client) ThreadMembers(threadID discord.ChannelID) ([]discord.ThreadMember, error) {
|
||||
var m []discord.ThreadMember
|
||||
return m, c.RequestJSON(&m, "GET", EndpointChannels+threadID.String()+"/thread-members")
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#list-active-threads-response-body
|
||||
type ActiveThreads struct {
|
||||
// Threads are the active threads, ordered by descending ID.
|
||||
Threads []discord.Channel `json:"threads"`
|
||||
// Members contains a thread member for each of the Threads the current
|
||||
// user has joined.
|
||||
Members []discord.ThreadMember `json:"members"`
|
||||
}
|
||||
|
||||
// ActiveThreads returns all the active threads in the guild, including public
|
||||
// and private threads.
|
||||
func (c *Client) ActiveThreads(guildID discord.GuildID) (*ActiveThreads, error) {
|
||||
var t *ActiveThreads
|
||||
return t, c.RequestJSON(&t, "GET", EndpointGuilds+guildID.String()+"/threads/active")
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#list-public-archived-threads-response-body
|
||||
// and
|
||||
// https://discord.com/developers/docs/resources/channel#list-private-archived-threads-response-body
|
||||
// and
|
||||
// https://discord.com/developers/docs/resources/channel#list-private-archived-threads-response-body
|
||||
type ArchivedThreads struct {
|
||||
ActiveThreads
|
||||
// More specifies whether there are potentially additional threads that
|
||||
// could be returned on a subsequent call.
|
||||
More bool `json:"has_more"`
|
||||
}
|
||||
|
||||
// PublicArchivedThreads returns archived threads in the channel that are
|
||||
// public.
|
||||
//
|
||||
// When called on a GUILD_TEXT channel, returns threads of type
|
||||
// GUILD_PUBLIC_THREAD. When called on a GUILD_NEWS channel returns threads of
|
||||
// type GUILD_NEWS_THREAD.
|
||||
//
|
||||
// Threads are ordered by ArchiveTimestamp, in descending order.
|
||||
//
|
||||
// Requires the READ_MESSAGE_HISTORY permission.
|
||||
func (c *Client) PublicArchivedThreads(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
|
||||
var param struct {
|
||||
Before string `schema:"before,omitempty"`
|
||||
Limit uint `schema:"limit,omitempty"`
|
||||
}
|
||||
|
||||
if before.IsValid() {
|
||||
param.Before = before.Format(discord.TimestampFormat)
|
||||
}
|
||||
param.Limit = limit
|
||||
|
||||
var t *ArchivedThreads
|
||||
return t, c.RequestJSON(
|
||||
&t, "GET",
|
||||
EndpointChannels+channelID.String()+"/threads/archived/public",
|
||||
httputil.WithSchema(c, param),
|
||||
)
|
||||
}
|
||||
|
||||
// PrivateArchivedThreads returns archived threads in the channel that are of
|
||||
// type GUILD_PRIVATE_THREAD.
|
||||
//
|
||||
// Threads are ordered by ArchiveTimestamp, in descending order.
|
||||
//
|
||||
// Requires both the READ_MESSAGE_HISTORY and MANAGE_THREADS permissions.
|
||||
func (c *Client) PrivateArchivedThreads(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
|
||||
var param struct {
|
||||
Before string `schema:"before,omitempty"`
|
||||
Limit uint `schema:"limit,omitempty"`
|
||||
}
|
||||
|
||||
if before.IsValid() {
|
||||
param.Before = before.Format(discord.TimestampFormat)
|
||||
}
|
||||
param.Limit = limit
|
||||
|
||||
var t *ArchivedThreads
|
||||
return t, c.RequestJSON(
|
||||
&t, "GET",
|
||||
EndpointChannels+channelID.String()+"/threads/archived/private",
|
||||
httputil.WithSchema(c, param),
|
||||
)
|
||||
}
|
||||
|
||||
// JoinedPrivateArchivedThreads returns archived threads in the channel that are
|
||||
// of type GUILD_PRIVATE_THREAD, and the user has joined.
|
||||
//
|
||||
// Threads are ordered by their ID, in descending order.
|
||||
//
|
||||
// Requires the READ_MESSAGE_HISTORY permission
|
||||
func (c *Client) JoinedPrivateArchivedThreads(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
|
||||
var param struct {
|
||||
Before string `schema:"before,omitempty"`
|
||||
Limit uint `schema:"limit,omitempty"`
|
||||
}
|
||||
|
||||
if before.IsValid() {
|
||||
param.Before = before.Format(discord.TimestampFormat)
|
||||
}
|
||||
param.Limit = limit
|
||||
|
||||
var t *ArchivedThreads
|
||||
return t, c.RequestJSON(
|
||||
&t, "GET",
|
||||
EndpointChannels+channelID.String()+"/users/@me/threads/archived/private",
|
||||
httputil.WithSchema(c, param),
|
||||
)
|
||||
}
|
||||
|
||||
// PublicArchivedThreadsBefore returns archived threads in the channel that are
|
||||
// public.
|
||||
//
|
||||
// Deprecated: Use PublicArchivedThreads instead.
|
||||
func (c *Client) PublicArchivedThreadsBefore(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
return c.PublicArchivedThreads(channelID, before, limit)
|
||||
}
|
||||
|
||||
// PrivateArchivedThreadsBefore returns archived threads in the channel that
|
||||
// are of type GUILD_PRIVATE_THREAD.
|
||||
//
|
||||
// Deprecated: Use PrivateArchivedThreads instead.
|
||||
func (c *Client) PrivateArchivedThreadsBefore(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
return c.PrivateArchivedThreads(channelID, before, limit)
|
||||
}
|
||||
|
||||
// JoinedPrivateArchivedThreadsBefore returns archived threads in the channel
|
||||
// that are of type GUILD_PRIVATE_THREAD, and the user has joined.
|
||||
//
|
||||
// Deprecated: Use JoinedPrivateArchivedThreads instead.
|
||||
func (c *Client) JoinedPrivateArchivedThreadsBefore(
|
||||
channelID discord.ChannelID,
|
||||
before discord.Timestamp, limit uint) (*ArchivedThreads, error) {
|
||||
return c.JoinedPrivateArchivedThreads(channelID, before, limit)
|
||||
}
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
package cmdroute
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// BulkCommandsOverwriter is an interface that allows to overwrite all commands
|
||||
// at once. Everything *api.Client will implement this interface, including
|
||||
// *state.State.
|
||||
type BulkCommandsOverwriter interface {
|
||||
CurrentApplication() (*discord.Application, error)
|
||||
BulkOverwriteCommands(appID discord.AppID, cmds []api.CreateCommandData) ([]discord.Command, error)
|
||||
}
|
||||
|
||||
var _ BulkCommandsOverwriter = (*api.Client)(nil)
|
||||
|
||||
// OverwriteCommands overwrites all commands for the current application.
|
||||
func OverwriteCommands(client BulkCommandsOverwriter, cmds []api.CreateCommandData) error {
|
||||
app, err := client.CurrentApplication()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "cannot get current app ID")
|
||||
}
|
||||
|
||||
_, err = client.BulkOverwriteCommands(app.ID, cmds)
|
||||
return errors.Wrap(err, "cannot overwrite commands")
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
package cmdroute
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
)
|
||||
|
||||
// InteractionHandler is similar to webhook.InteractionHandler, but it also
|
||||
// includes a context.
|
||||
type InteractionHandler interface {
|
||||
// HandleInteraction is expected to return a response synchronously, either
|
||||
// to be followed-up later by deferring the response or to be responded
|
||||
// immediately.
|
||||
HandleInteraction(context.Context, *discord.InteractionEvent) *api.InteractionResponse
|
||||
}
|
||||
|
||||
// InteractionHandlerFunc is a function that implements InteractionHandler.
|
||||
type InteractionHandlerFunc func(context.Context, *discord.InteractionEvent) *api.InteractionResponse
|
||||
|
||||
var _ InteractionHandler = InteractionHandlerFunc(nil)
|
||||
|
||||
// HandleInteraction implements InteractionHandler.
|
||||
func (f InteractionHandlerFunc) HandleInteraction(ctx context.Context, e *discord.InteractionEvent) *api.InteractionResponse {
|
||||
return f(ctx, e)
|
||||
}
|
||||
|
||||
// Middleware is a function type that wraps a Handler. It can be used as a
|
||||
// middleware for the handler.
|
||||
type Middleware = func(next InteractionHandler) InteractionHandler
|
||||
|
||||
/*
|
||||
* Command
|
||||
*/
|
||||
|
||||
// CommandData is passed to a CommandHandler's HandleCommand method.
|
||||
type CommandData struct {
|
||||
discord.CommandInteractionOption
|
||||
Event *discord.InteractionEvent
|
||||
}
|
||||
|
||||
// CommandHandler is a slash command handler.
|
||||
type CommandHandler interface {
|
||||
// HandleCommand is expected to return a response synchronously, either to
|
||||
// be followed-up later by deferring the response or to be responded
|
||||
// immediately.
|
||||
//
|
||||
// All HandleCommand invocations are given a 3-second deadline. If the
|
||||
// handler does not return a response within the deadline, the response will
|
||||
// be automatically deferred in a goroutine, and the returned response will
|
||||
// be sent to the user through the API instead.
|
||||
HandleCommand(ctx context.Context, data CommandData) *api.InteractionResponseData
|
||||
}
|
||||
|
||||
// CommandHandlerFunc is a function that implements CommandHandler.
|
||||
type CommandHandlerFunc func(ctx context.Context, data CommandData) *api.InteractionResponseData
|
||||
|
||||
var _ CommandHandler = CommandHandlerFunc(nil)
|
||||
|
||||
// HandleCommand implements CommandHandler.
|
||||
func (f CommandHandlerFunc) HandleCommand(ctx context.Context, data CommandData) *api.InteractionResponseData {
|
||||
return f(ctx, data)
|
||||
}
|
||||
|
||||
/*
|
||||
* Autocomplete
|
||||
*/
|
||||
|
||||
// AutocompleteData is passed to an Autocompleter's Autocomplete method.
|
||||
type AutocompleteData struct {
|
||||
discord.AutocompleteOption
|
||||
Event *discord.InteractionEvent
|
||||
}
|
||||
|
||||
// Autocompleter is a type for an autocompleter.
|
||||
type Autocompleter interface {
|
||||
// Autocomplete is expected to return a list of choices synchronously.
|
||||
// If nil is returned, then no responses will be sent. The function must
|
||||
// return an empty slice if there are no choices.
|
||||
Autocomplete(ctx context.Context, data AutocompleteData) api.AutocompleteChoices
|
||||
}
|
||||
|
||||
// AutocompleterFunc is a function that implements the Autocompleter interface.
|
||||
type AutocompleterFunc func(ctx context.Context, data AutocompleteData) api.AutocompleteChoices
|
||||
|
||||
var _ Autocompleter = (AutocompleterFunc)(nil)
|
||||
|
||||
// Autocomplete implements webhook.InteractionHandler.
|
||||
func (f AutocompleterFunc) Autocomplete(ctx context.Context, data AutocompleteData) api.AutocompleteChoices {
|
||||
return f(ctx, data)
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
package cmdroute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
)
|
||||
|
||||
type ctxKey uint8
|
||||
|
||||
const (
|
||||
_ ctxKey = iota
|
||||
ctxCtx
|
||||
deferTicketCtx
|
||||
)
|
||||
|
||||
// UseContext returns a middleware that override the handler context to the
|
||||
// given context. This middleware should only be used once in the parent-most
|
||||
// router.
|
||||
func UseContext(ctx context.Context) Middleware {
|
||||
return func(next InteractionHandler) InteractionHandler {
|
||||
return InteractionHandlerFunc(func(_ context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
return next.HandleInteraction(ctx, ev)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// FollowUpSender is a type that can send follow-up messages. Usually, anything
|
||||
// that extends *api.Client can be used as a FollowUpSender.
|
||||
type FollowUpSender interface {
|
||||
FollowUpInteraction(appID discord.AppID, token string, data api.InteractionResponseData) (*discord.Message, error)
|
||||
}
|
||||
|
||||
// DeferOpts is the options for Deferrable().
|
||||
type DeferOpts struct {
|
||||
// Timeout is the timeout for the handler to return a response. If the
|
||||
// handler does not return within this timeout, then it is deferred.
|
||||
//
|
||||
// Defaults to 1.5 seconds.
|
||||
Timeout time.Duration
|
||||
// Flags is the flags to set on the response.
|
||||
Flags discord.MessageFlags
|
||||
// Error is called when a follow-up message fails to send. If nil, it does
|
||||
// nothing.
|
||||
Error func(err error)
|
||||
// Done is called when the handler is done. If nil, it does nothing.
|
||||
Done func(*discord.Message)
|
||||
}
|
||||
|
||||
// Deferrable marks a router as deferrable, meaning if the handler does not
|
||||
// return a response within the deadline, the response will be automatically
|
||||
// deferred.
|
||||
func Deferrable(client FollowUpSender, opts DeferOpts) Middleware {
|
||||
if opts.Timeout == 0 {
|
||||
opts.Timeout = 1*time.Second + 500*time.Millisecond
|
||||
}
|
||||
|
||||
return func(next InteractionHandler) InteractionHandler {
|
||||
return InteractionHandlerFunc(func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
timeout, cancel := context.WithTimeout(ctx, opts.Timeout)
|
||||
defer cancel()
|
||||
|
||||
respCh := make(chan *api.InteractionResponse, 1)
|
||||
go func() {
|
||||
ctx := context.WithValue(ctx, deferTicketCtx, DeferTicket{
|
||||
ctx: timeout,
|
||||
deferFn: cancel,
|
||||
})
|
||||
|
||||
resp := next.HandleInteraction(ctx, ev)
|
||||
if resp != nil && opts.Flags > 0 {
|
||||
if resp.Data != nil {
|
||||
resp.Data.Flags = opts.Flags
|
||||
} else {
|
||||
resp.Data = &api.InteractionResponseData{
|
||||
Flags: opts.Flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
respCh <- resp
|
||||
}()
|
||||
|
||||
select {
|
||||
case resp := <-respCh:
|
||||
return resp
|
||||
case <-timeout.Done():
|
||||
go func() {
|
||||
resp := <-respCh
|
||||
if resp == nil || resp.Data == nil {
|
||||
return
|
||||
}
|
||||
m, err := client.FollowUpInteraction(ev.AppID, ev.Token, *resp.Data)
|
||||
if err != nil && opts.Error != nil {
|
||||
opts.Error(err)
|
||||
}
|
||||
if m != nil && opts.Done != nil {
|
||||
opts.Done(m)
|
||||
}
|
||||
}()
|
||||
return &api.InteractionResponse{
|
||||
Type: api.DeferredMessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Flags: opts.Flags,
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// DeferTicket is a ticket that can be used to defer a slash command. It can be
|
||||
// used to manually send a response later.
|
||||
type DeferTicket struct {
|
||||
ctx context.Context
|
||||
deferFn context.CancelFunc
|
||||
}
|
||||
|
||||
// DeferTicketFromContext returns the DeferTicket from the context. If no ticket
|
||||
// is found, it returns a zero-value ticket.
|
||||
func DeferTicketFromContext(ctx context.Context) DeferTicket {
|
||||
ticket, _ := ctx.Value(deferTicketCtx).(DeferTicket)
|
||||
return ticket
|
||||
}
|
||||
|
||||
// IsDeferred returns true if the handler has been deferred.
|
||||
func (t DeferTicket) IsDeferred() bool {
|
||||
return t.Context().Err() != nil
|
||||
}
|
||||
|
||||
// Context returns the context that is done when the handler is deferred. If
|
||||
// DeferTicket is zero-value, it returns the background context.
|
||||
func (t DeferTicket) Context() context.Context {
|
||||
if t.ctx == nil {
|
||||
return context.Background()
|
||||
}
|
||||
return t.ctx
|
||||
}
|
||||
|
||||
// Defer defers the response. If DeferTicket is zero-value, it does nothing.
|
||||
func (t DeferTicket) Defer() {
|
||||
t.deferFn()
|
||||
}
|
|
@ -1,295 +0,0 @@
|
|||
package cmdroute
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/webhook"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
)
|
||||
|
||||
// Router is a router for slash commands. A zero-value Router is a valid router.
|
||||
type Router struct {
|
||||
nodes map[string]routeNode
|
||||
mws []Middleware
|
||||
stack []*Router
|
||||
}
|
||||
|
||||
type routeNode struct {
|
||||
sub *Router
|
||||
cmd CommandHandler
|
||||
com Autocompleter
|
||||
}
|
||||
|
||||
var _ webhook.InteractionHandler = (*Router)(nil)
|
||||
|
||||
// NewRouter creates a new Router.
|
||||
func NewRouter() *Router {
|
||||
r := &Router{}
|
||||
r.init()
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *Router) init() {
|
||||
if r.stack == nil {
|
||||
r.stack = []*Router{r}
|
||||
}
|
||||
if r.nodes == nil {
|
||||
r.nodes = make(map[string]routeNode, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// Use adds a middleware to the router. The middleware is applied to all
|
||||
// subcommands and subrouters. Middlewares are applied in the order they are
|
||||
// added, with the middlewares in the parent router being applied first.
|
||||
func (r *Router) Use(mws ...Middleware) {
|
||||
r.init()
|
||||
r.mws = append(r.mws, mws...)
|
||||
}
|
||||
|
||||
// Sub creates a subrouter that handles all subcommands that are under the
|
||||
// parent command of the given name.
|
||||
func (r *Router) Sub(name string, f func(r *Router)) {
|
||||
r.init()
|
||||
|
||||
node, ok := r.nodes[name]
|
||||
if ok && node.sub == nil {
|
||||
panic("cmdroute: command " + name + " already exists")
|
||||
}
|
||||
|
||||
sub := NewRouter()
|
||||
sub.stack = append(append([]*Router(nil), r.stack...), sub)
|
||||
f(sub)
|
||||
|
||||
r.nodes[name] = routeNode{sub: sub}
|
||||
}
|
||||
|
||||
// Add registers a slash command handler for the given command name.
|
||||
func (r *Router) Add(name string, h CommandHandler) {
|
||||
r.init()
|
||||
|
||||
node, ok := r.nodes[name]
|
||||
if ok {
|
||||
panic("cmdroute: command " + name + " already exists")
|
||||
}
|
||||
|
||||
node.cmd = h
|
||||
r.nodes[name] = node
|
||||
}
|
||||
|
||||
// AddFunc is a convenience function that calls Handle with a
|
||||
// CommandHandlerFunc.
|
||||
func (r *Router) AddFunc(name string, f CommandHandlerFunc) {
|
||||
r.Add(name, f)
|
||||
}
|
||||
|
||||
// HandleInteraction implements webhook.InteractionHandler. It only handles
|
||||
// events of type CommandInteraction, otherwise nil is returned.
|
||||
func (r *Router) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
switch data := ev.Data.(type) {
|
||||
case *discord.CommandInteraction:
|
||||
return r.HandleCommand(ev, data)
|
||||
case *discord.AutocompleteInteraction:
|
||||
return r.HandleAutocompletion(ev, data)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleInteraction(ev *discord.InteractionEvent, fn InteractionHandlerFunc) *api.InteractionResponse {
|
||||
h := InteractionHandler(fn)
|
||||
|
||||
// Apply middlewares, parent last, first one added last. This ensures that
|
||||
// when we call the handler, the middlewares are applied in the order they
|
||||
// were added.
|
||||
for i := len(r.stack) - 1; i >= 0; i-- {
|
||||
r := r.stack[i]
|
||||
for j := len(r.mws) - 1; j >= 0; j-- {
|
||||
h = r.mws[j](h)
|
||||
}
|
||||
}
|
||||
|
||||
return h.HandleInteraction(context.Background(), ev)
|
||||
}
|
||||
|
||||
// HandleCommand implements CommandHandler. It applies middlewares onto the
|
||||
// handler to be executed.
|
||||
func (r *Router) HandleCommand(ev *discord.InteractionEvent, data *discord.CommandInteraction) *api.InteractionResponse {
|
||||
cmdType := discord.SubcommandOptionType
|
||||
if cmdIsGroup(data) {
|
||||
cmdType = discord.SubcommandGroupOptionType
|
||||
}
|
||||
|
||||
found, ok := r.findHandler(ev, discord.CommandInteractionOption{
|
||||
Type: cmdType,
|
||||
Name: data.Name,
|
||||
Options: data.Options,
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return found.router.handleCommand(ev, found)
|
||||
}
|
||||
|
||||
func (r *Router) handleCommand(ev *discord.InteractionEvent, found handlerData) *api.InteractionResponse {
|
||||
return r.handleInteraction(ev,
|
||||
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
data := found.handler.HandleCommand(ctx, CommandData{
|
||||
CommandInteractionOption: found.data,
|
||||
Event: ev,
|
||||
})
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: data,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func cmdIsGroup(data *discord.CommandInteraction) bool {
|
||||
for _, opt := range data.Options {
|
||||
switch opt.Type {
|
||||
case discord.SubcommandGroupOptionType, discord.SubcommandOptionType:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type handlerData struct {
|
||||
router *Router
|
||||
handler CommandHandler
|
||||
data discord.CommandInteractionOption
|
||||
}
|
||||
|
||||
func (r *Router) findHandler(ev *discord.InteractionEvent, data discord.CommandInteractionOption) (handlerData, bool) {
|
||||
node, ok := r.nodes[data.Name]
|
||||
if !ok {
|
||||
return handlerData{}, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case node.sub != nil:
|
||||
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
|
||||
break
|
||||
}
|
||||
return node.sub.findHandler(ev, data.Options[0])
|
||||
case node.cmd != nil:
|
||||
if data.Type != discord.SubcommandOptionType {
|
||||
break
|
||||
}
|
||||
return handlerData{
|
||||
router: r,
|
||||
handler: node.cmd,
|
||||
data: data,
|
||||
}, true
|
||||
}
|
||||
|
||||
return handlerData{}, false
|
||||
}
|
||||
|
||||
// AddAutocompleter registers an autocompleter for the given command name.
|
||||
func (r *Router) AddAutocompleter(name string, ac Autocompleter) {
|
||||
r.init()
|
||||
|
||||
node, ok := r.nodes[name]
|
||||
if !ok || node.cmd == nil {
|
||||
panic("cmdroute: command " + name + " does not exist or is not a (sub)command")
|
||||
}
|
||||
|
||||
node.com = ac
|
||||
r.nodes[name] = node
|
||||
}
|
||||
|
||||
// AddAutocompleterFunc is a convenience function that calls AddAutocompleter
|
||||
// with an AutocompleterFunc.
|
||||
func (r *Router) AddAutocompleterFunc(name string, f AutocompleterFunc) {
|
||||
r.AddAutocompleter(name, f)
|
||||
}
|
||||
|
||||
// HandleAutocompletion handles an autocompletion event.
|
||||
func (r *Router) HandleAutocompletion(ev *discord.InteractionEvent, data *discord.AutocompleteInteraction) *api.InteractionResponse {
|
||||
cmdType := discord.SubcommandOptionType
|
||||
if autocompIsGroup(data) {
|
||||
cmdType = discord.SubcommandGroupOptionType
|
||||
}
|
||||
|
||||
found, ok := r.findAutocompleter(ev, discord.AutocompleteOption{
|
||||
Type: cmdType,
|
||||
Name: data.Name,
|
||||
Options: data.Options,
|
||||
})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return found.router.handleAutocompletion(ev, found)
|
||||
}
|
||||
|
||||
func (r *Router) handleAutocompletion(ev *discord.InteractionEvent, found autocompleterData) *api.InteractionResponse {
|
||||
return r.handleInteraction(ev,
|
||||
func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
choices := found.handler.Autocomplete(ctx, AutocompleteData{
|
||||
AutocompleteOption: found.data,
|
||||
Event: ev,
|
||||
})
|
||||
if choices == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &api.InteractionResponse{
|
||||
Type: api.AutocompleteResult,
|
||||
Data: &api.InteractionResponseData{
|
||||
Choices: choices,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func autocompIsGroup(data *discord.AutocompleteInteraction) bool {
|
||||
for _, opt := range data.Options {
|
||||
switch opt.Type {
|
||||
case discord.SubcommandGroupOptionType, discord.SubcommandOptionType:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type autocompleterData struct {
|
||||
router *Router
|
||||
handler Autocompleter
|
||||
data discord.AutocompleteOption
|
||||
}
|
||||
|
||||
func (r *Router) findAutocompleter(ev *discord.InteractionEvent, data discord.AutocompleteOption) (autocompleterData, bool) {
|
||||
node, ok := r.nodes[data.Name]
|
||||
if !ok {
|
||||
return autocompleterData{}, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case node.sub != nil:
|
||||
if len(data.Options) != 1 || data.Type != discord.SubcommandGroupOptionType {
|
||||
break
|
||||
}
|
||||
return node.sub.findAutocompleter(ev, data.Options[0])
|
||||
case node.com != nil:
|
||||
if data.Type != discord.SubcommandOptionType {
|
||||
break
|
||||
}
|
||||
return autocompleterData{
|
||||
router: r,
|
||||
handler: node.com,
|
||||
data: data,
|
||||
}, true
|
||||
}
|
||||
|
||||
return autocompleterData{}, false
|
||||
}
|
|
@ -1,388 +0,0 @@
|
|||
package cmdroute
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
func TestRouter(t *testing.T) {
|
||||
t.Run("command", func(t *testing.T) {
|
||||
r := NewRouter()
|
||||
r.Add("test", assertHandler(t, mockOptions))
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "test",
|
||||
Options: mockOptions,
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("subcommand", func(t *testing.T) {
|
||||
r := NewRouter()
|
||||
r.Sub("test", func(r *Router) { r.Add("sub", assertHandler(t, mockOptions)) })
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "test",
|
||||
Options: []discord.CommandInteractionOption{
|
||||
{
|
||||
Name: "sub",
|
||||
Type: discord.SubcommandOptionType,
|
||||
Options: mockOptions,
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("unknown", func(t *testing.T) {
|
||||
r := NewRouter()
|
||||
r.AddFunc("test", func(ctx context.Context, data CommandData) *api.InteractionResponseData {
|
||||
t.Fatal("unexpected call")
|
||||
return nil
|
||||
})
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "unknown",
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("return", func(t *testing.T) {
|
||||
data := &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("pong"),
|
||||
}
|
||||
|
||||
r := NewRouter()
|
||||
r.AddFunc("ping", func(_ context.Context, _ CommandData) *api.InteractionResponseData {
|
||||
return data
|
||||
})
|
||||
resp := r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "ping",
|
||||
Options: mockOptions,
|
||||
}))
|
||||
|
||||
if resp.Data != data {
|
||||
t.Fatal("unexpected response")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("autocomplete", func(t *testing.T) {
|
||||
choices := []string{
|
||||
"foo",
|
||||
"bar",
|
||||
"baz",
|
||||
}
|
||||
|
||||
r := NewRouter()
|
||||
r.AddFunc("ping", func(_ context.Context, _ CommandData) *api.InteractionResponseData {
|
||||
return nil
|
||||
})
|
||||
r.AddAutocompleterFunc("ping", func(_ context.Context, comp AutocompleteData) api.AutocompleteChoices {
|
||||
var data struct {
|
||||
Str string `discord:"str"`
|
||||
}
|
||||
|
||||
if err := comp.Options.Unmarshal(&data); err != nil {
|
||||
t.Fatal("unexpected error:", err)
|
||||
}
|
||||
|
||||
switch comp.Options.Focused().Name {
|
||||
case "str":
|
||||
matches := api.AutocompleteStringChoices{}
|
||||
for _, choice := range choices {
|
||||
if strings.HasPrefix(choice, data.Str) {
|
||||
matches = append(matches, discord.StringChoice{
|
||||
Name: strings.ToUpper(choice),
|
||||
Value: choice,
|
||||
})
|
||||
}
|
||||
}
|
||||
return matches
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
assertInteractionResp(t,
|
||||
r.HandleInteraction(&discord.InteractionEvent{
|
||||
Token: "token",
|
||||
Data: &discord.AutocompleteInteraction{
|
||||
Name: "ping",
|
||||
CommandType: discord.ChatInputCommand,
|
||||
Options: []discord.AutocompleteOption{
|
||||
{
|
||||
Type: discord.StringOptionType,
|
||||
Name: "str",
|
||||
Value: json.Raw(`"b"`),
|
||||
Focused: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
&api.InteractionResponse{
|
||||
Type: api.AutocompleteResult,
|
||||
Data: &api.InteractionResponseData{
|
||||
Choices: api.AutocompleteStringChoices{
|
||||
{Name: "BAR", Value: "bar"},
|
||||
{Name: "BAZ", Value: "baz"},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("middlewares", func(t *testing.T) {
|
||||
var stack []string
|
||||
pushStack := func(s string) Middleware {
|
||||
return func(next InteractionHandler) InteractionHandler {
|
||||
return InteractionHandlerFunc(func(ctx context.Context, ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
stack = append(stack, s)
|
||||
return next.HandleInteraction(ctx, ev)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
r := NewRouter()
|
||||
r.Use(pushStack("root1"))
|
||||
r.Use(pushStack("root2"))
|
||||
r.Sub("test", func(r *Router) {
|
||||
r.Use(pushStack("sub1.1"))
|
||||
r.Use(pushStack("sub1.2"))
|
||||
r.Sub("sub1", func(r *Router) {
|
||||
r.Use(pushStack("sub2.1"))
|
||||
r.Use(pushStack("sub2.2"))
|
||||
r.Add("sub2", assertHandler(t, mockOptions))
|
||||
})
|
||||
})
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "test",
|
||||
Options: []discord.CommandInteractionOption{
|
||||
{
|
||||
Name: "sub1",
|
||||
Type: discord.SubcommandGroupOptionType,
|
||||
Options: []discord.CommandInteractionOption{
|
||||
{
|
||||
Name: "sub2",
|
||||
Type: discord.SubcommandOptionType,
|
||||
Options: mockOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
expects := []string{
|
||||
"root1",
|
||||
"root2",
|
||||
"sub1.1",
|
||||
"sub1.2",
|
||||
"sub2.1",
|
||||
"sub2.2",
|
||||
}
|
||||
if len(stack) != len(expects) {
|
||||
t.Fatalf("expected stack to have %d elements, got %d", len(expects), len(stack))
|
||||
}
|
||||
|
||||
for i := range expects {
|
||||
if stack[i] != expects[i] {
|
||||
t.Fatalf("expected stack[%d] to be %q, got %q", i, expects[i], stack[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("deferred", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := mockFollowUp(t, []followUpData{
|
||||
{
|
||||
token: "mock token",
|
||||
appID: 200,
|
||||
d: api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("pong-defer"),
|
||||
Flags: discord.EphemeralMessage,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
assertDeferred := func(t *testing.T, ctx context.Context, yes bool) {
|
||||
t.Helper()
|
||||
ticket := DeferTicketFromContext(ctx)
|
||||
if ticket.Context() == context.Background() {
|
||||
t.Error("expected ticket to be non-zero")
|
||||
}
|
||||
if ticket.IsDeferred() != yes {
|
||||
if yes {
|
||||
t.Error("expected ticket to not be deferred")
|
||||
} else {
|
||||
t.Error("expected ticket to be deferred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r := NewRouter()
|
||||
r.Use(Deferrable(client, DeferOpts{
|
||||
Timeout: 100 * time.Millisecond,
|
||||
Flags: discord.EphemeralMessage,
|
||||
Error: func(err error) { t.Error(err) },
|
||||
Done: func(*discord.Message) { wg.Done() },
|
||||
}))
|
||||
r.AddFunc("ping", func(ctx context.Context, data CommandData) *api.InteractionResponseData {
|
||||
assertDeferred(t, ctx, false)
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("pong"),
|
||||
}
|
||||
})
|
||||
r.AddFunc("ping-defer", func(ctx context.Context, data CommandData) *api.InteractionResponseData {
|
||||
assertDeferred(t, ctx, false)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
assertDeferred(t, ctx, true)
|
||||
return &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("pong-defer"),
|
||||
}
|
||||
})
|
||||
|
||||
assertInteractionResp(t,
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "ping",
|
||||
Options: mockOptions,
|
||||
})),
|
||||
&api.InteractionResponse{
|
||||
Type: api.MessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Content: option.NewNullableString("pong"),
|
||||
Flags: discord.EphemeralMessage,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
wg.Add(1)
|
||||
assertInteractionResp(t,
|
||||
r.HandleInteraction(newInteractionEvent(discord.CommandInteraction{
|
||||
ID: 4,
|
||||
Name: "ping-defer",
|
||||
Options: mockOptions,
|
||||
})),
|
||||
&api.InteractionResponse{
|
||||
Type: api.DeferredMessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Flags: discord.EphemeralMessage,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
wg.Wait()
|
||||
})
|
||||
}
|
||||
|
||||
func newInteractionEvent(data discord.CommandInteraction) *discord.InteractionEvent {
|
||||
return &discord.InteractionEvent{
|
||||
ID: 100,
|
||||
AppID: 200,
|
||||
ChannelID: 300,
|
||||
Token: "mock token",
|
||||
Data: &data,
|
||||
}
|
||||
}
|
||||
|
||||
var mockOptions = []discord.CommandInteractionOption{
|
||||
{
|
||||
Name: "value1",
|
||||
Type: discord.NumberOptionType,
|
||||
Value: json.Raw("1"),
|
||||
},
|
||||
{
|
||||
Name: "value2",
|
||||
Type: discord.StringOptionType,
|
||||
Value: json.Raw("\"2\""),
|
||||
},
|
||||
}
|
||||
|
||||
func assertHandler(t *testing.T, opts discord.CommandInteractionOptions) CommandHandler {
|
||||
return CommandHandlerFunc(func(ctx context.Context, data CommandData) *api.InteractionResponseData {
|
||||
if len(data.Options) != len(opts) {
|
||||
t.Fatalf("expected %d options, got %d", len(opts), len(data.Options))
|
||||
}
|
||||
|
||||
for i, opt := range opts {
|
||||
if data.Options[i].Name != opt.Name {
|
||||
t.Fatalf("expected option %d to be %q, got %q", i, opt.Name, data.Options[i].Name)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data.Options[i].Value, opt.Value) {
|
||||
t.Fatalf("expected option %d to be %q, got %q", i, opt.Value, data.Options[i].Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
type mockedFollowUpSender struct {
|
||||
t *testing.T
|
||||
d []followUpData
|
||||
}
|
||||
|
||||
type followUpData struct {
|
||||
appID discord.AppID
|
||||
token string
|
||||
d api.InteractionResponse
|
||||
}
|
||||
|
||||
func mockFollowUp(t *testing.T, data []followUpData) *mockedFollowUpSender {
|
||||
return &mockedFollowUpSender{
|
||||
t: t,
|
||||
d: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockedFollowUpSender) FollowUpInteraction(appID discord.AppID, token string, d api.InteractionResponseData) (*discord.Message, error) {
|
||||
expect := m.d[0]
|
||||
m.d = m.d[1:]
|
||||
|
||||
if appID != expect.appID {
|
||||
m.t.Errorf("expected appID to be %d, got %d", expect.appID, appID)
|
||||
}
|
||||
|
||||
if token != expect.token {
|
||||
m.t.Errorf("expected token to be %q, got %q", expect.token, token)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(d, *expect.d.Data) {
|
||||
m.t.Errorf("unexpected interaction data\n"+
|
||||
"expected: %#v\n"+
|
||||
"got: %#v", expect.d.Data, d)
|
||||
}
|
||||
|
||||
return &discord.Message{}, nil
|
||||
}
|
||||
|
||||
func assertInteractionResp(t *testing.T, got, expect *api.InteractionResponse) {
|
||||
if !reflect.DeepEqual(got, expect) {
|
||||
t.Fatalf("unexpected interaction\n"+
|
||||
"expected: %s\n"+
|
||||
"got: %s",
|
||||
strInteractionResp(expect),
|
||||
strInteractionResp(got))
|
||||
}
|
||||
}
|
||||
|
||||
func strInteractionResp(resp *api.InteractionResponse) string {
|
||||
if resp == nil {
|
||||
return "(*api.InteractionResponse)(nil)"
|
||||
}
|
||||
return fmt.Sprintf("%d:%#v", resp.Type, resp.Data)
|
||||
}
|
51
api/emoji.go
51
api/emoji.go
|
@ -1,10 +1,21 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
)
|
||||
|
||||
// Emoji is the API format of a regular Emoji, both Unicode or custom. This
|
||||
// could usually be formatted by calling (discord.Emoji).APIString().
|
||||
type Emoji = string
|
||||
|
||||
// NewCustomEmoji creates a new Emoji using a custom guild emoji as
|
||||
// base.
|
||||
// Unicode emojis should be directly passed to the function using Emoji.
|
||||
func NewCustomEmoji(id discord.EmojiID, name string) Emoji {
|
||||
return name + ":" + id.String()
|
||||
}
|
||||
|
||||
// Emojis returns a list of emoji objects for the given guild.
|
||||
func (c *Client) Emojis(guildID discord.GuildID) ([]discord.Emoji, error) {
|
||||
var e []discord.Emoji
|
||||
|
@ -24,20 +35,16 @@ type CreateEmojiData struct {
|
|||
Name string `json:"name"`
|
||||
// Image is the the 128x128 emoji image.
|
||||
Image Image `json:"image"`
|
||||
// Roles are the roles that can use the emoji.
|
||||
// Roles are the roles for which this emoji will be whitelisted.
|
||||
Roles *[]discord.RoleID `json:"roles,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// CreateEmoji creates a new emoji in the guild. This endpoint requires
|
||||
// MANAGE_EMOJIS. ContentType must be "image/jpeg", "image/png", or
|
||||
// "image/gif". However, ContentType can also be automatically detected (though
|
||||
// shouldn't be relied on).
|
||||
//
|
||||
// "image/gif". However, ContentType can also be automatically detected
|
||||
// (though shouldn't be relied on).
|
||||
// Emojis and animated emojis have a maximum file size of 256kb.
|
||||
func (c *Client) CreateEmoji(
|
||||
guildID discord.GuildID, data CreateEmojiData) (*discord.Emoji, error) {
|
||||
func (c *Client) CreateEmoji(guildID discord.GuildID, data CreateEmojiData) (*discord.Emoji, error) {
|
||||
|
||||
// Max 256KB
|
||||
if err := data.Image.Validate(256 * 1000); err != nil {
|
||||
|
@ -48,7 +55,7 @@ func (c *Client) CreateEmoji(
|
|||
return emj, c.RequestJSON(
|
||||
&emj, "POST",
|
||||
EndpointGuilds+guildID.String()+"/emojis",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -56,36 +63,26 @@ func (c *Client) CreateEmoji(
|
|||
type ModifyEmojiData struct {
|
||||
// Name is the name of the emoji.
|
||||
Name string `json:"name,omitempty"`
|
||||
// Roles are the roles that can use the emoji.
|
||||
// Roles are the roles to which this emoji will be whitelisted.
|
||||
Roles *[]discord.RoleID `json:"roles,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyEmoji changes an existing emoji. This requires MANAGE_EMOJIS. Name and
|
||||
// roles are optional fields (though you'd want to change either though).
|
||||
//
|
||||
// Fires a Guild Emojis Update Gateway event.
|
||||
func (c *Client) ModifyEmoji(
|
||||
guildID discord.GuildID, emojiID discord.EmojiID, data ModifyEmojiData) error {
|
||||
|
||||
func (c *Client) ModifyEmoji(guildID discord.GuildID, emojiID discord.EmojiID, data ModifyEmojiData) error {
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointGuilds+guildID.String()+"/emojis/"+emojiID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteEmoji deletes the given emoji.
|
||||
// Delete the given emoji.
|
||||
//
|
||||
// Requires the MANAGE_EMOJIS permission.
|
||||
//
|
||||
// Fires a Guild Emojis Update Gateway event.
|
||||
func (c *Client) DeleteEmoji(
|
||||
guildID discord.GuildID, emojiID discord.EmojiID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointGuilds+guildID.String()+"/emojis/"+emojiID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
func (c *Client) DeleteEmoji(guildID discord.GuildID, emojiID discord.EmojiID) error {
|
||||
return c.FastRequest("DELETE", EndpointGuilds+guildID.String()+"/emojis/"+emojiID.String())
|
||||
}
|
||||
|
|
144
api/guild.go
144
api/guild.go
|
@ -4,19 +4,18 @@ import (
|
|||
"io"
|
||||
"net/url"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord" // for clarity
|
||||
"github.com/diamondburned/arikawa/v3/internal/intmath"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord" // for clarity
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
// MaxGuildFetchLimit is the limit of max guilds per request, as imposed by
|
||||
// maxGuildFetchLimit is the limit of max guilds per request, as imposed by
|
||||
// Discord.
|
||||
const MaxGuildFetchLimit = 100
|
||||
const maxGuildFetchLimit = 100
|
||||
|
||||
var EndpointGuilds = Endpoint + "guilds/"
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#create-guild-json-params
|
||||
// https://discordapp.com/developers/docs/resources/guild#create-guild-json-params
|
||||
type CreateGuildData struct {
|
||||
// Name is the name of the guild (2-100 characters)
|
||||
Name string `json:"name"`
|
||||
|
@ -62,7 +61,7 @@ type CreateGuildData struct {
|
|||
// AFKChannelID is the id for the afk channel.
|
||||
AFKChannelID discord.ChannelID `json:"afk_channel_id,omitempty"`
|
||||
// AFKTimeout is the afk timeout in seconds.
|
||||
AFKTimeout discord.OptionalSeconds `json:"afk_timeout,omitempty"`
|
||||
AFKTimeout option.Seconds `json:"afk_timeout,omitempty"`
|
||||
|
||||
// SystemChannelID is the id of the channel where guild notices such as
|
||||
// welcome messages and boost events are posted.
|
||||
|
@ -79,7 +78,6 @@ func (c *Client) CreateGuild(data CreateGuildData) (*discord.Guild, error) {
|
|||
}
|
||||
|
||||
// Guild returns the guild object for the given id.
|
||||
//
|
||||
// ApproximateMembers and ApproximatePresences will not be set.
|
||||
func (c *Client) Guild(id discord.GuildID) (*discord.Guild, error) {
|
||||
var g *discord.Guild
|
||||
|
@ -95,9 +93,9 @@ func (c *Client) GuildPreview(id discord.GuildID) (*discord.GuildPreview, error)
|
|||
return g, c.RequestJSON(&g, "GET", EndpointGuilds+id.String()+"/preview")
|
||||
}
|
||||
|
||||
// GuildWithCount returns the guild object for the given id. This will also
|
||||
// set the ApproximateMembers and ApproximatePresences fields of the guild
|
||||
// struct.
|
||||
// GuildWithCount returns the guild object for the given id.
|
||||
// This will also set the ApproximateMembers and ApproximatePresences fields
|
||||
// of the guild struct.
|
||||
func (c *Client) GuildWithCount(id discord.GuildID) (*discord.Guild, error) {
|
||||
var g *discord.Guild
|
||||
return g, c.RequestJSON(
|
||||
|
@ -141,15 +139,17 @@ func (c *Client) Guilds(limit uint) ([]discord.Guild, error) {
|
|||
func (c *Client) GuildsBefore(before discord.GuildID, limit uint) ([]discord.Guild, error) {
|
||||
guilds := make([]discord.Guild, 0, limit)
|
||||
|
||||
fetch := uint(MaxGuildFetchLimit)
|
||||
fetch := uint(maxGuildFetchLimit)
|
||||
|
||||
unlimited := limit == 0
|
||||
|
||||
for limit > 0 || unlimited {
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(MaxGuildFetchLimit, int(limit)))
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= fetch
|
||||
}
|
||||
|
||||
|
@ -159,7 +159,7 @@ func (c *Client) GuildsBefore(before discord.GuildID, limit uint) ([]discord.Gui
|
|||
}
|
||||
guilds = append(g, guilds...)
|
||||
|
||||
if len(g) < MaxGuildFetchLimit {
|
||||
if len(g) < maxGuildFetchLimit {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -186,15 +186,17 @@ func (c *Client) GuildsBefore(before discord.GuildID, limit uint) ([]discord.Gui
|
|||
func (c *Client) GuildsAfter(after discord.GuildID, limit uint) ([]discord.Guild, error) {
|
||||
guilds := make([]discord.Guild, 0, limit)
|
||||
|
||||
fetch := uint(MaxGuildFetchLimit)
|
||||
fetch := uint(maxGuildFetchLimit)
|
||||
|
||||
unlimited := limit == 0
|
||||
|
||||
for limit > 0 || unlimited {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(MaxGuildFetchLimit, int(limit)))
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= fetch
|
||||
}
|
||||
|
||||
|
@ -204,7 +206,7 @@ func (c *Client) GuildsAfter(after discord.GuildID, limit uint) ([]discord.Guild
|
|||
}
|
||||
guilds = append(guilds, g...)
|
||||
|
||||
if len(g) < MaxGuildFetchLimit {
|
||||
if len(g) < maxGuildFetchLimit {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -218,7 +220,9 @@ func (c *Client) GuildsAfter(after discord.GuildID, limit uint) ([]discord.Guild
|
|||
return guilds, nil
|
||||
}
|
||||
|
||||
func (c *Client) guildsRange(before, after discord.GuildID, limit uint) ([]discord.Guild, error) {
|
||||
func (c *Client) guildsRange(
|
||||
before, after discord.GuildID, limit uint) ([]discord.Guild, error) {
|
||||
|
||||
var param struct {
|
||||
Before discord.GuildID `schema:"before,omitempty"`
|
||||
After discord.GuildID `schema:"after,omitempty"`
|
||||
|
@ -243,7 +247,7 @@ func (c *Client) LeaveGuild(id discord.GuildID) error {
|
|||
return c.FastRequest("DELETE", EndpointMe+"/guilds/"+id.String())
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#modify-guild-json-params
|
||||
// https://discordapp.com/developers/docs/resources/guild#modify-guild-json-params
|
||||
type ModifyGuildData struct {
|
||||
// Name is the guild's name.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
@ -268,7 +272,7 @@ type ModifyGuildData struct {
|
|||
// This field is nullable.
|
||||
AFKChannelID discord.ChannelID `json:"afk_channel_id,string,omitempty"`
|
||||
// AFKTimeout is the afk timeout in seconds.
|
||||
AFKTimeout discord.OptionalSeconds `json:"afk_timeout,omitempty"`
|
||||
AFKTimeout option.Seconds `json:"afk_timeout,omitempty"`
|
||||
// Icon is the base64 1024x1024 png/jpeg/gif image for the guild icon
|
||||
// (can be animated gif when the server has the ANIMATED_ICON feature).
|
||||
Icon *Image `json:"icon,omitempty"`
|
||||
|
@ -303,21 +307,16 @@ type ModifyGuildData struct {
|
|||
//
|
||||
// This defaults to "en-US".
|
||||
PreferredLocale option.NullableString `json:"preferred_locale,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyGuild modifies a guild's settings.
|
||||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
//
|
||||
// ModifyGuild modifies a guild's settings. Requires the MANAGE_GUILD permission.
|
||||
// Fires a Guild Update Gateway event.
|
||||
func (c *Client) ModifyGuild(id discord.GuildID, data ModifyGuildData) (*discord.Guild, error) {
|
||||
var g *discord.Guild
|
||||
return g, c.RequestJSON(
|
||||
&g, "PATCH",
|
||||
EndpointGuilds+id.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
|
||||
}
|
||||
|
@ -329,7 +328,7 @@ func (c *Client) DeleteGuild(id discord.GuildID) error {
|
|||
return c.FastRequest("DELETE", EndpointGuilds+id.String())
|
||||
}
|
||||
|
||||
// VoiceRegionsGuild is the same as /voice, but returns VIP ones as well if
|
||||
// GuildVoiceRegions is the same as /voice, but returns VIP ones as well if
|
||||
// available.
|
||||
func (c *Client) VoiceRegionsGuild(guildID discord.GuildID) ([]discord.VoiceRegion, error) {
|
||||
var vrs []discord.VoiceRegion
|
||||
|
@ -373,7 +372,6 @@ func (c *Client) AuditLog(guildID discord.GuildID, data AuditLogData) (*discord.
|
|||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
func (c *Client) Integrations(guildID discord.GuildID) ([]discord.Integration, error) {
|
||||
|
||||
var ints []discord.Integration
|
||||
return ints, c.RequestJSON(&ints, "GET", EndpointGuilds+guildID.String()+"/integrations")
|
||||
}
|
||||
|
@ -382,11 +380,10 @@ func (c *Client) Integrations(guildID discord.GuildID) ([]discord.Integration, e
|
|||
// the guild.
|
||||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
//
|
||||
// Fires a Guild Integrations Update Gateway event.
|
||||
func (c *Client) AttachIntegration(
|
||||
guildID discord.GuildID,
|
||||
integrationID discord.IntegrationID, integrationType discord.Service) error {
|
||||
guildID discord.GuildID, integrationID discord.IntegrationID,
|
||||
integrationType discord.Service) error {
|
||||
|
||||
var param struct {
|
||||
Type discord.Service `json:"type"`
|
||||
|
@ -422,9 +419,7 @@ type ModifyIntegrationData struct {
|
|||
// Requires the MANAGE_GUILD permission.
|
||||
// Fires a Guild Integrations Update Gateway event.
|
||||
func (c *Client) ModifyIntegration(
|
||||
guildID discord.GuildID,
|
||||
integrationID discord.IntegrationID, data ModifyIntegrationData) error {
|
||||
|
||||
guildID discord.GuildID, integrationID discord.IntegrationID, data ModifyIntegrationData) error {
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointGuilds+guildID.String()+"/integrations/"+integrationID.String(),
|
||||
|
@ -432,118 +427,97 @@ func (c *Client) ModifyIntegration(
|
|||
)
|
||||
}
|
||||
|
||||
// SyncIntegration syncs an integration.
|
||||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
func (c *Client) SyncIntegration(
|
||||
guildID discord.GuildID, integrationID discord.IntegrationID) error {
|
||||
|
||||
// Sync an integration. Requires the MANAGE_GUILD permission.
|
||||
func (c *Client) SyncIntegration(guildID discord.GuildID, integrationID discord.IntegrationID) error {
|
||||
return c.FastRequest(
|
||||
"POST",
|
||||
EndpointGuilds+guildID.String()+"/integrations/"+integrationID.String()+"/sync",
|
||||
)
|
||||
}
|
||||
|
||||
// GuildWidgetSettings returns the guild widget object.
|
||||
// GuildWidget returns the guild widget object.
|
||||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
func (c *Client) GuildWidgetSettings(
|
||||
guildID discord.GuildID) (*discord.GuildWidgetSettings, error) {
|
||||
|
||||
var ge *discord.GuildWidgetSettings
|
||||
func (c *Client) GuildWidget(guildID discord.GuildID) (*discord.GuildWidget, error) {
|
||||
var ge *discord.GuildWidget
|
||||
return ge, c.RequestJSON(&ge, "GET", EndpointGuilds+guildID.String()+"/widget")
|
||||
}
|
||||
|
||||
// ModifyGuildWidgetData is the structure to modify a guild widget object for
|
||||
// the guild. All attributes may be passed in with JSON and modified.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild#guild-widget-object
|
||||
// https://discord.com/developers/docs/resources/guild#guild-embed-object-guild-embed-structure
|
||||
type ModifyGuildWidgetData struct {
|
||||
// Enabled specifies whether the widget is enabled.
|
||||
Enabled option.Bool `json:"enabled,omitempty"`
|
||||
// ChannelID is the widget channel ID.
|
||||
// ChannelID is the widget channel id.
|
||||
ChannelID discord.ChannelID `json:"channel_id,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyGuildWidget modifies a guild widget object for the guild.
|
||||
//
|
||||
// Requires the MANAGE_GUILD permission.
|
||||
func (c *Client) ModifyGuildWidget(
|
||||
guildID discord.GuildID, data ModifyGuildWidgetData) (*discord.GuildWidgetSettings, error) {
|
||||
guildID discord.GuildID, data ModifyGuildWidgetData) (*discord.GuildWidget, error) {
|
||||
|
||||
var w *discord.GuildWidgetSettings
|
||||
var w *discord.GuildWidget
|
||||
return w, c.RequestJSON(
|
||||
&w, "PATCH",
|
||||
EndpointGuilds+guildID.String()+"/widget",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// GuildWidget returns the widget for the guild.
|
||||
func (c *Client) GuildWidget(guildID discord.GuildID) (*discord.GuildWidget, error) {
|
||||
var w *discord.GuildWidget
|
||||
return w, c.RequestJSON(
|
||||
&w, "GET",
|
||||
EndpointGuilds+guildID.String()+"/widget.json")
|
||||
}
|
||||
|
||||
// GuildVanityInvite returns the vanity invite for guilds that have that
|
||||
// feature enabled. Only Code and Uses are filled. Code will be "" if a vanity
|
||||
// url for the guild is not set.
|
||||
// GuildVanityURL returns *Invite for guilds that have that feature enabled,
|
||||
// but only Code and Uses are filled. Code will be "" if a vanity url for the
|
||||
// guild is not set.
|
||||
//
|
||||
// Requires MANAGE_GUILD.
|
||||
func (c *Client) GuildVanityInvite(guildID discord.GuildID) (*discord.Invite, error) {
|
||||
func (c *Client) GuildVanityURL(guildID discord.GuildID) (*discord.Invite, error) {
|
||||
var inv *discord.Invite
|
||||
return inv, c.RequestJSON(&inv, "GET", EndpointGuilds+guildID.String()+"/vanity-url")
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#get-guild-widget-image-widget-style-options
|
||||
type GuildWidgetImageStyle string
|
||||
type GuildImageStyle string
|
||||
|
||||
const (
|
||||
// GuildShield is a shield style widget with Discord icon and guild members
|
||||
// online count.
|
||||
//
|
||||
// Example: https://discordapp.com/api/guilds/81384788765712384/widget.png?style=shield
|
||||
GuildShield GuildWidgetImageStyle = "shield"
|
||||
GuildShield GuildImageStyle = "shield"
|
||||
// GuildBanner1 is a large image with guild icon, name and online count.
|
||||
// "POWERED BY DISCORD" as the footer of the widget.
|
||||
//
|
||||
// Example: https://discordapp.com/api/guilds/81384788765712384/widget.png?style=banner1
|
||||
GuildBanner1 GuildWidgetImageStyle = "banner1"
|
||||
GuildBanner1 GuildImageStyle = "banner1"
|
||||
// GuildBanner2 is a smaller widget style with guild icon, name and online
|
||||
// count. Split on the right with Discord logo.
|
||||
//
|
||||
// Example: https://discordapp.com/api/guilds/81384788765712384/widget.png?style=banner2
|
||||
GuildBanner2 GuildWidgetImageStyle = "banner2"
|
||||
GuildBanner2 GuildImageStyle = "banner2"
|
||||
// GuildBanner3 is a large image with guild icon, name and online count. In
|
||||
// the footer, Discord logo on the left and "Chat Now" on the right.
|
||||
//
|
||||
// Example: https://discordapp.com/api/guilds/81384788765712384/widget.png?style=banner3
|
||||
GuildBanner3 GuildWidgetImageStyle = "banner3"
|
||||
GuildBanner3 GuildImageStyle = "banner3"
|
||||
// GuildBanner4 is a large Discord logo at the top of the widget.
|
||||
// Guild icon, name and online count in the middle portion of the widget
|
||||
// and a "JOIN MY SERVER" button at the bottom.
|
||||
//
|
||||
// Example: https://discordapp.com/api/guilds/81384788765712384/widget.png?style=banner4
|
||||
GuildBanner4 GuildWidgetImageStyle = "banner4"
|
||||
GuildBanner4 GuildImageStyle = "banner4"
|
||||
)
|
||||
|
||||
// GuildWidgetImageURL returns a link to the PNG image widget for the guild.
|
||||
// GuildImageURL returns a link to the PNG image widget for the guild.
|
||||
//
|
||||
// Requires no permissions or authentication.
|
||||
func (c *Client) GuildWidgetImageURL(guildID discord.GuildID, img GuildWidgetImageStyle) string {
|
||||
func (c *Client) GuildImageURL(guildID discord.GuildID, img GuildImageStyle) string {
|
||||
return EndpointGuilds + guildID.String() + "/widget.png?style=" + string(img)
|
||||
}
|
||||
|
||||
// GuildWidgetImage returns a PNG image widget for the guild. Requires no permissions
|
||||
// GuildImage returns a PNG image widget for the guild. Requires no permissions
|
||||
// or authentication.
|
||||
func (c *Client) GuildWidgetImage(
|
||||
guildID discord.GuildID, img GuildWidgetImageStyle) (io.ReadCloser, error) {
|
||||
|
||||
r, err := c.Request("GET", c.GuildWidgetImageURL(guildID, img))
|
||||
func (c *Client) GuildImage(guildID discord.GuildID, img GuildImageStyle) (io.ReadCloser, error) {
|
||||
r, err := c.Request("GET", c.GuildImageURL(guildID, img))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
15
api/image.go
15
api/image.go
|
@ -6,36 +6,33 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrInvalidImageCT = errors.New("unknown image content-type")
|
||||
var ErrInvalidImageData = errors.New("invalid image data")
|
||||
|
||||
type ImageTooLargeError struct {
|
||||
type ErrImageTooLarge struct {
|
||||
Size, Max int
|
||||
}
|
||||
|
||||
func (err ImageTooLargeError) Error() string {
|
||||
func (err ErrImageTooLarge) Error() string {
|
||||
return fmt.Sprintf("Image is %.02fkb, larger than %.02fkb",
|
||||
float64(err.Size)/1000, float64(err.Max)/1000)
|
||||
}
|
||||
|
||||
// Image wraps around the Data URI Scheme that Discord uses:
|
||||
// https://discord.com/developers/docs/reference#image-data
|
||||
// https://discordapp.com/developers/docs/reference#image-data
|
||||
type Image struct {
|
||||
// ContentType is optional and will be automatically detected. However, it
|
||||
// should always return "image/jpeg," "image/png" or "image/gif".
|
||||
ContentType string
|
||||
|
||||
// Just raw content of the file.
|
||||
Content []byte
|
||||
}
|
||||
|
||||
// NullImage is an *Image value that marshals to a null value. Use this to unset
|
||||
// the image. It exists mostly for documentation purposes.
|
||||
var NullImage = &Image{}
|
||||
|
||||
func DecodeImage(data []byte) (*Image, error) {
|
||||
parts := bytes.SplitN(data, []byte{';'}, 2)
|
||||
if len(parts) < 2 {
|
||||
|
@ -62,7 +59,7 @@ func DecodeImage(data []byte) (*Image, error) {
|
|||
|
||||
func (i Image) Validate(maxSize int) error {
|
||||
if maxSize > 0 && len(i.Content) > maxSize {
|
||||
return ImageTooLargeError{len(i.Content), maxSize}
|
||||
return ErrImageTooLarge{len(i.Content), maxSize}
|
||||
}
|
||||
|
||||
switch i.ContentType {
|
||||
|
|
|
@ -1,17 +1,46 @@
|
|||
// +build integration
|
||||
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/internal/testenv"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
type testConfig struct {
|
||||
BotToken string
|
||||
ChannelID discord.ChannelID
|
||||
}
|
||||
|
||||
func mustConfig(t *testing.T) testConfig {
|
||||
var token = os.Getenv("BOT_TOKEN")
|
||||
if token == "" {
|
||||
t.Fatal("Missing $BOT_TOKEN")
|
||||
}
|
||||
|
||||
var cid = os.Getenv("CHANNEL_ID")
|
||||
if cid == "" {
|
||||
t.Fatal("Missing $CHANNEL_ID")
|
||||
}
|
||||
|
||||
id, err := discord.ParseSnowflake(cid)
|
||||
if err != nil {
|
||||
t.Fatal("Invalid $CHANNEL_ID:", err)
|
||||
}
|
||||
|
||||
return testConfig{
|
||||
BotToken: token,
|
||||
ChannelID: discord.ChannelID(id),
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
cfg := testenv.Must(t)
|
||||
cfg := mustConfig(t)
|
||||
|
||||
client := NewClient("Bot " + cfg.BotToken)
|
||||
|
||||
|
@ -24,37 +53,13 @@ func TestIntegration(t *testing.T) {
|
|||
log.Println("API user:", u.Username)
|
||||
|
||||
// POST with URL param and paginator
|
||||
guilds, err := client.Guilds(100)
|
||||
_, err = client.Guilds(100)
|
||||
if err != nil {
|
||||
t.Fatal("Can't get guilds:", err)
|
||||
}
|
||||
|
||||
for _, guild := range guilds {
|
||||
if !guild.ID.IsValid() {
|
||||
t.Errorf("guild %q has invalid ID", guild.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
channels, err := client.Channels(guild.ID)
|
||||
if err != nil {
|
||||
t.Errorf(
|
||||
"failed to fetch channels for guild %q (%v): %v",
|
||||
guild.Name, guild.ID, err,
|
||||
)
|
||||
}
|
||||
|
||||
for _, ch := range channels {
|
||||
if !ch.ID.IsValid() {
|
||||
t.Errorf(
|
||||
"channel %q of guild %q (%v) has invalid ID",
|
||||
ch.Name, guild.Name, guild.ID,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var emojisToSend = [...]discord.APIEmoji{
|
||||
var emojisToSend = [...]string{
|
||||
"🥺",
|
||||
"❤",
|
||||
"😂",
|
||||
|
@ -76,14 +81,14 @@ var emojisToSend = [...]discord.APIEmoji{
|
|||
}
|
||||
|
||||
func TestReactions(t *testing.T) {
|
||||
cfg := testenv.Must(t)
|
||||
cfg := mustConfig(t)
|
||||
|
||||
client := NewClient("Bot " + cfg.BotToken)
|
||||
|
||||
msg := fmt.Sprintf("This is a message sent at %v.", time.Now())
|
||||
|
||||
// Send a new message.
|
||||
m, err := client.SendMessage(cfg.ChannelID, msg)
|
||||
m, err := client.SendMessage(cfg.ChannelID, msg, nil)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to send message:", err)
|
||||
}
|
||||
|
@ -98,7 +103,7 @@ func TestReactions(t *testing.T) {
|
|||
|
||||
msg += fmt.Sprintf(" Total time taken to send all reactions: %v.", time.Now().Sub(now))
|
||||
|
||||
m, err = client.EditMessage(cfg.ChannelID, m.ID, msg)
|
||||
m, err = client.EditMessage(cfg.ChannelID, m.ID, msg, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to edit message:", err)
|
||||
}
|
||||
|
|
|
@ -1,338 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
)
|
||||
|
||||
var EndpointInteractions = Endpoint + "interactions/"
|
||||
|
||||
type InteractionResponseType uint
|
||||
|
||||
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-callback-type
|
||||
const (
|
||||
PongInteraction InteractionResponseType = iota + 1
|
||||
_
|
||||
_
|
||||
MessageInteractionWithSource
|
||||
DeferredMessageInteractionWithSource
|
||||
DeferredMessageUpdate
|
||||
UpdateMessage
|
||||
AutocompleteResult
|
||||
ModalResponse
|
||||
)
|
||||
|
||||
// InteractionResponseFlags implements flags for an
|
||||
// InteractionApplicationCommandCallbackData.
|
||||
//
|
||||
// Deprecated: use discord.MessageFlags instead.
|
||||
type InteractionResponseFlags = discord.MessageFlags
|
||||
|
||||
// EphemeralMessage specifies whether the message is only visible to the user
|
||||
// who invoked the Interaction.
|
||||
//
|
||||
// Deprecated: use discord.EphemeralMessage instead.
|
||||
const EphemeralResponse InteractionResponseFlags = discord.EphemeralMessage
|
||||
|
||||
type InteractionResponse struct {
|
||||
Type InteractionResponseType `json:"type"`
|
||||
Data *InteractionResponseData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the InteractionResponse has files.
|
||||
func (resp InteractionResponse) NeedsMultipart() bool {
|
||||
return resp.Data != nil && resp.Data.NeedsMultipart()
|
||||
}
|
||||
|
||||
func (resp InteractionResponse) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, resp, resp.Data.Files)
|
||||
}
|
||||
|
||||
// InteractionResponseData is InteractionApplicationCommandCallbackData in the
|
||||
// official documentation.
|
||||
type InteractionResponseData struct {
|
||||
// Content are the message contents (up to 2000 characters).
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Content option.NullableString `json:"content,omitempty"`
|
||||
// TTS is true if this is a TTS message.
|
||||
TTS bool `json:"tts,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Embeds *[]discord.Embed `json:"embeds,omitempty"`
|
||||
// Components is the list of components (such as buttons) to be attached to
|
||||
// the message.
|
||||
Components *discord.ContainerComponents `json:"components,omitempty"`
|
||||
// AllowedMentions are the allowed mentions for the message.
|
||||
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
// Flags are the interaction application command callback data flags.
|
||||
// Only SuppressEmbeds and EphemeralMessage may be set.
|
||||
Flags discord.MessageFlags `json:"flags,omitempty"`
|
||||
|
||||
// Files represents a list of files to upload. This will not be
|
||||
// JSON-encoded and will only be available through WriteMultipart.
|
||||
Files []sendpart.File `json:"-"`
|
||||
|
||||
// Choices are the results to display on autocomplete interaction events.
|
||||
//
|
||||
// During all other events, this should not be provided.
|
||||
Choices AutocompleteChoices `json:"choices,omitempty"`
|
||||
|
||||
// CustomID used with the modal
|
||||
CustomID option.NullableString `json:"custom_id,omitempty"`
|
||||
// Title is the heading of the modal window
|
||||
Title option.NullableString `json:"title,omitempty"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the InteractionResponseData has files.
|
||||
func (d InteractionResponseData) NeedsMultipart() bool {
|
||||
return len(d.Files) > 0
|
||||
}
|
||||
|
||||
func (d InteractionResponseData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, d, d.Files)
|
||||
}
|
||||
|
||||
// AutocompleteChoices are the choices to send back to Discord when sending a
|
||||
// ApplicationCommandAutocompleteResult interaction response.
|
||||
//
|
||||
// The following types implement this interface:
|
||||
//
|
||||
// - AutocompleteStringChoices
|
||||
// - AutocompleteIntegerChoices
|
||||
// - AutocompleteNumberChoices
|
||||
//
|
||||
type AutocompleteChoices interface {
|
||||
choices()
|
||||
}
|
||||
|
||||
// AutocompleteStringChoices are string choices to send back to Discord as
|
||||
// autocomplete results.
|
||||
type AutocompleteStringChoices []discord.StringChoice
|
||||
|
||||
func (c AutocompleteStringChoices) choices() {}
|
||||
|
||||
// AutocompleteIntegerChoices are integer choices to send back to Discord as
|
||||
// autocomplete results.
|
||||
type AutocompleteIntegerChoices []discord.IntegerChoice
|
||||
|
||||
func (c AutocompleteIntegerChoices) choices() {}
|
||||
|
||||
// AutocompleteNumberChoices are number choices to send back to Discord as
|
||||
// autocomplete results.
|
||||
type AutocompleteNumberChoices []discord.NumberChoice
|
||||
|
||||
func (c AutocompleteNumberChoices) choices() {}
|
||||
|
||||
// RespondInteraction responds to an incoming interaction. It is also known as
|
||||
// an "interaction callback".
|
||||
func (c *Client) RespondInteraction(
|
||||
id discord.InteractionID, token string, resp InteractionResponse) error {
|
||||
|
||||
if resp.Data != nil {
|
||||
switch resp.Type {
|
||||
case MessageInteractionWithSource:
|
||||
// A new message is being created, make sure none of the fields
|
||||
// are null or empty.
|
||||
if (resp.Data.Content == nil || resp.Data.Content.Val == "") &&
|
||||
(resp.Data.Embeds == nil || len(*resp.Data.Embeds) == 0) &&
|
||||
len(resp.Data.Files) == 0 {
|
||||
return ErrEmptyMessage
|
||||
}
|
||||
case UpdateMessage:
|
||||
// A component is being updated. We therefore don't know what
|
||||
// fields are filled. The only thing we can check is if content,
|
||||
// embeds and files are null.
|
||||
if (resp.Data.Content != nil && !resp.Data.Content.Init) &&
|
||||
(resp.Data.Embeds != nil && *resp.Data.Embeds == nil) && len(resp.Data.Files) == 0 {
|
||||
return ErrEmptyMessage
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Data.AllowedMentions != nil {
|
||||
if err := resp.Data.AllowedMentions.Verify(); err != nil {
|
||||
return errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Data.Embeds != nil {
|
||||
sum := 0
|
||||
for i, embed := range *resp.Data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
sum += embed.Length()
|
||||
if sum > 6000 {
|
||||
return &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
|
||||
}
|
||||
|
||||
(*resp.Data.Embeds)[i] = embed // embed.Validate changes fields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
URL := EndpointInteractions + id.String() + "/" + token + "/callback"
|
||||
return sendpart.POST(c.Client, resp, nil, URL)
|
||||
}
|
||||
|
||||
// InteractionResponse returns the initial interaction response.
|
||||
func (c *Client) InteractionResponse(
|
||||
appID discord.AppID, token string) (*discord.Message, error) {
|
||||
|
||||
var m *discord.Message
|
||||
return m, c.RequestJSON(
|
||||
&m, "GET",
|
||||
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
|
||||
}
|
||||
|
||||
type EditInteractionResponseData struct {
|
||||
// Content are the new message contents (up to 2000 characters).
|
||||
Content option.NullableString `json:"content,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
Embeds *[]discord.Embed `json:"embeds,omitempty"`
|
||||
// Components contains the new components to attach.
|
||||
Components *discord.ContainerComponents `json:"components,omitempty"`
|
||||
// AllowedMentions are the allowed mentions for the message.
|
||||
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
// Attachments are the attached files to keep.
|
||||
Attachments *[]discord.Attachment `json:"attachments,omitempty"`
|
||||
|
||||
// Files represents a list of files to upload. This will not be
|
||||
// JSON-encoded and will only be available through WriteMultipart.
|
||||
Files []sendpart.File `json:"-"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the SendMessageData has files.
|
||||
func (data EditInteractionResponseData) NeedsMultipart() bool {
|
||||
return len(data.Files) > 0
|
||||
}
|
||||
|
||||
func (data EditInteractionResponseData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, data, data.Files)
|
||||
}
|
||||
|
||||
// EditInteractionResponse edits the initial Interaction response.
|
||||
func (c *Client) EditInteractionResponse(
|
||||
appID discord.AppID,
|
||||
token string, data EditInteractionResponseData) (*discord.Message, error) {
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
if data.Embeds != nil {
|
||||
sum := 0
|
||||
for i, e := range *data.Embeds {
|
||||
if err := e.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error")
|
||||
}
|
||||
sum += e.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
|
||||
}
|
||||
|
||||
(*data.Embeds)[i] = e // e.Validate changes fields
|
||||
}
|
||||
}
|
||||
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.PATCH(c.Client, data, &msg,
|
||||
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
|
||||
}
|
||||
|
||||
// DeleteInteractionResponse deletes the initial interaction response.
|
||||
func (c *Client) DeleteInteractionResponse(appID discord.AppID, token string) error {
|
||||
return c.FastRequest("DELETE",
|
||||
EndpointWebhooks+appID.String()+"/"+token+"/messages/@original")
|
||||
}
|
||||
|
||||
// CreateInteractionFollowup creates a followup message for an interaction.
|
||||
//
|
||||
// Deprecated: use FollowUpInteraction instead.
|
||||
func (c *Client) CreateInteractionFollowup(
|
||||
appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {
|
||||
|
||||
return c.FollowUpInteraction(appID, token, data)
|
||||
}
|
||||
|
||||
// FollowUpInteraction creates a followup message for an interaction.
|
||||
func (c *Client) FollowUpInteraction(
|
||||
appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {
|
||||
|
||||
if (data.Content == nil || data.Content.Val == "") &&
|
||||
(data.Embeds == nil || len(*data.Embeds) == 0) && len(data.Files) == 0 {
|
||||
return nil, ErrEmptyMessage
|
||||
}
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
if data.Embeds != nil {
|
||||
sum := 0
|
||||
for i, embed := range *data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
sum += embed.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
|
||||
}
|
||||
|
||||
(*data.Embeds)[i] = embed // embed.Validate changes fields
|
||||
}
|
||||
}
|
||||
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.POST(
|
||||
c.Client, data, &msg, EndpointWebhooks+appID.String()+"/"+token+"?")
|
||||
}
|
||||
|
||||
func (c *Client) EditInteractionFollowup(
|
||||
appID discord.AppID, messageID discord.MessageID,
|
||||
token string, data EditInteractionResponseData) (*discord.Message, error) {
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
if data.Embeds != nil {
|
||||
sum := 0
|
||||
for i, e := range *data.Embeds {
|
||||
if err := e.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error")
|
||||
}
|
||||
sum += e.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of text in embeds"}
|
||||
}
|
||||
|
||||
(*data.Embeds)[i] = e // e.Validate changes fields
|
||||
}
|
||||
}
|
||||
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.PATCH(c.Client, data, &msg,
|
||||
EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// DeleteInteractionFollowup deletes a followup message for an interaction.
|
||||
func (c *Client) DeleteInteractionFollowup(
|
||||
appID discord.AppID, messageID discord.MessageID, token string) error {
|
||||
|
||||
return c.FastRequest("DELETE",
|
||||
EndpointWebhooks+appID.String()+"/"+token+"/messages/"+messageID.String())
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
var EndpointInvites = Endpoint + "invites/"
|
||||
|
@ -19,8 +19,7 @@ func (c *Client) Invite(code string) (*discord.Invite, error) {
|
|||
)
|
||||
}
|
||||
|
||||
// InviteWithCounts returns an invite object for the given code and fills
|
||||
// ApproxMembers.
|
||||
// Invite returns an invite object for the given code and fills ApproxMembers.
|
||||
func (c *Client) InviteWithCounts(code string) (*discord.Invite, error) {
|
||||
var params struct {
|
||||
WithCounts bool `schema:"with_counts,omitempty"`
|
||||
|
@ -76,8 +75,6 @@ type CreateInviteData struct {
|
|||
//
|
||||
// Default: false
|
||||
Unique bool `json:"unique,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// CreateInvite creates a new invite object for the channel. Only usable for
|
||||
|
@ -86,41 +83,20 @@ type CreateInviteData struct {
|
|||
// Requires the CREATE_INSTANT_INVITE permission.
|
||||
func (c *Client) CreateInvite(
|
||||
channelID discord.ChannelID, data CreateInviteData) (*discord.Invite, error) {
|
||||
|
||||
var inv *discord.Invite
|
||||
return inv, c.RequestJSON(
|
||||
&inv, "POST",
|
||||
EndpointChannels+channelID.String()+"/invites",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// JoinedInvite is returned after joining an invite.
|
||||
type JoinedInvite struct {
|
||||
Code string `json:"code"`
|
||||
NewMember bool `json:"new_member"`
|
||||
Guild discord.Guild `json:"guild"`
|
||||
Channel discord.Channel `json:"channel"` // id, name, type only
|
||||
}
|
||||
|
||||
// JoinInvite joins a guild using the given invite code. This endpoint is
|
||||
// undocumented.
|
||||
func (c *Client) JoinInvite(code string) (*JoinedInvite, error) {
|
||||
var inv *JoinedInvite
|
||||
return inv, c.RequestJSON(&inv, "POST", EndpointInvites+code)
|
||||
}
|
||||
|
||||
// DeleteInvite deletes an invite.
|
||||
//
|
||||
// Requires the MANAGE_CHANNELS permission on the channel this invite belongs
|
||||
// to, or MANAGE_GUILD to remove any invite across the guild.
|
||||
//
|
||||
// Fires an Invite Delete Gateway event.
|
||||
func (c *Client) DeleteInvite(code string, reason AuditLogReason) (*discord.Invite, error) {
|
||||
// Fires a Invite Delete Gateway event.
|
||||
func (c *Client) DeleteInvite(code string) (*discord.Invite, error) {
|
||||
var inv *discord.Invite
|
||||
return inv, c.RequestJSON(
|
||||
&inv,
|
||||
"DELETE", EndpointInvites+code,
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
return inv, c.RequestJSON(&inv, "DELETE", EndpointInvites+code)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package api
|
||||
|
||||
import "github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
import "github.com/diamondburned/arikawa/utils/httputil"
|
||||
|
||||
var (
|
||||
EndpointAuth = Endpoint + "auth/"
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/internal/intmath"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
const MaxMemberFetchLimit = 1000
|
||||
const maxMemberFetchLimit = 1000
|
||||
|
||||
// Member returns a guild member object for the specified user.
|
||||
func (c *Client) Member(guildID discord.GuildID, userID discord.UserID) (*discord.Member, error) {
|
||||
|
@ -40,15 +39,17 @@ func (c *Client) MembersAfter(
|
|||
|
||||
mems := make([]discord.Member, 0, limit)
|
||||
|
||||
fetch := uint(MaxMemberFetchLimit)
|
||||
fetch := uint(maxMemberFetchLimit)
|
||||
|
||||
unlimited := limit == 0
|
||||
|
||||
for limit > 0 || unlimited {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if limit > 0 {
|
||||
fetch = uint(intmath.Min(MaxMemberFetchLimit, int(limit)))
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= fetch
|
||||
}
|
||||
|
||||
|
@ -59,7 +60,7 @@ func (c *Client) MembersAfter(
|
|||
mems = append(mems, m...)
|
||||
|
||||
// There aren't any to fetch, even if this is less than limit.
|
||||
if len(m) < MaxMemberFetchLimit {
|
||||
if len(m) < maxMemberFetchLimit {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -134,7 +135,6 @@ type AddMemberData struct {
|
|||
// guild with CREATE_INSTANT_INVITE permission.
|
||||
func (c *Client) AddMember(
|
||||
guildID discord.GuildID, userID discord.UserID, data AddMemberData) (*discord.Member, error) {
|
||||
|
||||
var mem *discord.Member
|
||||
return mem, c.RequestJSON(
|
||||
&mem, "PUT",
|
||||
|
@ -167,26 +167,18 @@ type ModifyMemberData struct {
|
|||
//
|
||||
// Requires MOVE_MEMBER
|
||||
VoiceChannel discord.ChannelID `json:"channel_id,omitempty"`
|
||||
|
||||
// CommunicationDisabledUntil specifies when the user's timeout will expire.
|
||||
//
|
||||
// Requires MODERATE_MEMBERS
|
||||
CommunicationDisabledUntil *discord.Timestamp `json:"communication_disabled_until,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyMember modifies attributes of a guild member. If the channel_id is set
|
||||
// to null, this will force the target user to be disconnected from voice.
|
||||
//
|
||||
// Fires a Guild Member Update Gateway event.
|
||||
func (c *Client) ModifyMember(
|
||||
guildID discord.GuildID, userID discord.UserID, data ModifyMemberData) error {
|
||||
func (c *Client) ModifyMember(guildID discord.GuildID, userID discord.UserID, data ModifyMemberData) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointGuilds+guildID.String()+"/members/"+userID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -232,8 +224,6 @@ type PruneData struct {
|
|||
ReturnCount bool `schema:"compute_prune_count"`
|
||||
// IncludedRoles are the role(s) to include.
|
||||
IncludedRoles []discord.RoleID `schema:"include_roles,omitempty"`
|
||||
|
||||
AuditLogReason `schema:"-"`
|
||||
}
|
||||
|
||||
// Prune begins a prune. Days must be 1 or more, default 7.
|
||||
|
@ -244,7 +234,6 @@ type PruneData struct {
|
|||
// will be included in the prune and users with additional roles will not.
|
||||
//
|
||||
// Requires KICK_MEMBERS.
|
||||
//
|
||||
// Fires multiple Guild Member Remove Gateway events.
|
||||
func (c *Client) Prune(guildID discord.GuildID, data PruneData) (uint, error) {
|
||||
if data.Days == 0 {
|
||||
|
@ -258,22 +247,36 @@ func (c *Client) Prune(guildID discord.GuildID, data PruneData) (uint, error) {
|
|||
return resp.Pruned, c.RequestJSON(
|
||||
&resp, "POST",
|
||||
EndpointGuilds+guildID.String()+"/prune",
|
||||
httputil.WithSchema(c, data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithSchema(c, data),
|
||||
)
|
||||
}
|
||||
|
||||
// Kick removes a member from a guild.
|
||||
//
|
||||
// Requires KICK_MEMBERS permission.
|
||||
//
|
||||
// Fires a Guild Member Remove Gateway event.
|
||||
func (c *Client) Kick(
|
||||
guildID discord.GuildID, userID discord.UserID, reason AuditLogReason) error {
|
||||
func (c *Client) Kick(guildID discord.GuildID, userID discord.UserID) error {
|
||||
return c.KickWithReason(guildID, userID, "")
|
||||
}
|
||||
|
||||
// KickWithReason removes a member from a guild.
|
||||
// The reason, if non-empty, will be displayed in the audit log of the guild.
|
||||
//
|
||||
// Requires KICK_MEMBERS permission.
|
||||
// Fires a Guild Member Remove Gateway event.
|
||||
func (c *Client) KickWithReason(
|
||||
guildID discord.GuildID, userID discord.UserID, reason string) error {
|
||||
|
||||
var data struct {
|
||||
Reason string `schema:"reason,omitempty"`
|
||||
}
|
||||
|
||||
data.Reason = reason
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointGuilds+guildID.String()+"/members/"+userID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
httputil.WithSchema(c, data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -303,34 +306,26 @@ func (c *Client) GetBan(guildID discord.GuildID, userID discord.UserID) (*discor
|
|||
type BanData struct {
|
||||
// DeleteDays is the number of days to delete messages for (0-7).
|
||||
DeleteDays option.Uint `schema:"delete_message_days,omitempty"`
|
||||
|
||||
AuditLogReason `schema:"-"`
|
||||
// Reason is the reason for the ban.
|
||||
Reason option.String `schema:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// Ban creates a guild ban, and optionally delete previous messages sent by the
|
||||
// banned user.
|
||||
//
|
||||
// Requires the BAN_MEMBERS permission.
|
||||
//
|
||||
// Fires a Guild Ban Add Gateway event.
|
||||
func (c *Client) Ban(guildID discord.GuildID, userID discord.UserID, data BanData) error {
|
||||
return c.FastRequest(
|
||||
"PUT",
|
||||
EndpointGuilds+guildID.String()+"/bans/"+userID.String(),
|
||||
httputil.WithSchema(c, data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithSchema(c, data),
|
||||
)
|
||||
}
|
||||
|
||||
// Unban removes the ban for a user.
|
||||
//
|
||||
// Requires the BAN_MEMBERS permissions.
|
||||
//
|
||||
// Fires a Guild Ban Remove Gateway event.
|
||||
func (c *Client) Unban(
|
||||
guildID discord.GuildID, userID discord.UserID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointGuilds+guildID.String()+"/bans/"+userID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
func (c *Client) Unban(guildID discord.GuildID, userID discord.UserID) error {
|
||||
return c.FastRequest("DELETE", EndpointGuilds+guildID.String()+"/bans/"+userID.String())
|
||||
}
|
||||
|
|
238
api/message.go
238
api/message.go
|
@ -1,25 +1,15 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/internal/intmath"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
const (
|
||||
// the limit of max messages per request, as imposed by Discord
|
||||
maxMessageFetchLimit = 100
|
||||
// maxMessageDeleteLimit is the limit of max message that can be deleted
|
||||
// per bulk delete request, as imposed by Discord.
|
||||
maxMessageDeleteLimit = 100
|
||||
)
|
||||
// the limit of max messages per request, as imposed by Discord
|
||||
const maxMessageFetchLimit = 100
|
||||
|
||||
// Messages returns a slice filled with the most recent messages sent in the
|
||||
// channel with the passed ID. The method automatically paginates until it
|
||||
|
@ -71,8 +61,10 @@ func (c *Client) MessagesBefore(
|
|||
for limit > 0 || unlimited {
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(maxMessageFetchLimit, int(limit)))
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= maxMessageFetchLimit
|
||||
}
|
||||
|
||||
|
@ -128,8 +120,10 @@ func (c *Client) MessagesAfter(
|
|||
for limit > 0 || unlimited {
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(maxMessageFetchLimit, int(limit)))
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= maxMessageFetchLimit
|
||||
}
|
||||
|
||||
|
@ -155,8 +149,7 @@ func (c *Client) MessagesAfter(
|
|||
}
|
||||
|
||||
func (c *Client) messagesRange(
|
||||
channelID discord.ChannelID,
|
||||
before, after, around discord.MessageID, limit uint) ([]discord.Message, error) {
|
||||
channelID discord.ChannelID, before, after, around discord.MessageID, limit uint) ([]discord.Message, error) {
|
||||
|
||||
switch {
|
||||
case limit == 0:
|
||||
|
@ -190,57 +183,35 @@ func (c *Client) messagesRange(
|
|||
//
|
||||
// If operating on a guild channel, this endpoint requires the
|
||||
// READ_MESSAGE_HISTORY permission to be present on the current user.
|
||||
func (c *Client) Message(
|
||||
channelID discord.ChannelID, messageID discord.MessageID) (*discord.Message, error) {
|
||||
|
||||
func (c *Client) Message(channelID discord.ChannelID, messageID discord.MessageID) (*discord.Message, error) {
|
||||
var msg *discord.Message
|
||||
return msg, c.RequestJSON(&msg, "GET",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// SendTextReply posts a text-only reply to a message ID in a guild text or DM channel
|
||||
// SendText posts a only-text message to a guild text or DM channel.
|
||||
//
|
||||
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
|
||||
// permission to be present on the current user.
|
||||
//
|
||||
// Fires a Message Create Gateway event.
|
||||
func (c *Client) SendTextReply(
|
||||
channelID discord.ChannelID,
|
||||
content string, referenceID discord.MessageID) (*discord.Message, error) {
|
||||
|
||||
func (c *Client) SendText(channelID discord.ChannelID, content string) (*discord.Message, error) {
|
||||
return c.SendMessageComplex(channelID, SendMessageData{
|
||||
Content: content,
|
||||
Reference: &discord.MessageReference{MessageID: referenceID},
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
// SendEmbeds sends embeds to a guild text or DM channel.
|
||||
// SendEmbed posts an Embed to a guild text or DM channel.
|
||||
//
|
||||
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
|
||||
// permission to be present on the current user.
|
||||
//
|
||||
// Fires a Message Create Gateway event.
|
||||
func (c *Client) SendEmbeds(
|
||||
channelID discord.ChannelID, e ...discord.Embed) (*discord.Message, error) {
|
||||
func (c *Client) SendEmbed(
|
||||
channelID discord.ChannelID, e discord.Embed) (*discord.Message, error) {
|
||||
|
||||
return c.SendMessageComplex(channelID, SendMessageData{
|
||||
Embeds: e,
|
||||
})
|
||||
}
|
||||
|
||||
// SendEmbedReply posts an Embed reply to a message ID in a guild text or DM channel.
|
||||
//
|
||||
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
|
||||
// permission to be present on the current user.
|
||||
//
|
||||
// Fires a Message Create Gateway event.
|
||||
func (c *Client) SendEmbedReply(
|
||||
channelID discord.ChannelID,
|
||||
referenceID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
|
||||
|
||||
return c.SendMessageComplex(channelID, SendMessageData{
|
||||
Embeds: embeds,
|
||||
Reference: &discord.MessageReference{MessageID: referenceID},
|
||||
Embed: &e,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -251,104 +222,61 @@ func (c *Client) SendEmbedReply(
|
|||
//
|
||||
// Fires a Message Create Gateway event.
|
||||
func (c *Client) SendMessage(
|
||||
channelID discord.ChannelID,
|
||||
content string, embeds ...discord.Embed) (*discord.Message, error) {
|
||||
channelID discord.ChannelID, content string, embed *discord.Embed) (*discord.Message, error) {
|
||||
|
||||
data := SendMessageData{
|
||||
return c.SendMessageComplex(channelID, SendMessageData{
|
||||
Content: content,
|
||||
Embeds: embeds,
|
||||
}
|
||||
return c.SendMessageComplex(channelID, data)
|
||||
Embed: embed,
|
||||
})
|
||||
}
|
||||
|
||||
// SendMessageReply posts a reply to a message ID in a guild text or DM channel.
|
||||
//
|
||||
// If operating on a guild channel, this endpoint requires the SEND_MESSAGES
|
||||
// permission to be present on the current user.
|
||||
//
|
||||
// Fires a Message Create Gateway event.
|
||||
func (c *Client) SendMessageReply(
|
||||
channelID discord.ChannelID, content string,
|
||||
referenceID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
|
||||
|
||||
data := SendMessageData{
|
||||
Content: content,
|
||||
Reference: &discord.MessageReference{MessageID: referenceID},
|
||||
Embeds: embeds,
|
||||
}
|
||||
|
||||
return c.SendMessageComplex(channelID, data)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#edit-message
|
||||
// https://discord.com/developers/docs/resources/channel#edit-message-json-params
|
||||
type EditMessageData struct {
|
||||
// Content is the new message contents (up to 2000 characters).
|
||||
Content option.NullableString `json:"content,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
Embeds *[]discord.Embed `json:"embeds,omitempty"`
|
||||
// Components contains the new components to attach.
|
||||
Components *discord.ContainerComponents `json:"components,omitempty"`
|
||||
// Embed contains embedded rich content.
|
||||
Embed *discord.Embed `json:"embed,omitempty"`
|
||||
// AllowedMentions are the allowed mentions for a message.
|
||||
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
// Attachments are the attached files to keep
|
||||
Attachments *[]discord.Attachment `json:"attachments,omitempty"`
|
||||
// Flags edits the flags of a message (only SUPPRESS_EMBEDS can currently
|
||||
// be set/unset)
|
||||
//
|
||||
// This field is nullable.
|
||||
Flags *discord.MessageFlags `json:"flags,omitempty"`
|
||||
|
||||
Files []sendpart.File `json:"-"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the SendMessageData has files.
|
||||
func (data EditMessageData) NeedsMultipart() bool {
|
||||
return len(data.Files) > 0
|
||||
}
|
||||
|
||||
func (data EditMessageData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, data, data.Files)
|
||||
}
|
||||
|
||||
// EditText edits the contents of a previously sent message. For more
|
||||
// documentation, refer to EditMessageComplex.
|
||||
func (c *Client) EditText(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, content string) (*discord.Message, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, content string) (*discord.Message, error) {
|
||||
|
||||
return c.EditMessageComplex(channelID, messageID, EditMessageData{
|
||||
Content: option.NewNullableString(content),
|
||||
})
|
||||
}
|
||||
|
||||
// EditEmbeds edits the embed of a previously sent message. For more
|
||||
// EditEmbed edits the embed of a previously sent message. For more
|
||||
// documentation, refer to EditMessageComplex.
|
||||
func (c *Client) EditEmbeds(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, embeds ...discord.Embed) (*discord.Message, error) {
|
||||
func (c *Client) EditEmbed(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, embed discord.Embed) (*discord.Message, error) {
|
||||
|
||||
return c.EditMessageComplex(channelID, messageID, EditMessageData{
|
||||
Embeds: &embeds,
|
||||
Embed: &embed,
|
||||
})
|
||||
}
|
||||
|
||||
// EditMessage edits a previously sent message. If content or embeds are empty
|
||||
// the original content or embed will remain untouched. This means EditMessage
|
||||
// will only update, but not remove parts of the message.
|
||||
//
|
||||
// For more documentation, refer to EditMessageComplex.
|
||||
// EditMessage edits a previously sent message. For more documentation, refer to
|
||||
// EditMessageComplex.
|
||||
func (c *Client) EditMessage(
|
||||
channelID discord.ChannelID, messageID discord.MessageID,
|
||||
content string, embeds ...discord.Embed) (*discord.Message, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, content string,
|
||||
embed *discord.Embed, suppressEmbeds bool) (*discord.Message, error) {
|
||||
|
||||
var data EditMessageData
|
||||
|
||||
if len(content) > 0 {
|
||||
data.Content = option.NewNullableString(content)
|
||||
var data = EditMessageData{
|
||||
Content: option.NewNullableString(content),
|
||||
Embed: embed,
|
||||
}
|
||||
|
||||
if len(embeds) > 0 {
|
||||
data.Embeds = &embeds
|
||||
if suppressEmbeds {
|
||||
data.Flags = &discord.SuppressEmbeds
|
||||
}
|
||||
|
||||
return c.EditMessageComplex(channelID, messageID, data)
|
||||
|
@ -365,8 +293,7 @@ func (c *Client) EditMessage(
|
|||
//
|
||||
// Fires a Message Update Gateway event.
|
||||
func (c *Client) EditMessageComplex(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, data EditMessageData) (*discord.Message, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, data EditMessageData) (*discord.Message, error) {
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
|
@ -374,50 +301,26 @@ func (c *Client) EditMessageComplex(
|
|||
}
|
||||
}
|
||||
|
||||
if data.Embeds != nil {
|
||||
sum := 0
|
||||
for i, embed := range *data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
sum += embed.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
|
||||
}
|
||||
|
||||
(*data.Embeds)[i] = embed // embed.Validate changes fields
|
||||
if data.Embed != nil {
|
||||
if err := data.Embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error")
|
||||
}
|
||||
}
|
||||
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.PATCH(c.Client, data, &msg,
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// CrosspostMessage crossposts a message in a news channel to following channels.
|
||||
// This endpoint requires the SEND_MESSAGES permission if the current user sent the message,
|
||||
// or additionally the MANAGE_MESSAGES permission for all other messages.
|
||||
func (c *Client) CrosspostMessage(
|
||||
channelID discord.ChannelID, messageID discord.MessageID) (*discord.Message, error) {
|
||||
|
||||
var msg *discord.Message
|
||||
|
||||
return msg, c.RequestJSON(
|
||||
&msg,
|
||||
"POST",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+"/crosspost",
|
||||
&msg, "PATCH",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String(),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteMessage delete a message. If operating on a guild channel and trying
|
||||
// to delete a message that was not sent by the current user, this endpoint
|
||||
// requires the MANAGE_MESSAGES permission.
|
||||
func (c *Client) DeleteMessage(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, reason AuditLogReason) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointChannels+channelID.String()+"/messages/"+messageID.String(),
|
||||
httputil.WithHeaders(reason.Header()))
|
||||
func (c *Client) DeleteMessage(channelID discord.ChannelID, messageID discord.MessageID) error {
|
||||
return c.FastRequest("DELETE", EndpointChannels+channelID.String()+
|
||||
"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// DeleteMessages deletes multiple messages in a single request. This endpoint
|
||||
|
@ -428,39 +331,8 @@ func (c *Client) DeleteMessage(
|
|||
// any message provided is older than that or if any duplicate message IDs are
|
||||
// provided.
|
||||
//
|
||||
// Because the underlying endpoint only supports a maximum of 100 message IDs
|
||||
// per request, DeleteMessages will make a total of messageIDs/100 rounded up
|
||||
// requests.
|
||||
//
|
||||
// Fires a Message Delete Bulk Gateway event.
|
||||
func (c *Client) DeleteMessages(
|
||||
channelID discord.ChannelID, messageIDs []discord.MessageID, reason AuditLogReason) error {
|
||||
|
||||
switch {
|
||||
case len(messageIDs) == 0:
|
||||
return nil
|
||||
case len(messageIDs) == 1:
|
||||
return c.DeleteMessage(channelID, messageIDs[0], reason)
|
||||
case len(messageIDs) <= maxMessageDeleteLimit: // Fast path
|
||||
return c.deleteMessages(channelID, messageIDs, reason)
|
||||
}
|
||||
|
||||
// If the number of messages to be deleted exceeds the amount discord is willing
|
||||
// to accept at one time then batches of messages will be deleted
|
||||
for start := 0; start < len(messageIDs); start += maxMessageDeleteLimit {
|
||||
end := intmath.Min(len(messageIDs), start+maxMessageDeleteLimit)
|
||||
err := c.deleteMessages(channelID, messageIDs[start:end], reason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) deleteMessages(
|
||||
channelID discord.ChannelID, messageIDs []discord.MessageID, reason AuditLogReason) error {
|
||||
|
||||
func (c *Client) DeleteMessages(channelID discord.ChannelID, messageIDs []discord.MessageID) error {
|
||||
var param struct {
|
||||
Messages []discord.MessageID `json:"messages"`
|
||||
}
|
||||
|
@ -470,6 +342,6 @@ func (c *Client) deleteMessages(
|
|||
return c.FastRequest(
|
||||
"POST",
|
||||
EndpointChannels+channelID.String()+"/messages/bulk-delete",
|
||||
httputil.WithJSONBody(param), httputil.WithHeaders(reason.Header()),
|
||||
httputil.WithJSONBody(param),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/internal/intmath"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
)
|
||||
|
||||
const MaxMessageReactionFetchLimit = 100
|
||||
const maxMessageReactionFetchLimit = 100
|
||||
|
||||
// React creates a reaction for the message.
|
||||
//
|
||||
|
@ -14,22 +15,16 @@ const MaxMessageReactionFetchLimit = 100
|
|||
// the current user. Additionally, if nobody else has reacted to the message
|
||||
// using this emoji, this endpoint requires the 'ADD_REACTIONS' permission to
|
||||
// be present on the current user.
|
||||
func (c *Client) React(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, emoji discord.APIEmoji) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"PUT",
|
||||
EndpointChannels+channelID.String()+
|
||||
"/messages/"+messageID.String()+
|
||||
"/reactions/"+emoji.PathString()+"/@me",
|
||||
)
|
||||
func (c *Client) React(channelID discord.ChannelID, messageID discord.MessageID, emoji Emoji) error {
|
||||
var msgURL = EndpointChannels + channelID.String() +
|
||||
"/messages/" + messageID.String() +
|
||||
"/reactions/" + url.PathEscape(emoji) + "/@me"
|
||||
return c.FastRequest("PUT", msgURL)
|
||||
}
|
||||
|
||||
// Unreact removes a reaction the current user has made for the message.
|
||||
func (c *Client) Unreact(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, emoji discord.APIEmoji) error {
|
||||
|
||||
return c.DeleteUserReaction(channelID, messageID, 0, emoji)
|
||||
func (c *Client) Unreact(chID discord.ChannelID, msgID discord.MessageID, emoji Emoji) error {
|
||||
return c.DeleteUserReaction(chID, msgID, 0, emoji)
|
||||
}
|
||||
|
||||
// Reactions returns a list of users that reacted with the passed Emoji. This
|
||||
|
@ -42,8 +37,7 @@ func (c *Client) Unreact(
|
|||
//
|
||||
// When fetching the users, those with the smallest ID will be fetched first.
|
||||
func (c *Client) Reactions(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, emoji discord.APIEmoji, limit uint) ([]discord.User, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, emoji Emoji, limit uint) ([]discord.User, error) {
|
||||
|
||||
return c.ReactionsAfter(channelID, messageID, 0, emoji, limit)
|
||||
}
|
||||
|
@ -57,20 +51,22 @@ func (c *Client) Reactions(
|
|||
// maximum a total of limit/100 rounded up requests will be made, although they
|
||||
// may be less, if no more guilds are available.
|
||||
func (c *Client) ReactionsBefore(
|
||||
channelID discord.ChannelID, messageID discord.MessageID,
|
||||
before discord.UserID, emoji discord.APIEmoji, limit uint) ([]discord.User, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, before discord.UserID, emoji Emoji,
|
||||
limit uint) ([]discord.User, error) {
|
||||
|
||||
users := make([]discord.User, 0, limit)
|
||||
|
||||
fetch := uint(MaxMessageReactionFetchLimit)
|
||||
fetch := uint(maxMessageReactionFetchLimit)
|
||||
|
||||
unlimited := limit == 0
|
||||
|
||||
for limit > 0 || unlimited {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(MaxMessageReactionFetchLimit, int(limit)))
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= fetch
|
||||
}
|
||||
|
||||
|
@ -80,7 +76,7 @@ func (c *Client) ReactionsBefore(
|
|||
}
|
||||
users = append(r, users...)
|
||||
|
||||
if len(r) < MaxMessageReactionFetchLimit {
|
||||
if len(r) < maxMessageReactionFetchLimit {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -103,20 +99,22 @@ func (c *Client) ReactionsBefore(
|
|||
// maximum a total of limit/100 rounded up requests will be made, although they
|
||||
// may be less, if no more guilds are available.
|
||||
func (c *Client) ReactionsAfter(
|
||||
channelID discord.ChannelID, messageID discord.MessageID,
|
||||
after discord.UserID, emoji discord.APIEmoji, limit uint) ([]discord.User, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, after discord.UserID, emoji Emoji,
|
||||
limit uint) ([]discord.User, error) {
|
||||
|
||||
users := make([]discord.User, 0, limit)
|
||||
|
||||
fetch := uint(MaxMessageReactionFetchLimit)
|
||||
fetch := uint(maxMessageReactionFetchLimit)
|
||||
|
||||
unlimited := limit == 0
|
||||
|
||||
for limit > 0 || unlimited {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch min(fetch, limit).
|
||||
if limit > 0 {
|
||||
// Only fetch as much as we need. Since limit gradually decreases,
|
||||
// we only need to fetch intmath.Min(fetch, limit).
|
||||
fetch = uint(intmath.Min(MaxMessageReactionFetchLimit, int(limit)))
|
||||
if fetch > limit {
|
||||
fetch = limit
|
||||
}
|
||||
limit -= fetch
|
||||
}
|
||||
|
||||
|
@ -126,7 +124,7 @@ func (c *Client) ReactionsAfter(
|
|||
}
|
||||
users = append(users, r...)
|
||||
|
||||
if len(r) < MaxMessageReactionFetchLimit {
|
||||
if len(r) < maxMessageReactionFetchLimit {
|
||||
break
|
||||
}
|
||||
|
||||
|
@ -143,8 +141,8 @@ func (c *Client) ReactionsAfter(
|
|||
// reactionsRange get users before and after IDs. Before, after, and limit are
|
||||
// optional. A maximum limit of only 100 reactions could be returned.
|
||||
func (c *Client) reactionsRange(
|
||||
channelID discord.ChannelID, messageID discord.MessageID,
|
||||
before, after discord.UserID, emoji discord.APIEmoji, limit uint) ([]discord.User, error) {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, before, after discord.UserID, emoji Emoji,
|
||||
limit uint) ([]discord.User, error) {
|
||||
|
||||
switch {
|
||||
case limit == 0:
|
||||
|
@ -168,18 +166,17 @@ func (c *Client) reactionsRange(
|
|||
return users, c.RequestJSON(
|
||||
&users, "GET", EndpointChannels+channelID.String()+
|
||||
"/messages/"+messageID.String()+
|
||||
"/reactions/"+emoji.PathString(),
|
||||
"/reactions/"+url.PathEscape(emoji),
|
||||
httputil.WithSchema(c, param),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteUserReaction deletes another user's reaction.
|
||||
// DeleteReaction deletes another user's reaction.
|
||||
//
|
||||
// This endpoint requires the MANAGE_MESSAGES permission to be present on the
|
||||
// current user.
|
||||
func (c *Client) DeleteUserReaction(
|
||||
channelID discord.ChannelID,
|
||||
messageID discord.MessageID, userID discord.UserID, emoji discord.APIEmoji) error {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, userID discord.UserID, emoji Emoji) error {
|
||||
|
||||
var user = "@me"
|
||||
if userID > 0 {
|
||||
|
@ -188,9 +185,8 @@ func (c *Client) DeleteUserReaction(
|
|||
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointChannels+channelID.String()+
|
||||
"/messages/"+messageID.String()+
|
||||
"/reactions/"+emoji.PathString()+"/"+user,
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+
|
||||
"/reactions/"+url.PathEscape(emoji)+"/"+user,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -198,16 +194,14 @@ func (c *Client) DeleteUserReaction(
|
|||
//
|
||||
// This endpoint requires the MANAGE_MESSAGES permission to be present on the
|
||||
// current user.
|
||||
//
|
||||
// Fires a Message Reaction Remove Emoji Gateway event.
|
||||
func (c *Client) DeleteReactions(
|
||||
channelID discord.ChannelID, messageID discord.MessageID, emoji discord.APIEmoji) error {
|
||||
channelID discord.ChannelID, messageID discord.MessageID, emoji Emoji) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointChannels+channelID.String()+
|
||||
"/messages/"+messageID.String()+
|
||||
"/reactions/"+emoji.PathString(),
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+
|
||||
"/reactions/"+url.PathEscape(emoji),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -215,13 +209,10 @@ func (c *Client) DeleteReactions(
|
|||
//
|
||||
// This endpoint requires the MANAGE_MESSAGES permission to be present on the
|
||||
// current user.
|
||||
//
|
||||
// Fires a Message Reaction Remove All Gateway event.
|
||||
func (c *Client) DeleteAllReactions(
|
||||
channelID discord.ChannelID, messageID discord.MessageID) error {
|
||||
|
||||
func (c *Client) DeleteAllReactions(channelID discord.ChannelID, messageID discord.MessageID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+"/reactions",
|
||||
EndpointChannels+channelID.String()+"/messages/"+messageID.String()+"/reactions/",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,21 +9,16 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/internal/moreatomic"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/internal/moreatomic"
|
||||
)
|
||||
|
||||
// ExtraDelay because Discord is trash. I've seen this in both litcord and
|
||||
// discordgo, with dgo claiming from experiments.
|
||||
// discordgo, with dgo claiming from his experiments.
|
||||
// RE: Those who want others to fix it for them: release the source code then.
|
||||
const ExtraDelay = 250 * time.Millisecond
|
||||
|
||||
// ErrTimedOutEarly is the error returned by Limiter.Acquire, if a rate limit
|
||||
// exceeds the deadline of the context.Context or api.AcquireOptions.DontWait
|
||||
// is set to true
|
||||
var ErrTimedOutEarly = errors.New(
|
||||
"rate: rate limit exceeds context deadline or is blocked acquire options")
|
||||
|
||||
// This makes me suicidal.
|
||||
// https://github.com/bwmarrin/discordgo/blob/master/ratelimit.go
|
||||
|
||||
|
@ -33,11 +28,8 @@ type Limiter struct {
|
|||
|
||||
Prefix string
|
||||
|
||||
// global is a pointer to prevent ARM-compatibility alignment.
|
||||
global *int64 // atomic guarded, unixnano
|
||||
|
||||
bucketMu sync.Mutex
|
||||
buckets map[string]*bucket
|
||||
global *int64 // atomic guarded, unixnano
|
||||
buckets sync.Map
|
||||
}
|
||||
|
||||
type CustomRateLimit struct {
|
||||
|
@ -45,25 +37,6 @@ type CustomRateLimit struct {
|
|||
Reset time.Duration
|
||||
}
|
||||
|
||||
type contextKey uint8
|
||||
|
||||
const (
|
||||
// AcquireOptionsKey is the key used to store the AcquireOptions in the
|
||||
// context.
|
||||
acquireOptionsKey contextKey = iota
|
||||
)
|
||||
|
||||
type AcquireOptions struct {
|
||||
// DontWait prevents rate.Limiters from waiting for a rate limit. Instead
|
||||
// they will return an rate.ErrTimedOutEarly.
|
||||
DontWait bool
|
||||
}
|
||||
|
||||
// Context wraps the given ctx to have the AcquireOptions.
|
||||
func (opts AcquireOptions) Context(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, acquireOptionsKey, opts)
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
lock moreatomic.CtxMutex
|
||||
custom *CustomRateLimit
|
||||
|
@ -85,7 +58,7 @@ func NewLimiter(prefix string) *Limiter {
|
|||
return &Limiter{
|
||||
Prefix: prefix,
|
||||
global: new(int64),
|
||||
buckets: map[string]*bucket{},
|
||||
buckets: sync.Map{},
|
||||
CustomLimits: []*CustomRateLimit{},
|
||||
}
|
||||
}
|
||||
|
@ -93,10 +66,7 @@ func NewLimiter(prefix string) *Limiter {
|
|||
func (l *Limiter) getBucket(path string, store bool) *bucket {
|
||||
path = ParseBucketKey(strings.TrimPrefix(path, l.Prefix))
|
||||
|
||||
l.bucketMu.Lock()
|
||||
defer l.bucketMu.Unlock()
|
||||
|
||||
bc, ok := l.buckets[path]
|
||||
bc, ok := l.buckets.Load(path)
|
||||
if !ok && !store {
|
||||
return nil
|
||||
}
|
||||
|
@ -111,52 +81,42 @@ func (l *Limiter) getBucket(path string, store bool) *bucket {
|
|||
}
|
||||
}
|
||||
|
||||
l.buckets[path] = bc
|
||||
l.buckets.Store(path, bc)
|
||||
return bc
|
||||
}
|
||||
|
||||
return bc
|
||||
return bc.(*bucket)
|
||||
}
|
||||
|
||||
// Acquire acquires the rate limiter for the given URL bucket.
|
||||
func (l *Limiter) Acquire(ctx context.Context, path string) error {
|
||||
var options AcquireOptions
|
||||
|
||||
if untypedOptions := ctx.Value(acquireOptionsKey); untypedOptions != nil {
|
||||
// Zero value are default anyways, so we can ignore ok.
|
||||
options, _ = untypedOptions.(AcquireOptions)
|
||||
}
|
||||
|
||||
b := l.getBucket(path, true)
|
||||
|
||||
if err := b.lock.Lock(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Deadline until the limiter is released.
|
||||
until := time.Time{}
|
||||
now := time.Now()
|
||||
// Time to sleep
|
||||
var sleep time.Duration
|
||||
|
||||
if b.remaining == 0 && b.reset.After(now) {
|
||||
if b.remaining == 0 && b.reset.After(time.Now()) {
|
||||
// out of turns, gotta wait
|
||||
until = b.reset
|
||||
sleep = time.Until(b.reset)
|
||||
} else {
|
||||
// maybe global rate limit has it
|
||||
until = time.Unix(0, atomic.LoadInt64(l.global))
|
||||
now := time.Now()
|
||||
until := time.Unix(0, atomic.LoadInt64(l.global))
|
||||
|
||||
if until.After(now) {
|
||||
sleep = until.Sub(now)
|
||||
}
|
||||
}
|
||||
|
||||
if until.After(now) {
|
||||
if options.DontWait {
|
||||
return ErrTimedOutEarly
|
||||
} else if deadline, ok := ctx.Deadline(); ok && until.After(deadline) {
|
||||
return ErrTimedOutEarly
|
||||
}
|
||||
|
||||
if sleep > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
b.lock.Unlock()
|
||||
return ctx.Err()
|
||||
case <-time.After(until.Sub(now)):
|
||||
case <-time.After(sleep):
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,7 +161,7 @@ func (l *Limiter) Release(path string, headers http.Header) error {
|
|||
|
||||
// seconds
|
||||
remaining = headers.Get("X-RateLimit-Remaining")
|
||||
reset = headers.Get("X-RateLimit-Reset") // float
|
||||
reset = headers.Get("X-RateLimit-Reset")
|
||||
retryAfter = headers.Get("Retry-After")
|
||||
)
|
||||
|
||||
|
@ -209,12 +169,12 @@ func (l *Limiter) Release(path string, headers http.Header) error {
|
|||
case retryAfter != "":
|
||||
i, err := strconv.Atoi(retryAfter)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "invalid retryAfter %q", retryAfter)
|
||||
return errors.Wrap(err, "invalid retryAfter "+retryAfter)
|
||||
}
|
||||
|
||||
at := time.Now().Add(time.Duration(i) * time.Second)
|
||||
at := time.Now().Add(time.Duration(i) * time.Millisecond)
|
||||
|
||||
if global != "" { // probably "true"
|
||||
if global != "" { // probably true
|
||||
atomic.StoreInt64(l.global, at.UnixNano())
|
||||
} else {
|
||||
b.reset = at
|
||||
|
@ -226,10 +186,8 @@ func (l *Limiter) Release(path string, headers http.Header) error {
|
|||
return errors.Wrap(err, "invalid reset "+reset)
|
||||
}
|
||||
|
||||
sec := int64(unix)
|
||||
nsec := int64((unix - float64(sec)) * float64(time.Second))
|
||||
|
||||
b.reset = time.Unix(sec, nsec).Add(ExtraDelay)
|
||||
b.reset = time.Unix(0, int64(unix*float64(time.Second))).
|
||||
Add(ExtraDelay)
|
||||
}
|
||||
|
||||
if remaining != "" {
|
||||
|
|
|
@ -2,8 +2,8 @@ package rate
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
@ -24,14 +24,10 @@ func mockRequest(t *testing.T, l *Limiter, path string, headers http.Header) {
|
|||
func TestRatelimitReset(t *testing.T) {
|
||||
l := NewLimiter("")
|
||||
|
||||
const msToSec = time.Second / time.Millisecond
|
||||
|
||||
until := time.Now().Add(2 * time.Second)
|
||||
reset := float64(until.UnixNano()/int64(time.Millisecond)) / float64(msToSec)
|
||||
|
||||
headers := http.Header{}
|
||||
headers.Set("X-RateLimit-Remaining", "0")
|
||||
headers.Set("X-RateLimit-Reset", fmt.Sprintf("%.3f", reset))
|
||||
headers.Set("X-RateLimit-Reset",
|
||||
strconv.FormatInt(time.Now().Add(time.Second*2).Unix(), 10))
|
||||
headers.Set("Date", time.Now().Format(time.RFC850))
|
||||
|
||||
sent := time.Now()
|
||||
|
@ -47,10 +43,10 @@ func TestRatelimitReset(t *testing.T) {
|
|||
// We hit the same endpoint 2 times, so we should only be ratelimited 2
|
||||
// second and always less than 4 seconds (unless you're on a stoneage
|
||||
// computer or using swap or something...)
|
||||
if since := time.Since(sent); since >= time.Second && since < time.Second*4 {
|
||||
t.Log("OK", since)
|
||||
if time.Since(sent) >= time.Second && time.Since(sent) < time.Second*4 {
|
||||
t.Log("OK", time.Since(sent))
|
||||
} else {
|
||||
t.Error("did not ratelimit correctly, got:", since)
|
||||
t.Error("did not ratelimit correctly, got:", time.Since(sent))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,7 +57,7 @@ func TestRatelimitGlobal(t *testing.T) {
|
|||
headers := http.Header{}
|
||||
headers.Set("X-RateLimit-Global", "1.002")
|
||||
// Reset for approx 1 seconds from now
|
||||
headers.Set("Retry-After", "1")
|
||||
headers.Set("Retry-After", "1000")
|
||||
|
||||
sent := time.Now()
|
||||
|
||||
|
|
96
api/role.go
96
api/role.go
|
@ -1,42 +1,29 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
type AddRoleData struct {
|
||||
AuditLogReason
|
||||
}
|
||||
|
||||
// AddRole adds a role to a guild member.
|
||||
// Adds a role to a guild member.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
func (c *Client) AddRole(
|
||||
guildID discord.GuildID,
|
||||
userID discord.UserID, roleID discord.RoleID, data AddRoleData) error {
|
||||
|
||||
func (c *Client) AddRole(guildID discord.GuildID, userID discord.UserID, roleID discord.RoleID) error {
|
||||
return c.FastRequest(
|
||||
"PUT",
|
||||
EndpointGuilds+guildID.String()+"/members/"+userID.String()+"/roles/"+roleID.String(),
|
||||
httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
// RemoveRole removes a role from a guild member.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
//
|
||||
// Fires a Guild Member Update Gateway event.
|
||||
func (c *Client) RemoveRole(
|
||||
guildID discord.GuildID,
|
||||
userID discord.UserID, roleID discord.RoleID, reason AuditLogReason) error {
|
||||
|
||||
func (c *Client) RemoveRole(guildID discord.GuildID, userID discord.UserID, roleID discord.RoleID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointGuilds+guildID.String()+"/members/"+userID.String()+"/roles/"+roleID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -55,7 +42,7 @@ type CreateRoleData struct {
|
|||
// Permissions is the bitwise value of the enabled/disabled permissions.
|
||||
//
|
||||
// Default: @everyone permissions in guild
|
||||
Permissions discord.Permissions `json:"permissions,string,omitempty"`
|
||||
Permissions discord.Permissions `json:"permissions,omitempty,string"`
|
||||
// Color is the RGB color value of the role.
|
||||
//
|
||||
// Default: 0
|
||||
|
@ -69,60 +56,40 @@ type CreateRoleData struct {
|
|||
//
|
||||
// Default: false
|
||||
Mentionable bool `json:"mentionable,omitempty"`
|
||||
|
||||
// Icon is the icon of the role. Requires the guild to have the ROLE_ICONS feature.
|
||||
//
|
||||
// Default: null
|
||||
Icon *Image `json:"icon,omitempty"`
|
||||
// UnicodeEmoji is the role's unicode emoji. Requires the guild to have the ROLE_ICONS feature.
|
||||
//
|
||||
// Default: null
|
||||
UnicodeEmoji string `json:"unicode_emoji,omitempty"`
|
||||
|
||||
AddRoleData `json:"-"`
|
||||
}
|
||||
|
||||
// CreateRole creates a new role for the guild.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
//
|
||||
// Fires a Guild Role Create Gateway event.
|
||||
func (c *Client) CreateRole(guildID discord.GuildID, data CreateRoleData) (*discord.Role, error) {
|
||||
|
||||
var role *discord.Role
|
||||
return role, c.RequestJSON(
|
||||
&role, "POST",
|
||||
EndpointGuilds+guildID.String()+"/roles",
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
type (
|
||||
MoveRolesData struct {
|
||||
Roles []MoveRoleData
|
||||
// https://discord.com/developers/docs/resources/guild#modify-guild-role-positions-json-params
|
||||
type MoveRoleData struct {
|
||||
// ID is the id of the role.
|
||||
ID discord.RoleID `json:"id"`
|
||||
// Position is the sorting position of the role.
|
||||
Position option.NullableInt `json:"position,omitempty"`
|
||||
}
|
||||
|
||||
AuditLogReason
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#modify-guild-role-positions-json-params
|
||||
MoveRoleData struct {
|
||||
// ID is the id of the role.
|
||||
ID discord.RoleID `json:"id"`
|
||||
// Position is the sorting position of the role.
|
||||
Position option.NullableInt `json:"position,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// MoveRoles modifies the positions of a set of role objects for the guild.
|
||||
// MoveRole modifies the positions of a set of role objects for the guild.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
//
|
||||
// Fires multiple Guild Role Update Gateway events.
|
||||
func (c *Client) MoveRoles(guildID discord.GuildID, data MoveRolesData) ([]discord.Role, error) {
|
||||
func (c *Client) MoveRole(guildID discord.GuildID, data []MoveRoleData) ([]discord.Role, error) {
|
||||
var roles []discord.Role
|
||||
return roles, c.RequestJSON(
|
||||
&roles, "PATCH",
|
||||
EndpointGuilds+guildID.String()+"/roles",
|
||||
httputil.WithJSONBody(data.Roles), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -131,50 +98,37 @@ type ModifyRoleData struct {
|
|||
// Name is the name of the role.
|
||||
Name option.NullableString `json:"name,omitempty"`
|
||||
// Permissions is the bitwise value of the enabled/disabled permissions.
|
||||
Permissions *discord.Permissions `json:"permissions,string,omitempty"`
|
||||
Permissions *discord.Permissions `json:"permissions,omitempty,string"`
|
||||
// Permissions is the bitwise value of the enabled/disabled permissions.
|
||||
//
|
||||
// This value is nullable.
|
||||
Color discord.Color `json:"color,omitempty"`
|
||||
Color option.NullableColor `json:"color,omitempty"`
|
||||
// Hoist specifies whether the role should be displayed separately in the
|
||||
// sidebar.
|
||||
Hoist option.NullableBool `json:"hoist,omitempty"`
|
||||
// Mentionable specifies whether the role should be mentionable.
|
||||
Mentionable option.NullableBool `json:"mentionable,omitempty"`
|
||||
|
||||
// Icon is the icon of the role. Requires the guild to have the ROLE_ICONS feature.
|
||||
// This value is nullable.
|
||||
// To reset the role's icon, set this to NullImage.
|
||||
Icon *Image `json:"icon,omitempty"`
|
||||
// UnicodeEmoji is the role's unicode emoji. Requires the guild to have the ROLE_ICONS feature.
|
||||
UnicodeEmoji option.NullableString `json:"unicode_emoji,omitempty"`
|
||||
|
||||
AddRoleData `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyRole modifies a guild role.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
func (c *Client) ModifyRole(
|
||||
guildID discord.GuildID, roleID discord.RoleID, data ModifyRoleData) (*discord.Role, error) {
|
||||
guildID discord.GuildID, roleID discord.RoleID,
|
||||
data ModifyRoleData) (*discord.Role, error) {
|
||||
|
||||
var role *discord.Role
|
||||
return role, c.RequestJSON(
|
||||
&role, "PATCH",
|
||||
EndpointGuilds+guildID.String()+"/roles/"+roleID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteRole deletes a guild role.
|
||||
//
|
||||
// Requires the MANAGE_ROLES permission.
|
||||
func (c *Client) DeleteRole(
|
||||
guildID discord.GuildID, roleID discord.RoleID, reason AuditLogReason) error {
|
||||
|
||||
func (c *Client) DeleteRole(guildID discord.GuildID, roleID discord.RoleID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE",
|
||||
EndpointGuilds+guildID.String()+"/roles/"+roleID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,162 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
// CreateScheduledEventData is the structure for creating a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event-json-params
|
||||
type CreateScheduledEventData struct {
|
||||
// ChannelID is the channel id of the scheduled event.
|
||||
ChannelID discord.ChannelID `json:"channel_id"`
|
||||
// EntityMetadata is the entity metadata of the scheduled event.
|
||||
EntityMetadata *discord.EntityMetadata `json:"entity_metadata"`
|
||||
// Name is the name of the scheduled event.
|
||||
Name string `json:"name"`
|
||||
// PrivacyLevel is the privacy level of the scheduled event.
|
||||
PrivacyLevel discord.ScheduledEventPrivacyLevel `json:"privacy_level"`
|
||||
// StartTime is when the scheduled event begins.
|
||||
StartTime discord.Timestamp `json:"scheduled_start_time"`
|
||||
// EndTime is when the scheduled event ends, if it does.
|
||||
EndTime *discord.Timestamp `json:"scheduled_end_time,omitempty"`
|
||||
// Description is the description of the schduled event.
|
||||
Description string `json:"description"`
|
||||
// EntityType is the entity type of the scheduled event.
|
||||
EntityType discord.EntityType `json:"entity_type"`
|
||||
// Image is the cover image of the scheduled event.
|
||||
Image Image `json:"image"`
|
||||
}
|
||||
|
||||
// EditScheduledEventData is the structure for modifying a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event-json-params
|
||||
type EditScheduledEventData struct {
|
||||
// ChannelID is the new channel id of the scheduled event.
|
||||
ChannelID discord.ChannelID `json:"channel_id,omitempty"`
|
||||
// EntityMetadata is the new entity metadata of the scheduled event.
|
||||
EntityMetadata *discord.EntityMetadata `json:"entity_metadata,omitempty"`
|
||||
// Name is the new name of the scheduled event.
|
||||
Name option.NullableString `json:"name,omitempty"`
|
||||
// PrivacyLevel is the new privacy level of the scheduled event.
|
||||
PrivacyLevel discord.ScheduledEventPrivacyLevel `json:"privacy_level,omitempty"`
|
||||
// StartTime is the new starting time for when the scheduled event begins.
|
||||
StartTime *discord.Timestamp `json:"scheduled_start_time,omitempty"`
|
||||
// EndTime is the new time of which the scheduled event ends
|
||||
EndTime *discord.Timestamp `json:"scheduled_end_time,omitempty"`
|
||||
// Description is the new description of the scheduled event.
|
||||
Description option.NullableString `json:"description,omitempty"`
|
||||
// EntityType is the new entity type of the scheduled event.
|
||||
EntityType discord.EntityType `json:"entity_type,omitempty"`
|
||||
// Status is the new event status of the scheduled event.
|
||||
Status discord.EventStatus `json:"status,omitempty"`
|
||||
// Image is the new image of the scheduled event.
|
||||
Image *Image `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
// GuildScheduledEventUser represents a user interested in a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-user-object
|
||||
type GuildScheduledEventUser struct {
|
||||
// EventID is the id of the scheduled event.
|
||||
EventID discord.EventID `json:"guild_scheduled_event_id"`
|
||||
// User is the user object of the user.
|
||||
User discord.User `json:"user"`
|
||||
// Member is the member object of the user.
|
||||
Member *discord.Member `json:"member"`
|
||||
}
|
||||
|
||||
// ListScheduledEventUsers returns a list of users currently in a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users
|
||||
func (c *Client) ListScheduledEventUsers(
|
||||
guildID discord.GuildID, eventID discord.EventID, limit option.NullableInt,
|
||||
withMember bool, before, after discord.UserID) ([]GuildScheduledEventUser, error) {
|
||||
var eventUsers []GuildScheduledEventUser
|
||||
var params struct {
|
||||
Limit option.NullableInt `schema:"limit,omitempty"`
|
||||
WithMember bool `schema:"with_member,omitempty"`
|
||||
Before discord.UserID `schema:"before,omitempty"`
|
||||
After discord.UserID `schema:"after,omitempty"`
|
||||
}
|
||||
params.Limit = limit
|
||||
params.WithMember = withMember
|
||||
params.Before = before
|
||||
params.After = after
|
||||
|
||||
return eventUsers, c.RequestJSON(
|
||||
&eventUsers, "GET", EndpointGuilds+guildID.String()+"/scheduled-events/"+eventID.String()+"/users",
|
||||
httputil.WithSchema(c, params),
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
// ListScheduledEvents lists the scheduled events in a guild.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event-users
|
||||
func (c *Client) ListScheduledEvents(guildID discord.GuildID, withUserCount bool) ([]discord.GuildScheduledEvent, error) {
|
||||
var scheduledEvents []discord.GuildScheduledEvent
|
||||
var params struct {
|
||||
WithUserCount bool `schema:"with_user_count"`
|
||||
}
|
||||
params.WithUserCount = withUserCount
|
||||
return scheduledEvents, c.RequestJSON(
|
||||
&scheduledEvents, "GET", EndpointGuilds+guildID.String()+"/scheduled-events",
|
||||
httputil.WithSchema(c, params),
|
||||
)
|
||||
}
|
||||
|
||||
// CreateScheduledEvent creates a new scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#create-guild-scheduled-event
|
||||
func (c *Client) CreateScheduledEvent(guildID discord.GuildID, reason AuditLogReason,
|
||||
data CreateScheduledEventData) (*discord.GuildScheduledEvent, error) {
|
||||
var scheduledEvent *discord.GuildScheduledEvent
|
||||
return scheduledEvent, c.RequestJSON(
|
||||
&scheduledEvent, "POST",
|
||||
EndpointGuilds+guildID.String()+"/scheduled-events",
|
||||
httputil.WithJSONBody(data),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
// EditScheduledEvent modifies the attributes of a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#modify-guild-scheduled-event
|
||||
func (c *Client) EditScheduledEvent(guildID discord.GuildID, eventID discord.EventID, reason AuditLogReason,
|
||||
data EditScheduledEventData) (*discord.GuildScheduledEvent, error) {
|
||||
var modifiedEvent *discord.GuildScheduledEvent
|
||||
return modifiedEvent, c.RequestJSON(
|
||||
&modifiedEvent,
|
||||
"PATCH", EndpointGuilds+guildID.String()+"/scheduled-events/"+eventID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// DeleteScheduledEvent deletes a scheduled event.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#delete-guild-scheduled-event
|
||||
func (c *Client) DeleteScheduledEvent(guildID discord.GuildID, eventID discord.EventID) error {
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointGuilds+guildID.String()+"/scheduled-events/"+eventID.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// ScheduledEvent retrieves the information on the scheduled event
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#get-guild-scheduled-event
|
||||
func (c *Client) ScheduledEvent(guildID discord.GuildID, eventID discord.EventID, withUserCount bool) (*discord.GuildScheduledEvent, error) {
|
||||
var params struct {
|
||||
WithUserCount bool `schema:"with_user_count"`
|
||||
}
|
||||
params.WithUserCount = withUserCount
|
||||
var event *discord.GuildScheduledEvent
|
||||
return event, c.RequestJSON(
|
||||
&event, "GET", EndpointGuilds+guildID.String()+"/scheduled-events/"+eventID.String(),
|
||||
httputil.WithSchema(c, params),
|
||||
)
|
||||
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
)
|
||||
|
||||
type SearchData struct {
|
||||
Offset uint `schema:"offset,omitempty"`
|
||||
Content string `schema:"content,omitempty"`
|
||||
Has string `schema:"has,omitempty"`
|
||||
SortBy string `schema:"sort_by,omitempty"`
|
||||
SortOrder string `schema:"sort_order,omitempty"`
|
||||
ChannelID discord.ChannelID `schema:"channel_id,omitempty"`
|
||||
AuthorID discord.UserID `schema:"author_id,omitempty"`
|
||||
Mentions discord.UserID `schema:"mentions,omitempty"`
|
||||
MaxID discord.MessageID `schema:"max_id,omitempty"`
|
||||
MinID discord.MessageID `schema:"min_id,omitempty"`
|
||||
}
|
||||
|
||||
type SearchResponse struct {
|
||||
AnalyticsID string `json:"analytics_id"`
|
||||
Messages [][]discord.Message `json:"messages"`
|
||||
TotalResults uint `json:"total_results"`
|
||||
}
|
||||
|
||||
// Search searches through a guild's messages. It only works for user accounts.
|
||||
func (c *Client) Search(guildID discord.GuildID, data SearchData) (SearchResponse, error) {
|
||||
var resp SearchResponse
|
||||
|
||||
return resp, c.RequestJSON(
|
||||
&resp, "GET",
|
||||
EndpointGuilds+guildID.String()+"/messages/search",
|
||||
httputil.WithSchema(c, data),
|
||||
)
|
||||
}
|
161
api/send.go
161
api/send.go
|
@ -1,26 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
)
|
||||
|
||||
const AttachmentSpoilerPrefix = "SPOILER_"
|
||||
|
||||
// AllowedMentions is a allowlist of mentions for a message.
|
||||
// AllowedMentions is a whitelist of mentions for a message.
|
||||
// https://discordapp.com/developers/docs/resources/channel#allowed-mentions-object
|
||||
//
|
||||
// Allowlists
|
||||
// Whitelists
|
||||
//
|
||||
// Roles and Users are slices that act as allowlists for IDs that are allowed
|
||||
// to be mentioned. For example, if only 1 ID is provided in Users, then only
|
||||
// that ID will be parsed in the message. No other IDs will be. The same
|
||||
// example also applies for roles.
|
||||
// Roles and Users are slices that act as whitelists for IDs that are allowed to
|
||||
// be mentioned. For example, if only 1 ID is provided in Users, then only that
|
||||
// ID will be parsed in the message. No other IDs will be. The same example also
|
||||
// applies for roles.
|
||||
//
|
||||
// If Parse is an empty slice and both Users and Roles are empty slices, then no
|
||||
// mentions will be parsed.
|
||||
|
@ -31,9 +33,7 @@ const AttachmentSpoilerPrefix = "SPOILER_"
|
|||
// Likewise, if the Roles slice is not empty, then Parse must not have
|
||||
// AllowRoleMention. This is because everything provided in Parse will make
|
||||
// Discord parse it completely, meaning they would be mutually exclusive with
|
||||
// Roles and Users.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/channel#allowed-mentions-object
|
||||
// whitelist slices, Roles and Users.
|
||||
type AllowedMentions struct {
|
||||
// Parse is an array of allowed mention types to parse from the content.
|
||||
Parse []AllowedMentionType `json:"parse"`
|
||||
|
@ -41,17 +41,13 @@ type AllowedMentions struct {
|
|||
Roles []discord.RoleID `json:"roles,omitempty"`
|
||||
// Users is an array of user_ids to mention (Max size of 100).
|
||||
Users []discord.UserID `json:"users,omitempty"`
|
||||
// RepliedUser is used specifically for inline replies to specify, whether
|
||||
// to mention the author of the message you are replying to or not.
|
||||
RepliedUser option.Bool `json:"replied_user,omitempty"`
|
||||
}
|
||||
|
||||
// AllowedMentionType is a constant that tells Discord what is allowed to parse
|
||||
// from a message content. This can help prevent things such as an
|
||||
// unintentional @everyone mention.
|
||||
// from a message content. This can help prevent things such as an unintentional
|
||||
// @everyone mention.
|
||||
type AllowedMentionType string
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#allowed-mentions-object-allowed-mention-types
|
||||
const (
|
||||
// AllowRoleMention makes Discord parse roles in the content.
|
||||
AllowRoleMention AllowedMentionType = "roles"
|
||||
|
@ -88,9 +84,15 @@ func (am AllowedMentions) Verify() error {
|
|||
}
|
||||
|
||||
// ErrEmptyMessage is returned if either a SendMessageData or an
|
||||
// ExecuteWebhookData is missing content, embeds, and files.
|
||||
// ExecuteWebhookData has both an empty Content and no Embed(s).
|
||||
var ErrEmptyMessage = errors.New("message is empty")
|
||||
|
||||
// SendMessageFile represents a file to be uploaded to Discord.
|
||||
type SendMessageFile struct {
|
||||
Name string
|
||||
Reader io.Reader
|
||||
}
|
||||
|
||||
// SendMessageData is the full structure to send a new message to Discord with.
|
||||
type SendMessageData struct {
|
||||
// Content are the message contents (up to 2000 characters).
|
||||
|
@ -101,36 +103,16 @@ type SendMessageData struct {
|
|||
// TTS is true if this is a TTS message.
|
||||
TTS bool `json:"tts,omitempty"`
|
||||
// Embed is embedded rich content.
|
||||
Embeds []discord.Embed `json:"embeds,omitempty"`
|
||||
Embed *discord.Embed `json:"embed,omitempty"`
|
||||
|
||||
// Files is the list of file attachments to be uploaded. To reference a file
|
||||
// in an embed, use (sendpart.File).AttachmentURI().
|
||||
Files []sendpart.File `json:"-"`
|
||||
// Components is the list of components (such as buttons) to be attached to
|
||||
// the message.
|
||||
Components discord.ContainerComponents `json:"components,omitempty"`
|
||||
Files []SendMessageFile `json:"-"`
|
||||
|
||||
// AllowedMentions are the allowed mentions for a message.
|
||||
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
// Reference allows you to reference another message to create a reply. The
|
||||
// referenced message must be from the same channel.
|
||||
//
|
||||
// Only MessageID is necessary. You may also include a channel_id and
|
||||
// guild_id in the reference. However, they are not necessary, but will be
|
||||
// validated if sent.
|
||||
Reference *discord.MessageReference `json:"message_reference,omitempty"`
|
||||
|
||||
// Flags specifies the message flags to set (only `SuppressEmbeds` and `SuppressNotifications` can be set).
|
||||
Flags discord.MessageFlags `json:"flags,omitempty"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the SendMessageData has files.
|
||||
func (data SendMessageData) NeedsMultipart() bool {
|
||||
return len(data.Files) > 0
|
||||
}
|
||||
|
||||
func (data SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, data, data.Files)
|
||||
func (data *SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
||||
return writeMultipart(body, data, data.Files)
|
||||
}
|
||||
|
||||
// SendMessageComplex posts a message to a guild text or DM channel. If
|
||||
|
@ -154,7 +136,8 @@ func (data SendMessageData) WriteMultipart(body *multipart.Writer) error {
|
|||
// Content-Disposition subpart header MUST contain a filename parameter.
|
||||
func (c *Client) SendMessageComplex(
|
||||
channelID discord.ChannelID, data SendMessageData) (*discord.Message, error) {
|
||||
if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
|
||||
|
||||
if data.Content == "" && data.Embed == nil && len(data.Files) == 0 {
|
||||
return nil, ErrEmptyMessage
|
||||
}
|
||||
|
||||
|
@ -164,20 +147,88 @@ func (c *Client) SendMessageComplex(
|
|||
}
|
||||
}
|
||||
|
||||
sum := 0
|
||||
for i, embed := range data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
if data.Embed != nil {
|
||||
if err := data.Embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error")
|
||||
}
|
||||
sum += embed.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{Count: sum, Max: 6000, Thing: "sum of all text in embeds"}
|
||||
}
|
||||
|
||||
data.Embeds[i] = embed // embed.Validate changes fields
|
||||
}
|
||||
|
||||
var URL = EndpointChannels + channelID.String() + "/messages"
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.POST(c.Client, data, &msg, URL)
|
||||
|
||||
if len(data.Files) == 0 {
|
||||
// No files, so no need for streaming.
|
||||
return msg, c.RequestJSON(&msg, "POST", URL, httputil.WithJSONBody(data))
|
||||
}
|
||||
|
||||
writer := func(mw *multipart.Writer) error {
|
||||
return data.WriteMultipart(mw)
|
||||
}
|
||||
|
||||
resp, err := c.MeanwhileMultipart(writer, "POST", URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body = resp.GetBody()
|
||||
defer body.Close()
|
||||
|
||||
return msg, json.DecodeStream(body, &msg)
|
||||
}
|
||||
|
||||
type ExecuteWebhookData struct {
|
||||
// Content are the message contents (up to 2000 characters).
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Content string `json:"content,omitempty"`
|
||||
|
||||
// Username overrides the default username of the webhook
|
||||
Username string `json:"username,omitempty"`
|
||||
// AvatarURL overrides the default avatar of the webhook.
|
||||
AvatarURL discord.URL `json:"avatar_url,omitempty"`
|
||||
|
||||
// TTS is true if this is a TTS message.
|
||||
TTS bool `json:"tts,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Embeds []discord.Embed `json:"embeds,omitempty"`
|
||||
|
||||
Files []SendMessageFile `json:"-"`
|
||||
|
||||
// AllowedMentions are the allowed mentions for the message.
|
||||
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
}
|
||||
|
||||
func (data *ExecuteWebhookData) WriteMultipart(body *multipart.Writer) error {
|
||||
return writeMultipart(body, data, data.Files)
|
||||
}
|
||||
|
||||
func writeMultipart(body *multipart.Writer, item interface{}, files []SendMessageFile) error {
|
||||
defer body.Close()
|
||||
|
||||
// Encode the JSON body first
|
||||
w, err := body.CreateFormField("payload_json")
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for JSON")
|
||||
}
|
||||
|
||||
if err := json.EncodeStream(w, item); err != nil {
|
||||
return errors.Wrap(err, "failed to encode JSON")
|
||||
}
|
||||
|
||||
for i, file := range files {
|
||||
num := strconv.Itoa(i)
|
||||
|
||||
w, err := body.CreateFormFile("file"+num, file.Name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create bodypart for "+num)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, file.Reader); err != nil {
|
||||
return errors.Wrap(err, "failed to write for file "+num)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
func TestMarshalAllowedMentions(t *testing.T) {
|
||||
|
@ -101,7 +100,10 @@ func TestSendMessage(t *testing.T) {
|
|||
}
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
var empty SendMessageData
|
||||
var empty = SendMessageData{
|
||||
Content: "",
|
||||
Embed: nil,
|
||||
}
|
||||
|
||||
if err := send(empty); err != ErrEmptyMessage {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
|
@ -110,7 +112,7 @@ func TestSendMessage(t *testing.T) {
|
|||
|
||||
t.Run("files only", func(t *testing.T) {
|
||||
var empty = SendMessageData{
|
||||
Files: []sendpart.File{{Name: "test.jpg"}},
|
||||
Files: []SendMessageFile{{Name: "test.jpg"}},
|
||||
}
|
||||
|
||||
if err := send(empty); err != nil {
|
||||
|
@ -133,10 +135,10 @@ func TestSendMessage(t *testing.T) {
|
|||
|
||||
t.Run("invalid embed", func(t *testing.T) {
|
||||
var data = SendMessageData{
|
||||
Embeds: []discord.Embed{{
|
||||
Embed: &discord.Embed{
|
||||
// max 256
|
||||
Title: spaces(257),
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
err := send(data)
|
||||
|
|
67
api/stage.go
67
api/stage.go
|
@ -1,67 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
)
|
||||
|
||||
var EndpointStageInstances = Endpoint + "stage-instances/"
|
||||
|
||||
// https://discord.com/developers/docs/resources/stage-instance#create-stage-instance-json-params
|
||||
type CreateStageInstanceData struct {
|
||||
// ChannelID is the id of the Stage channel.
|
||||
ChannelID discord.ChannelID `json:"channel_id"`
|
||||
// Topic is the topic of the Stage instance (1-120 characters).
|
||||
Topic string `json:"topic"`
|
||||
// PrivacyLevel is the privacy level of the Stage instance.
|
||||
//
|
||||
// Defaults to discord.GuildOnlyStage.
|
||||
PrivacyLevel discord.PrivacyLevel `json:"privacy_level,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// CreateStageInstance creates a new Stage instance associated to a Stage
|
||||
// channel.
|
||||
//
|
||||
// It requires the user to be a moderator of the Stage channel.
|
||||
func (c *Client) CreateStageInstance(
|
||||
data CreateStageInstanceData) (*discord.StageInstance, error) {
|
||||
|
||||
var s *discord.StageInstance
|
||||
return s, c.RequestJSON(
|
||||
&s, "POST",
|
||||
EndpointStageInstances,
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/stage-instance#update-stage-instance-json-params
|
||||
type UpdateStageInstanceData struct {
|
||||
// Topic is the topic of the Stage instance (1-120 characters).
|
||||
Topic string `json:"topic,omitempty"`
|
||||
// PrivacyLevel is the privacy level of the Stage instance.
|
||||
PrivacyLevel discord.PrivacyLevel `json:"privacy_level,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// UpdateStageInstance updates fields of an existing Stage instance.
|
||||
//
|
||||
// It requires the user to be a moderator of the Stage channel.
|
||||
func (c *Client) UpdateStageInstance(
|
||||
channelID discord.ChannelID, data UpdateStageInstanceData) error {
|
||||
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointStageInstances+channelID.String(),
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
}
|
||||
|
||||
func (c *Client) DeleteStageInstance(channelID discord.ChannelID, reason AuditLogReason) error {
|
||||
return c.FastRequest(
|
||||
"DELETE", EndpointStageInstances+channelID.String(),
|
||||
httputil.WithHeaders(reason.Header()),
|
||||
)
|
||||
}
|
36
api/user.go
36
api/user.go
|
@ -1,9 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -26,30 +26,24 @@ func (c *Client) Me() (*discord.User, error) {
|
|||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/user#modify-current-user-json-params
|
||||
type ModifyCurrentUserData struct {
|
||||
type ModifySelfData struct {
|
||||
// Username is the user's username, if changed may cause the user's
|
||||
// discriminator to be randomized.
|
||||
Username option.String `json:"username,omitempty"`
|
||||
// Avatar modifies the user's avatar.
|
||||
Avatar *Image `json:"image,omitempty"`
|
||||
|
||||
AuditLogReason `json:"-"`
|
||||
}
|
||||
|
||||
// ModifyCurrentUser modifies the requester's user account settings.
|
||||
func (c *Client) ModifyCurrentUser(data ModifyCurrentUserData) (*discord.User, error) {
|
||||
// ModifyMe modifies the requester's user account settings.
|
||||
func (c *Client) ModifyMe(data ModifySelfData) (*discord.User, error) {
|
||||
var u *discord.User
|
||||
return u, c.RequestJSON(
|
||||
&u,
|
||||
"PATCH", EndpointMe,
|
||||
httputil.WithJSONBody(data), httputil.WithHeaders(data.Header()),
|
||||
)
|
||||
return u, c.RequestJSON(&u, "PATCH", EndpointMe, httputil.WithJSONBody(data))
|
||||
}
|
||||
|
||||
// ModifyCurrentMember modifies the nickname of the current user in a guild.
|
||||
// ChangeOwnNickname modifies the nickname of the current user in a guild.
|
||||
//
|
||||
// Fires a Guild Member Update Gateway event.
|
||||
func (c *Client) ModifyCurrentMember(
|
||||
func (c *Client) ChangeOwnNickname(
|
||||
guildID discord.GuildID, nick string) error {
|
||||
|
||||
var param struct {
|
||||
|
@ -60,7 +54,7 @@ func (c *Client) ModifyCurrentMember(
|
|||
|
||||
return c.FastRequest(
|
||||
"PATCH",
|
||||
EndpointGuilds+guildID.String()+"/members/@me",
|
||||
EndpointGuilds+guildID.String()+"/members/@me/nick",
|
||||
httputil.WithJSONBody(param),
|
||||
)
|
||||
}
|
||||
|
@ -92,16 +86,6 @@ func (c *Client) UserConnections() ([]discord.Connection, error) {
|
|||
return conn, c.RequestJSON(&conn, "GET", EndpointMe+"/connections")
|
||||
}
|
||||
|
||||
// Note gets the note for the given user. This endpoint is undocumented and
|
||||
// might only work for user accounts.
|
||||
func (c *Client) Note(userID discord.UserID) (string, error) {
|
||||
var body struct {
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
return body.Note, c.RequestJSON(&body, "GET", EndpointMe+"/notes/"+userID.String())
|
||||
}
|
||||
|
||||
// SetNote sets a note for the user. This endpoint is undocumented and might
|
||||
// only work for user accounts.
|
||||
func (c *Client) SetNote(userID discord.UserID, note string) error {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/utils/json/option"
|
||||
)
|
||||
|
||||
var EndpointWebhooks = Endpoint + "webhooks/"
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func writeError(w http.ResponseWriter, code int, err error) {
|
||||
var resp struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
} else {
|
||||
resp.Error = http.StatusText(code)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
log.Panicln("cannot marshal error response:", err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
// InteractionHandler is a type whose method is called on every incoming event.
|
||||
type InteractionHandler interface {
|
||||
// HandleInteraction is expected to return a response synchronously, either
|
||||
// to be followed-up later by deferring the response or to be responded
|
||||
// immediately.
|
||||
HandleInteraction(*discord.InteractionEvent) *api.InteractionResponse
|
||||
}
|
||||
|
||||
// InteractionHandlerFunc is a function type that implements the interface.
|
||||
type InteractionHandlerFunc func(*discord.InteractionEvent) *api.InteractionResponse
|
||||
|
||||
var _ InteractionHandler = InteractionHandlerFunc(nil)
|
||||
|
||||
func (f InteractionHandlerFunc) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
return f(ev)
|
||||
}
|
||||
|
||||
type alwaysDeferInteraction struct {
|
||||
f func(*discord.InteractionEvent)
|
||||
flags discord.MessageFlags
|
||||
}
|
||||
|
||||
// AlwaysDeferInteraction always returns a DeferredMessageInteractionWithSource
|
||||
// then invokes f in the background. This allows f to always use the follow-up
|
||||
// functions.
|
||||
func AlwaysDeferInteraction(flags discord.MessageFlags, f func(*discord.InteractionEvent)) InteractionHandler {
|
||||
return alwaysDeferInteraction{f, flags}
|
||||
}
|
||||
|
||||
func (f alwaysDeferInteraction) HandleInteraction(ev *discord.InteractionEvent) *api.InteractionResponse {
|
||||
go f.f(ev)
|
||||
return &api.InteractionResponse{
|
||||
Type: api.DeferredMessageInteractionWithSource,
|
||||
Data: &api.InteractionResponseData{
|
||||
Flags: f.flags,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// InteractionErrorFunc is called to write an error. err may be nil with a
|
||||
// non-2xx code.
|
||||
type InteractionErrorFunc func(w http.ResponseWriter, r *http.Request, code int, err error)
|
||||
|
||||
// InteractionServer provides a HTTP handler to verify and handle Interaction
|
||||
// Create events sent by Discord into a HTTP endpoint..
|
||||
type InteractionServer struct {
|
||||
ErrorFunc InteractionErrorFunc
|
||||
|
||||
interactionHandler InteractionHandler
|
||||
httpHandler http.Handler
|
||||
pubkey ed25519.PublicKey
|
||||
}
|
||||
|
||||
// NewInteractionServer creates a new InteractionServer instance. pubkey should
|
||||
// be hex-encoded.
|
||||
func NewInteractionServer(pubkey string, handler InteractionHandler) (*InteractionServer, error) {
|
||||
pubkeyB, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "cannot decode hex pubkey")
|
||||
}
|
||||
|
||||
s := InteractionServer{
|
||||
ErrorFunc: func(w http.ResponseWriter, r *http.Request, code int, err error) {
|
||||
writeError(w, code, err)
|
||||
},
|
||||
interactionHandler: handler,
|
||||
httpHandler: nil,
|
||||
pubkey: pubkeyB,
|
||||
}
|
||||
|
||||
s.httpHandler = http.HandlerFunc(s.handle)
|
||||
if len(s.pubkey) != 0 {
|
||||
s.httpHandler = s.withVerification(s.httpHandler)
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (s *InteractionServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.httpHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *InteractionServer) handle(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
var ev discord.InteractionEvent
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&ev); err != nil {
|
||||
s.ErrorFunc(w, r, 400, errors.Wrap(err, "cannot decode interaction body"))
|
||||
return
|
||||
}
|
||||
|
||||
switch ev.Data.(type) {
|
||||
case *discord.PingInteraction:
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(api.InteractionResponse{
|
||||
Type: api.PongInteraction,
|
||||
})
|
||||
}
|
||||
|
||||
resp := s.interactionHandler.HandleInteraction(&ev)
|
||||
if resp != nil && resp.Type != api.PongInteraction {
|
||||
if resp.NeedsMultipart() {
|
||||
body := multipart.NewWriter(w)
|
||||
w.Header().Set("Content-Type", body.FormDataContentType())
|
||||
resp.WriteMultipart(body)
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
}
|
||||
default:
|
||||
s.ErrorFunc(w, r, http.StatusMethodNotAllowed, errors.New("method not allowed"))
|
||||
}
|
||||
}
|
||||
|
||||
// withVerification was written thanks to @bsdlp and their code
|
||||
// https://github.com/bsdlp/discord-interactions-go/blob/a2ba844/interactions/verify_example_test.go#L63.
|
||||
func (s *InteractionServer) withVerification(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
signature := r.Header.Get("X-Signature-Ed25519")
|
||||
if signature == "" {
|
||||
s.ErrorFunc(w, r, 401, errors.New("missing header X-Signature-Ed25519"))
|
||||
return
|
||||
}
|
||||
|
||||
sig, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
s.ErrorFunc(w, r, 400, errors.Wrap(err, "X-Signature-Ed25519 is not valid hex-encoded"))
|
||||
return
|
||||
}
|
||||
|
||||
if len(sig) != ed25519.SignatureSize || sig[63]&224 != 0 {
|
||||
s.ErrorFunc(w, r, 400, errors.New("invalid X-Signature-Ed25519 data"))
|
||||
return
|
||||
}
|
||||
|
||||
timestamp := r.Header.Get("X-Signature-Timestamp")
|
||||
if timestamp == "" {
|
||||
s.ErrorFunc(w, r, 401, errors.New("missing header X-Signature-Timestamp"))
|
||||
return
|
||||
}
|
||||
|
||||
var msg bytes.Buffer
|
||||
msg.Grow(int(r.ContentLength+1) + len(timestamp))
|
||||
msg.WriteString(timestamp)
|
||||
|
||||
if _, err := io.Copy(&msg, r.Body); err != nil {
|
||||
s.ErrorFunc(w, r, 500, errors.Wrap(err, "cannot read body"))
|
||||
return
|
||||
}
|
||||
|
||||
if !ed25519.Verify(s.pubkey, msg.Bytes(), sig) {
|
||||
s.ErrorFunc(w, r, 401, errors.New("signature mismatch"))
|
||||
return
|
||||
}
|
||||
|
||||
// Return the request body for use.
|
||||
body := msg.Bytes()[len(timestamp):]
|
||||
r.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
|
@ -1,288 +0,0 @@
|
|||
// Package webhook provides means to interact with webhooks directly and not
|
||||
// through the bot API.
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/api/rate"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/v3/utils/sendpart"
|
||||
)
|
||||
|
||||
// TODO: if there's ever an Arikawa v3, then a new Client abstraction could be
|
||||
// made that wraps around Session being an interface. Just a food for thought.
|
||||
|
||||
// Session keeps a single webhook session. It is referenced by other webhook
|
||||
// clients using the same session.
|
||||
type Session struct {
|
||||
// Limiter is the rate limiter used for the client. This field should not be
|
||||
// changed, as doing so is potentially racy.
|
||||
Limiter *rate.Limiter
|
||||
|
||||
// ID is the ID of the webhook.
|
||||
ID discord.WebhookID
|
||||
// Token is the token of the webhook.
|
||||
Token string
|
||||
}
|
||||
|
||||
// OnRequest should be called on each client request to inject itself.
|
||||
func (s *Session) OnRequest(r httpdriver.Request) error {
|
||||
return s.Limiter.Acquire(r.GetContext(), r.GetPath())
|
||||
}
|
||||
|
||||
// OnResponse should be called after each client request to clean itself up.
|
||||
func (s *Session) OnResponse(r httpdriver.Request, resp httpdriver.Response) error {
|
||||
return s.Limiter.Release(r.GetPath(), httpdriver.OptHeader(resp))
|
||||
}
|
||||
|
||||
// Client is the client used to interact with a webhook.
|
||||
type Client struct {
|
||||
// Client is the httputil.Client used to call Discord's API.
|
||||
*httputil.Client
|
||||
*Session
|
||||
}
|
||||
|
||||
// New creates a new Client using the passed webhook token and ID. It uses its
|
||||
// own rate limiter.
|
||||
func New(id discord.WebhookID, token string) *Client {
|
||||
return NewCustom(id, token, httputil.NewClient())
|
||||
}
|
||||
|
||||
// NewCustom creates a new webhook client using the passed webhook token, ID and
|
||||
// a copy of the given httputil.Client. The copy will have a new rate limiter
|
||||
// added in.
|
||||
func NewCustom(id discord.WebhookID, token string, hcl *httputil.Client) *Client {
|
||||
ses := Session{
|
||||
Limiter: rate.NewLimiter(api.Path),
|
||||
ID: id,
|
||||
Token: token,
|
||||
}
|
||||
|
||||
hcl = hcl.Copy()
|
||||
hcl.OnRequest = append(hcl.OnRequest, ses.OnRequest)
|
||||
hcl.OnResponse = append(hcl.OnResponse, ses.OnResponse)
|
||||
|
||||
return &Client{
|
||||
Client: hcl,
|
||||
Session: &ses,
|
||||
}
|
||||
}
|
||||
|
||||
// FromAPI creates a new client that shares the same internal HTTP client with
|
||||
// the one in the API's. This is often useful for bots that need webhook
|
||||
// interaction, since the rate limiter is shared.
|
||||
func FromAPI(id discord.WebhookID, token string, c *api.Client) *Client {
|
||||
return &Client{
|
||||
Client: c.Client,
|
||||
Session: &Session{
|
||||
Limiter: c.Limiter,
|
||||
ID: id,
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext returns a shallow copy of Client with the given context. It's
|
||||
// used for method timeouts and such. This method is thread-safe.
|
||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
||||
return &Client{
|
||||
Client: c.Client.WithContext(ctx),
|
||||
Session: c.Session,
|
||||
}
|
||||
}
|
||||
|
||||
// Get gets the webhook.
|
||||
func (c *Client) Get() (*discord.Webhook, error) {
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(&w, "GET", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
|
||||
}
|
||||
|
||||
// Modify modifies the webhook.
|
||||
func (c *Client) Modify(data api.ModifyWebhookData) (*discord.Webhook, error) {
|
||||
var w *discord.Webhook
|
||||
return w, c.RequestJSON(
|
||||
&w, "PATCH",
|
||||
api.EndpointWebhooks+c.ID.String()+"/"+c.Token,
|
||||
httputil.WithJSONBody(data),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete deletes a webhook permanently.
|
||||
func (c *Client) Delete() error {
|
||||
return c.FastRequest("DELETE", api.EndpointWebhooks+c.ID.String()+"/"+c.Token)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/webhook#execute-webhook-jsonform-params
|
||||
type ExecuteData struct {
|
||||
// Content are the message contents (up to 2000 characters).
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Content string `json:"content,omitempty"`
|
||||
|
||||
// ThreadID causes the message to be sent to the specified thread within
|
||||
// the webhook's channel. The thread will automatically be unarchived.
|
||||
ThreadID discord.CommandID `json:"-"`
|
||||
|
||||
// Username overrides the default username of the webhook
|
||||
Username string `json:"username,omitempty"`
|
||||
// AvatarURL overrides the default avatar of the webhook.
|
||||
AvatarURL discord.URL `json:"avatar_url,omitempty"`
|
||||
|
||||
// TTS is true if this is a TTS message.
|
||||
TTS bool `json:"tts,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
//
|
||||
// Required: one of content, file, embeds
|
||||
Embeds []discord.Embed `json:"embeds,omitempty"`
|
||||
|
||||
// Components is the list of components (such as buttons) to be attached to
|
||||
// the message.
|
||||
Components discord.ContainerComponents `json:"components,omitempty"`
|
||||
|
||||
// Files represents a list of files to upload. This will not be
|
||||
// JSON-encoded and will only be available through WriteMultipart.
|
||||
Files []sendpart.File `json:"-"`
|
||||
|
||||
// AllowedMentions are the allowed mentions for the message.
|
||||
AllowedMentions *api.AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the ExecuteWebhookData has files.
|
||||
func (data ExecuteData) NeedsMultipart() bool {
|
||||
return len(data.Files) > 0
|
||||
}
|
||||
|
||||
// WriteMultipart writes the webhook data into the given multipart body. It does
|
||||
// not close body.
|
||||
func (data ExecuteData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, data, data.Files)
|
||||
}
|
||||
|
||||
// Execute sends a message to the webhook, but doesn't wait for the message to
|
||||
// get created. This is generally faster, but only applicable if no further
|
||||
// interaction is required.
|
||||
func (c *Client) Execute(data ExecuteData) (err error) {
|
||||
_, err = c.execute(data, false)
|
||||
return
|
||||
}
|
||||
|
||||
// ExecuteAndWait executes the webhook, and waits for the generated
|
||||
// discord.Message to be returned.
|
||||
func (c *Client) ExecuteAndWait(data ExecuteData) (*discord.Message, error) {
|
||||
return c.execute(data, true)
|
||||
}
|
||||
|
||||
func (c *Client) execute(data ExecuteData, wait bool) (*discord.Message, error) {
|
||||
if data.Content == "" && len(data.Embeds) == 0 && len(data.Files) == 0 {
|
||||
return nil, api.ErrEmptyMessage
|
||||
}
|
||||
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
|
||||
sum := 0
|
||||
for i, embed := range data.Embeds {
|
||||
if err := embed.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error at "+strconv.Itoa(i))
|
||||
}
|
||||
sum += embed.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{sum, 6000, "sum of all text in embeds"}
|
||||
}
|
||||
}
|
||||
|
||||
param := make(url.Values, 2)
|
||||
if wait {
|
||||
param["wait"] = []string{"true"}
|
||||
}
|
||||
if data.ThreadID.IsValid() {
|
||||
param["thread_id"] = []string{data.ThreadID.String()}
|
||||
}
|
||||
|
||||
var URL = api.EndpointWebhooks + c.ID.String() + "/" + c.Token + "?" + param.Encode()
|
||||
|
||||
var msg *discord.Message
|
||||
var ptr interface{}
|
||||
if wait {
|
||||
ptr = &msg
|
||||
}
|
||||
|
||||
return msg, sendpart.POST(c.Client, data, ptr, URL)
|
||||
}
|
||||
|
||||
// Message returns a previously-sent webhook message from the same token.
|
||||
func (c *Client) Message(messageID discord.MessageID) (*discord.Message, error) {
|
||||
var m *discord.Message
|
||||
return m, c.RequestJSON(
|
||||
&m, "GET",
|
||||
api.EndpointWebhooks+c.ID.String()+"/"+c.Token+"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params
|
||||
type EditMessageData struct {
|
||||
// Content is the new message contents (up to 2000 characters).
|
||||
Content option.NullableString `json:"content,omitempty"`
|
||||
// Embeds contains embedded rich content.
|
||||
Embeds *[]discord.Embed `json:"embeds,omitempty"`
|
||||
// Components contains the new components to attach.
|
||||
Components *discord.ContainerComponents `json:"components,omitempty"`
|
||||
// AllowedMentions are the allowed mentions for a message.
|
||||
AllowedMentions *api.AllowedMentions `json:"allowed_mentions,omitempty"`
|
||||
// Attachments are the attached files to keep
|
||||
Attachments *[]discord.Attachment `json:"attachments,omitempty"`
|
||||
|
||||
Files []sendpart.File `json:"-"`
|
||||
}
|
||||
|
||||
// EditMessage edits a previously-sent webhook message from the same webhook.
|
||||
func (c *Client) EditMessage(messageID discord.MessageID, data EditMessageData) (*discord.Message, error) {
|
||||
if data.AllowedMentions != nil {
|
||||
if err := data.AllowedMentions.Verify(); err != nil {
|
||||
return nil, errors.Wrap(err, "allowedMentions error")
|
||||
}
|
||||
}
|
||||
if data.Embeds != nil {
|
||||
sum := 0
|
||||
for _, e := range *data.Embeds {
|
||||
if err := e.Validate(); err != nil {
|
||||
return nil, errors.Wrap(err, "embed error")
|
||||
}
|
||||
sum += e.Length()
|
||||
if sum > 6000 {
|
||||
return nil, &discord.OverboundError{sum, 6000, "sum of text in embeds"}
|
||||
}
|
||||
}
|
||||
}
|
||||
var msg *discord.Message
|
||||
return msg, sendpart.PATCH(c.Client, data, &msg,
|
||||
api.EndpointWebhooks+c.ID.String()+"/"+c.Token+"/messages/"+messageID.String())
|
||||
}
|
||||
|
||||
// NeedsMultipart returns true if the SendMessageData has files.
|
||||
func (data EditMessageData) NeedsMultipart() bool {
|
||||
return len(data.Files) > 0
|
||||
}
|
||||
|
||||
func (data EditMessageData) WriteMultipart(body *multipart.Writer) error {
|
||||
return sendpart.Write(body, data, data.Files)
|
||||
}
|
||||
|
||||
// DeleteMessage deletes a message that was previously created by the same
|
||||
// webhook.
|
||||
func (c *Client) DeleteMessage(messageID discord.MessageID) error {
|
||||
return c.FastRequest("DELETE",
|
||||
api.EndpointWebhooks+c.ID.String()+"/"+c.Token+"/messages/"+messageID.String())
|
||||
}
|
11
arikawa.go
11
arikawa.go
|
@ -30,11 +30,12 @@ package arikawa
|
|||
|
||||
import (
|
||||
// Packages that most should use.
|
||||
_ "github.com/diamondburned/arikawa/v3/session"
|
||||
_ "github.com/diamondburned/arikawa/v3/state"
|
||||
_ "github.com/diamondburned/arikawa/v3/voice"
|
||||
_ "github.com/diamondburned/arikawa/bot"
|
||||
_ "github.com/diamondburned/arikawa/session"
|
||||
_ "github.com/diamondburned/arikawa/state"
|
||||
_ "github.com/diamondburned/arikawa/voice"
|
||||
|
||||
// Low level packages.
|
||||
_ "github.com/diamondburned/arikawa/v3/api"
|
||||
_ "github.com/diamondburned/arikawa/v3/gateway"
|
||||
_ "github.com/diamondburned/arikawa/api"
|
||||
_ "github.com/diamondburned/arikawa/gateway"
|
||||
)
|
||||
|
|
|
@ -70,9 +70,10 @@ func (r ArgumentParts) Usage() string {
|
|||
}
|
||||
|
||||
// CustomParser has a CustomParse method, which would be passed in the full
|
||||
// message content with the prefix, command, subcommand and space trimmed. This
|
||||
// is used for commands that require more advanced parsing than the default
|
||||
// parser.
|
||||
// message content with the prefix and command trimmed. This is used
|
||||
// for commands that require more advanced parsing than the default parser.
|
||||
//
|
||||
// Keep in mind that this does not trim arguments before it.
|
||||
type CustomParser interface {
|
||||
CustomParse(arguments string) error
|
||||
}
|
||||
|
@ -99,10 +100,9 @@ type Argument struct {
|
|||
pointer bool
|
||||
|
||||
// if nil, then manual
|
||||
fn argumentValueFn
|
||||
|
||||
manual func(ManualParser, []string) error
|
||||
custom func(CustomParser, string) error
|
||||
fn argumentValueFn
|
||||
manual *reflect.Method
|
||||
custom *reflect.Method
|
||||
}
|
||||
|
||||
func (a *Argument) Type() reflect.Type {
|
||||
|
@ -132,6 +132,9 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
|
||||
// This shouldn't be variadic.
|
||||
if !variadic && typeI.Implements(typeICusP) {
|
||||
mt, _ := typeI.MethodByName("CustomParse")
|
||||
|
||||
// TODO: maybe ish?
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
@ -140,12 +143,14 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
String: fromUsager(t),
|
||||
rtype: t,
|
||||
pointer: ptr,
|
||||
custom: CustomParser.CustomParse,
|
||||
custom: &mt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This shouldn't be variadic either.
|
||||
if !variadic && typeI.Implements(typeIManP) {
|
||||
mt, _ := typeI.MethodByName("ParseContent")
|
||||
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
@ -154,7 +159,7 @@ func newArgument(t reflect.Type, variadic bool) (*Argument, error) {
|
|||
String: fromUsager(t),
|
||||
rtype: t,
|
||||
pointer: ptr,
|
||||
manual: ManualParser.ParseContent,
|
||||
manual: &mt,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -2,31 +2,8 @@ package bot
|
|||
|
||||
import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/utils/ws"
|
||||
)
|
||||
|
||||
var (
|
||||
// eventIntents maps event pointer types to intents.
|
||||
eventIntents map[reflect.Type]gateway.Intents
|
||||
eventIntentsOnce sync.Once
|
||||
)
|
||||
|
||||
func ensureEventIntents() {
|
||||
eventIntentsOnce.Do(func() {
|
||||
eventIntents = map[reflect.Type]gateway.Intents{}
|
||||
gateway.OpUnmarshalers.Each(func(_ ws.OpCode, t ws.EventType, f ws.OpFunc) bool {
|
||||
intent, ok := gateway.EventIntents[t]
|
||||
if ok {
|
||||
eventIntents[reflect.TypeOf(f())] = intent
|
||||
}
|
||||
return false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type command struct {
|
||||
value reflect.Value // Func
|
||||
event reflect.Type
|
||||
|
@ -49,17 +26,6 @@ func (c *command) call(arg0 interface{}, argv ...reflect.Value) (interface{}, er
|
|||
return callWith(c.value, arg0, argv...)
|
||||
}
|
||||
|
||||
// intents returns the command's intents from the event.
|
||||
func (c *command) intents() gateway.Intents {
|
||||
ensureEventIntents()
|
||||
|
||||
intents, ok := eventIntents[c.event]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return intents
|
||||
}
|
||||
|
||||
func callWith(caller reflect.Value, arg0 interface{}, argv ...reflect.Value) (interface{}, error) {
|
||||
var callargs = make([]reflect.Value, 0, 1+len(argv))
|
||||
|
|
@ -1,26 +1,18 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/v3/session/shard"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/state/store/defaultstore"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot/extras/shellwords"
|
||||
"github.com/diamondburned/arikawa/v3/utils/handler"
|
||||
"github.com/diamondburned/arikawa/api"
|
||||
"github.com/diamondburned/arikawa/bot/extras/shellwords"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
)
|
||||
|
||||
// Prefixer checks a message if it starts with the desired prefix. By default,
|
||||
|
@ -46,26 +38,8 @@ type ArgsParser func(content string) ([]string, error)
|
|||
|
||||
// DefaultArgsParser implements a parser similar to that of shell's,
|
||||
// implementing quotes as well as escapes.
|
||||
var DefaultArgsParser = shellwords.Parse
|
||||
|
||||
// NewShardFunc creates a shard constructor that shares the same internal store.
|
||||
// If opts sets its own cabinet, then a new store isn't created.
|
||||
func NewShardFunc(fn func(*state.State) (*Context, error)) shard.NewShardFunc {
|
||||
if fn == nil {
|
||||
panic("bot.NewShardFunc missing fn")
|
||||
}
|
||||
|
||||
return func(m *shard.Manager, id *gateway.Identifier) (shard.Shard, error) {
|
||||
sessn := session.NewCustom(*id, api.NewClient(id.Token), handler.New())
|
||||
state := state.NewFromSession(sessn, defaultstore.New())
|
||||
|
||||
bot, err := fn(state)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create bot instance")
|
||||
}
|
||||
|
||||
return bot, nil
|
||||
}
|
||||
func DefaultArgsParser() ArgsParser {
|
||||
return shellwords.Parse
|
||||
}
|
||||
|
||||
// Context is the bot state for commands and subcommands.
|
||||
|
@ -123,7 +97,7 @@ type Context struct {
|
|||
|
||||
// QuietUnknownCommand, if true, will not make the bot reply with an unknown
|
||||
// command error into the chat. This will apply to all other subcommands.
|
||||
// SilentUnknown controls whether or not an UnknownCommandError should be
|
||||
// SilentUnknown controls whether or not an ErrUnknownCommand should be
|
||||
// returned (instead of a silent error).
|
||||
SilentUnknown struct {
|
||||
// Command when true will silent only unknown commands. Known
|
||||
|
@ -151,15 +125,6 @@ type Context struct {
|
|||
// MessageCreate events.
|
||||
ReplyError bool
|
||||
|
||||
// ErrorReplier is an optional function that allows changing how the error
|
||||
// is replied. It overrides ReplyError and is only used for MessageCreate
|
||||
// events.
|
||||
//
|
||||
// Note that errors that are passed in here will bypas FormatError; in other
|
||||
// words, the implementation might only care about ErrorReplier and leave
|
||||
// FormatError as it is.
|
||||
ErrorReplier func(err error, src *gateway.MessageCreateEvent) api.SendMessageData
|
||||
|
||||
// EditableCommands when true will also listen for MessageUpdateEvent and
|
||||
// treat them as newly created messages. This is convenient if you want
|
||||
// to quickly edit a message and re-execute the command.
|
||||
|
@ -172,8 +137,6 @@ type Context struct {
|
|||
// Quick access map from event types to pointers. This map will never have
|
||||
// MessageCreateEvent's type.
|
||||
typeCache sync.Map // map[reflect.Type][]*CommandContext
|
||||
|
||||
stopFunc func() // unbind function, see Start()
|
||||
}
|
||||
|
||||
// Start quickly starts a bot with the given command. It will prepend "Bot"
|
||||
|
@ -182,82 +145,45 @@ func Start(
|
|||
token string, cmd interface{},
|
||||
opts func(*Context) error) (wait func() error, err error) {
|
||||
|
||||
if token == "" {
|
||||
return nil, errors.New("token is not given")
|
||||
s, err := state.New("Bot " + token)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create a dgo session")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token, "Bot ") {
|
||||
token = "Bot " + token
|
||||
c, err := New(s, cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create rfrouter")
|
||||
}
|
||||
|
||||
newShard := NewShardFunc(func(s *state.State) (*Context, error) {
|
||||
ctx, err := New(s, cmd)
|
||||
if err != nil {
|
||||
s.Gateway.ErrorLog = func(err error) {
|
||||
c.ErrorLogger(err)
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
if err := opts(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// fail api request if they (will) take up more than 5 minutes
|
||||
ctx.Client.Client.Timeout = 5 * time.Minute
|
||||
|
||||
if opts != nil {
|
||||
if err := opts(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddIntents(ctx.DeriveIntents())
|
||||
ctx.AddIntents(gateway.IntentGuilds) // for channel event caching
|
||||
|
||||
return ctx, nil
|
||||
})
|
||||
|
||||
m, err := shard.NewManager(token, newShard)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create shard manager")
|
||||
}
|
||||
|
||||
if err := m.Open(context.Background()); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open")
|
||||
cancel := c.Start()
|
||||
|
||||
if err := s.Open(); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to connect to Discord")
|
||||
}
|
||||
|
||||
return func() error {
|
||||
WaitForInterrupt()
|
||||
|
||||
// Close the shards first.
|
||||
closeErr := m.Close()
|
||||
|
||||
// Remove all handlers to clean up.
|
||||
m.ForEach(func(s shard.Shard) {
|
||||
ctx := s.(*Context)
|
||||
|
||||
stop := ctx.Start()
|
||||
stop()
|
||||
})
|
||||
|
||||
return closeErr
|
||||
Wait()
|
||||
// remove handler first
|
||||
cancel()
|
||||
// then finish closing session
|
||||
return s.Close()
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Run starts the bot, prints a message into the console, and blocks until
|
||||
// SIGINT. "Bot" is prepended into the token automatically, similar to Start.
|
||||
// The function will call os.Exit(1) on an initialization or cleanup error.
|
||||
func Run(token string, cmd interface{}, opts func(*Context) error) {
|
||||
wait, err := Start(token, cmd, opts)
|
||||
if err != nil {
|
||||
log.Fatalln("failed to start:", err)
|
||||
}
|
||||
|
||||
log.Println("Bot is running.")
|
||||
|
||||
if err := wait(); err != nil {
|
||||
log.Fatalln("cleanup error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForInterrupt blocks until SIGINT.
|
||||
func WaitForInterrupt() {
|
||||
// Wait blocks until SIGINT.
|
||||
func Wait() {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
||||
signal.Notify(sigs, os.Interrupt)
|
||||
<-sigs
|
||||
}
|
||||
|
||||
|
@ -284,7 +210,7 @@ func New(s *state.State, cmd interface{}) (*Context, error) {
|
|||
ctx := &Context{
|
||||
Subcommand: c,
|
||||
State: s,
|
||||
ParseArgs: DefaultArgsParser,
|
||||
ParseArgs: DefaultArgsParser(),
|
||||
HasPrefix: NewPrefix("~"),
|
||||
FormatError: func(err error) string {
|
||||
// Escape all pings, including @everyone.
|
||||
|
@ -303,10 +229,10 @@ func New(s *state.State, cmd interface{}) (*Context, error) {
|
|||
return ctx, nil
|
||||
}
|
||||
|
||||
// AddIntents adds the given Gateway Intent into the Gateway. This is a
|
||||
// AddIntent adds the given Gateway Intent into the Gateway. This is a
|
||||
// convenient function that calls Gateway's AddIntent.
|
||||
func (ctx *Context) AddIntents(i gateway.Intents) {
|
||||
ctx.Session.AddIntents(i)
|
||||
func (ctx *Context) AddIntent(i gateway.Intents) {
|
||||
ctx.Gateway.AddIntent(i)
|
||||
}
|
||||
|
||||
// Subcommands returns the slice of subcommands. To add subcommands, use
|
||||
|
@ -339,16 +265,14 @@ func (ctx *Context) FindCommand(structName, methodName string) *MethodContext {
|
|||
// MustRegisterSubcommand tries to register a subcommand, and will panic if it
|
||||
// fails. This is recommended, as subcommands won't change after initializing
|
||||
// once in runtime, thus fairly harmless after development.
|
||||
//
|
||||
// If no names are given or if the first name is empty, then the subcommand name
|
||||
// will be derived from the struct name. If one name is given, then that name
|
||||
// will override the struct name. Any other name values will be aliases.
|
||||
//
|
||||
// It is recommended to use this method to add subcommand aliases over manually
|
||||
// altering the Aliases slice of each Subcommand, as it does collision checks
|
||||
// against other subcommands as well.
|
||||
func (ctx *Context) MustRegisterSubcommand(cmd interface{}, names ...string) *Subcommand {
|
||||
s, err := ctx.RegisterSubcommand(cmd, names...)
|
||||
func (ctx *Context) MustRegisterSubcommand(cmd interface{}) *Subcommand {
|
||||
return ctx.MustRegisterSubcommandCustom(cmd, "")
|
||||
}
|
||||
|
||||
// MustRegisterSubcommandCustom works similarly to MustRegisterSubcommand, but
|
||||
// takes an extra argument for a command name override.
|
||||
func (ctx *Context) MustRegisterSubcommandCustom(cmd interface{}, name string) *Subcommand {
|
||||
s, err := ctx.RegisterSubcommandCustom(cmd, name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -356,9 +280,14 @@ func (ctx *Context) MustRegisterSubcommand(cmd interface{}, names ...string) *Su
|
|||
}
|
||||
|
||||
// RegisterSubcommand registers and adds cmd to the list of subcommands. It will
|
||||
// also return the resulting Subcommand. Refer to MustRegisterSubcommand for the
|
||||
// names argument.
|
||||
func (ctx *Context) RegisterSubcommand(cmd interface{}, names ...string) (*Subcommand, error) {
|
||||
// also return the resulting Subcommand.
|
||||
func (ctx *Context) RegisterSubcommand(cmd interface{}) (*Subcommand, error) {
|
||||
return ctx.RegisterSubcommandCustom(cmd, "")
|
||||
}
|
||||
|
||||
// RegisterSubcommand registers and adds cmd to the list of subcommands with a
|
||||
// custom command name (optional).
|
||||
func (ctx *Context) RegisterSubcommandCustom(cmd interface{}, name string) (*Subcommand, error) {
|
||||
s, err := NewSubcommand(cmd)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to add subcommand")
|
||||
|
@ -367,36 +296,18 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}, names ...string) (*Subco
|
|||
// Register the subcommand's name.
|
||||
s.NeedsName()
|
||||
|
||||
if len(names) > 0 && names[0] != "" {
|
||||
s.Command = names[0]
|
||||
}
|
||||
|
||||
if len(names) > 1 {
|
||||
// Copy the slice for expected behaviors.
|
||||
s.Aliases = append([]string(nil), names[1:]...)
|
||||
if name != "" {
|
||||
s.Command = name
|
||||
}
|
||||
|
||||
if err := s.InitCommands(ctx); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to initialize subcommand")
|
||||
}
|
||||
|
||||
// Check if the existing command name already exists. This could really be
|
||||
// optimized, but since it's in a cold path, who cares.
|
||||
var subcommandNames = append([]string{s.Command}, s.Aliases...)
|
||||
|
||||
for _, name := range subcommandNames {
|
||||
for _, sub := range ctx.subcommands {
|
||||
// Check each alias against the subcommand name.
|
||||
if sub.Command == name {
|
||||
return nil, fmt.Errorf("new subcommand has duplicate name: %q", name)
|
||||
}
|
||||
|
||||
// Also check each alias against other subcommands' aliases.
|
||||
for _, subalias := range sub.Aliases {
|
||||
if subalias == name {
|
||||
return nil, fmt.Errorf("new subcommand has duplicate alias: %q", name)
|
||||
}
|
||||
}
|
||||
// Do a collision check
|
||||
for _, sub := range ctx.subcommands {
|
||||
if sub.Command == s.Command {
|
||||
return nil, errors.New("new subcommand has duplicate name: " + s.Command)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -404,37 +315,56 @@ func (ctx *Context) RegisterSubcommand(cmd interface{}, names ...string) (*Subco
|
|||
return s, nil
|
||||
}
|
||||
|
||||
// emptyMentionTypes is used by Start() to not parse any mentions.
|
||||
var emptyMentionTypes = []api.AllowedMentionType{}
|
||||
|
||||
// Start adds itself into the session handlers. If Start is called more than
|
||||
// once, then it does nothing. The caller doesn't have to call Start if they
|
||||
// call Open.
|
||||
//
|
||||
// The returned function is a delete function, which removes itself from the
|
||||
// Session handlers. The delete function is not safe to use concurrently.
|
||||
// Start adds itself into the session handlers. This needs to be run. The
|
||||
// returned function is a delete function, which removes itself from the
|
||||
// Session handlers.
|
||||
func (ctx *Context) Start() func() {
|
||||
if ctx.stopFunc == nil {
|
||||
cancel := ctx.State.AddHandler(func(v interface{}) {
|
||||
if err := ctx.callCmd(v); err != nil {
|
||||
return ctx.State.AddHandler(func(v interface{}) {
|
||||
err := ctx.callCmd(v)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
str := ctx.FormatError(err)
|
||||
if str == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mc, isMessage := v.(*gateway.MessageCreateEvent)
|
||||
|
||||
// Log the main error if reply is disabled or if the event isn't a
|
||||
// message.
|
||||
if !ctx.ReplyError || !isMessage {
|
||||
// Ignore trivial errors:
|
||||
switch err.(type) {
|
||||
case *ErrInvalidUsage, *ErrUnknownCommand:
|
||||
// Ignore
|
||||
default:
|
||||
ctx.ErrorLogger(errors.Wrap(err, "command error"))
|
||||
}
|
||||
})
|
||||
|
||||
ctx.stopFunc = func() {
|
||||
cancel()
|
||||
ctx.stopFunc = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.stopFunc
|
||||
}
|
||||
// Only reply if the event is not a message.
|
||||
if !isMessage {
|
||||
return
|
||||
}
|
||||
|
||||
// Open starts the bot context and the gateway connection. It automatically
|
||||
// binds the needed handlers.
|
||||
func (ctx *Context) Open(cancelCtx context.Context) error {
|
||||
ctx.Start()
|
||||
return ctx.State.Open(cancelCtx)
|
||||
_, err = ctx.SendMessageComplex(mc.ChannelID, api.SendMessageData{
|
||||
// Escape the error using the message sanitizer:
|
||||
Content: ctx.SanitizeMessage(str),
|
||||
AllowedMentions: &api.AllowedMentions{
|
||||
// Don't allow mentions.
|
||||
Parse: []api.AllowedMentionType{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.ErrorLogger(err)
|
||||
|
||||
// TODO: there ought to be a better way lol
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Call should only be used if you know what you're doing.
|
||||
|
@ -486,30 +416,14 @@ func (ctx *Context) HelpGenerate(showHidden bool) string {
|
|||
if help == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
help = IndentLines(help)
|
||||
|
||||
builder := strings.Builder{}
|
||||
builder.WriteString("**")
|
||||
builder.WriteString(sub.Command)
|
||||
builder.WriteString("**")
|
||||
|
||||
for _, alias := range sub.Aliases {
|
||||
builder.WriteString("|")
|
||||
builder.WriteString("**")
|
||||
builder.WriteString(alias)
|
||||
builder.WriteString("**")
|
||||
}
|
||||
|
||||
var header = "**" + sub.Command + "**"
|
||||
if sub.Description != "" {
|
||||
builder.WriteString(": ")
|
||||
builder.WriteString(sub.Description)
|
||||
header += ": " + sub.Description
|
||||
}
|
||||
|
||||
builder.WriteByte('\n')
|
||||
builder.WriteString(help)
|
||||
|
||||
subhelps = append(subhelps, builder.String())
|
||||
subhelps = append(subhelps, header+"\n"+help)
|
||||
}
|
||||
|
||||
if len(subhelps) > 0 {
|
||||
|
@ -530,13 +444,3 @@ func IndentLines(input string) string {
|
|||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
// DeriveIntents derives all possible gateway intents from this context and all
|
||||
// its subcommands' method handlers and middlewares.
|
||||
func (ctx *Context) DeriveIntents() gateway.Intents {
|
||||
var intents = ctx.Subcommand.DeriveIntents()
|
||||
for _, subcmd := range ctx.subcommands {
|
||||
intents |= subcmd.DeriveIntents()
|
||||
}
|
||||
return intents
|
||||
}
|
|
@ -4,10 +4,9 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/diamondburned/arikawa/api"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -59,10 +58,6 @@ func (ctx *Context) callCmd(ev interface{}) (bottomError error) {
|
|||
}
|
||||
}
|
||||
|
||||
if bottomError != nil {
|
||||
return bottomError
|
||||
}
|
||||
|
||||
var msc *gateway.MessageCreateEvent
|
||||
|
||||
// We call the messages later, since we want MessageCreate middlewares to
|
||||
|
@ -80,7 +75,7 @@ func (ctx *Context) callCmd(ev interface{}) (bottomError error) {
|
|||
}
|
||||
|
||||
// Query the updated message.
|
||||
m, err := ctx.Cabinet.Message(up.ChannelID, up.ID)
|
||||
m, err := ctx.Store.Message(up.ChannelID, up.ID)
|
||||
if err != nil {
|
||||
// It's probably safe to ignore this.
|
||||
return nil
|
||||
|
@ -92,7 +87,7 @@ func (ctx *Context) callCmd(ev interface{}) (bottomError error) {
|
|||
|
||||
// Fill up member, if available.
|
||||
if m.GuildID.IsValid() && up.Member == nil {
|
||||
if mem, err := ctx.Cabinet.Member(m.GuildID, m.Author.ID); err == nil {
|
||||
if mem, err := ctx.Store.Member(m.GuildID, m.Author.ID); err == nil {
|
||||
msc.Member = mem
|
||||
}
|
||||
}
|
||||
|
@ -110,74 +105,23 @@ func (ctx *Context) callCmd(ev interface{}) (bottomError error) {
|
|||
return ctx.callMessageCreate(msc, evV)
|
||||
}
|
||||
|
||||
func (ctx *Context) callMessageCreate(
|
||||
mc *gateway.MessageCreateEvent, value reflect.Value) error {
|
||||
|
||||
v, err := ctx.callMessageCreateNoReply(mc, value)
|
||||
if err == nil && v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil && !ctx.ReplyError && ctx.ErrorReplier == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data api.SendMessageData
|
||||
|
||||
if err != nil {
|
||||
if ctx.ErrorReplier != nil {
|
||||
data = ctx.ErrorReplier(err, mc)
|
||||
} else {
|
||||
data.Content = ctx.FormatError(err)
|
||||
}
|
||||
} else {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
data.Content = v
|
||||
case *discord.Embed:
|
||||
data.Embeds = []discord.Embed{*v}
|
||||
case *api.SendMessageData:
|
||||
data = *v
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if data.Reference == nil {
|
||||
data.Reference = &discord.MessageReference{MessageID: mc.ID}
|
||||
}
|
||||
|
||||
if data.AllowedMentions == nil {
|
||||
// Do not mention on reply by default. Only allow author mentions.
|
||||
data.AllowedMentions = &api.AllowedMentions{
|
||||
Users: []discord.UserID{mc.Author.ID},
|
||||
RepliedUser: option.False,
|
||||
}
|
||||
}
|
||||
|
||||
_, err = ctx.SendMessageComplex(mc.ChannelID, data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ctx *Context) callMessageCreateNoReply(
|
||||
mc *gateway.MessageCreateEvent, value reflect.Value) (interface{}, error) {
|
||||
|
||||
func (ctx *Context) callMessageCreate(mc *gateway.MessageCreateEvent, value reflect.Value) error {
|
||||
// check if bot
|
||||
if !ctx.AllowBot && mc.Author.Bot {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if prefix
|
||||
pf, ok := ctx.HasPrefix(mc)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// trim the prefix before splitting, this way multi-words prefixes work
|
||||
content := mc.Content[len(pf):]
|
||||
|
||||
if content == "" {
|
||||
return nil, nil // just the prefix only
|
||||
return nil // just the prefix only
|
||||
}
|
||||
|
||||
// parse arguments
|
||||
|
@ -186,28 +130,21 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
// ignore it.
|
||||
|
||||
if len(parts) == 0 {
|
||||
return nil, parseErr
|
||||
return parseErr
|
||||
}
|
||||
|
||||
// Find the command and subcommand.
|
||||
commandCtx, err := ctx.findCommand(parts)
|
||||
arguments, cmd, sub, err := ctx.findCommand(parts)
|
||||
if err != nil {
|
||||
return nil, errNoBreak(err)
|
||||
return errNoBreak(err)
|
||||
}
|
||||
|
||||
var (
|
||||
arguments = commandCtx.parts
|
||||
cmd = commandCtx.method
|
||||
sub = commandCtx.subcmd
|
||||
plumbed = commandCtx.plumbed
|
||||
)
|
||||
|
||||
// We don't run the subcommand's middlewares here, as the callCmd function
|
||||
// already handles that.
|
||||
|
||||
// Run command middlewares.
|
||||
if err := cmd.walkMiddlewares(value); err != nil {
|
||||
return nil, errNoBreak(err)
|
||||
return errNoBreak(err)
|
||||
}
|
||||
|
||||
// Start converting
|
||||
|
@ -220,7 +157,7 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
// Here's an edge case: when the handler takes no arguments, we allow that
|
||||
// anyway, as they might've used the raw content.
|
||||
if len(cmd.Arguments) == 0 {
|
||||
return cmd.call(value, argv...)
|
||||
goto Call
|
||||
}
|
||||
|
||||
// Argument count check.
|
||||
|
@ -246,7 +183,7 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, &InvalidUsageError{
|
||||
return &ErrInvalidUsage{
|
||||
Prefix: pf,
|
||||
Args: parts,
|
||||
Index: len(parts) - 1,
|
||||
|
@ -267,7 +204,7 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
for i := 0; i < argc; i++ {
|
||||
v, err := cmd.Arguments[i].fn(arguments[0])
|
||||
if err != nil {
|
||||
return nil, &InvalidUsageError{
|
||||
return &ErrInvalidUsage{
|
||||
Prefix: pf,
|
||||
Args: parts,
|
||||
Index: len(parts) - len(arguments) + i,
|
||||
|
@ -292,7 +229,7 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
for i := 0; len(arguments) > 0; i++ {
|
||||
v, err := last.fn(arguments[0])
|
||||
if err != nil {
|
||||
return nil, &InvalidUsageError{
|
||||
return &ErrInvalidUsage{
|
||||
Prefix: pf,
|
||||
Args: parts,
|
||||
Index: len(parts) - len(arguments) + i,
|
||||
|
@ -310,13 +247,13 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
} else {
|
||||
// Create a zero value instance of this:
|
||||
v := reflect.New(last.rtype)
|
||||
var err error // return nil, error
|
||||
var err error // return error
|
||||
|
||||
switch {
|
||||
// If the argument wants all arguments:
|
||||
case last.manual != nil:
|
||||
// Call the manual parse method:
|
||||
err = last.manual(v.Interface().(ManualParser), arguments)
|
||||
_, err = callWith(last.manual.Func, v, reflect.ValueOf(arguments))
|
||||
|
||||
// If the argument wants all arguments in string:
|
||||
case last.custom != nil:
|
||||
|
@ -324,22 +261,37 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
// have erroneous hanging quotes.
|
||||
parseErr = nil
|
||||
|
||||
content = trimPrefixStringAndSlice(content, sub.Command, sub.Aliases)
|
||||
// Manual string seeking is a must here. This is because the string
|
||||
// could contain multiple whitespaces, and the parser would not
|
||||
// count them.
|
||||
var seekTo = cmd.Command
|
||||
// We can't rely on the plumbing behavior.
|
||||
if sub.plumbed != nil {
|
||||
seekTo = sub.Command
|
||||
}
|
||||
|
||||
// If the current command is not the plumbed command, then we can
|
||||
// keep trimming. We have to check for this, as a plumbed subcommand
|
||||
// may return nil, other non-plumbed commands.
|
||||
if !plumbed {
|
||||
content = trimPrefixStringAndSlice(content, cmd.Command, cmd.Aliases)
|
||||
// Seek to the string.
|
||||
var i = strings.Index(content, seekTo)
|
||||
// Edge case if the subcommand is the same as the command.
|
||||
if cmd.Command == sub.Command {
|
||||
// Seek again past the command.
|
||||
i = strings.Index(content[i+len(seekTo):], seekTo)
|
||||
}
|
||||
|
||||
if i > -1 {
|
||||
// Seek past the substring.
|
||||
i += len(seekTo)
|
||||
|
||||
content = strings.TrimSpace(content[i:])
|
||||
}
|
||||
|
||||
// Call the method with the raw unparsed command:
|
||||
err = last.custom(v.Interface().(CustomParser), content)
|
||||
_, err = callWith(last.custom.Func, v, reflect.ValueOf(content))
|
||||
}
|
||||
|
||||
// Check the returned error:
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if the argument wants a non-pointer:
|
||||
|
@ -353,100 +305,94 @@ func (ctx *Context) callMessageCreateNoReply(
|
|||
|
||||
// Check for parsing errors after parsing arguments.
|
||||
if parseErr != nil {
|
||||
return nil, parseErr
|
||||
return parseErr
|
||||
}
|
||||
|
||||
return cmd.call(value, argv...)
|
||||
}
|
||||
Call:
|
||||
// call the function and parse the error return value
|
||||
v, err := cmd.call(value, argv...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// commandContext contains related command values to call one. It is returned
|
||||
// from findCommand.
|
||||
type commandContext struct {
|
||||
parts []string
|
||||
plumbed bool
|
||||
method *MethodContext
|
||||
subcmd *Subcommand
|
||||
}
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
v = sub.SanitizeMessage(v)
|
||||
_, err = ctx.SendMessage(mc.ChannelID, v, nil)
|
||||
case *discord.Embed:
|
||||
_, err = ctx.SendMessage(mc.ChannelID, "", v)
|
||||
case *api.SendMessageData:
|
||||
if v.Content != "" {
|
||||
v.Content = sub.SanitizeMessage(v.Content)
|
||||
}
|
||||
_, err = ctx.SendMessageComplex(mc.ChannelID, *v)
|
||||
}
|
||||
|
||||
var emptyCommand = commandContext{}
|
||||
return err
|
||||
}
|
||||
|
||||
// findCommand filters.
|
||||
func (ctx *Context) findCommand(parts []string) (commandContext, error) {
|
||||
func (ctx *Context) findCommand(parts []string) ([]string, *MethodContext, *Subcommand, error) {
|
||||
// Main command entrypoint cannot have plumb.
|
||||
for _, c := range ctx.Commands {
|
||||
if searchStringAndSlice(parts[0], c.Command, c.Aliases) {
|
||||
return commandContext{parts[1:], false, c, ctx.Subcommand}, nil
|
||||
if c.Command == parts[0] {
|
||||
return parts[1:], c, ctx.Subcommand, nil
|
||||
}
|
||||
// Check for alias
|
||||
for _, alias := range c.Aliases {
|
||||
if alias == parts[0] {
|
||||
return parts[1:], c, ctx.Subcommand, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Can't find the command, look for subcommands if len(args) has a 2nd
|
||||
// entry.
|
||||
for _, s := range ctx.subcommands {
|
||||
if !searchStringAndSlice(parts[0], s.Command, s.Aliases) {
|
||||
if s.Command != parts[0] {
|
||||
continue
|
||||
}
|
||||
|
||||
// The new plumbing behavior allows other commands to co-exist with a
|
||||
// plumbed command. Those commands will override the second argument,
|
||||
// similarly to a non-plumbed command.
|
||||
// Only actually plumb if we actually have a plumbed handler AND
|
||||
// 1. We only have one command handler OR
|
||||
// 2. We only have the subcommand name but no command.
|
||||
if s.plumbed != nil && (len(s.Commands) == 1 || len(parts) <= 2) {
|
||||
return parts[1:], s.plumbed, s, nil
|
||||
}
|
||||
|
||||
if len(parts) >= 2 {
|
||||
for _, c := range s.Commands {
|
||||
if searchStringAndSlice(parts[1], c.Command, c.Aliases) {
|
||||
return commandContext{parts[2:], false, c, s}, nil
|
||||
if c.Command == parts[1] {
|
||||
return parts[2:], c, s, nil
|
||||
}
|
||||
// Check for aliases
|
||||
for _, alias := range c.Aliases {
|
||||
if alias == parts[1] {
|
||||
return parts[2:], c, s, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.IsPlumbed() {
|
||||
return commandContext{parts[1:], true, s.plumbed, s}, nil
|
||||
}
|
||||
|
||||
// If unknown command is disabled or the subcommand is hidden:
|
||||
if ctx.SilentUnknown.Subcommand || s.Hidden {
|
||||
return emptyCommand, Break
|
||||
return nil, nil, nil, Break
|
||||
}
|
||||
|
||||
return emptyCommand, newErrUnknownCommand(s, parts)
|
||||
return nil, nil, nil, &ErrUnknownCommand{
|
||||
Parts: parts,
|
||||
Subcmd: s,
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.SilentUnknown.Command {
|
||||
return emptyCommand, Break
|
||||
return nil, nil, nil, Break
|
||||
}
|
||||
|
||||
return emptyCommand, newErrUnknownCommand(ctx.Subcommand, parts)
|
||||
}
|
||||
|
||||
// searchStringAndSlice searches if str is equal to isString or any of the given
|
||||
// otherStrings. It is used for alias matching.
|
||||
func searchStringAndSlice(str string, isString string, otherStrings []string) bool {
|
||||
if str == isString {
|
||||
return true
|
||||
return nil, nil, nil, &ErrUnknownCommand{
|
||||
Parts: parts,
|
||||
Subcmd: ctx.Subcommand,
|
||||
}
|
||||
|
||||
for _, other := range otherStrings {
|
||||
if other == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// trimPrefixStringAndSlice behaves similarly to searchStringAndSlice, but it
|
||||
// trims the prefix and the surrounding spaces after a match.
|
||||
func trimPrefixStringAndSlice(str string, prefix string, prefixes []string) string {
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return strings.TrimSpace(str[len(prefix):])
|
||||
}
|
||||
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return strings.TrimSpace(str[len(prefix):])
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func errNoBreak(err error) error {
|
|
@ -0,0 +1,112 @@
|
|||
package bot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
)
|
||||
|
||||
type hasPlumb struct {
|
||||
Ctx *Context
|
||||
|
||||
Plumbed string
|
||||
NotPlumbed bool
|
||||
}
|
||||
|
||||
func (h *hasPlumb) Setup(sub *Subcommand) {
|
||||
sub.SetPlumb("Plumber")
|
||||
}
|
||||
|
||||
func (h *hasPlumb) Normal(_ *gateway.MessageCreateEvent) error {
|
||||
h.NotPlumbed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *hasPlumb) Plumber(_ *gateway.MessageCreateEvent, c RawArguments) error {
|
||||
h.Plumbed = string(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSubcommandPlumb(t *testing.T) {
|
||||
var s = &state.State{
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
c, err := New(s, &testc{})
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create new context:", err)
|
||||
}
|
||||
c.HasPrefix = NewPrefix("")
|
||||
|
||||
p := &hasPlumb{}
|
||||
|
||||
_, err = c.RegisterSubcommand(p)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to register hasPlumb:", err)
|
||||
}
|
||||
|
||||
// Try call exactly what's in the Plumb example:
|
||||
m := &gateway.MessageCreateEvent{
|
||||
Message: discord.Message{
|
||||
Content: "hasPlumb",
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.callCmd(m); err != nil {
|
||||
t.Fatal("Failed to call message:", err)
|
||||
}
|
||||
|
||||
if p.NotPlumbed {
|
||||
t.Fatal("Normal method called for hasPlumb")
|
||||
}
|
||||
}
|
||||
|
||||
type onlyPlumb struct {
|
||||
Ctx *Context
|
||||
Plumbed string
|
||||
}
|
||||
|
||||
func (h *onlyPlumb) Setup(sub *Subcommand) {
|
||||
sub.SetPlumb("Plumber")
|
||||
}
|
||||
|
||||
func (h *onlyPlumb) Plumber(_ *gateway.MessageCreateEvent, c RawArguments) error {
|
||||
h.Plumbed = string(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestSubcommandOnlyPlumb(t *testing.T) {
|
||||
var s = &state.State{
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
c, err := New(s, &testc{})
|
||||
if err != nil {
|
||||
t.Fatal("Failed to create new context:", err)
|
||||
}
|
||||
c.HasPrefix = NewPrefix("")
|
||||
|
||||
p := &onlyPlumb{}
|
||||
|
||||
_, err = c.RegisterSubcommand(p)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to register hasPlumb:", err)
|
||||
}
|
||||
|
||||
// Try call exactly what's in the Plumb example:
|
||||
m := &gateway.MessageCreateEvent{
|
||||
Message: discord.Message{
|
||||
Content: "onlyPlumb test command",
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.callCmd(m); err != nil {
|
||||
t.Fatal("Failed to call message:", err)
|
||||
}
|
||||
|
||||
if p.Plumbed != "test command" {
|
||||
t.Fatal("Unexpected custom argument for plumbed:", p.Plumbed)
|
||||
}
|
||||
}
|
|
@ -9,32 +9,12 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/state/store"
|
||||
"github.com/diamondburned/arikawa/v3/utils/handler"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/arikawa/utils/handler"
|
||||
)
|
||||
|
||||
type testUnexportedCtx struct {
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
func TestUnexportedCtx(t *testing.T) {
|
||||
s := &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
}
|
||||
|
||||
_, err := New(s, &testUnexportedCtx{})
|
||||
if err == nil {
|
||||
t.Fatal("New returned unexpected nil error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "no exported field with *bot.Context found") {
|
||||
t.Fatal("unexpected New error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
type testc struct {
|
||||
Ctx *Context
|
||||
Return chan interface{}
|
||||
|
@ -43,17 +23,17 @@ type testc struct {
|
|||
}
|
||||
|
||||
func (t *testc) Setup(sub *Subcommand) {
|
||||
sub.AddMiddleware([]string{"*", "GetCounter"}, func(v interface{}) {
|
||||
sub.AddMiddleware("*,GetCounter", func(v interface{}) {
|
||||
t.Counter++
|
||||
})
|
||||
sub.AddMiddleware("*", func(*gateway.MessageCreateEvent) {
|
||||
t.Counter++
|
||||
})
|
||||
// stub middleware for testing
|
||||
sub.AddMiddleware(t.OnTyping, func(*gateway.TypingStartEvent) {
|
||||
sub.AddMiddleware("OnTyping", func(*gateway.TypingStartEvent) {
|
||||
t.Typed = 2
|
||||
})
|
||||
sub.Hide(t.Hidden)
|
||||
sub.Hide("Hidden")
|
||||
}
|
||||
func (t *testc) Hidden(*gateway.MessageCreateEvent) {}
|
||||
func (t *testc) Noop(*gateway.MessageCreateEvent) {}
|
||||
|
@ -85,7 +65,7 @@ func (t *testc) OnTyping(*gateway.TypingStartEvent) {
|
|||
|
||||
func TestNewContext(t *testing.T) {
|
||||
var s = &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
c, err := New(s, &testc{})
|
||||
|
@ -101,7 +81,7 @@ func TestNewContext(t *testing.T) {
|
|||
func TestContext(t *testing.T) {
|
||||
var given = &testc{}
|
||||
var s = &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
Store: state.NewDefaultStore(nil),
|
||||
Handler: handler.New(),
|
||||
}
|
||||
|
||||
|
@ -116,7 +96,7 @@ func TestContext(t *testing.T) {
|
|||
|
||||
Subcommand: sub,
|
||||
State: s,
|
||||
ParseArgs: DefaultArgsParser,
|
||||
ParseArgs: DefaultArgsParser(),
|
||||
}
|
||||
|
||||
t.Run("init commands", func(t *testing.T) {
|
||||
|
@ -124,8 +104,12 @@ func TestContext(t *testing.T) {
|
|||
t.Fatal("Failed to init commands:", err)
|
||||
}
|
||||
|
||||
if given.Ctx != ctx {
|
||||
t.Fatal("given Context field has invalid pointer")
|
||||
if given.Ctx == nil {
|
||||
t.Fatal("given'sub Context field is nil")
|
||||
}
|
||||
|
||||
if given.Ctx.State.Store == nil {
|
||||
t.Fatal("given'sub State is nil")
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -136,6 +120,26 @@ func TestContext(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("help", func(t *testing.T) {
|
||||
ctx.MustRegisterSubcommandCustom(&testc{}, "helper")
|
||||
|
||||
h := ctx.Help()
|
||||
if h == "" {
|
||||
t.Fatal("Empty help?")
|
||||
}
|
||||
|
||||
if strings.Contains(h, "hidden") {
|
||||
t.Fatal("Hidden command shown in help.")
|
||||
}
|
||||
|
||||
if !strings.Contains(h, "arikawa/bot test") {
|
||||
t.Fatal("Name not found.")
|
||||
}
|
||||
if !strings.Contains(h, "Just a test.") {
|
||||
t.Fatal("Description not found.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("middleware", func(t *testing.T) {
|
||||
ctx.HasPrefix = NewPrefix("pls do ")
|
||||
|
||||
|
@ -145,21 +149,6 @@ func TestContext(t *testing.T) {
|
|||
}
|
||||
})
|
||||
|
||||
t.Run("derive intents", func(t *testing.T) {
|
||||
intents := ctx.DeriveIntents()
|
||||
|
||||
assertIntents := func(target gateway.Intents, name string) {
|
||||
if !intents.Has(target) {
|
||||
t.Error("Derived intents do not have", name)
|
||||
}
|
||||
}
|
||||
|
||||
assertIntents(gateway.IntentGuildMessages, "guild messages")
|
||||
assertIntents(gateway.IntentDirectMessages, "direct messages")
|
||||
assertIntents(gateway.IntentGuildMessageTyping, "guild typing")
|
||||
assertIntents(gateway.IntentDirectMessageTyping, "direct message typing")
|
||||
})
|
||||
|
||||
t.Run("typing event", func(t *testing.T) {
|
||||
typing := &gateway.TypingStartEvent{}
|
||||
|
||||
|
@ -294,42 +283,14 @@ func TestContext(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("register subcommand custom", func(t *testing.T) {
|
||||
ctx.MustRegisterSubcommand(&testc{}, "arikawa", "a")
|
||||
ctx.MustRegisterSubcommandCustom(&testc{}, "arikawa")
|
||||
})
|
||||
|
||||
t.Run("duplicate subcommand", func(t *testing.T) {
|
||||
_, err := ctx.RegisterSubcommand(&testc{}, "arikawa")
|
||||
_, err := ctx.RegisterSubcommandCustom(&testc{}, "arikawa")
|
||||
if err := err.Error(); !strings.Contains(err, "duplicate") {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
|
||||
_, err = ctx.RegisterSubcommand(&testc{}, "a")
|
||||
if err := err.Error(); !strings.Contains(err, "duplicate") {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("help", func(t *testing.T) {
|
||||
ctx.MustRegisterSubcommand(&testc{}, "helper")
|
||||
|
||||
h := ctx.Help()
|
||||
if h == "" {
|
||||
t.Fatal("Empty help?")
|
||||
}
|
||||
|
||||
if strings.Contains(h, "hidden") {
|
||||
t.Fatal("Hidden command shown in help.")
|
||||
}
|
||||
|
||||
if !strings.Contains(h, "arikawa/bot test") {
|
||||
t.Fatal("Name not found.")
|
||||
}
|
||||
if !strings.Contains(h, "Just a test.") {
|
||||
t.Fatal("Description not found.")
|
||||
}
|
||||
if !strings.Contains(h, "**a**") {
|
||||
t.Fatal("arikawa alias `a' not found.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("start", func(t *testing.T) {
|
||||
|
@ -395,7 +356,7 @@ func sendMsg(ctx *Context, given *testc, into interface{}, content string) (call
|
|||
|
||||
func BenchmarkConstructor(b *testing.B) {
|
||||
var s = &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
@ -406,7 +367,7 @@ func BenchmarkConstructor(b *testing.B) {
|
|||
func BenchmarkCall(b *testing.B) {
|
||||
var given = &testc{}
|
||||
var s = &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
sub, _ := NewSubcommand(given)
|
||||
|
@ -415,7 +376,7 @@ func BenchmarkCall(b *testing.B) {
|
|||
Subcommand: sub,
|
||||
State: s,
|
||||
HasPrefix: NewPrefix("~"),
|
||||
ParseArgs: DefaultArgsParser,
|
||||
ParseArgs: DefaultArgsParser(),
|
||||
}
|
||||
|
||||
m := &gateway.MessageCreateEvent{
|
||||
|
@ -434,7 +395,7 @@ func BenchmarkCall(b *testing.B) {
|
|||
func BenchmarkHelp(b *testing.B) {
|
||||
var given = &testc{}
|
||||
var s = &state.State{
|
||||
Cabinet: store.NoopCabinet,
|
||||
Store: state.NewDefaultStore(nil),
|
||||
}
|
||||
|
||||
sub, _ := NewSubcommand(given)
|
||||
|
@ -443,7 +404,7 @@ func BenchmarkHelp(b *testing.B) {
|
|||
Subcommand: sub,
|
||||
State: s,
|
||||
HasPrefix: NewPrefix("~"),
|
||||
ParseArgs: DefaultArgsParser,
|
||||
ParseArgs: DefaultArgsParser(),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
|
@ -2,37 +2,23 @@ package bot
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UnknownCommandError struct {
|
||||
type ErrUnknownCommand struct {
|
||||
Parts []string // max len 2
|
||||
Subcmd *Subcommand
|
||||
}
|
||||
|
||||
func newErrUnknownCommand(s *Subcommand, parts []string) error {
|
||||
if len(parts) > 2 {
|
||||
parts = parts[:2]
|
||||
func (err *ErrUnknownCommand) Error() string {
|
||||
if len(err.Parts) > 2 {
|
||||
err.Parts = err.Parts[:2]
|
||||
}
|
||||
|
||||
return &UnknownCommandError{
|
||||
Parts: parts,
|
||||
Subcmd: s,
|
||||
}
|
||||
}
|
||||
|
||||
func (err *UnknownCommandError) Error() string {
|
||||
return UnknownCommandString(err)
|
||||
}
|
||||
|
||||
var UnknownCommandString = func(err *UnknownCommandError) string {
|
||||
// Subcommand check.
|
||||
if err.Subcmd.StructName == "" || len(err.Parts) < 2 {
|
||||
return "unknown command: " + err.Parts[0] + "."
|
||||
}
|
||||
|
||||
return fmt.Sprintf("unknown %s subcommand: %s.", err.Parts[0], err.Parts[1])
|
||||
var UnknownCommandString = func(err *ErrUnknownCommand) string {
|
||||
return "unknown command: " + strings.Join(err.Parts, " ")
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -40,7 +26,7 @@ var (
|
|||
ErrNotEnoughArgs = errors.New("not enough arguments given")
|
||||
)
|
||||
|
||||
type InvalidUsageError struct {
|
||||
type ErrInvalidUsage struct {
|
||||
Prefix string
|
||||
Args []string
|
||||
Index int
|
||||
|
@ -51,15 +37,15 @@ type InvalidUsageError struct {
|
|||
Ctx *MethodContext
|
||||
}
|
||||
|
||||
func (err *InvalidUsageError) Error() string {
|
||||
func (err *ErrInvalidUsage) Error() string {
|
||||
return InvalidUsageString(err)
|
||||
}
|
||||
|
||||
func (err *InvalidUsageError) Unwrap() error {
|
||||
func (err *ErrInvalidUsage) Unwrap() error {
|
||||
return err.Wrap
|
||||
}
|
||||
|
||||
var InvalidUsageString = func(err *InvalidUsageError) string {
|
||||
var InvalidUsageString = func(err *ErrInvalidUsage) string {
|
||||
if err.Index == 0 && err.Wrap != nil {
|
||||
return "invalid usage, error: " + err.Wrap.Error() + "."
|
||||
}
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func TestInvalidUsage(t *testing.T) {
|
||||
t.Run("fmt", func(t *testing.T) {
|
||||
err := InvalidUsageError{
|
||||
err := ErrInvalidUsage{
|
||||
Prefix: "!",
|
||||
Args: []string{"hime", "arikawa"},
|
||||
Index: 1,
|
||||
|
@ -26,7 +26,7 @@ func TestInvalidUsage(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("missing arguments", func(t *testing.T) {
|
||||
err := InvalidUsageError{}
|
||||
err := ErrInvalidUsage{}
|
||||
str := err.Error()
|
||||
|
||||
if str != "missing arguments. Refer to help." {
|
||||
|
@ -35,7 +35,7 @@ func TestInvalidUsage(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("no index", func(t *testing.T) {
|
||||
err := InvalidUsageError{Wrap: errors.New("astolfo")}
|
||||
err := ErrInvalidUsage{Wrap: errors.New("astolfo")}
|
||||
str := err.Error()
|
||||
|
||||
if str != "invalid usage, error: astolfo." {
|
||||
|
@ -45,7 +45,7 @@ func TestInvalidUsage(t *testing.T) {
|
|||
|
||||
t.Run("unwrap", func(t *testing.T) {
|
||||
var err = errors.New("hackadoll no. 3")
|
||||
var wrap = &InvalidUsageError{
|
||||
var wrap = &ErrInvalidUsage{
|
||||
Wrap: err,
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package arguments
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot"
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
)
|
||||
|
||||
// Joined implements ManualParseable, in case you want all arguments but
|
|
@ -4,8 +4,8 @@ import (
|
|||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/api/rate"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/api/rate"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
var (
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
// (empty) so it matches standard links
|
|
@ -4,7 +4,7 @@ import (
|
|||
"errors"
|
||||
"regexp"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
var (
|
|
@ -3,7 +3,7 @@ package arguments
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
func TestChannelMention(t *testing.T) {
|
|
@ -8,7 +8,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
// ChannelID looks for fields with name ChannelID, Channel, or in some special
|
|
@ -3,7 +3,7 @@ package infer
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
type hasID struct {
|
|
@ -1,9 +1,9 @@
|
|||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot/extras/infer"
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
"github.com/diamondburned/arikawa/bot/extras/infer"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
)
|
||||
|
||||
func AdminOnly(ctx *bot.Context) func(interface{}) error {
|
|
@ -4,19 +4,16 @@ import (
|
|||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/v3/session"
|
||||
"github.com/diamondburned/arikawa/v3/state"
|
||||
"github.com/diamondburned/arikawa/v3/state/store"
|
||||
"github.com/diamondburned/arikawa/v3/utils/bot"
|
||||
"github.com/diamondburned/arikawa/bot"
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/arikawa/state"
|
||||
)
|
||||
|
||||
func TestAdminOnly(t *testing.T) {
|
||||
var ctx = &bot.Context{
|
||||
State: &state.State{
|
||||
Session: session.New(""),
|
||||
Cabinet: mockCabinet(),
|
||||
Store: &mockStore{},
|
||||
},
|
||||
}
|
||||
var middleware = AdminOnly(ctx)
|
||||
|
@ -25,7 +22,7 @@ func TestAdminOnly(t *testing.T) {
|
|||
var msg = &gateway.MessageCreateEvent{
|
||||
Message: discord.Message{
|
||||
ID: 1,
|
||||
ChannelID: 69420,
|
||||
ChannelID: 1337,
|
||||
Author: discord.User{ID: 69420},
|
||||
},
|
||||
}
|
||||
|
@ -53,8 +50,7 @@ func TestAdminOnly(t *testing.T) {
|
|||
func TestGuildOnly(t *testing.T) {
|
||||
var ctx = &bot.Context{
|
||||
State: &state.State{
|
||||
Session: session.New(""),
|
||||
Cabinet: mockCabinet(),
|
||||
Store: &mockStore{},
|
||||
},
|
||||
}
|
||||
var middleware = GuildOnly(ctx)
|
||||
|
@ -116,7 +112,7 @@ func expectBreak(t *testing.T, err error) {
|
|||
func BenchmarkGuildOnly(b *testing.B) {
|
||||
var ctx = &bot.Context{
|
||||
State: &state.State{
|
||||
Cabinet: mockCabinet(),
|
||||
Store: &mockStore{},
|
||||
},
|
||||
}
|
||||
var middleware = GuildOnly(ctx)
|
||||
|
@ -141,7 +137,7 @@ func BenchmarkGuildOnly(b *testing.B) {
|
|||
func BenchmarkAdminOnly(b *testing.B) {
|
||||
var ctx = &bot.Context{
|
||||
State: &state.State{
|
||||
Cabinet: mockCabinet(),
|
||||
Store: &mockStore{},
|
||||
},
|
||||
}
|
||||
var middleware = AdminOnly(ctx)
|
||||
|
@ -163,16 +159,7 @@ func BenchmarkAdminOnly(b *testing.B) {
|
|||
}
|
||||
|
||||
type mockStore struct {
|
||||
store.NoopStore
|
||||
}
|
||||
|
||||
func mockCabinet() *store.Cabinet {
|
||||
c := *store.NoopCabinet
|
||||
c.GuildStore = &mockStore{}
|
||||
c.MemberStore = &mockStore{}
|
||||
c.ChannelStore = &mockStore{}
|
||||
|
||||
return &c
|
||||
state.NoopStore
|
||||
}
|
||||
|
||||
func (s *mockStore) Guild(id discord.GuildID) (*discord.Guild, error) {
|
|
@ -1,35 +1,48 @@
|
|||
package shellwords
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WordOffset is the offset from the position cursor to print on the error.
|
||||
const WordOffset = 7
|
||||
|
||||
var escaper = strings.NewReplacer(
|
||||
"__", "\\_\\_",
|
||||
"`", "\\`",
|
||||
"@", "\\@",
|
||||
"\\", "\\\\",
|
||||
)
|
||||
|
||||
// MissingCloseError is returned when the parsed line is missing a closing quote.
|
||||
type MissingCloseError struct {
|
||||
type ErrParse struct {
|
||||
Position int
|
||||
Words string // joined
|
||||
}
|
||||
|
||||
func (e MissingCloseError) Error() string {
|
||||
// Underline 7 characters around.
|
||||
var start = e.Position
|
||||
func (e ErrParse) Error() string {
|
||||
// Magic number 5.
|
||||
var a = max(0, e.Position-WordOffset)
|
||||
var b = min(len(e.Words), e.Position+WordOffset)
|
||||
var word = e.Words[a:b]
|
||||
var uidx = e.Position - a
|
||||
|
||||
errstr := strings.Builder{}
|
||||
errstr.WriteString("missing quote close")
|
||||
errstr.WriteString("Unexpected quote or escape")
|
||||
|
||||
if e.Words[start:] != "" {
|
||||
errstr.WriteString(": ")
|
||||
errstr.WriteString(escaper.Replace(e.Words[:start]))
|
||||
errstr.WriteString("__")
|
||||
errstr.WriteString(escaper.Replace(e.Words[start:]))
|
||||
errstr.WriteString("__")
|
||||
// Do a bound check.
|
||||
if uidx+1 > len(word) {
|
||||
// Invalid.
|
||||
errstr.WriteString(".")
|
||||
return errstr.String()
|
||||
}
|
||||
|
||||
// Write the pre-underline part.
|
||||
fmt.Fprintf(
|
||||
&errstr, ": %s__%s__",
|
||||
escaper.Replace(word[:uidx]),
|
||||
escaper.Replace(string(word[uidx:])),
|
||||
)
|
||||
|
||||
return errstr.String()
|
||||
}
|
||||
|
||||
|
@ -76,7 +89,7 @@ func Parse(line string) ([]string, error) {
|
|||
}
|
||||
|
||||
switch r {
|
||||
case '"', '“', '”':
|
||||
case '"':
|
||||
if !singleQuoted {
|
||||
if doubleQuoted {
|
||||
got = true
|
||||
|
@ -84,7 +97,7 @@ func Parse(line string) ([]string, error) {
|
|||
doubleQuoted = !doubleQuoted
|
||||
continue
|
||||
}
|
||||
case '\'', '`', '‘', '’':
|
||||
case '\'', '`':
|
||||
if !doubleQuoted {
|
||||
if singleQuoted {
|
||||
got = true
|
||||
|
@ -104,7 +117,7 @@ func Parse(line string) ([]string, error) {
|
|||
}
|
||||
|
||||
if escaped || singleQuoted || doubleQuoted {
|
||||
return args, MissingCloseError{
|
||||
return args, &ErrParse{
|
||||
Position: cursor + buf.Len(),
|
||||
Words: strings.Join(args, " "),
|
||||
}
|
||||
|
@ -120,3 +133,17 @@ func isSpace(r rune) bool {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func min(i, j int) int {
|
||||
if i < j {
|
||||
return i
|
||||
}
|
||||
return j
|
||||
}
|
||||
|
||||
func max(i, j int) int {
|
||||
if i < j {
|
||||
return j
|
||||
}
|
||||
return i
|
||||
}
|
|
@ -14,17 +14,7 @@ type wordsTest struct {
|
|||
func TestParse(t *testing.T) {
|
||||
var tests = []wordsTest{
|
||||
{
|
||||
"",
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"'",
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
`this is a "te""st"`,
|
||||
`this is a "test"`,
|
||||
[]string{"this", "is", "a", "test"},
|
||||
false,
|
||||
},
|
||||
|
@ -58,16 +48,6 @@ func TestParse(t *testing.T) {
|
|||
[]string{"this", "should", "not", "crash"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"iPhone “double quoted” text",
|
||||
[]string{"iPhone", "double quoted", "text"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"iPhone ‘single quoted’ text",
|
||||
[]string{"iPhone", "single quoted", "text"},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
|
@ -2,25 +2,23 @@ package bot
|
|||
|
||||
import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/gateway"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
)
|
||||
|
||||
var (
|
||||
typeMessageCreate = reflect.TypeOf((*gateway.MessageCreateEvent)(nil))
|
||||
typeMessageUpdate = reflect.TypeOf((*gateway.MessageUpdateEvent)(nil))
|
||||
|
||||
typeContextPtr = reflect.TypeOf((*Context)(nil))
|
||||
typeIError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
typeIManP = reflect.TypeOf((*ManualParser)(nil)).Elem()
|
||||
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
|
||||
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
||||
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
||||
typeSetupFn = methodType((*CanSetup)(nil), "Setup")
|
||||
typeIError = reflect.TypeOf((*error)(nil)).Elem()
|
||||
typeIManP = reflect.TypeOf((*ManualParser)(nil)).Elem()
|
||||
typeICusP = reflect.TypeOf((*CustomParser)(nil)).Elem()
|
||||
typeIParser = reflect.TypeOf((*Parser)(nil)).Elem()
|
||||
typeIUsager = reflect.TypeOf((*Usager)(nil)).Elem()
|
||||
typeSetupFn = methodType((*CanSetup)(nil), "Setup")
|
||||
)
|
||||
|
||||
func methodType(iface interface{}, name string) reflect.Type {
|
||||
|
@ -74,11 +72,12 @@ type Subcommand struct {
|
|||
// Parsed command name:
|
||||
Command string
|
||||
|
||||
// Aliases is alternative way to call this subcommand in Discord.
|
||||
Aliases []string
|
||||
// SanitizeMessage is executed on the message content if the method returns
|
||||
// a string content or a SendMessageData.
|
||||
SanitizeMessage func(content string) string
|
||||
|
||||
// Commands can return either a string, a *discord.Embed, or an
|
||||
// *api.SendMessageData, with error as the second argument.
|
||||
// Commands can actually return either a string, an embed, or a
|
||||
// SendMessageData, with error as the second argument.
|
||||
|
||||
// All registered method contexts:
|
||||
Events []*MethodContext
|
||||
|
@ -119,7 +118,12 @@ type CanHelp interface {
|
|||
// NewSubcommand is used to make a new subcommand. You usually wouldn't call
|
||||
// this function, but instead use (*Context).RegisterSubcommand().
|
||||
func NewSubcommand(cmd interface{}) (*Subcommand, error) {
|
||||
sub := Subcommand{command: cmd}
|
||||
var sub = Subcommand{
|
||||
command: cmd,
|
||||
SanitizeMessage: func(c string) string {
|
||||
return c
|
||||
},
|
||||
}
|
||||
|
||||
if err := sub.reflectCommands(); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to reflect commands")
|
||||
|
@ -139,78 +143,20 @@ func (sub *Subcommand) NeedsName() {
|
|||
sub.Command = lowerFirstLetter(sub.StructName)
|
||||
}
|
||||
|
||||
func lowerFirstLetter(name string) string {
|
||||
return strings.ToLower(string(name[0])) + name[1:]
|
||||
}
|
||||
|
||||
// FindCommand finds the MethodContext using either the given method or the
|
||||
// given method name. It panics if the given method is not found.
|
||||
//
|
||||
// There are two ways to use FindCommand:
|
||||
//
|
||||
// sub.FindCommand("MethodName")
|
||||
// sub.FindCommand(thing.MethodName)
|
||||
//
|
||||
func (sub *Subcommand) FindCommand(method interface{}) *MethodContext {
|
||||
return sub.findMethod(method, false)
|
||||
}
|
||||
|
||||
func (sub *Subcommand) findMethod(method interface{}, inclEvents bool) *MethodContext {
|
||||
methodName, ok := method.(string)
|
||||
if !ok {
|
||||
methodName = runtimeMethodName(method)
|
||||
}
|
||||
|
||||
// FindCommand finds the MethodContext. It panics if methodName is not found.
|
||||
func (sub *Subcommand) FindCommand(methodName string) *MethodContext {
|
||||
for _, c := range sub.Commands {
|
||||
if c.MethodName == methodName {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
if inclEvents {
|
||||
for _, ev := range sub.Events {
|
||||
if ev.MethodName == methodName {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
panic("can't find method " + methodName)
|
||||
panic("Can't find method " + methodName)
|
||||
}
|
||||
|
||||
// runtimeMethodName returns the name of the method from the given method call.
|
||||
// It is used as such:
|
||||
//
|
||||
// fmt.Println(methodName(t.Method_dash))
|
||||
// // Output: main.T.Method_dash-fm
|
||||
//
|
||||
func runtimeMethodName(v interface{}) string {
|
||||
// https://github.com/diamondburned/arikawa/issues/146
|
||||
|
||||
ptr := reflect.ValueOf(v).Pointer()
|
||||
|
||||
funcPC := runtime.FuncForPC(ptr)
|
||||
if funcPC == nil {
|
||||
panic("given method is not a function")
|
||||
}
|
||||
|
||||
funcName := funcPC.Name()
|
||||
|
||||
// Do weird string parsing because Go wants us to.
|
||||
nameParts := strings.Split(funcName, ".")
|
||||
mName := nameParts[len(nameParts)-1]
|
||||
nameParts = strings.Split(mName, "-")
|
||||
if len(nameParts) > 1 { // extract the string before -fm if possible
|
||||
mName = nameParts[len(nameParts)-2]
|
||||
}
|
||||
|
||||
return mName
|
||||
}
|
||||
|
||||
// ChangeCommandInfo changes the matched method's Command and Description.
|
||||
// Empty means unchanged. This function panics if the given method is not found.
|
||||
func (sub *Subcommand) ChangeCommandInfo(method interface{}, cmd, desc string) {
|
||||
var command = sub.FindCommand(method)
|
||||
// ChangeCommandInfo changes the matched methodName's Command and Description.
|
||||
// Empty means unchanged. This function panics if methodName is not found.
|
||||
func (sub *Subcommand) ChangeCommandInfo(methodName, cmd, desc string) {
|
||||
var command = sub.FindCommand(methodName)
|
||||
if cmd != "" {
|
||||
command.Command = cmd
|
||||
}
|
||||
|
@ -236,11 +182,13 @@ func (sub *Subcommand) HelpShowHidden(showHidden bool) string {
|
|||
return sub.HelpGenerate(showHidden)
|
||||
}
|
||||
|
||||
// HelpGenerate auto-generates a help message, which contains only a list of
|
||||
// commands. It does not print the subcommand header. Use this only if you want
|
||||
// to override the Subcommand's help, else use Help(). This function will show
|
||||
// HelpGenerate auto-generates a help message. Use this only if you want to
|
||||
// override the Subcommand's help, else use Help(). This function will show
|
||||
// hidden commands if showHidden is true.
|
||||
func (sub *Subcommand) HelpGenerate(showHidden bool) string {
|
||||
// A wider space character.
|
||||
const s = "\u2000"
|
||||
|
||||
var buf strings.Builder
|
||||
|
||||
for i, cmd := range sub.Commands {
|
||||
|
@ -248,45 +196,22 @@ func (sub *Subcommand) HelpGenerate(showHidden bool) string {
|
|||
continue
|
||||
}
|
||||
|
||||
if sub.Command != "" {
|
||||
buf.WriteString(sub.Command)
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
if cmd == sub.PlumbedMethod() {
|
||||
buf.WriteByte('[')
|
||||
}
|
||||
|
||||
buf.WriteString(cmd.Command)
|
||||
|
||||
for _, alias := range cmd.Aliases {
|
||||
buf.WriteByte('|')
|
||||
buf.WriteString(alias)
|
||||
}
|
||||
|
||||
if cmd == sub.PlumbedMethod() {
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
buf.WriteString(sub.Command + " " + cmd.Command)
|
||||
|
||||
// Write the usages first.
|
||||
var usages = cmd.Usage()
|
||||
for _, usage := range cmd.Usage() {
|
||||
// Is the last argument trailing? If so, append ellipsis.
|
||||
if cmd.Variadic {
|
||||
usage += "..."
|
||||
}
|
||||
|
||||
for _, usage := range usages {
|
||||
buf.WriteByte(' ')
|
||||
buf.WriteString("__")
|
||||
buf.WriteString(usage)
|
||||
buf.WriteString("__")
|
||||
}
|
||||
|
||||
// Is the last argument trailing? If so, append ellipsis.
|
||||
if len(usages) > 0 && cmd.Variadic {
|
||||
buf.WriteString("...")
|
||||
// Uses \u2000, which is wider than a space.
|
||||
buf.WriteString(s + "__" + usage + "__")
|
||||
}
|
||||
|
||||
// Write the description if there's any.
|
||||
if cmd.Description != "" {
|
||||
buf.WriteString(": ")
|
||||
buf.WriteString(cmd.Description)
|
||||
buf.WriteString(": " + cmd.Description)
|
||||
}
|
||||
|
||||
// Add a new line if this isn't the last command.
|
||||
|
@ -300,8 +225,8 @@ func (sub *Subcommand) HelpGenerate(showHidden bool) string {
|
|||
|
||||
// Hide marks a command as hidden, meaning it won't be shown in help and its
|
||||
// UnknownCommand errors will be suppressed.
|
||||
func (sub *Subcommand) Hide(method interface{}) {
|
||||
sub.FindCommand(method).Hidden = true
|
||||
func (sub *Subcommand) Hide(methodName string) {
|
||||
sub.FindCommand(methodName).Hidden = true
|
||||
}
|
||||
|
||||
func (sub *Subcommand) reflectCommands() error {
|
||||
|
@ -352,10 +277,14 @@ func (sub *Subcommand) InitCommands(ctx *Context) error {
|
|||
}
|
||||
|
||||
func (sub *Subcommand) fillStruct(ctx *Context) error {
|
||||
for i := 0; i < sub.cmdType.NumField(); i++ {
|
||||
for i := 0; i < sub.cmdValue.NumField(); i++ {
|
||||
field := sub.cmdValue.Field(i)
|
||||
|
||||
if !field.CanSet() || field.Type() != typeContextPtr {
|
||||
if !field.CanSet() || !field.CanInterface() {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := field.Interface().(*Context); !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -363,7 +292,7 @@ func (sub *Subcommand) fillStruct(ctx *Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
return errors.New("no exported field with *bot.Context found")
|
||||
return errors.New("no fields with *bot.Context found")
|
||||
}
|
||||
|
||||
func (sub *Subcommand) parseCommands() error {
|
||||
|
@ -414,7 +343,7 @@ func (sub *Subcommand) parseCommands() error {
|
|||
//
|
||||
// Note that although technically all of the above function signatures are
|
||||
// acceptable, one should almost always return only an error.
|
||||
func (sub *Subcommand) AddMiddleware(method, middleware interface{}) {
|
||||
func (sub *Subcommand) AddMiddleware(methodName string, middleware interface{}) {
|
||||
var mw *MiddlewareContext
|
||||
// Allow *MiddlewareContext to be passed into.
|
||||
if v, ok := middleware.(*MiddlewareContext); ok {
|
||||
|
@ -423,18 +352,8 @@ func (sub *Subcommand) AddMiddleware(method, middleware interface{}) {
|
|||
mw = ParseMiddleware(middleware)
|
||||
}
|
||||
|
||||
switch v := method.(type) {
|
||||
case string:
|
||||
sub.addMiddleware(mw, strings.Split(v, ","))
|
||||
case []string:
|
||||
sub.addMiddleware(mw, v)
|
||||
default:
|
||||
sub.findMethod(v, true).addMiddleware(mw)
|
||||
}
|
||||
}
|
||||
|
||||
func (sub *Subcommand) addMiddleware(mw *MiddlewareContext, methods []string) {
|
||||
for _, method := range methods {
|
||||
// Parse method name:
|
||||
for _, method := range strings.Split(methodName, ",") {
|
||||
// Trim space.
|
||||
if method = strings.TrimSpace(method); method == "*" {
|
||||
// Append middleware to global middleware slice.
|
||||
|
@ -442,10 +361,19 @@ func (sub *Subcommand) addMiddleware(mw *MiddlewareContext, methods []string) {
|
|||
continue
|
||||
}
|
||||
// Append middleware to that individual function.
|
||||
sub.findMethod(method, true).addMiddleware(mw)
|
||||
sub.findMethod(method).addMiddleware(mw)
|
||||
}
|
||||
}
|
||||
|
||||
func (sub *Subcommand) findMethod(name string) *MethodContext {
|
||||
for _, ev := range sub.Events {
|
||||
if ev.MethodName == name {
|
||||
return ev
|
||||
}
|
||||
}
|
||||
return sub.FindCommand(name)
|
||||
}
|
||||
|
||||
func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
||||
// Search for global middlewares.
|
||||
for _, mw := range sub.globalmws {
|
||||
|
@ -471,36 +399,13 @@ func (sub *Subcommand) eventCallers(evT reflect.Type) (callers []caller) {
|
|||
return
|
||||
}
|
||||
|
||||
// IsPlumbed returns true if the subcommand is plumbed. To get the plumbed
|
||||
// method, use PlumbedMethod().
|
||||
func (sub *Subcommand) IsPlumbed() bool {
|
||||
return sub.plumbed != nil
|
||||
}
|
||||
|
||||
// PlumbedMethod returns the plumbed method's context, or nil if the subcommand
|
||||
// is not plumbed.
|
||||
func (sub *Subcommand) PlumbedMethod() *MethodContext {
|
||||
return sub.plumbed
|
||||
}
|
||||
|
||||
// SetPlumb sets the method as the plumbed command. If method is nil, then the
|
||||
// plumbing is also disabled.
|
||||
func (sub *Subcommand) SetPlumb(method interface{}) {
|
||||
// Ensure that SetPlumb isn't being called on the main context.
|
||||
if sub.Command == "" {
|
||||
panic("invalid SetPlumb call on *Context")
|
||||
}
|
||||
|
||||
if method == nil {
|
||||
sub.plumbed = nil
|
||||
return
|
||||
}
|
||||
|
||||
sub.plumbed = sub.FindCommand(method)
|
||||
// SetPlumb sets the method as the plumbed command.
|
||||
func (sub *Subcommand) SetPlumb(methodName string) {
|
||||
sub.plumbed = sub.FindCommand(methodName)
|
||||
}
|
||||
|
||||
// AddAliases add alias(es) to specific command (defined with commandName).
|
||||
func (sub *Subcommand) AddAliases(commandName interface{}, aliases ...string) {
|
||||
func (sub *Subcommand) AddAliases(commandName string, aliases ...string) {
|
||||
// Get command
|
||||
command := sub.FindCommand(commandName)
|
||||
|
||||
|
@ -508,23 +413,6 @@ func (sub *Subcommand) AddAliases(commandName interface{}, aliases ...string) {
|
|||
command.Aliases = append(command.Aliases, aliases...)
|
||||
}
|
||||
|
||||
// DeriveIntents derives all possible gateway intents from the method handlers
|
||||
// and middlewares.
|
||||
func (sub *Subcommand) DeriveIntents() gateway.Intents {
|
||||
var intents gateway.Intents
|
||||
|
||||
for _, event := range sub.Events {
|
||||
intents |= event.intents()
|
||||
}
|
||||
for _, command := range sub.Commands {
|
||||
intents |= command.intents()
|
||||
}
|
||||
if sub.IsPlumbed() {
|
||||
intents |= sub.plumbed.intents()
|
||||
}
|
||||
for _, middleware := range sub.globalmws {
|
||||
intents |= middleware.intents()
|
||||
}
|
||||
|
||||
return intents
|
||||
func lowerFirstLetter(name string) string {
|
||||
return strings.ToLower(string(name[0])) + name[1:]
|
||||
}
|
|
@ -1,133 +0,0 @@
|
|||
package discord
|
||||
|
||||
type Application struct {
|
||||
// ID is the ID of the app.
|
||||
ID AppID `json:"id"`
|
||||
// Name is the name of the app.
|
||||
Name string `json:"name"`
|
||||
// Icon is the icon hash of the app.
|
||||
Icon *Hash `json:"icon"`
|
||||
// Description is the description of the app.
|
||||
Description string `json:"description"`
|
||||
// RPCOrigins is the RPC origin urls, if RPC is enabled.
|
||||
RPCOrigins []string `json:"rpc_origins"`
|
||||
// BotPublic is whether users besides the app owner can join the app's bot
|
||||
// to guilds.
|
||||
BotPublic bool `json:"bot_public"`
|
||||
// BotRequiredCodeGrant is whether the app's bot will only join upon
|
||||
// completion of the full oauth2 code grant flow.
|
||||
BotRequireCodeGrant bool `json:"bot_require_code_grant"`
|
||||
// TermsOfServiceURL is the url of the app's terms of service.
|
||||
TermsOfServiceURL string `json:"terms_of_service_url"`
|
||||
// PrivacyPolicyURL is the url of the app's privacy policy.
|
||||
PrivacyPolicyURL string `json:"privacy_policy_url"`
|
||||
// Owner is a partial user object containing info on the owner of the
|
||||
// application.
|
||||
Owner *User `json:"owner"`
|
||||
// VerifyKey is the hex encoded key for verification in interactions and
|
||||
// the GameSDK's GetTicket.
|
||||
VerifyKey string `json:"verify_key"`
|
||||
// Team is the team that the application belongs to, if it belongs to one.
|
||||
Team *Team `json:"team"`
|
||||
// CoverImage the application's default rich presence invite cover image
|
||||
// hash.
|
||||
CoverImage *Hash `json:"cover_image"`
|
||||
// Flags is the application's public flags.
|
||||
Flags ApplicationFlags `json:"flags"`
|
||||
|
||||
// The following fields are only present on applications that are games
|
||||
// sold on Discord.
|
||||
|
||||
// Summary is the summary field for the store page of the game's primary
|
||||
// SKU.
|
||||
Summary string `json:"summary"`
|
||||
// GuildID is the guild to which the game has been linked.
|
||||
GuildID GuildID `json:"guild_ID"`
|
||||
// PrimarySKUID is the ID of the "Game SKU" that is created, if it exists.
|
||||
PrimarySKUID Snowflake `json:"primary_sku_id"`
|
||||
// Slug is the URL slug that links to the game's store page.
|
||||
Slug string `json:"slug"`
|
||||
// Tags is a slice of strings containing up to 5 tags describing the content and functionality of the application
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
// InstallParams is the settings for the application's default in-app authorization link, if enabled.
|
||||
InstallParams InstallParams `json:"install_params,omitempty"`
|
||||
// CustomInstallURL is the application's default custom authorization link, if enabled.
|
||||
CustomInstallURL string `json:"custom_install_url,omitempty"`
|
||||
// RoleConnectionsVerificationURL is the application's role connection verification entry point, which when configured will render the app as a verification method in the guild role verification configuration.
|
||||
RoleConnectionsVerificationURL string `json:"role_connections_verification_url,omitempty"`
|
||||
}
|
||||
|
||||
type ApplicationFlags uint32
|
||||
|
||||
const AppFlagAutoModerationRuleCreateBadge ApplicationFlags = 1 << 6
|
||||
|
||||
const (
|
||||
AppFlagGatewayPresence ApplicationFlags = 1 << (iota + 12)
|
||||
AppFlagGatewayPresenceLimited
|
||||
AppFlagGatewayGuildMembers
|
||||
AppFlagGatewayGuildMembersLimited
|
||||
AppFlagVerificationPendingGuildLimit
|
||||
AppFlagEmbedded
|
||||
)
|
||||
|
||||
type Team struct {
|
||||
// Icon is a hash of the image of the team's icon.
|
||||
Icon *Hash `json:"hash"`
|
||||
// ID is the unique ID of the team.
|
||||
ID TeamID `json:"id"`
|
||||
// Members is the members of the team.
|
||||
Members []TeamMember `json:"members"`
|
||||
// Name is the name of the team.
|
||||
Name string `json:"name"`
|
||||
// OwnerUserID is the user ID of the current team owner.
|
||||
OwnerID UserID `json:"owner_user_id"`
|
||||
}
|
||||
|
||||
type TeamMember struct {
|
||||
// MembershipState is the user's membership state on the team.
|
||||
MembershipState MembershipState `json:"membership_state"`
|
||||
// Permissions will always be {"*"}
|
||||
Permissions []string `json:"permissions"`
|
||||
// TeamID is the ID of the parent team of which they are a member.
|
||||
TeamID TeamID `json:"team_id"`
|
||||
// User is the avatar, discriminator, ID, and username of the user.
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
||||
type MembershipState uint8
|
||||
|
||||
const (
|
||||
MembershipInvited MembershipState = iota + 1
|
||||
MembershipAccepted
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/interactions/slash-commands#application-command-permissions-object-guild-application-command-permissions-structure
|
||||
type GuildCommandPermissions struct {
|
||||
ID CommandID `json:"id"`
|
||||
AppID AppID `json:"application_id"`
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
Permissions []CommandPermissions `json:"permissions"`
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/interactions/slash-commands#application-command-permissions-object-application-command-permissions-structure
|
||||
type CommandPermissions struct {
|
||||
ID Snowflake `json:"id"`
|
||||
Type CommandPermissionType `json:"type"`
|
||||
Permission bool `json:"permission"`
|
||||
}
|
||||
|
||||
type CommandPermissionType uint8
|
||||
|
||||
// https://discord.com/developers/docs/interactions/slash-commands#application-command-permissions-object-application-command-permission-type
|
||||
const (
|
||||
RoleCommandPermission = iota + 1
|
||||
UserCommandPermission
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/application#install-params-object
|
||||
type InstallParams struct {
|
||||
// Scopes is the scopes to add the application to the server with.
|
||||
Scopes []string `json:"scopes"`
|
||||
// Permissions is the permissions to request for the bot role.
|
||||
Permissions Permissions `json:"permissions,string"`
|
||||
}
|
|
@ -1,11 +1,9 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/diamondburned/arikawa/utils/json"
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/audit-log#audit-log-object
|
||||
|
@ -28,7 +26,7 @@ type AuditLogEntry struct {
|
|||
// ID is the id of the entry.
|
||||
ID AuditLogEntryID `json:"id"`
|
||||
// TargetID is the id of the affected entity (webhook, user, role, etc.).
|
||||
TargetID Snowflake `json:"target_id"`
|
||||
TargetID string `json:"target_id,omitempty"`
|
||||
// Changes are the changes made to the TargetID.
|
||||
Changes []AuditLogChange `json:"changes,omitempty"`
|
||||
// UserID is the id of the user who made the changes.
|
||||
|
@ -43,11 +41,6 @@ type AuditLogEntry struct {
|
|||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the audit log entry was created.
|
||||
func (e AuditLogEntry) CreatedAt() time.Time {
|
||||
return e.ID.Time()
|
||||
}
|
||||
|
||||
// AuditLogEvent is the type of audit log action that occurred.
|
||||
type AuditLogEvent uint8
|
||||
|
||||
|
@ -123,7 +116,7 @@ type AuditEntryInfo struct {
|
|||
//
|
||||
// Events: CHANNEL_OVERWRITE_CREATE, CHANNEL_OVERWRITE_UPDATE,
|
||||
// CHANNEL_OVERWRITE_DELETE
|
||||
Type OverwriteType `json:"type,string,omitempty"`
|
||||
Type ChannelOverwritten `json:"type,omitempty"`
|
||||
// RoleName is the name of the role if type is "role".
|
||||
//
|
||||
// Events: CHANNEL_OVERWRITE_CREATE, CHANNEL_OVERWRITE_UPDATE,
|
||||
|
@ -131,6 +124,15 @@ type AuditEntryInfo struct {
|
|||
RoleName string `json:"role_name,omitempty"`
|
||||
}
|
||||
|
||||
// ChannelOverwritten is the type of overwritten entity in
|
||||
// (AuditEntryInfo).Type.
|
||||
type ChannelOverwritten string
|
||||
|
||||
const (
|
||||
MemberChannelOverwritten ChannelOverwritten = "member"
|
||||
RoleChannelOverwritten ChannelOverwritten = "role"
|
||||
)
|
||||
|
||||
// AuditLogChange is a single key type to changed value audit log entry. The
|
||||
// type can be found in the key's comment. Values can be nil.
|
||||
//
|
||||
|
|
|
@ -1,70 +1,37 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
import "github.com/diamondburned/arikawa/utils/json"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
)
|
||||
|
||||
// ChannelFlags are the channel flags combined as a bitfield.
|
||||
type ChannelFlags uint64
|
||||
|
||||
const (
|
||||
_ ChannelFlags = 1 << iota
|
||||
// PinnedThread means this thread is pinned to the top of its parent
|
||||
// GuildForum channel.
|
||||
PinnedThread
|
||||
_
|
||||
_
|
||||
// ThreadRequireTag is whether a tag is required to be specified when
|
||||
// creating a thread in a GuildForum channel. Tags are specified in the
|
||||
// AppliedTags field.
|
||||
ThreadRequireTag
|
||||
)
|
||||
|
||||
// Channel represents a guild or DM channel within Discord.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object
|
||||
type Channel struct {
|
||||
// ID is the id of this channel.
|
||||
ID ChannelID `json:"id"`
|
||||
// GuildID is the id of the guild.
|
||||
//
|
||||
// This field may be missing for some channel objects received over gateway
|
||||
// guild dispatches.
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
|
||||
ID ChannelID `json:"id,string"`
|
||||
// Type is the type of channel.
|
||||
Type ChannelType `json:"type,omitempty"`
|
||||
// NSFW specifies whether the channel is nsfw.
|
||||
NSFW bool `json:"nsfw,omitempty"`
|
||||
Type ChannelType `json:"type"`
|
||||
// GuildID is the id of the guild.
|
||||
GuildID GuildID `json:"guild_id,string,omitempty"`
|
||||
|
||||
// Position is the sorting position of the channel.
|
||||
Position int `json:"position,omitempty"`
|
||||
// Overwrites are the explicit permission overrides for members
|
||||
// and roles.
|
||||
Overwrites []Overwrite `json:"permission_overwrites,omitempty"`
|
||||
// Permissions are the explicit permission overrides for members and roles.
|
||||
Permissions []Overwrite `json:"permission_overwrites,omitempty"`
|
||||
|
||||
// Name is the name of the channel (2-100 characters).
|
||||
Name string `json:"name,omitempty"`
|
||||
// Topic is the channel topic (0-1024 characters).
|
||||
Topic string `json:"topic,omitempty"`
|
||||
// NSFW specifies whether the channel is nsfw.
|
||||
NSFW bool `json:"nsfw"`
|
||||
|
||||
// LastMessageID is the id of the last message sent in this channel (may
|
||||
// not point to an existing or valid message).
|
||||
LastMessageID MessageID `json:"last_message_id,omitempty"`
|
||||
LastMessageID MessageID `json:"last_message_id,string,omitempty"`
|
||||
|
||||
// VoiceBitrate is the bitrate (in bits) of the voice channel.
|
||||
VoiceBitrate uint `json:"bitrate,omitempty"`
|
||||
// VoiceUserLimit is the user limit of the voice channel.
|
||||
VoiceUserLimit uint `json:"user_limit,omitempty"`
|
||||
|
||||
// Flags is a bitmask that contains if a thread is pinned, for example.
|
||||
Flags ChannelFlags `json:"flags,omitempty"`
|
||||
|
||||
// UserRateLimit is the amount of seconds a user has to wait before sending
|
||||
// another message (0-21600). Bots, as well as users with the permission
|
||||
// manage_messages or manage_channel, are unaffected.
|
||||
|
@ -74,86 +41,18 @@ type Channel struct {
|
|||
DMRecipients []User `json:"recipients,omitempty"`
|
||||
// Icon is the icon hash.
|
||||
Icon Hash `json:"icon,omitempty"`
|
||||
// DMOwnerID is the id of the DM creator.
|
||||
DMOwnerID UserID `json:"owner_id,string,omitempty"`
|
||||
|
||||
// OwnerID is the id of the DM or thread creator.
|
||||
OwnerID UserID `json:"owner_id,omitempty"`
|
||||
// AppID is the application id of the group DM creator if it is
|
||||
// bot-created.
|
||||
AppID AppID `json:"application_id,omitempty"`
|
||||
// ParentID for guild channels: id of the parent category for a channel
|
||||
// (each parent category can contain up to 50 channels), for threads: the
|
||||
// id of the text channel this thread was created.
|
||||
ParentID ChannelID `json:"parent_id,omitempty"`
|
||||
AppID AppID `json:"application_id,string,omitempty"`
|
||||
|
||||
// CategoryID is the id of the parent category for a channel (each parent
|
||||
// category can contain up to 50 channels).
|
||||
CategoryID ChannelID `json:"parent_id,string,omitempty"`
|
||||
// LastPinTime is when the last pinned message was pinned.
|
||||
LastPinTime Timestamp `json:"last_pin_timestamp,omitempty"`
|
||||
|
||||
// RTCRegionID is the voice region id for the voice channel.
|
||||
RTCRegionID string `json:"rtc_region,omitempty"`
|
||||
// VideoQualityMode is the camera video quality mode of the voice channel.
|
||||
VideoQualityMode VideoQualityMode `json:"video_quality_mode,omitempty"`
|
||||
|
||||
// MessageCount is an approximate count of messages in a thread. However,
|
||||
// counting stops at 50.
|
||||
MessageCount int `json:"message_count,omitempty"`
|
||||
// MemberCount is an approximate count of users in a thread. However,
|
||||
// counting stops at 50.
|
||||
MemberCount int `json:"member_count,omitempty"`
|
||||
|
||||
// ThreadMetadata contains thread-specific fields not needed by other
|
||||
// channels.
|
||||
ThreadMetadata *ThreadMetadata `json:"thread_metadata,omitempty"`
|
||||
// ThreadMember is the thread member object for the current user, if they
|
||||
// have joined the thread, only included on certain API endpoints.
|
||||
ThreadMember *ThreadMember `json:"thread_member,omitempty"`
|
||||
// DefaultAutoArchiveDuration is the default duration for newly created
|
||||
// threads, in minutes, to automatically archive the thread after recent
|
||||
// activity.
|
||||
DefaultAutoArchiveDuration ArchiveDuration `json:"default_auto_archive_duration,omitempty"`
|
||||
|
||||
// SelfPermissions are the computed permissions for the invoking user in
|
||||
// the channel, including overwrites, only included when part of the
|
||||
// resolved data received on a slash command interaction.
|
||||
SelfPermissions Permissions `json:"permissions,omitempty,string"`
|
||||
|
||||
// AvailableTags is the set of tags that can be used in a GuildForum
|
||||
// channel.
|
||||
AvailableTags []Tag `json:"available_tags,omitempty"`
|
||||
// AppliedTags are the IDs of the set of tags that have been applied to a
|
||||
// thread in a GuildForum channel.
|
||||
AppliedTags []TagID `json:"applied_tags,omitempty"`
|
||||
// DefaultReactionEmoji is the emoji to show in the add reaction button on a
|
||||
// thread in a GuildForum channel
|
||||
DefaultReactionEmoji *ForumReaction `json:"default_reaction_emoji,omitempty"`
|
||||
// DefaultThreadRateLimitPerUser is the initial rate_limit_per_user to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update.
|
||||
DefaultThreadRateLimitPerUser int `json:"default_thread_rate_limit_per_user,omitempty"`
|
||||
// DefaultSoftOrder is the default sort order type used to order posts in GUILD_FORUM channels. Defaults to null, which indicates a preferred sort order hasn't been set by a channel admin.
|
||||
DefaultSoftOrder *SortOrderType `json:"default_sort_order,omitempty"`
|
||||
// DefaultForumLayout is the default forum layout view used to display posts in GUILD_FORUM channels. Defaults to 0, which indicates a layout view has not been set by a channel admin.
|
||||
DefaultForumLayout ForumLayoutType `json:"default_forum_layout,omitempty"`
|
||||
}
|
||||
|
||||
func (ch *Channel) UnmarshalJSON(data []byte) error {
|
||||
type RawChannel Channel
|
||||
if err := json.Unmarshal(data, (*RawChannel)(ch)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In the docs, Discord states that if VideoQualityMode is omitted, it is
|
||||
// actually 1 aka. AutoVideoQuality, and they just didn't bother to send
|
||||
// it.
|
||||
// Refer to:
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-structure
|
||||
if ch.VideoQualityMode == 0 {
|
||||
ch.VideoQualityMode = 1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the channel was created.
|
||||
func (ch Channel) CreatedAt() time.Time {
|
||||
return ch.ID.Time()
|
||||
}
|
||||
|
||||
// Mention returns a mention of the channel.
|
||||
|
@ -180,64 +79,34 @@ func (ch Channel) IconURLWithType(t ImageType) string {
|
|||
ch.ID.String() + "/" + t.format(ch.Icon)
|
||||
}
|
||||
|
||||
// ChannelType describes the type of the channel.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
type ChannelType uint16
|
||||
type ChannelType uint8
|
||||
|
||||
const (
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-channel-types
|
||||
var (
|
||||
// GuildText is a text channel within a server.
|
||||
GuildText ChannelType = iota
|
||||
GuildText ChannelType = 0
|
||||
// DirectMessage is a direct message between users.
|
||||
DirectMessage
|
||||
DirectMessage ChannelType = 1
|
||||
// GuildVoice is a voice channel within a server.
|
||||
GuildVoice
|
||||
GuildVoice ChannelType = 2
|
||||
// GroupDM is a direct message between multiple users.
|
||||
GroupDM
|
||||
GroupDM ChannelType = 3
|
||||
// GuildCategory is an organizational category that contains up to 50
|
||||
// channels.
|
||||
GuildCategory
|
||||
// GuildAnnouncement is a channel that users can follow and crosspost into
|
||||
// their own server.
|
||||
GuildAnnouncement
|
||||
GuildCategory ChannelType = 4
|
||||
// GuildNews is a channel that users can follow and crosspost into their
|
||||
// own server.
|
||||
GuildNews ChannelType = 5
|
||||
// GuildStore is a channel in which game developers can sell their game on
|
||||
// Discord.
|
||||
GuildStore
|
||||
_
|
||||
_
|
||||
_
|
||||
// GuildAnnouncementThread is a temporary sub-channel within a GUILD_NEWS channel
|
||||
GuildAnnouncementThread
|
||||
// GuildPublicThread is a temporary sub-channel within a GUILD_TEXT
|
||||
// channel.
|
||||
GuildPublicThread
|
||||
// GuildPrivateThread isa temporary sub-channel within a GUILD_TEXT channel
|
||||
// that is only viewable by those invited and those with the MANAGE_THREADS
|
||||
// permission.
|
||||
GuildPrivateThread
|
||||
// GuildStageVoice is a voice channel for hosting events with an audience.
|
||||
GuildStageVoice
|
||||
// GuildDirectory is the channel in a hub containing the listed servers.
|
||||
GuildDirectory
|
||||
// GuildForum is a channel that can only contain threads.
|
||||
GuildForum
|
||||
GuildStore ChannelType = 6
|
||||
)
|
||||
|
||||
// GuildNews aliases to GuildAnnouncement.
|
||||
//
|
||||
// Deprecated: use GuildAnnouncement instead.
|
||||
const GuildNews = GuildAnnouncement
|
||||
|
||||
// GuildNewsThread aliases to GuildAnnouncementThread.
|
||||
//
|
||||
// Deprecated: use GuildAnnouncementThread instead.
|
||||
const GuildNewsThread = GuildAnnouncementThread
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#overwrite-object
|
||||
type Overwrite struct {
|
||||
// ID is the role or user id.
|
||||
ID Snowflake `json:"id"`
|
||||
// Type indicates the entity overwritten: role or member.
|
||||
// Type is either "role" or "member".
|
||||
Type OverwriteType `json:"type"`
|
||||
// Allow is a permission bit set for granted permissions.
|
||||
Allow Permissions `json:"allow,string"`
|
||||
|
@ -245,136 +114,35 @@ type Overwrite struct {
|
|||
Deny Permissions `json:"deny,string"`
|
||||
}
|
||||
|
||||
// OverwriteType is an enumerated type to indicate the entity being overwritten:
|
||||
// role or member
|
||||
type OverwriteType uint8
|
||||
// UnmarshalJSON unmarshals the passed json data into the Overwrite.
|
||||
// This is necessary because Discord has different names for fields when
|
||||
// sending than receiving.
|
||||
func (o *Overwrite) UnmarshalJSON(data []byte) (err error) {
|
||||
var recv struct {
|
||||
ID Snowflake `json:"id"`
|
||||
Type OverwriteType `json:"type"`
|
||||
Allow Permissions `json:"allow_new,string"`
|
||||
Deny Permissions `json:"deny_new,string"`
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &recv)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
o.ID = recv.ID
|
||||
o.Type = recv.Type
|
||||
o.Allow = recv.Allow
|
||||
o.Deny = recv.Deny
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type OverwriteType string
|
||||
|
||||
const (
|
||||
// OverwriteRole is an overwrite for a role.
|
||||
OverwriteRole OverwriteType = iota
|
||||
OverwriteRole OverwriteType = "role"
|
||||
// OverwriteMember is an overwrite for a member.
|
||||
OverwriteMember
|
||||
)
|
||||
|
||||
// UnmarshalJSON unmarshalls both a string-quoted number and a regular number
|
||||
// into OverwriteType. We need to do this because Discord is so bad that they
|
||||
// can't even handle 1s and 0s properly.
|
||||
func (otype *OverwriteType) UnmarshalJSON(b []byte) error {
|
||||
s := strings.Trim(string(b), `"`)
|
||||
|
||||
// It has been observed that discord still uses the "legacy" string
|
||||
// overwrite types in at least the guild create event.
|
||||
// Therefore this string check.
|
||||
switch s {
|
||||
case "role":
|
||||
*otype = OverwriteRole
|
||||
return nil
|
||||
case "member":
|
||||
*otype = OverwriteMember
|
||||
return nil
|
||||
}
|
||||
|
||||
u, err := strconv.ParseUint(s, 10, 8)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*otype = OverwriteType(u)
|
||||
return nil
|
||||
}
|
||||
|
||||
type VideoQualityMode uint8
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-video-quality-modes
|
||||
const (
|
||||
AutoVideoQuality VideoQualityMode = iota + 1
|
||||
FullVideoQuality
|
||||
)
|
||||
|
||||
// ThreadMetadata contains a number of thread-specific channel fields that are
|
||||
// not needed by other channel types.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/channel#thread-metadata-object
|
||||
type ThreadMetadata struct {
|
||||
// Archived specifies whether the thread is archived.
|
||||
Archived bool `json:"archived"`
|
||||
// AutoArchiveDuration is the duration in minutes to automatically archive
|
||||
// the thread after recent activity.
|
||||
AutoArchiveDuration ArchiveDuration `json:"auto_archive_duration"`
|
||||
// ArchiveTimestamp timestamp when the thread's archive status was last
|
||||
// changed, used for calculating recent activity.
|
||||
ArchiveTimestamp Timestamp `json:"archive_timestamp"`
|
||||
// Locked specifies whether the thread is locked; when a thread is locked,
|
||||
// only users with MANAGE_THREADS can unarchive it.
|
||||
Locked bool `json:"locked"`
|
||||
// Invitable specifies whether non-moderators can add other
|
||||
// non-moderators to a thread; only available on private threads.
|
||||
Invitable bool `json:"invitable,omitempty"`
|
||||
}
|
||||
|
||||
type ThreadMember struct {
|
||||
// ID is the id of the thread.
|
||||
//
|
||||
// This field will be omitted on the member sent within each thread in the
|
||||
// guild create event.
|
||||
ID ChannelID `json:"id,omitempty"`
|
||||
// UserID is the id of the user.
|
||||
//
|
||||
// This field will be omitted on the member sent within each thread in the
|
||||
// guild create event.
|
||||
UserID UserID `json:"user_id,omitempty"`
|
||||
// Member is the member, only included in Thread Members Update Events.
|
||||
Member *Member `json:"member,omitempty"`
|
||||
// Presence is the presence, only included in Thread Members Update Events.
|
||||
Presence *Presence `json:"presence,omitempty"`
|
||||
// JoinTimestamp is the time the current user last joined the thread.
|
||||
JoinTimestamp Timestamp `json:"join_timestamp"`
|
||||
// Flags are any user-thread settings.
|
||||
Flags ThreadMemberFlags `json:"flags"`
|
||||
}
|
||||
|
||||
// ThreadMemberFlags are the flags of a ThreadMember.
|
||||
// Currently, none are documented.
|
||||
type ThreadMemberFlags uint64
|
||||
|
||||
// Tag represents a tag that is able to be applied to a thread in a GuildForum
|
||||
// channel.
|
||||
type Tag struct {
|
||||
ID TagID `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Moderated bool `json:"moderated"`
|
||||
ForumReaction
|
||||
}
|
||||
|
||||
// ForumReaction is used in several forum-related structures. It is officially
|
||||
// named the "Default Reaction" object.
|
||||
type ForumReaction struct {
|
||||
// EmojiID is set when there is a custom emoji used.
|
||||
// Only one of EmojiID and EmojiName can be set
|
||||
EmojiID EmojiID `json:"emoji_id"`
|
||||
// EmojiName is set when the emoji is a normal unicode emoji.
|
||||
// Only one of EmojiID and EmojiName can be set
|
||||
EmojiName option.String `json:"emoji_name"`
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-sort-order-types
|
||||
type SortOrderType uint8
|
||||
|
||||
const (
|
||||
// Sort forum posts by activity.
|
||||
SortOrderTypeLatestActivity SortOrderType = iota
|
||||
// Sort forum posts by creation time (from most recent to oldest)
|
||||
SoftOrderTypeCreationDate
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-object-forum-layout-types
|
||||
type ForumLayoutType uint8
|
||||
|
||||
const (
|
||||
// No default has been set for forum channel.
|
||||
ForumLayoutTypeNotSet ForumLayoutType = iota
|
||||
// Display posts as a list.
|
||||
ForumLayoutTypeListView
|
||||
// Display posts as a collection of tiles.
|
||||
ForumLayoutTypeGalleryView
|
||||
OverwriteMember OverwriteType = "member"
|
||||
)
|
||||
|
|
|
@ -1,914 +0,0 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/option"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// CommandType is the type of the command, which describes the intended
|
||||
// invokation source of the command.
|
||||
type CommandType uint
|
||||
|
||||
const (
|
||||
ChatInputCommand CommandType = iota + 1
|
||||
UserCommand
|
||||
MessageCommand
|
||||
)
|
||||
|
||||
// Command is the base "command" model that belongs to an application. This is
|
||||
// what you are creating when you POST a new command.
|
||||
//
|
||||
// https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure
|
||||
type Command struct {
|
||||
// ID is the unique id of the command.
|
||||
ID CommandID `json:"id"`
|
||||
// Type is the intended source of the command.
|
||||
Type CommandType `json:"type,omitempty"`
|
||||
// AppID is the unique id of the parent application.
|
||||
AppID AppID `json:"application_id"`
|
||||
// GuildID is the guild id of the command, if not global.
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
// Name is the 1-32 lowercase character name matching ^[\w-]{1,32}$.
|
||||
Name string `json:"name"`
|
||||
NameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
// Description is the 1-100 character description.
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
// LocalizedName is only populated when this is received from Discord's API.
|
||||
LocalizedName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
// Options are the parameters for the command. Its types are value types,
|
||||
// which can either be a SubcommandOption or a SubcommandGroupOption.
|
||||
//
|
||||
// Note that required options must be listed before optional options, and
|
||||
// a command, or each individual subcommand, can have a maximum of 25
|
||||
// options.
|
||||
//
|
||||
// It is only present on ChatInputCommands.
|
||||
Options CommandOptions `json:"options,omitempty"`
|
||||
// DefaultMemberPermissions is set of permissions.
|
||||
DefaultMemberPermissions *Permissions `json:"default_member_permissions,string,omitempty"`
|
||||
// NoDMPermission indicates whether the command is NOT available in DMs with
|
||||
// the app, only for globally-scoped commands. By default, commands are visible.
|
||||
NoDMPermission bool `json:"-"`
|
||||
// NoDefaultPermissions defines whether the command is NOT enabled by
|
||||
// default when the app is added to a guild.
|
||||
NoDefaultPermission bool `json:"-"`
|
||||
// Version is an autoincrementing version identifier updated during
|
||||
// substantial record changes
|
||||
Version Snowflake `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// Language is a string type for language codes, such as "en-US" or "fr". Refer
|
||||
// to the constants for valid language codes.
|
||||
//
|
||||
// The list of all valid language codes are at
|
||||
// https://discord.com/developers/docs/reference#locales
|
||||
type Language string
|
||||
|
||||
// StringLocales is the map mapping a language code to a localized string.
|
||||
type StringLocales map[Language]string
|
||||
|
||||
const (
|
||||
Danish Language = "da"
|
||||
German Language = "de"
|
||||
EnglishUK Language = "en-GB"
|
||||
EnglishUS Language = "en-US"
|
||||
Spanish Language = "es-ES"
|
||||
French Language = "fr"
|
||||
Croatian Language = "hr"
|
||||
Italian Language = "it"
|
||||
Lithuanian Language = "lt"
|
||||
Hungarian Language = "hu"
|
||||
Dutch Language = "nl"
|
||||
Norwegian Language = "no"
|
||||
Polish Language = "pl"
|
||||
PortugueseBR Language = "pt-BR"
|
||||
Romanian Language = "ro"
|
||||
Finnish Language = "fi"
|
||||
Swedish Language = "sv-SE"
|
||||
Vietnamses Language = "vi"
|
||||
Turkish Language = "tr"
|
||||
Czech Language = "cs"
|
||||
Greek Language = "el"
|
||||
Bulgarian Language = "bg"
|
||||
Russian Language = "ru"
|
||||
Ukrainian Language = "uk"
|
||||
Hindi Language = "hi"
|
||||
Thai Language = "th"
|
||||
ChineseChina Language = "zh-CN"
|
||||
Japanese Language = "ja"
|
||||
ChineseTaiwan Language = "zh-TW"
|
||||
Korean Language = "ko"
|
||||
)
|
||||
|
||||
// CreatedAt returns a time object representing when the command was created.
|
||||
func (c *Command) CreatedAt() time.Time {
|
||||
return c.ID.Time()
|
||||
}
|
||||
|
||||
func (c *Command) MarshalJSON() ([]byte, error) {
|
||||
type RawCommand Command
|
||||
cmd := struct {
|
||||
*RawCommand
|
||||
DMPermission bool `json:"dm_permission"`
|
||||
DefaultPermission bool `json:"default_permission"`
|
||||
}{RawCommand: (*RawCommand)(c)}
|
||||
|
||||
// Discord defaults default_permission to true, so we need to invert the
|
||||
// meaning of the field (>No<DefaultPermission) to match Go's default
|
||||
// value, false.
|
||||
cmd.DefaultPermission = !c.NoDefaultPermission
|
||||
cmd.DMPermission = !c.NoDMPermission
|
||||
|
||||
return json.Marshal(cmd)
|
||||
}
|
||||
|
||||
func (c *Command) UnmarshalJSON(data []byte) error {
|
||||
type rawCommand Command
|
||||
|
||||
cmd := struct {
|
||||
*rawCommand
|
||||
DMPermission bool `json:"dm_permission"`
|
||||
DefaultPermission bool `json:"default_permission"`
|
||||
}{
|
||||
rawCommand: (*rawCommand)(c),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Discord defaults default_permission to true, so we need to invert the
|
||||
// meaning of the field (>No<DefaultPermission) to match Go's default
|
||||
// value, false.
|
||||
c.NoDefaultPermission = !cmd.DefaultPermission
|
||||
c.NoDMPermission = !cmd.DMPermission
|
||||
|
||||
// Discord defaults type to 1 if omitted.
|
||||
if c.Type == 0 {
|
||||
c.Type = ChatInputCommand
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// commandTypeCheckError is returned if a one of Command's Options fails the
|
||||
// type check.
|
||||
type commandTypeCheckError struct {
|
||||
name string
|
||||
got interface{}
|
||||
expect string
|
||||
}
|
||||
|
||||
// Name returns the name of the erroneous command.
|
||||
func (err commandTypeCheckError) Name() string {
|
||||
return err.name
|
||||
}
|
||||
|
||||
// Data returns the erroneous data that belongs to this error. It is usually
|
||||
// either a CommandOption or a CommandOptionValue.
|
||||
func (err commandTypeCheckError) Data() interface{} {
|
||||
return err.got
|
||||
}
|
||||
|
||||
// Error implements error.
|
||||
func (err commandTypeCheckError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"error at option name %q: expected %s, got %T",
|
||||
err.name, err.expect, err.got,
|
||||
)
|
||||
}
|
||||
|
||||
// CommandOptions is used primarily for unmarshaling.
|
||||
type CommandOptions []CommandOption
|
||||
|
||||
// UnmarshalJSON unmarshals b into these CommandOptions.
|
||||
func (c *CommandOptions) UnmarshalJSON(b []byte) error {
|
||||
var unknowns []UnknownCommandOption
|
||||
if err := json.Unmarshal(b, &unknowns); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(unknowns) == 0 {
|
||||
*c = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
*c = make([]CommandOption, len(unknowns))
|
||||
for i, v := range unknowns {
|
||||
(*c)[i] = v.data
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnknownCommandOption is used for unknown or unmarshaled CommandOption values.
|
||||
// It is used in the unmarshaling stage for all CommandOption types.
|
||||
//
|
||||
// An UnknownCommandOption will satisfy both CommandOption and
|
||||
// CommandOptionValue. Code that type-switches on either of them should not
|
||||
// assume that only the expected types are used.
|
||||
type UnknownCommandOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionType CommandOptionType `json:"type"`
|
||||
|
||||
raw json.Raw
|
||||
data CommandOption
|
||||
}
|
||||
|
||||
// Name returns the supposeed name for this UnknownCommandOption.
|
||||
func (u *UnknownCommandOption) Name() string {
|
||||
return u.OptionName
|
||||
}
|
||||
|
||||
// Type returns the supposed type for this UnknownCommandOption.
|
||||
func (u *UnknownCommandOption) Type() CommandOptionType {
|
||||
return u.OptionType
|
||||
}
|
||||
|
||||
// Raw returns the raw JSON of this UnknownCommandOption. It will only return a
|
||||
// non-nil blob of JSON if the command option's type cannot be found. If this
|
||||
// method doesn't return nil, then Data's type will be UnknownCommandOption.
|
||||
func (u *UnknownCommandOption) Raw() json.Raw {
|
||||
return u.raw
|
||||
}
|
||||
|
||||
// Data returns the underlying data type, which is a type that satisfies either
|
||||
// CommandOption or CommandOptionValue.
|
||||
func (u *UnknownCommandOption) Data() CommandOption {
|
||||
return u.data
|
||||
}
|
||||
|
||||
// Implement both CommandOption and CommandOptionValue.
|
||||
func (u *UnknownCommandOption) _val() {}
|
||||
|
||||
// UnmarshalJSON parses the JSON into the struct as-is then reads all its
|
||||
// children Options/Choices (if subcommand(group)). Typed command options are
|
||||
// created into u.Data, or u.Raw if the type is unknown. This is done from the
|
||||
// bottom up.
|
||||
func (u *UnknownCommandOption) UnmarshalJSON(b []byte) error {
|
||||
type unknown UnknownCommandOption
|
||||
|
||||
if err := json.Unmarshal(b, (*unknown)(u)); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal unknown")
|
||||
}
|
||||
|
||||
switch u.Type() {
|
||||
case SubcommandOptionType:
|
||||
u.data = &SubcommandOption{}
|
||||
case SubcommandGroupOptionType:
|
||||
u.data = &SubcommandGroupOption{}
|
||||
case StringOptionType:
|
||||
u.data = &StringOption{}
|
||||
case IntegerOptionType:
|
||||
u.data = &IntegerOption{}
|
||||
case BooleanOptionType:
|
||||
u.data = &BooleanOption{}
|
||||
case UserOptionType:
|
||||
u.data = &UserOption{}
|
||||
case ChannelOptionType:
|
||||
u.data = &ChannelOption{}
|
||||
case RoleOptionType:
|
||||
u.data = &RoleOption{}
|
||||
case MentionableOptionType:
|
||||
u.data = &MentionableOption{}
|
||||
case NumberOptionType:
|
||||
u.data = &NumberOption{}
|
||||
default:
|
||||
// Copy the blob of bytes into a new slice.
|
||||
u.raw = append(json.Raw(nil), b...)
|
||||
u.data = u
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, u.data); err != nil {
|
||||
return errors.Wrapf(err, "failed to unmarshal type %d", u.Type())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandOptionType is the enumerated integer type for command options. The
|
||||
// user usually won't have to touch any of these enum constants.
|
||||
type CommandOptionType uint
|
||||
|
||||
const (
|
||||
SubcommandOptionType CommandOptionType = iota + 1
|
||||
SubcommandGroupOptionType
|
||||
StringOptionType
|
||||
IntegerOptionType
|
||||
BooleanOptionType
|
||||
UserOptionType
|
||||
ChannelOptionType
|
||||
RoleOptionType
|
||||
MentionableOptionType
|
||||
NumberOptionType
|
||||
AttachmentOptionType
|
||||
maxOptionType // for bound checking
|
||||
)
|
||||
|
||||
// CommandOption is a union of command option types. The constructors for
|
||||
// CommandOption will hint the types that can be a CommandOption.
|
||||
//
|
||||
// The following types implement this interface:
|
||||
//
|
||||
// - *SubcommandGroupOption
|
||||
// - *SubcommandOption
|
||||
// - *StringOption
|
||||
// - *IntegerOption
|
||||
// - *BooleanOption
|
||||
// - *UserOption
|
||||
// - *ChannelOption
|
||||
// - *RoleOption
|
||||
// - *MentionableOption
|
||||
// - *NumberOption
|
||||
// - *AttachmentOption
|
||||
//
|
||||
type CommandOption interface {
|
||||
Name() string
|
||||
Type() CommandOptionType
|
||||
}
|
||||
|
||||
// Maintaining these structs is quite an effort. If a new field is added into
|
||||
// the generic CommandOption type, you MUST update ALL CommandOption structs.
|
||||
// This means copy-pasting, yes.
|
||||
|
||||
// SubcommandGroupOption is a subcommand group that fits into a CommandOption.
|
||||
type SubcommandGroupOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Subcommands []*SubcommandOption `json:"options"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (s *SubcommandGroupOption) Name() string { return s.OptionName }
|
||||
|
||||
// Type implements CommandOption.
|
||||
func (s *SubcommandGroupOption) Type() CommandOptionType { return SubcommandGroupOptionType }
|
||||
|
||||
// SubcommandOption is a subcommand option that fits into a CommandOption.
|
||||
type SubcommandOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// Options contains command option values. All CommandOption types except
|
||||
// for SubcommandOption and SubcommandGroupOption will implement this
|
||||
// interface.
|
||||
Options []CommandOptionValue `json:"options"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (s *SubcommandOption) Name() string { return s.OptionName }
|
||||
|
||||
// Type implements CommandOption.
|
||||
func (s *SubcommandOption) Type() CommandOptionType { return SubcommandOptionType }
|
||||
|
||||
// UnmarshalJSON unmarshals the given JSON bytes. It actually does
|
||||
// type-checking.
|
||||
func (s *SubcommandOption) UnmarshalJSON(b []byte) error {
|
||||
type raw SubcommandOption
|
||||
|
||||
var opt struct {
|
||||
*raw
|
||||
Type CommandOptionType `json:"type"`
|
||||
Options []UnknownCommandOption `json:"options"`
|
||||
}
|
||||
|
||||
opt.raw = (*raw)(s)
|
||||
|
||||
if err := json.Unmarshal(b, &opt); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if opt.Type != SubcommandOptionType {
|
||||
return fmt.Errorf("unexpected (not SubcommandOption) type %d", s.Type())
|
||||
}
|
||||
|
||||
s.Options = make([]CommandOptionValue, len(opt.Options))
|
||||
for i, opt := range opt.Options {
|
||||
ov, ok := opt.data.(CommandOptionValue)
|
||||
if !ok {
|
||||
return commandTypeCheckError{opt.OptionName, opt.data, "CommandOptionValue"}
|
||||
}
|
||||
s.Options[i] = ov
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommandOptionValue is a subcommand option that fits into a subcommand.
|
||||
//
|
||||
// The following types implement this interface:
|
||||
//
|
||||
// - *StringOption
|
||||
// - *IntegerOption
|
||||
// - *BooleanOption
|
||||
// - *UserOption
|
||||
// - *ChannelOption
|
||||
// - *RoleOption
|
||||
// - *MentionableOption
|
||||
// - *NumberOption
|
||||
// - *AttachmentOption
|
||||
//
|
||||
type CommandOptionValue interface {
|
||||
CommandOption
|
||||
_val()
|
||||
}
|
||||
|
||||
// StringOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type StringOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Choices []StringChoice `json:"choices,omitempty"`
|
||||
MinLength option.Int `json:"min_length,omitempty"`
|
||||
MaxLength option.Int `json:"max_length,omitempty"`
|
||||
// Autocomplete must not be true if Choices are present.
|
||||
Autocomplete bool `json:"autocomplete"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (s *StringOption) Name() string { return s.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (s *StringOption) Type() CommandOptionType { return StringOptionType }
|
||||
func (s *StringOption) _val() {}
|
||||
|
||||
// StringChoice is a pair of string key to a string.
|
||||
type StringChoice struct {
|
||||
Name string `json:"name"`
|
||||
NameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Value string `json:"value"`
|
||||
// LocalizedName is only populated when this is received from Discord's API.
|
||||
LocalizedName string `json:"name_localized,omitempty"`
|
||||
}
|
||||
|
||||
// IntegerOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type IntegerOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Min option.Int `json:"min_value,omitempty"`
|
||||
Max option.Int `json:"max_value,omitempty"`
|
||||
Choices []IntegerChoice `json:"choices,omitempty"`
|
||||
// Autocomplete must not be true if Choices are present.
|
||||
Autocomplete bool `json:"autocomplete"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (i *IntegerOption) Name() string { return i.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (i *IntegerOption) Type() CommandOptionType { return IntegerOptionType }
|
||||
func (i *IntegerOption) _val() {}
|
||||
|
||||
// IntegerChoice is a pair of string key to an integer.
|
||||
type IntegerChoice struct {
|
||||
Name string `json:"name"`
|
||||
NameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Value int `json:"value"`
|
||||
// LocalizedName is only populated when this is received from Discord's API.
|
||||
LocalizedName string `json:"name_localized,omitempty"`
|
||||
}
|
||||
|
||||
// BooleanOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type BooleanOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (b *BooleanOption) Name() string { return b.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (b *BooleanOption) Type() CommandOptionType { return BooleanOptionType }
|
||||
func (b *BooleanOption) _val() {}
|
||||
|
||||
// UserOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type UserOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (u *UserOption) Name() string { return u.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (u *UserOption) Type() CommandOptionType { return UserOptionType }
|
||||
func (u *UserOption) _val() {}
|
||||
|
||||
// ChannelOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type ChannelOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
ChannelTypes []ChannelType `json:"channel_types,omitempty"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (c *ChannelOption) Name() string { return c.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (c *ChannelOption) Type() CommandOptionType { return ChannelOptionType }
|
||||
func (c *ChannelOption) _val() {}
|
||||
|
||||
// RoleOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type RoleOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (r *RoleOption) Name() string { return r.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (r *RoleOption) Type() CommandOptionType { return RoleOptionType }
|
||||
func (r *RoleOption) _val() {}
|
||||
|
||||
// MentionableOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type MentionableOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (m *MentionableOption) Name() string { return m.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (m *MentionableOption) Type() CommandOptionType { return MentionableOptionType }
|
||||
func (m *MentionableOption) _val() {}
|
||||
|
||||
// NumberOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type NumberOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
Min option.Float `json:"min_value,omitempty"`
|
||||
Max option.Float `json:"max_value,omitempty"`
|
||||
Choices []NumberChoice `json:"choices,omitempty"`
|
||||
// Autocomplete must not be true if Choices are present.
|
||||
Autocomplete bool `json:"autocomplete"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (n *NumberOption) Name() string { return n.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (n *NumberOption) Type() CommandOptionType { return NumberOptionType }
|
||||
func (n *NumberOption) _val() {}
|
||||
|
||||
// NumberChoice is a pair of string key to a float64 values.
|
||||
type NumberChoice struct {
|
||||
Name string `json:"name"`
|
||||
NameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Value float64 `json:"value"`
|
||||
// LocalizedName is only populated when this is received from Discord's API.
|
||||
LocalizedName string `json:"name_localized,omitempty"`
|
||||
}
|
||||
|
||||
// AttachmentOption is a subcommand option that fits into a CommandOptionValue.
|
||||
type AttachmentOption struct {
|
||||
OptionName string `json:"name"`
|
||||
OptionNameLocalizations StringLocales `json:"name_localizations,omitempty"`
|
||||
Description string `json:"description"`
|
||||
DescriptionLocalizations StringLocales `json:"description_localizations,omitempty"`
|
||||
Required bool `json:"required"`
|
||||
// LocalizedOptionName is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedOptionName string `json:"name_localized,omitempty"`
|
||||
// LocalizedDescription is only populated when this is received from
|
||||
// Discord's API.
|
||||
LocalizedDescription string `json:"description_localized,omitempty"`
|
||||
}
|
||||
|
||||
// Name implements CommandOption.
|
||||
func (n *AttachmentOption) Name() string { return n.OptionName }
|
||||
|
||||
// Type implements CommandOptionValue.
|
||||
func (n *AttachmentOption) Type() CommandOptionType { return AttachmentOptionType }
|
||||
func (n *AttachmentOption) _val() {}
|
||||
|
||||
// NewCommand creates a new command.
|
||||
func NewCommand(name, description string, options ...CommandOption) Command {
|
||||
return Command{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSubcommandGroupOption creates a new subcommand group option.
|
||||
func NewSubcommandGroupOption(name, description string, subs ...*SubcommandOption) *SubcommandGroupOption {
|
||||
return &SubcommandGroupOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Subcommands: subs,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSubcommandOption creates a new subcommand option.
|
||||
func NewSubcommandOption(name, description string, options ...CommandOptionValue) *SubcommandOption {
|
||||
return &SubcommandOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Options: options,
|
||||
}
|
||||
}
|
||||
|
||||
// NewStringOption creates a new string option.
|
||||
func NewStringOption(name, description string, required bool) *StringOption {
|
||||
return &StringOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewIntegerOption creates a new integer option.
|
||||
func NewIntegerOption(name, description string, required bool) *IntegerOption {
|
||||
return &IntegerOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBooleanOption creates a new boolean option.
|
||||
func NewBooleanOption(name, description string, required bool) *BooleanOption {
|
||||
return &BooleanOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewUserOption creates a new user option.
|
||||
func NewUserOption(name, description string, required bool) *UserOption {
|
||||
return &UserOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewChannelOption creates a new channel option.
|
||||
func NewChannelOption(name, description string, required bool) *ChannelOption {
|
||||
return &ChannelOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRoleOption creates a new role option.
|
||||
func NewRoleOption(name, description string, required bool) *RoleOption {
|
||||
return &RoleOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMentionableOption creates a new mentionable option.
|
||||
func NewMentionableOption(name, description string, required bool) *MentionableOption {
|
||||
return &MentionableOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// NewNumberOption creates a new number option.
|
||||
func NewNumberOption(name, description string, required bool) *NumberOption {
|
||||
return &NumberOption{
|
||||
OptionName: name,
|
||||
Description: description,
|
||||
Required: required,
|
||||
}
|
||||
}
|
||||
|
||||
// Generated with utils/generate-option-marshalers.sh
|
||||
|
||||
// MarshalJSON marshals SubcommandOption to JSON with the "type" field.
|
||||
func (s *SubcommandOption) MarshalJSON() ([]byte, error) {
|
||||
type raw SubcommandOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: s.Type(),
|
||||
raw: (*raw)(s),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals SubcommandGroupOption to JSON with the "type" field.
|
||||
func (s *SubcommandGroupOption) MarshalJSON() ([]byte, error) {
|
||||
type raw SubcommandGroupOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: s.Type(),
|
||||
raw: (*raw)(s),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals StringOption to JSON with the "type" field.
|
||||
func (s *StringOption) MarshalJSON() ([]byte, error) {
|
||||
type raw StringOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: s.Type(),
|
||||
raw: (*raw)(s),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals IntegerOption to JSON with the "type" field.
|
||||
func (i *IntegerOption) MarshalJSON() ([]byte, error) {
|
||||
type raw IntegerOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: i.Type(),
|
||||
raw: (*raw)(i),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals BooleanOption to JSON with the "type" field.
|
||||
func (b *BooleanOption) MarshalJSON() ([]byte, error) {
|
||||
type raw BooleanOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: b.Type(),
|
||||
raw: (*raw)(b),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals UserOption to JSON with the "type" field.
|
||||
func (u *UserOption) MarshalJSON() ([]byte, error) {
|
||||
type raw UserOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: u.Type(),
|
||||
raw: (*raw)(u),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals ChannelOption to JSON with the "type" field.
|
||||
func (c *ChannelOption) MarshalJSON() ([]byte, error) {
|
||||
type raw ChannelOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: c.Type(),
|
||||
raw: (*raw)(c),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals RoleOption to JSON with the "type" field.
|
||||
func (r *RoleOption) MarshalJSON() ([]byte, error) {
|
||||
type raw RoleOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: r.Type(),
|
||||
raw: (*raw)(r),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals MentionableOption to JSON with the "type" field.
|
||||
func (m *MentionableOption) MarshalJSON() ([]byte, error) {
|
||||
type raw MentionableOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: m.Type(),
|
||||
raw: (*raw)(m),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals NumberOption to JSON with the "type" field.
|
||||
func (n *NumberOption) MarshalJSON() ([]byte, error) {
|
||||
type raw NumberOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: n.Type(),
|
||||
raw: (*raw)(n),
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON marshals AttachmentOption to JSON with the "type" field.
|
||||
func (a *AttachmentOption) MarshalJSON() ([]byte, error) {
|
||||
type raw AttachmentOption
|
||||
return json.Marshal(struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
*raw
|
||||
}{
|
||||
Type: a.Type(),
|
||||
raw: (*raw)(a),
|
||||
})
|
||||
}
|
|
@ -1,945 +0,0 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ComponentType is the type of a component.
|
||||
type ComponentType uint
|
||||
|
||||
const (
|
||||
_ ComponentType = iota
|
||||
ActionRowComponentType
|
||||
ButtonComponentType
|
||||
StringSelectComponentType
|
||||
TextInputComponentType
|
||||
UserSelectComponentType
|
||||
RoleSelectComponentType
|
||||
MentionableSelectComponentType
|
||||
ChannelSelectComponentType
|
||||
)
|
||||
|
||||
// String formats Type's name as a string.
|
||||
func (t ComponentType) String() string {
|
||||
switch t {
|
||||
case ActionRowComponentType:
|
||||
return "ActionRow"
|
||||
case ButtonComponentType:
|
||||
return "Button"
|
||||
case StringSelectComponentType:
|
||||
return "StringSelect"
|
||||
case TextInputComponentType:
|
||||
return "TextInput"
|
||||
case UserSelectComponentType:
|
||||
return "User"
|
||||
case RoleSelectComponentType:
|
||||
return "Role"
|
||||
case MentionableSelectComponentType:
|
||||
return "Mentionable"
|
||||
case ChannelSelectComponentType:
|
||||
return "Channel"
|
||||
default:
|
||||
return fmt.Sprintf("ComponentType(%d)", int(t))
|
||||
}
|
||||
}
|
||||
|
||||
// ContainerComponents is primarily used for unmarshaling. It is the top-level
|
||||
// type for component lists.
|
||||
type ContainerComponents []ContainerComponent
|
||||
|
||||
// Find finds any component with the given custom ID.
|
||||
func (c *ContainerComponents) Find(customID ComponentID) Component {
|
||||
for _, component := range *c {
|
||||
switch component := component.(type) {
|
||||
case *ActionRowComponent:
|
||||
if component := component.Find(customID); component != nil {
|
||||
return component
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the components into the struct pointer v. Each struct
|
||||
// field must be exported and is of a supported type.
|
||||
//
|
||||
// Fields that don't satisfy any of the above are ignored. The "discord" struct
|
||||
// tag with a value "-" is ignored. Fields that aren't found in the list of
|
||||
// options and have a "?" at the end of the "discord" struct tag are ignored.
|
||||
//
|
||||
// Each struct field will be used to search the tree of components for a
|
||||
// matching custom ID. The struct must be a flat struct that lists all the
|
||||
// components it needs using the custom ID.
|
||||
//
|
||||
// Supported Types
|
||||
//
|
||||
// The following types are supported:
|
||||
//
|
||||
// - string (SelectComponent if range = [n, 1], TextInputComponent)
|
||||
// - int*, uint*, float* (uses Parse{Int,Uint,Float}, SelectComponent if range = [n, 1], TextInputComponent)
|
||||
// - bool (ButtonComponent or any component, true if present)
|
||||
// - []string (SelectComponent)
|
||||
//
|
||||
// Any types that are derived from any of the above built-in types are also
|
||||
// supported.
|
||||
//
|
||||
// Pointer types to any of the above types are also supported and will also
|
||||
// implicitly imply optionality.
|
||||
func (c *ContainerComponents) Unmarshal(v interface{}) error {
|
||||
rv, rt, err := rfutil.StructValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numField := rt.NumField()
|
||||
for i := 0; i < numField; i++ {
|
||||
fieldStruct := rt.Field(i)
|
||||
if !fieldStruct.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fieldStruct.Tag.Get("discord")
|
||||
switch name {
|
||||
case "-":
|
||||
continue
|
||||
case "?":
|
||||
name = fieldStruct.Name + "?"
|
||||
case "":
|
||||
name = fieldStruct.Name
|
||||
}
|
||||
|
||||
component := c.Find(ComponentID(strings.TrimSuffix(name, "?")))
|
||||
fieldv := rv.Field(i)
|
||||
fieldt := fieldStruct.Type
|
||||
|
||||
if strings.HasSuffix(name, "?") {
|
||||
name = strings.TrimSuffix(name, "?")
|
||||
if component == nil {
|
||||
// not found
|
||||
continue
|
||||
}
|
||||
} else if fieldStruct.Type.Kind() == reflect.Ptr {
|
||||
fieldt = fieldt.Elem()
|
||||
if component == nil {
|
||||
// not found
|
||||
fieldv.Set(reflect.NewAt(fieldt, nil))
|
||||
continue
|
||||
}
|
||||
// found, so allocate new value and use that to set
|
||||
newv := reflect.New(fieldt)
|
||||
fieldv.Set(newv)
|
||||
fieldv = newv.Elem()
|
||||
} else if component == nil {
|
||||
// not found AND the field is not a pointer, so error out
|
||||
return fmt.Errorf("component %q is required but not found", name)
|
||||
}
|
||||
|
||||
switch fieldk := fieldt.Kind(); fieldk {
|
||||
case reflect.Bool:
|
||||
// Intended for ButtonComponents.
|
||||
fieldv.Set(reflect.ValueOf(true).Convert(fieldt))
|
||||
case reflect.String,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
var v string
|
||||
|
||||
switch component := component.(type) {
|
||||
case *TextInputComponent:
|
||||
v = component.Value
|
||||
case *StringSelectComponent:
|
||||
switch len(component.Options) {
|
||||
case 0:
|
||||
// ok
|
||||
case 1:
|
||||
v = component.Options[0].Value
|
||||
default:
|
||||
return fmt.Errorf("component %q selected more than one item (bug, check ValueRange)", name)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("component %q is of unsupported type %T", name, component)
|
||||
}
|
||||
|
||||
switch fieldk {
|
||||
case reflect.String:
|
||||
fieldv.Set(reflect.ValueOf(v).Convert(fieldt))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
i, err := strconv.ParseInt(v, 10, rfutil.KindBits(fieldk))
|
||||
if err != nil {
|
||||
return fmt.Errorf("component %q has invalid integer: %v", name, err)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(i).Convert(fieldt))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
u, err := strconv.ParseUint(v, 10, rfutil.KindBits(fieldk))
|
||||
if err != nil {
|
||||
return fmt.Errorf("component %q has invalid unsigned (positive) integer: %v", name, err)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(u).Convert(fieldt))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
f, err := strconv.ParseFloat(v, rfutil.KindBits(fieldk))
|
||||
if err != nil {
|
||||
return fmt.Errorf("component %q has invalid floating-point number: %v", name, err)
|
||||
}
|
||||
fieldv.Set(reflect.ValueOf(f).Convert(fieldt))
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
case reflect.Slice:
|
||||
elemt := fieldt.Elem()
|
||||
|
||||
switch elemt.Kind() {
|
||||
case reflect.String:
|
||||
switch component := component.(type) {
|
||||
case *StringSelectComponent:
|
||||
fieldv.Set(reflect.MakeSlice(fieldt, len(component.Options), len(component.Options)))
|
||||
for i, option := range component.Options {
|
||||
fieldv.Index(i).Set(reflect.ValueOf(option.Value).Convert(elemt))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("component %q is of unsupported type %T", name, component)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field %s (%q) has unknown slice type %s", fieldStruct.Name, name, fieldt)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("field %s (%q) has unknown type %s", fieldStruct.Name, name, fieldt)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals JSON into the component. It does type-checking and
|
||||
// will only accept container components.
|
||||
func (c *ContainerComponents) UnmarshalJSON(b []byte) error {
|
||||
var jsons []json.Raw
|
||||
if err := json.Unmarshal(b, &jsons); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*c = make([]ContainerComponent, len(jsons))
|
||||
|
||||
for i, b := range jsons {
|
||||
p, err := ParseComponent(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cc, ok := p.(ContainerComponent)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected container, got %T", p)
|
||||
}
|
||||
(*c)[i] = cc
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Component is a component that can be attached to an interaction response. A
|
||||
// Component is either an InteractiveComponent or a ContainerComponent. See
|
||||
// those appropriate types for more information.
|
||||
//
|
||||
// The following types satisfy this interface:
|
||||
//
|
||||
// - *ActionRowComponent
|
||||
// - *ButtonComponent
|
||||
// - *StringSelectComponent
|
||||
// - *TextInputComponent
|
||||
// - *UserSelectComponent
|
||||
// - *RoleSelectComponent
|
||||
// - *MentionableSelectComponent
|
||||
// - *ChannelSelectComponent
|
||||
//
|
||||
type Component interface {
|
||||
// Type returns the type of the underlying component.
|
||||
Type() ComponentType
|
||||
_cmp()
|
||||
}
|
||||
|
||||
// InteractiveComponent extends the Component for components that are
|
||||
// interactible, or components that aren't containers (like ActionRow). This is
|
||||
// useful for ActionRow to type-check that no nested ActionRows are allowed.
|
||||
//
|
||||
// The following types satisfy this interface:
|
||||
//
|
||||
// - *ButtonComponent
|
||||
// - *SelectComponent
|
||||
// - *TextInputComponent
|
||||
// - *UserSelectComponent
|
||||
// - *RoleSelectComponent
|
||||
// - *MentionableSelectComponent
|
||||
// - *ChannelSelectComponent
|
||||
//
|
||||
type InteractiveComponent interface {
|
||||
Component
|
||||
// ID returns the ID of the underlying component.
|
||||
ID() ComponentID
|
||||
_icp()
|
||||
}
|
||||
|
||||
// ContainerComponent is the opposite of InteractiveComponent: it describes
|
||||
// components that only contain other components. The only component that
|
||||
// satisfies that is ActionRow.
|
||||
//
|
||||
// The following types satisfy this interface:
|
||||
//
|
||||
// - *ActionRowComponent
|
||||
//
|
||||
type ContainerComponent interface {
|
||||
Component
|
||||
_ctn()
|
||||
}
|
||||
|
||||
// NewComponent returns a new Component from the given type that's matched with
|
||||
// the global ComponentFunc map. If the type is unknown, then Unknown is used.
|
||||
func ParseComponent(b []byte) (Component, error) {
|
||||
var t struct {
|
||||
Type ComponentType
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &t); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal component type")
|
||||
}
|
||||
|
||||
var c Component
|
||||
|
||||
switch t.Type {
|
||||
case ActionRowComponentType:
|
||||
c = &ActionRowComponent{}
|
||||
case ButtonComponentType:
|
||||
c = &ButtonComponent{}
|
||||
case StringSelectComponentType:
|
||||
c = &StringSelectComponent{}
|
||||
case TextInputComponentType:
|
||||
c = &TextInputComponent{}
|
||||
default:
|
||||
c = &UnknownComponent{typ: t.Type}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal component body")
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// ActionRow is a row of components at the bottom of a message. Its type,
|
||||
// InteractiveComponent, ensures that only non-ActionRow components are allowed
|
||||
// on it.
|
||||
type ActionRowComponent []InteractiveComponent
|
||||
|
||||
// Components wraps the given list of components inside ActionRows if it's not
|
||||
// already in one. This is a convenient function that wraps components inside
|
||||
// ActionRows for the user. It panics if any of the action rows have nested
|
||||
// action rows in them.
|
||||
//
|
||||
// Here's an example of how to use it:
|
||||
//
|
||||
// discord.Components(
|
||||
// discord.TextButtonComponent("Hello, world!"),
|
||||
// discord.Components(
|
||||
// discord.TextButtonComponent("Hello!"),
|
||||
// discord.TextButtonComponent("Delete."),
|
||||
// ),
|
||||
// )
|
||||
//
|
||||
func Components(components ...Component) ContainerComponents {
|
||||
new := make([]ContainerComponent, len(components))
|
||||
|
||||
for i, comp := range components {
|
||||
cc, ok := comp.(ContainerComponent)
|
||||
if !ok {
|
||||
// Wrap. We're asserting that comp is either a ContainerComponent or
|
||||
// an InteractiveComponent. Neither would be a bug, therefore panic.
|
||||
cc = &ActionRowComponent{comp.(InteractiveComponent)}
|
||||
}
|
||||
|
||||
new[i] = cc
|
||||
}
|
||||
|
||||
return new
|
||||
}
|
||||
|
||||
// ComponentsPtr returns the pointer to Components' return. This is a
|
||||
// convenient function.
|
||||
func ComponentsPtr(components ...Component) *ContainerComponents {
|
||||
v := Components(components...)
|
||||
return &v
|
||||
}
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (a *ActionRowComponent) Type() ComponentType {
|
||||
return ActionRowComponentType
|
||||
}
|
||||
|
||||
func (a *ActionRowComponent) _cmp() {}
|
||||
func (a *ActionRowComponent) _ctn() {}
|
||||
|
||||
// Find finds any component with the given custom ID.
|
||||
func (a *ActionRowComponent) Find(customID ComponentID) Component {
|
||||
for _, component := range *a {
|
||||
if component.ID() == customID {
|
||||
return component
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the action row in the format Discord expects.
|
||||
func (a *ActionRowComponent) MarshalJSON() ([]byte, error) {
|
||||
var actionRow struct {
|
||||
Type ComponentType `json:"type"`
|
||||
Components *[]InteractiveComponent `json:"components"`
|
||||
}
|
||||
|
||||
actionRow.Components = (*[]InteractiveComponent)(a)
|
||||
actionRow.Type = a.Type()
|
||||
|
||||
return json.Marshal(actionRow)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals JSON into the components. It does type-checking and
|
||||
// will only accept interactive components.
|
||||
func (a *ActionRowComponent) UnmarshalJSON(b []byte) error {
|
||||
var row struct {
|
||||
Components []json.Raw `json:"components"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &row); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*a = make(ActionRowComponent, len(row.Components))
|
||||
|
||||
for i, b := range row.Components {
|
||||
p, err := ParseComponent(b)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse component %d", i)
|
||||
}
|
||||
|
||||
ic, ok := p.(InteractiveComponent)
|
||||
if !ok {
|
||||
return fmt.Errorf("expected interactive, got %T", p)
|
||||
}
|
||||
(*a)[i] = ic
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentID is the type for a component's custom ID. It is NOT a snowflake,
|
||||
// but rather a user-defined opaque string.
|
||||
type ComponentID string
|
||||
|
||||
// ComponentEmoji is the emoji displayed on the button before the text. For more
|
||||
// information, see Emoji.
|
||||
type ComponentEmoji struct {
|
||||
ID EmojiID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
}
|
||||
|
||||
// ButtonComponentStyle is the style to display a button in. Use one of the
|
||||
// ButtonStyle constructor functions.
|
||||
type ButtonComponentStyle interface {
|
||||
style() int
|
||||
}
|
||||
|
||||
type basicButtonStyle int
|
||||
|
||||
func (s basicButtonStyle) style() int { return int(s) }
|
||||
|
||||
const (
|
||||
_ basicButtonStyle = iota
|
||||
primaryButtonStyle
|
||||
secondaryButtonStyle
|
||||
successButtonStyle
|
||||
dangerButtonStyle
|
||||
linkButtonStyleNum
|
||||
basicButtonStyleLen
|
||||
)
|
||||
|
||||
// PrimaryButtonStyle is a style for a blurple button.
|
||||
func PrimaryButtonStyle() ButtonComponentStyle { return primaryButtonStyle }
|
||||
|
||||
// SecondaryButtonStyle is a style for a grey button.
|
||||
func SecondaryButtonStyle() ButtonComponentStyle { return secondaryButtonStyle }
|
||||
|
||||
// SuccessButtonStyle is a style for a green button.
|
||||
func SuccessButtonStyle() ButtonComponentStyle { return successButtonStyle }
|
||||
|
||||
// DangerButtonStyle is a style for a red button.
|
||||
func DangerButtonStyle() ButtonComponentStyle { return dangerButtonStyle }
|
||||
|
||||
type linkButtonStyle URL
|
||||
|
||||
func (s linkButtonStyle) style() int { return int(linkButtonStyleNum) }
|
||||
|
||||
// LinkButtonStyle is a button style that navigates to a URL.
|
||||
func LinkButtonStyle(url URL) ButtonComponentStyle { return linkButtonStyle(url) }
|
||||
|
||||
// Button is a clickable button that may be added to an interaction
|
||||
// response.
|
||||
type ButtonComponent struct {
|
||||
// Style is one of the button styles.
|
||||
Style ButtonComponentStyle `json:"style"`
|
||||
// CustomID attached to InteractionCreate event when clicked.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Label is the text that appears on the button. It can have maximum 100
|
||||
// characters.
|
||||
Label string `json:"label,omitempty"`
|
||||
// Emoji should have Name, ID and Animated filled.
|
||||
Emoji *ComponentEmoji `json:"emoji,omitempty"`
|
||||
// Disabled determines whether the button is disabled.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// TextButtonComponent creates a new button with the given label used for the label and
|
||||
// the custom ID.
|
||||
func TextButtonComponent(style ButtonComponentStyle, label string) ButtonComponent {
|
||||
return ButtonComponent{
|
||||
Style: style,
|
||||
Label: label,
|
||||
CustomID: ComponentID(label),
|
||||
}
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (b *ButtonComponent) ID() ComponentID { return b.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (b *ButtonComponent) Type() ComponentType {
|
||||
return ButtonComponentType
|
||||
}
|
||||
|
||||
func (b *ButtonComponent) _cmp() {}
|
||||
func (b *ButtonComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the button in the format Discord expects.
|
||||
func (b *ButtonComponent) MarshalJSON() ([]byte, error) {
|
||||
if b.Style == nil {
|
||||
b.Style = PrimaryButtonStyle() // Sane default for button.
|
||||
}
|
||||
|
||||
type button ButtonComponent
|
||||
|
||||
type Msg struct {
|
||||
*button
|
||||
Type ComponentType `json:"type"`
|
||||
Style int `json:"style"`
|
||||
URL URL `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: ButtonComponentType,
|
||||
Style: b.Style.style(),
|
||||
button: (*button)(b),
|
||||
}
|
||||
|
||||
if link, ok := b.Style.(linkButtonStyle); ok {
|
||||
msg.URL = URL(link)
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a component JSON into the button. It does NOT do
|
||||
// type-checking; use ParseComponent for that.
|
||||
func (b *ButtonComponent) UnmarshalJSON(j []byte) error {
|
||||
type button ButtonComponent
|
||||
|
||||
msg := struct {
|
||||
*button
|
||||
Style basicButtonStyle `json:"style"`
|
||||
URL URL `json:"url,omitempty"`
|
||||
}{
|
||||
button: (*button)(b),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(j, &msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if 0 > msg.Style || msg.Style >= basicButtonStyleLen {
|
||||
return fmt.Errorf("unknown button style %d", msg.Style)
|
||||
}
|
||||
|
||||
switch msg.Style {
|
||||
case linkButtonStyleNum:
|
||||
b.Style = LinkButtonStyle(msg.URL)
|
||||
default:
|
||||
b.Style = msg.Style
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StringSelectComponent is a dropdown menu that may be added to an interaction
|
||||
// response.
|
||||
type StringSelectComponent struct {
|
||||
// Options are the choices in the select.
|
||||
Options []SelectOption `json:"options"`
|
||||
// CustomID is the custom unique ID.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||
// 100 characters.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
// ValueLimits is the minimum and maximum number of items that can be
|
||||
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||
ValueLimits [2]int `json:"-"`
|
||||
// Disabled disables the select if true.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// SelectOption is an option in the select component.
|
||||
type SelectOption struct {
|
||||
// Label is the user-facing name of the option. Max 100 characters.
|
||||
Label string `json:"label"`
|
||||
// Value is the internal value that is echoed back to the program. It's
|
||||
// similar to the custom ID. Max 100 characters.
|
||||
Value string `json:"value"`
|
||||
// Description is the additional description of an option. Max 100 characters.
|
||||
Description string `json:"description,omitempty"`
|
||||
// Emoji is the optional emoji object.
|
||||
Emoji *ComponentEmoji `json:"emoji,omitempty"`
|
||||
// Default will render this option as selected by default if true.
|
||||
Default bool `json:"default,omitempty"`
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (s *StringSelectComponent) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (s *StringSelectComponent) Type() ComponentType {
|
||||
return StringSelectComponentType
|
||||
}
|
||||
|
||||
func (s *StringSelectComponent) _cmp() {}
|
||||
func (s *StringSelectComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the select in the format Discord expects.
|
||||
func (s *StringSelectComponent) MarshalJSON() ([]byte, error) {
|
||||
type sel StringSelectComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*sel
|
||||
MinValues *int `json:"min_values,omitempty"`
|
||||
MaxValues *int `json:"max_values,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: StringSelectComponentType,
|
||||
sel: (*sel)(s),
|
||||
}
|
||||
|
||||
if s.ValueLimits != [2]int{0, 0} {
|
||||
msg.MinValues = new(int)
|
||||
msg.MaxValues = new(int)
|
||||
|
||||
*msg.MinValues = s.ValueLimits[0]
|
||||
*msg.MaxValues = s.ValueLimits[1]
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
type TextInputStyle uint8
|
||||
|
||||
const (
|
||||
_ TextInputStyle = iota
|
||||
TextInputShortStyle
|
||||
TextInputParagraphStyle
|
||||
)
|
||||
|
||||
// TextInputComponents provide a user-facing text box to be filled out. They can only
|
||||
// be used with modals.
|
||||
type TextInputComponent struct {
|
||||
// CustomID provides a developer-defined ID for the input (max 100 chars)
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
// Style determines if the component should use the short or paragraph style
|
||||
Style TextInputStyle `json:"style"`
|
||||
// Label is the title of this component, describing its use
|
||||
Label string `json:"label"`
|
||||
// LengthLimits is the minimum and maximum length for the input
|
||||
LengthLimits [2]int `json:"-"`
|
||||
// Required dictates whether or not the user must fill out the component
|
||||
Required bool `json:"required"`
|
||||
// Value is the pre-filled value of this component (max 4000 chars)
|
||||
Value string `json:"value,omitempty"`
|
||||
// Placeholder is the text that appears when the input is empty (max 100 chars)
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
func (s *TextInputComponent) _cmp() {}
|
||||
func (s *TextInputComponent) _icp() {}
|
||||
|
||||
func (i *TextInputComponent) ID() ComponentID {
|
||||
return i.CustomID
|
||||
}
|
||||
|
||||
func (i *TextInputComponent) Type() ComponentType {
|
||||
return TextInputComponentType
|
||||
}
|
||||
|
||||
func (i *TextInputComponent) MarshalJSON() ([]byte, error) {
|
||||
type text TextInputComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*text
|
||||
MinLength *int `json:"min_length,omitempty"`
|
||||
MaxLength *int `json:"max_length,omitempty"`
|
||||
}
|
||||
|
||||
m := Msg{
|
||||
Type: i.Type(),
|
||||
text: (*text)(i),
|
||||
}
|
||||
|
||||
if i.LengthLimits != [2]int{0, 0} {
|
||||
m.MinLength = new(int)
|
||||
m.MaxLength = new(int)
|
||||
|
||||
*m.MinLength = i.LengthLimits[0]
|
||||
*m.MaxLength = i.LengthLimits[1]
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
type UserSelectComponent struct {
|
||||
// CustomID is the custom unique ID.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||
// 100 characters.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
// ValueLimits is the minimum and maximum number of items that can be
|
||||
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||
ValueLimits [2]int `json:"-"`
|
||||
// Disabled disables the select if true.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (s *UserSelectComponent) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (s *UserSelectComponent) Type() ComponentType {
|
||||
return UserSelectComponentType
|
||||
}
|
||||
|
||||
func (s *UserSelectComponent) _cmp() {}
|
||||
func (s *UserSelectComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the select in the format Discord expects.
|
||||
func (s *UserSelectComponent) MarshalJSON() ([]byte, error) {
|
||||
type sel UserSelectComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*sel
|
||||
MinValues *int `json:"min_values,omitempty"`
|
||||
MaxValues *int `json:"max_values,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: UserSelectComponentType,
|
||||
sel: (*sel)(s),
|
||||
}
|
||||
|
||||
if s.ValueLimits != [2]int{0, 0} {
|
||||
msg.MinValues = new(int)
|
||||
msg.MaxValues = new(int)
|
||||
|
||||
*msg.MinValues = s.ValueLimits[0]
|
||||
*msg.MaxValues = s.ValueLimits[1]
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
type RoleSelectComponent struct {
|
||||
// CustomID is the custom unique ID.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||
// 100 characters.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
// ValueLimits is the minimum and maximum number of items that can be
|
||||
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||
ValueLimits [2]int `json:"-"`
|
||||
// Disabled disables the select if true.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (s *RoleSelectComponent) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (s *RoleSelectComponent) Type() ComponentType {
|
||||
return RoleSelectComponentType
|
||||
}
|
||||
|
||||
func (s *RoleSelectComponent) _cmp() {}
|
||||
func (s *RoleSelectComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the select in the format Discord expects.
|
||||
func (s *RoleSelectComponent) MarshalJSON() ([]byte, error) {
|
||||
type sel RoleSelectComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*sel
|
||||
MinValues *int `json:"min_values,omitempty"`
|
||||
MaxValues *int `json:"max_values,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: RoleSelectComponentType,
|
||||
sel: (*sel)(s),
|
||||
}
|
||||
|
||||
if s.ValueLimits != [2]int{0, 0} {
|
||||
msg.MinValues = new(int)
|
||||
msg.MaxValues = new(int)
|
||||
|
||||
*msg.MinValues = s.ValueLimits[0]
|
||||
*msg.MaxValues = s.ValueLimits[1]
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
type MentionableSelectComponent struct {
|
||||
// CustomID is the custom unique ID.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||
// 100 characters.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
// ValueLimits is the minimum and maximum number of items that can be
|
||||
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||
ValueLimits [2]int `json:"-"`
|
||||
// Disabled disables the select if true.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (s *MentionableSelectComponent) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (s *MentionableSelectComponent) Type() ComponentType {
|
||||
return MentionableSelectComponentType
|
||||
}
|
||||
|
||||
func (s *MentionableSelectComponent) _cmp() {}
|
||||
func (s *MentionableSelectComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the select in the format Discord expects.
|
||||
func (s *MentionableSelectComponent) MarshalJSON() ([]byte, error) {
|
||||
type sel MentionableSelectComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*sel
|
||||
MinValues *int `json:"min_values,omitempty"`
|
||||
MaxValues *int `json:"max_values,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: MentionableSelectComponentType,
|
||||
sel: (*sel)(s),
|
||||
}
|
||||
|
||||
if s.ValueLimits != [2]int{0, 0} {
|
||||
msg.MinValues = new(int)
|
||||
msg.MaxValues = new(int)
|
||||
|
||||
*msg.MinValues = s.ValueLimits[0]
|
||||
*msg.MaxValues = s.ValueLimits[1]
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
type ChannelSelectComponent struct {
|
||||
// CustomID is the custom unique ID.
|
||||
CustomID ComponentID `json:"custom_id,omitempty"`
|
||||
// Placeholder is the custom placeholder text if nothing is selected. Max
|
||||
// 100 characters.
|
||||
Placeholder string `json:"placeholder,omitempty"`
|
||||
// ValueLimits is the minimum and maximum number of items that can be
|
||||
// chosen. The default is [1, 1] if ValueLimits is a zero-value.
|
||||
ValueLimits [2]int `json:"-"`
|
||||
// Disabled disables the select if true.
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
// ChannelTypes is the types of channels that can be chosen from.
|
||||
ChannelTypes []ChannelType `json:"channel_types,omitempty"`
|
||||
}
|
||||
|
||||
// ID implements the Component interface.
|
||||
func (s *ChannelSelectComponent) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements the Component interface.
|
||||
func (s *ChannelSelectComponent) Type() ComponentType {
|
||||
return ChannelSelectComponentType
|
||||
}
|
||||
|
||||
func (s *ChannelSelectComponent) _cmp() {}
|
||||
func (s *ChannelSelectComponent) _icp() {}
|
||||
|
||||
// MarshalJSON marshals the select in the format Discord expects.
|
||||
func (s *ChannelSelectComponent) MarshalJSON() ([]byte, error) {
|
||||
type sel ChannelSelectComponent
|
||||
|
||||
type Msg struct {
|
||||
Type ComponentType `json:"type"`
|
||||
*sel
|
||||
MinValues *int `json:"min_values,omitempty"`
|
||||
MaxValues *int `json:"max_values,omitempty"`
|
||||
}
|
||||
|
||||
msg := Msg{
|
||||
Type: ChannelSelectComponentType,
|
||||
sel: (*sel)(s),
|
||||
}
|
||||
|
||||
if s.ValueLimits != [2]int{0, 0} {
|
||||
msg.MinValues = new(int)
|
||||
msg.MaxValues = new(int)
|
||||
|
||||
*msg.MinValues = s.ValueLimits[0]
|
||||
*msg.MaxValues = s.ValueLimits[1]
|
||||
}
|
||||
|
||||
return json.Marshal(msg)
|
||||
}
|
||||
|
||||
// Unknown is reserved for components with unknown or not yet implemented
|
||||
// components types. It can also be used in place of a ComponentInteraction.
|
||||
type UnknownComponent struct {
|
||||
json.Raw
|
||||
id ComponentID
|
||||
typ ComponentType
|
||||
}
|
||||
|
||||
// ID implements the Component and ComponentInteraction interfaces.
|
||||
func (u *UnknownComponent) ID() ComponentID { return u.id }
|
||||
|
||||
// Type implements the Component and ComponentInteraction interfaces.
|
||||
func (u *UnknownComponent) Type() ComponentType { return u.typ }
|
||||
|
||||
// Type implements InteractionData.
|
||||
func (u *UnknownComponent) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (u *UnknownComponent) resp() {}
|
||||
func (u *UnknownComponent) data() {}
|
||||
func (u *UnknownComponent) _cmp() {}
|
||||
func (u *UnknownComponent) _icp() {}
|
|
@ -1,83 +0,0 @@
|
|||
package discord_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
)
|
||||
|
||||
func ExampleContainerComponents_Unmarshal() {
|
||||
components := &discord.ContainerComponents{
|
||||
&discord.ActionRowComponent{
|
||||
&discord.TextInputComponent{
|
||||
CustomID: "text1",
|
||||
Value: "hello",
|
||||
},
|
||||
},
|
||||
&discord.ActionRowComponent{
|
||||
&discord.TextInputComponent{
|
||||
CustomID: "text2",
|
||||
Value: "hello 2",
|
||||
},
|
||||
&discord.TextInputComponent{
|
||||
CustomID: "text3",
|
||||
Value: "hello 3",
|
||||
},
|
||||
},
|
||||
&discord.ActionRowComponent{
|
||||
&discord.StringSelectComponent{
|
||||
CustomID: "select1",
|
||||
Options: []discord.SelectOption{
|
||||
{Value: "option 1"},
|
||||
{Value: "option 2"},
|
||||
},
|
||||
},
|
||||
&discord.ButtonComponent{
|
||||
CustomID: "button1",
|
||||
},
|
||||
},
|
||||
&discord.ActionRowComponent{
|
||||
&discord.StringSelectComponent{
|
||||
CustomID: "select2",
|
||||
Options: []discord.SelectOption{
|
||||
{Value: "option 1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Text1 string `discord:"text1"`
|
||||
Text2 string `discord:"text2?"`
|
||||
Text3 *string `discord:"text3"`
|
||||
Text4 string `discord:"text4?"`
|
||||
Text5 *string `discord:"text5"`
|
||||
Select1 []string `discord:"select1"`
|
||||
Select2 string `discord:"select2"`
|
||||
Button1 bool `discord:"button1"`
|
||||
}
|
||||
|
||||
if err := components.Unmarshal(&data); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
b, _ := json.MarshalIndent(data, "", " ")
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// {
|
||||
// "Text1": "hello",
|
||||
// "Text2": "hello 2",
|
||||
// "Text3": "hello 3",
|
||||
// "Text4": "",
|
||||
// "Text5": null,
|
||||
// "Select1": [
|
||||
// "option 1",
|
||||
// "option 2"
|
||||
// ],
|
||||
// "Select2": "option 1",
|
||||
// "Button1": true
|
||||
// }
|
||||
}
|
|
@ -3,29 +3,9 @@
|
|||
// structures.
|
||||
package discord
|
||||
|
||||
import "fmt"
|
||||
|
||||
// HasFlag is returns true if has is in the flag. In other words, it checks if
|
||||
// has is OR'ed into flag. This function could be used for different constants
|
||||
// has is OR'd into flag. This function could be used for different constants
|
||||
// such as Permission.
|
||||
func HasFlag(flag, has uint64) bool {
|
||||
return flag&has == has
|
||||
}
|
||||
|
||||
// OverboundError is an error that's returned if any value is too long.
|
||||
type OverboundError struct {
|
||||
Count int
|
||||
Max int
|
||||
|
||||
Thing string
|
||||
}
|
||||
|
||||
var _ error = (*OverboundError)(nil)
|
||||
|
||||
func (e *OverboundError) Error() string {
|
||||
if e.Thing == "" {
|
||||
return fmt.Sprintf("Overbound error: %d > %d", e.Count, e.Max)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(e.Thing+" overbound: %d > %d", e.Count, e.Max)
|
||||
}
|
||||
|
|
|
@ -1,62 +1,19 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
// https://discord.com/developers/docs/resources/emoji#emoji-object
|
||||
type Emoji struct {
|
||||
// ID is the ID of the Emoji.
|
||||
// The ID will be NullSnowflake, if the Emoji is a Unicode emoji.
|
||||
ID EmojiID `json:"id"`
|
||||
// Name is the name of the emoji.
|
||||
Name string `json:"name"`
|
||||
ID EmojiID `json:"id,string"` // NullSnowflake for unicode emojis
|
||||
Name string `json:"name"`
|
||||
|
||||
// These fields are optional
|
||||
|
||||
// RoleIDs are the roles the emoji is whitelisted to.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
RoleIDs []RoleID `json:"roles,omitempty"`
|
||||
// User is the user that created the emoji.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
User User `json:"user,omitempty"`
|
||||
User User `json:"user,omitempty"`
|
||||
|
||||
// RequireColons specifies whether the emoji must be wrapped in colons.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
RequireColons bool `json:"require_colons,omitempty"`
|
||||
// Managed specifies whether the emoji is managed.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
Managed bool `json:"managed,omitempty"`
|
||||
// Animated specifies whether the emoji is animated.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
// Available specifies whether the emoji can be used.
|
||||
// This may be false due to loss of Server Boosts.
|
||||
//
|
||||
// This field is only available for custom emojis.
|
||||
Available bool `json:"available,omitempty"`
|
||||
}
|
||||
|
||||
// IsCustom returns whether the emoji is a custom emoji.
|
||||
func (e Emoji) IsCustom() bool {
|
||||
return e.ID.IsValid()
|
||||
}
|
||||
|
||||
// IsUnicode returns whether the emoji is a unicode emoji.
|
||||
func (e Emoji) IsUnicode() bool {
|
||||
return !e.IsCustom()
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the emoji was created.
|
||||
//
|
||||
// This will only work for custom emojis.
|
||||
func (e Emoji) CreatedAt() time.Time {
|
||||
return e.ID.Time()
|
||||
Managed bool `json:"managed,omitempty"`
|
||||
Animated bool `json:"animated,omitempty"`
|
||||
}
|
||||
|
||||
// EmojiURL returns the URL of the emoji and auto-detects a suitable type.
|
||||
|
@ -76,7 +33,7 @@ func (e Emoji) EmojiURL() string {
|
|||
//
|
||||
// Supported ImageTypes: PNG, GIF
|
||||
func (e Emoji) EmojiURLWithType(t ImageType) string {
|
||||
if e.IsUnicode() {
|
||||
if e.ID.IsNull() {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
@ -87,46 +44,18 @@ func (e Emoji) EmojiURLWithType(t ImageType) string {
|
|||
return "https://cdn.discordapp.com/emojis/" + t.format(e.ID.String())
|
||||
}
|
||||
|
||||
// APIEmoji represents an emoji identifier string formatted to be used with the
|
||||
// API. It is formatted using Emoji's APIString method as well as the
|
||||
// NewCustomEmoji function. If the emoji is a stock Unicode emoji, then this
|
||||
// string contains it. Otherwise, it is formatted like "emoji_name:123123123",
|
||||
// where "123123123" is the emoji ID.
|
||||
type APIEmoji string
|
||||
|
||||
// NewAPIEmoji creates a new APIEmoji string from the given emoji ID and name.
|
||||
func NewAPIEmoji(id EmojiID, name string) APIEmoji {
|
||||
if !id.IsValid() {
|
||||
return APIEmoji(name)
|
||||
}
|
||||
return APIEmoji(name + ":" + id.String())
|
||||
}
|
||||
|
||||
// NewCustomEmoji creates a new Emoji using a custom guild emoji as base.
|
||||
// Unicode emojis should be directly converted.
|
||||
//
|
||||
// Deprecated: Use NewAPIEmoji, it does the same exact thing.
|
||||
func NewCustomEmoji(id EmojiID, name string) APIEmoji {
|
||||
return NewAPIEmoji(id, name)
|
||||
}
|
||||
|
||||
// PathString returns the APIEmoji as a path-encoded string.
|
||||
func (e APIEmoji) PathString() string {
|
||||
return url.PathEscape(string(e))
|
||||
}
|
||||
|
||||
// APIString returns a string usable for sending over to the API.
|
||||
func (e Emoji) APIString() APIEmoji {
|
||||
if e.IsUnicode() {
|
||||
return APIEmoji(e.Name)
|
||||
func (e Emoji) APIString() string {
|
||||
if !e.ID.IsValid() {
|
||||
return e.Name // is unicode
|
||||
}
|
||||
|
||||
return NewCustomEmoji(e.ID, e.Name)
|
||||
return e.Name + ":" + e.ID.String()
|
||||
}
|
||||
|
||||
// String formats the string like how the client does.
|
||||
func (e Emoji) String() string {
|
||||
if !e.ID.IsValid() {
|
||||
if e.ID == 0 {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
|
|
360
discord/guild.go
360
discord/guild.go
|
@ -1,11 +1,9 @@
|
|||
package discord
|
||||
|
||||
import "time"
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-object
|
||||
type Guild struct {
|
||||
// ID is the guild id.
|
||||
ID GuildID `json:"id"`
|
||||
ID GuildID `json:"id,string"`
|
||||
// Name is the guild name (2-100 characters, excluding trailing and leading
|
||||
// whitespace).
|
||||
Name string `json:"name"`
|
||||
|
@ -20,54 +18,64 @@ type Guild struct {
|
|||
|
||||
// Owner is true if the user is the owner of the guild.
|
||||
Owner bool `json:"owner,omitempty"`
|
||||
// Widget is true if the server widget is enabled.
|
||||
Widget bool `json:"widget_enabled,omitempty"`
|
||||
// OwnerID is the id of owner.
|
||||
OwnerID UserID `json:"owner_id,string"`
|
||||
|
||||
// Permissions are the total permissions for the user in the guild
|
||||
// (excludes overrides).
|
||||
Permissions Permissions `json:"permissions_new,omitempty,string"`
|
||||
|
||||
// VoiceRegion is the voice region id for the guild.
|
||||
VoiceRegion string `json:"region"`
|
||||
|
||||
// AFKChannelID is the id of the afk channel.
|
||||
AFKChannelID ChannelID `json:"afk_channel_id,string,omitempty"`
|
||||
// AFKTimeout is the afk timeout in seconds.
|
||||
AFKTimeout Seconds `json:"afk_timeout"`
|
||||
|
||||
// Embeddable is true if the server widget is enabled.
|
||||
//
|
||||
// Deprecated: replaced with WidgetEnabled
|
||||
Embeddable bool `json:"embed_enabled,omitempty"`
|
||||
// EmbedChannelID is the channel id that the widget will generate an invite
|
||||
// to, or null if set to no invite .
|
||||
//
|
||||
// Deprecated: replaced with WidgetChannelID
|
||||
EmbedChannelID ChannelID `json:"embed_channel_id,string,omitempty"`
|
||||
|
||||
// SystemChannelFlags are the system channel flags.
|
||||
SystemChannelFlags SystemChannelFlags `json:"system_channel_flags"`
|
||||
// Verification is the verification level required for the guild.
|
||||
Verification Verification `json:"verification_level"`
|
||||
// Notification is the default message notifications level.
|
||||
Notification Notification `json:"default_message_notifications"`
|
||||
// ExplicitFilter is the explicit content filter level.
|
||||
ExplicitFilter ExplicitFilter `json:"explicit_content_filter"`
|
||||
// NitroBoost is the premium tier (Server Boost level).
|
||||
NitroBoost NitroBoost `json:"premium_tier"`
|
||||
// MFA is the required MFA level for the guild.
|
||||
MFA MFALevel `json:"mfa_level"`
|
||||
|
||||
// OwnerID is the id of owner.
|
||||
OwnerID UserID `json:"owner_id"`
|
||||
// WidgetChannelID is the channel id that the widget will generate an
|
||||
// invite to, or null if set to no invite.
|
||||
WidgetChannelID ChannelID `json:"widget_channel_id,omitempty"`
|
||||
// SystemChannelID is the the id of the channel where guild notices such as
|
||||
// welcome messages and boost events are posted.
|
||||
SystemChannelID ChannelID `json:"system_channel_id,omitempty"`
|
||||
|
||||
// Permissions are the total permissions for the user in the guild
|
||||
// (excludes overrides).
|
||||
Permissions Permissions `json:"permissions,string,omitempty"`
|
||||
|
||||
// VoiceRegion is the voice region id for the guild.
|
||||
VoiceRegion string `json:"region"`
|
||||
|
||||
// AFKChannelID is the id of the afk channel.
|
||||
AFKChannelID ChannelID `json:"afk_channel_id,omitempty"`
|
||||
// AFKTimeout is the afk timeout in seconds.
|
||||
AFKTimeout Seconds `json:"afk_timeout"`
|
||||
|
||||
// Roles are the roles in the guild.
|
||||
Roles []Role `json:"roles"`
|
||||
// Emojis are the custom guild emojis.
|
||||
Emojis []Emoji `json:"emojis"`
|
||||
// Features are the enabled guild features.
|
||||
Features []GuildFeature `json:"features"`
|
||||
Features []GuildFeature `json:"guild_features"`
|
||||
|
||||
// MFA is the required MFA level for the guild.
|
||||
MFA MFALevel `json:"mfa"`
|
||||
|
||||
// AppID is the application id of the guild creator if it is bot-created.
|
||||
//
|
||||
// This field is nullable.
|
||||
AppID AppID `json:"application_id,omitempty"`
|
||||
AppID AppID `json:"application_id,string,omitempty"`
|
||||
|
||||
// Widget is true if the server widget is enabled.
|
||||
Widget bool `json:"widget_enabled,omitempty"`
|
||||
// WidgetChannelID is the channel id that the widget will generate an
|
||||
// invite to, or null if set to no invite.
|
||||
WidgetChannelID ChannelID `json:"widget_channel_id,string,omitempty"`
|
||||
|
||||
// SystemChannelID is the the id of the channel where guild notices such as
|
||||
// welcome messages and boost events are posted.
|
||||
SystemChannelID ChannelID `json:"system_channel_id,string,omitempty"`
|
||||
// SystemChannelFlags are the system channel flags.
|
||||
SystemChannelFlags SystemChannelFlags `json:"system_channel_flags"`
|
||||
|
||||
// RulesChannelID is the id of the channel where guilds with the "PUBLIC"
|
||||
// feature can display rules and/or guidelines.
|
||||
|
@ -89,6 +97,8 @@ type Guild struct {
|
|||
// Banner is the banner hash.
|
||||
Banner Hash `json:"banner,omitempty"`
|
||||
|
||||
// NitroBoost is the premium tier (Server Boost level).
|
||||
NitroBoost NitroBoost `json:"premium_tier"`
|
||||
// NitroBoosters is the number of boosts this guild currently has.
|
||||
NitroBoosters uint64 `json:"premium_subscription_count,omitempty"`
|
||||
|
||||
|
@ -105,19 +115,11 @@ type Guild struct {
|
|||
// MaxVideoChannelUsers is the maximum amount of users in a video channel.
|
||||
MaxVideoChannelUsers uint64 `json:"max_video_channel_users,omitempty"`
|
||||
|
||||
// ApproximateMembers is the approximate number of members in this guild,
|
||||
// returned by the GuildWithCount method.
|
||||
// ApproximateMembers is the approximate number of members in this guild, returned from the GET /guild/<id> endpoint when with_counts is true
|
||||
ApproximateMembers uint64 `json:"approximate_member_count,omitempty"`
|
||||
// ApproximatePresences is the approximate number of non-offline members in
|
||||
// this guild, returned by the GuildWithCount method.
|
||||
ApproximatePresences uint64 `json:"approximate_presence_count,omitempty"`
|
||||
// NSFWLevel is the level of NSFW of the guild.
|
||||
NSFWLevel NSFWLevel `json:"nsfw_level"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the guild was created.
|
||||
func (g Guild) CreatedAt() time.Time {
|
||||
return g.ID.Time()
|
||||
}
|
||||
|
||||
// IconURL returns the URL to the guild icon and auto detects a suitable type.
|
||||
|
@ -195,16 +197,6 @@ func (g Guild) DiscoverySplashURLWithType(t ImageType) string {
|
|||
g.ID.String() + "/" + t.format(g.DiscoverySplash)
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level
|
||||
type NSFWLevel uint8
|
||||
|
||||
const (
|
||||
NSFWLevelDefault NSFWLevel = iota
|
||||
NSFWLevelExplicit
|
||||
NSFWLevelSafe
|
||||
NSFWLevelAgeRestricted
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-preview-object
|
||||
type GuildPreview struct {
|
||||
// ID is the guild id.
|
||||
|
@ -234,12 +226,6 @@ type GuildPreview struct {
|
|||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the guild the preview
|
||||
// represents was created.
|
||||
func (g GuildPreview) CreatedAt() time.Time {
|
||||
return g.ID.Time()
|
||||
}
|
||||
|
||||
// IconURL returns the URL to the guild icon and auto detects a suitable type.
|
||||
// An empty string is returned if there's no icon.
|
||||
func (g GuildPreview) IconURL() string {
|
||||
|
@ -299,45 +285,24 @@ func (g GuildPreview) DiscoverySplashURLWithType(t ImageType) string {
|
|||
// https://discord.com/developers/docs/topics/permissions#role-object
|
||||
type Role struct {
|
||||
// ID is the role id.
|
||||
ID RoleID `json:"id"`
|
||||
ID RoleID `json:"id,string"`
|
||||
// Name is the role name.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Permissions is the permission bit set.
|
||||
Permissions Permissions `json:"permissions,string"`
|
||||
|
||||
// Position is the position of this role.
|
||||
Position int `json:"position"`
|
||||
// Color is the integer representation of hexadecimal color code.
|
||||
Color Color `json:"color"`
|
||||
|
||||
// Hoist specifies if this role is pinned in the user listing.
|
||||
Hoist bool `json:"hoist"`
|
||||
// Position is the position of this role.
|
||||
Position int `json:"position"`
|
||||
|
||||
// Permissions is the permission bit set.
|
||||
Permissions Permissions `json:"permissions_new,string"`
|
||||
|
||||
// Manages specifies whether this role is managed by an integration.
|
||||
Managed bool `json:"managed"`
|
||||
// Mentionable specifies whether this role is mentionable.
|
||||
Mentionable bool `json:"mentionable"`
|
||||
|
||||
// Icon is the icon hash of this role.
|
||||
Icon Hash `json:"icon,omitempty"`
|
||||
// UnicodeEmoji is the unicode emoji of this role.
|
||||
UnicodeEmoji string `json:"unicode_emoji,omitempty"`
|
||||
// Tags are the RoleTags of this role.
|
||||
Tags RoleTags `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type RoleTags struct {
|
||||
// BotID is the id of the bot this role belongs to.
|
||||
BotID UserID `json:"bot_id,omitempty"`
|
||||
// IntegrationID is the id of the integration this role belongs to.
|
||||
IntegrationID IntegrationID `json:"integration_id,omitempty"`
|
||||
// PremiumSubscriber specifies whether this is the guild's premium subscriber role.
|
||||
PremiumSubscriber bool `json:"premium_subscriber,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the role was created.
|
||||
func (r Role) CreatedAt() time.Time {
|
||||
return r.ID.Time()
|
||||
}
|
||||
|
||||
// Mention returns the mention of the Role.
|
||||
|
@ -345,22 +310,44 @@ func (r Role) Mention() string {
|
|||
return r.ID.Mention()
|
||||
}
|
||||
|
||||
// IconURL returns the URL to the role icon png.
|
||||
// An empty string is returned if there's no icon.
|
||||
func (r Role) IconURL() string {
|
||||
return r.IconURLWithType(PNGImage)
|
||||
}
|
||||
// https://discord.com/developers/docs/topics/gateway#presence-update
|
||||
type Presence struct {
|
||||
// User is the user presence is being updated for.
|
||||
User User `json:"user"`
|
||||
// RoleIDs are the roles this user is in.
|
||||
RoleIDs []RoleID `json:"roles"`
|
||||
|
||||
// IconURLWithType returns the URL to the role icon using the passed
|
||||
// ImageType. An empty string is returned if there's no icon.
|
||||
//
|
||||
// Supported ImageTypes: PNG, JPEG, WebP
|
||||
func (r Role) IconURLWithType(t ImageType) string {
|
||||
if r.Icon == "" {
|
||||
return ""
|
||||
}
|
||||
// These fields are only filled in gateway events, according to the
|
||||
// documentation.
|
||||
|
||||
return "https://cdn.discordapp.com/role-icons/" + r.ID.String() + "/" + t.format(r.Icon)
|
||||
// Game is null, or the user's current activity.
|
||||
Game *Activity `json:"game"`
|
||||
|
||||
// GuildID is the id of the guild
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
|
||||
// Status is either "idle", "dnd", "online", or "offline".
|
||||
Status Status `json:"status"`
|
||||
// Activities are the user's current activities.
|
||||
Activities []Activity `json:"activities"`
|
||||
// ClientStaus is the user's platform-dependent status.
|
||||
//
|
||||
// https://discord.com/developers/docs/topics/gateway#client-status-object
|
||||
ClientStatus struct {
|
||||
// Desktop is the user's status set for an active desktop (Windows,
|
||||
// Linux, Mac) application session.
|
||||
Desktop Status `json:"desktop,omitempty"`
|
||||
// Mobile is the user's status set for an active mobile (iOS, Android)
|
||||
// application session.
|
||||
Mobile Status `json:"mobile,omitempty"`
|
||||
// Web is the user's status set for an active web (browser, bot
|
||||
// account) application session.
|
||||
Web Status `json:"web,omitempty"`
|
||||
} `json:"client_status"`
|
||||
// Premium since specifies when the user started boosting the guild.
|
||||
PremiumSince Timestamp `json:"premium_since,omitempty"`
|
||||
// Nick is this users guild nickname (if one is set).
|
||||
Nick string `json:"nick,omitempty"`
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-member-object
|
||||
|
@ -374,60 +361,23 @@ type Member struct {
|
|||
Nick string `json:"nick,omitempty"`
|
||||
// RoleIDs is an array of role object ids.
|
||||
RoleIDs []RoleID `json:"roles"`
|
||||
// Avatar is this member's guild avatar.
|
||||
Avatar Hash `json:"avatar,omitempty"`
|
||||
|
||||
// Joined specifies when the user joined the guild.
|
||||
Joined Timestamp `json:"joined_at"`
|
||||
// BoostedSince specifies when the user started boosting the guild.
|
||||
BoostedSince Timestamp `json:"premium_since,omitempty"`
|
||||
// CommunicationDisabledUntil specifies when the user's timeout will expire.
|
||||
CommunicationDisabledUntil Timestamp `json:"communication_disabled_until"`
|
||||
|
||||
// Deaf specifies whether the user is deafened in voice channels.
|
||||
Deaf bool `json:"deaf"`
|
||||
// Mute specifies whether the user is muted in voice channels.
|
||||
Mute bool `json:"mute"`
|
||||
|
||||
// Flags is the member's flags represented as a bit set, defaults to 0.
|
||||
Flags MemberFlags `json:"flags"`
|
||||
|
||||
// IsPending specifies whether the user has not yet passed the guild's Membership Screening requirements
|
||||
IsPending bool `json:"pending"`
|
||||
}
|
||||
|
||||
// Mention returns the mention of the role.
|
||||
func (m Member) Mention() string {
|
||||
return "<@!" + m.User.ID.String() + ">"
|
||||
return m.User.Mention()
|
||||
}
|
||||
|
||||
// AvatarURL returns the URL of the Avatar Image. It automatically detects a
|
||||
// suitable type.
|
||||
func (m Member) AvatarURL(guildID GuildID) string {
|
||||
return m.AvatarURLWithType(AutoImage, guildID)
|
||||
}
|
||||
|
||||
// AvatarURLWithType returns the URL of the Avatar Image using the passed type.
|
||||
// If the member has no Avatar, an empty string will be returned.
|
||||
//
|
||||
// Supported Image Types: PNG, JPEG, WebP, GIF
|
||||
func (m Member) AvatarURLWithType(t ImageType, guildID GuildID) string {
|
||||
if m.Avatar == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return "https://cdn.discordapp.com/guilds/" + guildID.String() + "/users/" + m.User.ID.String() + "/avatars/" + t.format(m.Avatar)
|
||||
}
|
||||
|
||||
type MemberFlags uint8
|
||||
|
||||
const (
|
||||
MemberFlagsDidRejoin = 1 << iota
|
||||
MemberFlagsCompletedOnboarding
|
||||
MemberFlagsBypassesVerification
|
||||
MemberFlagsStartedOnboarding
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#ban-object
|
||||
type Ban struct {
|
||||
// Reason is the reason for the ban.
|
||||
|
@ -442,115 +392,58 @@ type Integration struct {
|
|||
ID IntegrationID `json:"id"`
|
||||
// Name is the integration name.
|
||||
Name string `json:"name"`
|
||||
// Type is the integration type (twitch, youtube, discord).
|
||||
// Type is the integration type (twitch, youtube, etc).
|
||||
Type Service `json:"type"`
|
||||
|
||||
// Enables specifies if the integration is enabled.
|
||||
Enabled bool `json:"enabled"`
|
||||
// Syncing specifies if the integration is syncing.
|
||||
// This field is not provided for bot integrations.
|
||||
Syncing bool `json:"syncing,omitempty"`
|
||||
Syncing bool `json:"syncing"`
|
||||
|
||||
// RoleID is the id that this integration uses for "subscribers".
|
||||
// This field is not provided for bot integrations.
|
||||
RoleID RoleID `json:"role_id,omitempty"`
|
||||
RoleID RoleID `json:"role_id"`
|
||||
|
||||
// EnableEmoticons specifies whether emoticons should be synced for this
|
||||
// integration (twitch only currently).
|
||||
// This field is not provided for bot integrations.
|
||||
EnableEmoticons bool `json:"enable_emoticons,omitempty"`
|
||||
|
||||
// ExpireBehavior is the behavior of expiring subscribers.
|
||||
// This field is not provided for bot integrations.
|
||||
ExpireBehavior ExpireBehavior `json:"expire_behavior,omitempty"`
|
||||
// ExpireBehavior is the behavior of expiring subscribers
|
||||
ExpireBehavior ExpireBehavior `json:"expire_behavior"`
|
||||
// ExpireGracePeriod is the grace period (in days) before expiring
|
||||
// subscribers.
|
||||
// This field is not provided for bot integrations.
|
||||
ExpireGracePeriod int `json:"expire_grace_period,omitempty"`
|
||||
ExpireGracePeriod int `json:"expire_grace_period"`
|
||||
|
||||
// User is the user for this integration.
|
||||
// This field is not provided for bot integrations.
|
||||
User User `json:"user,omitempty"`
|
||||
User User `json:"user"`
|
||||
// Account is the integration account information.
|
||||
Account IntegrationAccount `json:"account"`
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild#integration-account-object
|
||||
Account struct {
|
||||
// ID is the id of the account.
|
||||
ID string `json:"id"`
|
||||
// Name is the name of the account.
|
||||
Name string `json:"name"`
|
||||
} `json:"account"`
|
||||
|
||||
// SyncedAt specifies when this integration was last synced.
|
||||
// This field is not provided for bot integrations.
|
||||
SyncedAt Timestamp `json:"synced_at,omitempty"`
|
||||
// SubscriberCount specifies how many subscribers the integration has.
|
||||
// This field is not provided for bot integrations.
|
||||
SubscriberCount int `json:"subscriber_count,omitempty"`
|
||||
// Revoked specifies whether the integration has been revoked.
|
||||
// This field is not provided for bot integrations.
|
||||
Revoked bool `json:"revoked,omitempty"`
|
||||
// Application is the bot/OAuth2 application for integrations.
|
||||
Application *IntegrationApplication `json:"application,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the integration was created.
|
||||
func (i Integration) CreatedAt() time.Time {
|
||||
return i.ID.Time()
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#integration-account-object
|
||||
type IntegrationAccount struct {
|
||||
// ID is the id of the account.
|
||||
ID string `json:"id"`
|
||||
// Name is the name of the account.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#integration-application-object
|
||||
type IntegrationApplication struct {
|
||||
// ID is the id of the app.
|
||||
ID IntegrationID `json:"id"`
|
||||
// Name is the name of the app.
|
||||
Name string `json:"name"`
|
||||
// Icon is the icon hash of the app.
|
||||
Icon *Hash `json:"icon"`
|
||||
// Description is the description of the app.
|
||||
Description string `json:"description"`
|
||||
// Summary is a summary of the app.
|
||||
Summary string `json:"summary"`
|
||||
// Bot is the bot associated with the app.
|
||||
Bot User `json:"bot,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the integration application
|
||||
// was created.
|
||||
func (i IntegrationApplication) CreatedAt() time.Time {
|
||||
return i.ID.Time()
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#get-guild-widget-example-get-guild-widget
|
||||
type GuildWidget struct {
|
||||
// ID is the ID of the guild.
|
||||
ID GuildID `json:"id"`
|
||||
// Name is the name of the guild.
|
||||
Name string `json:"name"`
|
||||
// InviteURl is the url of an instant invite to the guild.
|
||||
InviteURL string `json:"instant_invite"`
|
||||
Channels []Channel `json:"channels"`
|
||||
Members []User `json:"members"`
|
||||
// Presence count is the amount of presences in the guild
|
||||
PresenceCount int `json:"presence_count"`
|
||||
SyncedAt Timestamp `json:"synced_at"`
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-widget-object
|
||||
type GuildWidgetSettings struct {
|
||||
type GuildWidget struct {
|
||||
// Enabled specifies whether the widget is enabled.
|
||||
Enabled bool `json:"enabled"`
|
||||
// ChannelID is the widget channel id.
|
||||
ChannelID ChannelID `json:"channel_id,omitempty"`
|
||||
}
|
||||
|
||||
// DefaultMemberColor is the color used for members without colored roles.
|
||||
var DefaultMemberColor Color = 0x0
|
||||
|
||||
// MemberColor computes the effective color of the Member, taking into account
|
||||
// the role colors.
|
||||
//
|
||||
// Deprecated: MemberColor relies on Guild, which may not have a []Role if it
|
||||
// comes from the state. Use State's MemberColor instead.
|
||||
func MemberColor(guild Guild, member Member) (Color, bool) {
|
||||
c := NullColor
|
||||
func MemberColor(guild Guild, member Member) Color {
|
||||
var c = DefaultMemberColor
|
||||
var pos int
|
||||
|
||||
for _, r := range guild.Roles {
|
||||
|
@ -566,34 +459,5 @@ func MemberColor(guild Guild, member Member) (Color, bool) {
|
|||
}
|
||||
}
|
||||
|
||||
return c, c > NullColor
|
||||
}
|
||||
|
||||
// Presence represents a partial Presence structure used by other structs to be
|
||||
// easily embedded. It does not contain any ID to identify who it belongs
|
||||
// to. For more information, refer to the PresenceUpdateEvent struct.
|
||||
type Presence struct {
|
||||
// User is the user presence is being updated for. Only the ID field is
|
||||
// guaranteed to be valid per Discord documentation.
|
||||
User User `json:"user"`
|
||||
// GuildID is the id of the guild
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
// Status is either "idle", "dnd", "online", or "offline".
|
||||
Status Status `json:"status"`
|
||||
// Activities are the user's current activities.
|
||||
Activities []Activity `json:"activities"`
|
||||
// ClientStatus is the user's platform-dependent status.
|
||||
ClientStatus ClientStatus `json:"client_status"`
|
||||
}
|
||||
|
||||
type ClientStatus struct {
|
||||
// Desktop is the user's status set for an active desktop (Windows,
|
||||
// Linux, Mac) application session.
|
||||
Desktop Status `json:"desktop,omitempty"`
|
||||
// Mobile is the user's status set for an active mobile (iOS, Android)
|
||||
// application session.
|
||||
Mobile Status `json:"mobile,omitempty"`
|
||||
// Web is the user's status set for an active web (browser, bot
|
||||
// account) application session.
|
||||
Web Status `json:"web,omitempty"`
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/enum"
|
||||
"github.com/diamondburned/arikawa/utils/json/enum"
|
||||
)
|
||||
|
||||
// Guild.MaxPresences is this value when it's 0.
|
||||
|
@ -77,18 +77,17 @@ const (
|
|||
// ExplicitFilter is the explicit content filter level of a guild.
|
||||
type ExplicitFilter enum.Enum
|
||||
|
||||
// NullExplicitFilter serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
const NullExplicitFilter ExplicitFilter = enum.Null
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level
|
||||
const (
|
||||
var (
|
||||
// NullExplicitFilter serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
NullExplicitFilter ExplicitFilter = enum.Null
|
||||
// NoContentFilter disables content filtering for the guild.
|
||||
NoContentFilter ExplicitFilter = iota
|
||||
NoContentFilter ExplicitFilter = 0
|
||||
// MembersWithoutRoles filters only members without roles.
|
||||
MembersWithoutRoles
|
||||
MembersWithoutRoles ExplicitFilter = 1
|
||||
// AllMembers enables content filtering for all members.
|
||||
AllMembers
|
||||
AllMembers ExplicitFilter = 2
|
||||
)
|
||||
|
||||
func (f *ExplicitFilter) UnmarshalJSON(b []byte) error {
|
||||
|
@ -105,16 +104,15 @@ func (f ExplicitFilter) MarshalJSON() ([]byte, error) {
|
|||
// Notification is the default message notification level of a guild.
|
||||
type Notification enum.Enum
|
||||
|
||||
// NullNotification serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
const NullNotification Notification = enum.Null
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level
|
||||
const (
|
||||
var (
|
||||
// NullNotification serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
NullNotification Notification = enum.Null
|
||||
// AllMessages sends notifications for all messages.
|
||||
AllMessages Notification = iota
|
||||
AllMessages Notification = 0
|
||||
// OnlyMentions sends notifications only on mention.
|
||||
OnlyMentions
|
||||
OnlyMentions Notification = 1
|
||||
)
|
||||
|
||||
func (n *Notification) UnmarshalJSON(b []byte) error {
|
||||
|
@ -129,25 +127,24 @@ func (n Notification) MarshalJSON() ([]byte, error) { return enum.ToJSON(enum.En
|
|||
// Verification is the verification level required for a guild.
|
||||
type Verification enum.Enum
|
||||
|
||||
// NullVerification serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
const NullVerification Verification = enum.Null
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#guild-object-verification-level
|
||||
const (
|
||||
var (
|
||||
// NullVerification serialized to JSON null.
|
||||
// This should only be used on nullable fields.
|
||||
NullVerification Verification = enum.Null
|
||||
// NoVerification required no verification.
|
||||
NoVerification Verification = iota
|
||||
NoVerification Verification = 0
|
||||
// LowVerification requires a verified email
|
||||
LowVerification
|
||||
LowVerification Verification = 1
|
||||
// MediumVerification requires the user be registered for at least 5
|
||||
// minutes.
|
||||
MediumVerification
|
||||
MediumVerification Verification = 2
|
||||
// HighVerification requires the member be in the server for more than 10
|
||||
// minutes.
|
||||
HighVerification
|
||||
HighVerification Verification = 3
|
||||
// VeryHighVerification requires the member to have a verified phone
|
||||
// number.
|
||||
VeryHighVerification
|
||||
VeryHighVerification Verification = 4
|
||||
)
|
||||
|
||||
func (v *Verification) UnmarshalJSON(b []byte) error {
|
||||
|
@ -163,30 +160,17 @@ func (v Verification) MarshalJSON() ([]byte, error) { return enum.ToJSON(enum.En
|
|||
type Service string
|
||||
|
||||
const (
|
||||
TwitchService Service = "twitch"
|
||||
YouTubeService Service = "youtube"
|
||||
DiscordService Service = "discord"
|
||||
Twitch Service = "twitch"
|
||||
YouTube Service = "youtube"
|
||||
)
|
||||
|
||||
// ExpireBehavior is the integration expire behavior that regulates what happens, if a subscriber expires.
|
||||
type ExpireBehavior uint8
|
||||
|
||||
// https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors
|
||||
const (
|
||||
var (
|
||||
// RemoveRole removes the role of the subscriber.
|
||||
RemoveRole ExpireBehavior = iota
|
||||
RemoveRole ExpireBehavior = 0
|
||||
// Kick kicks the subscriber from the guild.
|
||||
Kick
|
||||
)
|
||||
|
||||
// Status is the enumerate type for a user's status.
|
||||
type Status string
|
||||
|
||||
const (
|
||||
UnknownStatus Status = ""
|
||||
OnlineStatus Status = "online"
|
||||
DoNotDisturbStatus Status = "dnd"
|
||||
IdleStatus Status = "idle"
|
||||
InvisibleStatus Status = "invisible"
|
||||
OfflineStatus Status = "offline"
|
||||
Kick ExpireBehavior = 1
|
||||
)
|
|
@ -1,778 +0,0 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/internal/rfutil"
|
||||
"github.com/diamondburned/arikawa/v3/utils/json"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// InteractionEvent describes the full incoming interaction event. It may be a
|
||||
// gateway event or a webhook event.
|
||||
//
|
||||
// https://discord.com/developers/docs/topics/gateway#interactions
|
||||
type InteractionEvent struct {
|
||||
ID InteractionID `json:"id"`
|
||||
Data InteractionData `json:"data"`
|
||||
AppID AppID `json:"application_id"`
|
||||
ChannelID ChannelID `json:"channel_id,omitempty"`
|
||||
Token string `json:"token"`
|
||||
Version int `json:"version"`
|
||||
|
||||
// Channel is the channel that the interaction was sent from.
|
||||
Channel *Channel `json:"channel,omitempty"`
|
||||
|
||||
// Message is the message the component was attached to.
|
||||
// Only present for component interactions, not command interactions.
|
||||
Message *Message `json:"message,omitempty"`
|
||||
|
||||
// Member is only present if this came from a guild. To get a user, use the
|
||||
// Sender method.
|
||||
Member *Member `json:"member,omitempty"`
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
|
||||
// User is only present if this didn't come from a guild. To get a user, use
|
||||
// the Sender method.
|
||||
User *User `json:"user,omitempty"`
|
||||
|
||||
// Locale is the selected language of the invoking user. It is returned in
|
||||
// all interactions except ping interactions. Use this Locale field to
|
||||
// obtain the language of the user who used the interaction.
|
||||
Locale Language `json:"locale,omitempty"`
|
||||
// GuildLocale is the guild's preferred locale, if invoked in a guild.
|
||||
GuildLocale string `json:"guild_locale,omitempty"`
|
||||
}
|
||||
|
||||
// Sender returns the sender of this event from either the Member field or the
|
||||
// User field. If neither of those fields are available, then nil is returned.
|
||||
func (e *InteractionEvent) Sender() *User {
|
||||
if e.User != nil {
|
||||
return e.User
|
||||
}
|
||||
if e.Member != nil {
|
||||
return &e.Member.User
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SenderID returns the sender's ID. See Sender for more information. If Sender
|
||||
// returns nil, then 0 is returned.
|
||||
func (e *InteractionEvent) SenderID() UserID {
|
||||
if sender := e.Sender(); sender != nil {
|
||||
return sender.ID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (e *InteractionEvent) UnmarshalJSON(b []byte) error {
|
||||
type event InteractionEvent
|
||||
|
||||
target := struct {
|
||||
Type InteractionDataType `json:"type"`
|
||||
Data json.Raw `json:"data"`
|
||||
*event
|
||||
}{
|
||||
event: (*event)(e),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
switch target.Type {
|
||||
case PingInteractionType:
|
||||
e.Data = &PingInteraction{}
|
||||
return nil // Ping isn't actually an object.
|
||||
case CommandInteractionType:
|
||||
e.Data = &CommandInteraction{}
|
||||
case ComponentInteractionType:
|
||||
d, err := ParseComponentInteraction(target.Data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal component interaction event data")
|
||||
}
|
||||
e.Data = d
|
||||
return nil
|
||||
case AutocompleteInteractionType:
|
||||
e.Data = &AutocompleteInteraction{}
|
||||
case ModalInteractionType:
|
||||
e.Data = &ModalInteraction{}
|
||||
default:
|
||||
e.Data = &UnknownInteractionData{
|
||||
Raw: target.Data,
|
||||
typ: target.Type,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(target.Data, e.Data); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal interaction event data")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *InteractionEvent) MarshalJSON() ([]byte, error) {
|
||||
type event InteractionEvent
|
||||
|
||||
if e.Data == nil {
|
||||
return nil, errors.New("missing InteractionEvent.Data")
|
||||
}
|
||||
if e.Data.InteractionType() == 0 {
|
||||
return nil, errors.New("unexpected 0 InteractionEvent.Data.Type")
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Type InteractionDataType `json:"type"`
|
||||
*event
|
||||
}{
|
||||
Type: e.Data.InteractionType(),
|
||||
event: (*event)(e),
|
||||
}
|
||||
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
// InteractionDataType is the type of each Interaction, enumerated in
|
||||
// integers.
|
||||
type InteractionDataType uint
|
||||
|
||||
const (
|
||||
PingInteractionType InteractionDataType = iota + 1
|
||||
CommandInteractionType
|
||||
ComponentInteractionType
|
||||
AutocompleteInteractionType
|
||||
ModalInteractionType
|
||||
)
|
||||
|
||||
// InteractionData holds the respose data of an interaction, or more
|
||||
// specifically, the data that Discord sends to us. Type assertions should be
|
||||
// made on it to access the underlying data.
|
||||
//
|
||||
// The following types implement this interface:
|
||||
//
|
||||
// - *PingInteraction
|
||||
// - *AutocompleteInteraction
|
||||
// - *CommandInteraction
|
||||
// - *ModalInteraction
|
||||
// - *StringSelectInteraction (also ComponentInteraction)
|
||||
// - *RoleSelectInteraction (also ComponentInteraction)
|
||||
// - *UserSelectInteraction (also ComponentInteraction)
|
||||
// - *ChannelSelectInteraction (also ComponentInteraction)
|
||||
// - *MentionableSelectInteraction (also ComponentInteraction)
|
||||
// - *ButtonInteraction (also ComponentInteraction)
|
||||
//
|
||||
type InteractionData interface {
|
||||
InteractionType() InteractionDataType
|
||||
data()
|
||||
}
|
||||
|
||||
// PingInteraction is a ping Interaction response.
|
||||
type PingInteraction struct{}
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (*PingInteraction) InteractionType() InteractionDataType { return PingInteractionType }
|
||||
func (*PingInteraction) data() {}
|
||||
|
||||
// AutocompleteInteraction is an autocompletion Interaction response.
|
||||
type AutocompleteInteraction struct {
|
||||
CommandID CommandID `json:"id"`
|
||||
|
||||
// Name of command autocomplete is triggered for.
|
||||
Name string `json:"name"`
|
||||
CommandType CommandType `json:"type"`
|
||||
Version string `json:"version"`
|
||||
Options AutocompleteOptions `json:"options"`
|
||||
}
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (*AutocompleteInteraction) InteractionType() InteractionDataType {
|
||||
return AutocompleteInteractionType
|
||||
}
|
||||
func (*AutocompleteInteraction) data() {}
|
||||
|
||||
// AutocompleteOptions is a list of autocompletion options.
|
||||
// Use `Find` to get your named autocompletion option.
|
||||
type AutocompleteOptions []AutocompleteOption
|
||||
|
||||
// Find returns the named autocomplete option.
|
||||
func (o AutocompleteOptions) Find(name string) AutocompleteOption {
|
||||
for _, opt := range o {
|
||||
if strings.EqualFold(opt.Name, name) {
|
||||
return opt
|
||||
}
|
||||
}
|
||||
return AutocompleteOption{}
|
||||
}
|
||||
|
||||
// Focused returns the option that the user is currently focused on.
|
||||
func (o AutocompleteOptions) Focused() AutocompleteOption {
|
||||
for _, opt := range o {
|
||||
if opt.Focused {
|
||||
return opt
|
||||
}
|
||||
}
|
||||
return AutocompleteOption{}
|
||||
}
|
||||
|
||||
// Unmarshal behaves similarly to CommandInteractionOptions.Unmarshal. It
|
||||
// supports the same types. Refer to its documentation for more.
|
||||
func (o AutocompleteOptions) Unmarshal(v interface{}) error {
|
||||
return unmarshalOptions(
|
||||
func(name string) unmarshalingOption { return o.Find(name).forUnmarshal() },
|
||||
reflect.ValueOf(v),
|
||||
)
|
||||
}
|
||||
|
||||
func (o AutocompleteOption) forUnmarshal() unmarshalingOption {
|
||||
return unmarshalingOption{
|
||||
Type: o.Type,
|
||||
Name: o.Name,
|
||||
Value: o.Value,
|
||||
Find: func(name string) unmarshalingOption { return o.Options.Find(name).forUnmarshal() },
|
||||
}
|
||||
}
|
||||
|
||||
// AutocompleteOption is an autocompletion option in an AutocompleteInteraction.
|
||||
type AutocompleteOption struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Value json.Raw `json:"value,omitempty"`
|
||||
Focused bool `json:"focused,omitempty"`
|
||||
Options AutocompleteOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
// String will return the value if the option's value is a valid string.
|
||||
// Otherwise, it will return the raw JSON value of the other type.
|
||||
func (o AutocompleteOption) String() string {
|
||||
var value string
|
||||
if err := json.Unmarshal(o.Value, &value); err != nil {
|
||||
return string(o.Value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// IntValue reads the option's value as an int.
|
||||
func (o AutocompleteOption) IntValue() (int64, error) {
|
||||
var i int64
|
||||
err := o.Value.UnmarshalTo(&i)
|
||||
return i, err
|
||||
}
|
||||
|
||||
// BoolValue reads the option's value as a bool.
|
||||
func (o AutocompleteOption) BoolValue() (bool, error) {
|
||||
var b bool
|
||||
err := o.Value.UnmarshalTo(&b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// SnowflakeValue reads the option's value as a snowflake.
|
||||
func (o AutocompleteOption) SnowflakeValue() (Snowflake, error) {
|
||||
var id Snowflake
|
||||
err := o.Value.UnmarshalTo(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// FloatValue reads the option's value as a float64.
|
||||
func (o AutocompleteOption) FloatValue() (float64, error) {
|
||||
var f float64
|
||||
err := o.Value.UnmarshalTo(&f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// ComponentInteraction is a union component interaction response types. The
|
||||
// types can be whatever the constructors for this type will return.
|
||||
//
|
||||
// The following types implement this interface:
|
||||
//
|
||||
// - *StringSelectInteraction
|
||||
// - *ChannelSelectInteraction
|
||||
// - *RoleSelectInteraction
|
||||
// - *UserSelectInteraction
|
||||
// - *MentionableSelectInteraction
|
||||
// - *ButtonInteraction
|
||||
//
|
||||
type ComponentInteraction interface {
|
||||
InteractionData
|
||||
// ID returns the ID of the component in response. Not all component
|
||||
// interactions will have a component ID.
|
||||
ID() ComponentID
|
||||
// Type returns the type of the component in response.
|
||||
Type() ComponentType
|
||||
resp()
|
||||
}
|
||||
|
||||
// SelectInteraction is a select component's response.
|
||||
//
|
||||
// Deprecated: Use StringSelectInteraction instead.
|
||||
type SelectInteraction = StringSelectInteraction
|
||||
|
||||
// StringSelectInteraction is a string select component's response.
|
||||
type StringSelectInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Values []string `json:"values"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (s *StringSelectInteraction) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (s *StringSelectInteraction) Type() ComponentType { return StringSelectComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (s *StringSelectInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (s *StringSelectInteraction) resp() {}
|
||||
func (s *StringSelectInteraction) data() {}
|
||||
|
||||
// ChannelSelectInteraction is a channel select component's response.
|
||||
type ChannelSelectInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Values []ChannelID `json:"values"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (s *ChannelSelectInteraction) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (s *ChannelSelectInteraction) Type() ComponentType { return ChannelSelectComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (s *ChannelSelectInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (s *ChannelSelectInteraction) resp() {}
|
||||
func (s *ChannelSelectInteraction) data() {}
|
||||
|
||||
// RoleSelectInteraction is a role select component's response.
|
||||
type RoleSelectInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Values []RoleID `json:"values"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (s *RoleSelectInteraction) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (s *RoleSelectInteraction) Type() ComponentType { return RoleSelectComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (s *RoleSelectInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (s *RoleSelectInteraction) resp() {}
|
||||
func (s *RoleSelectInteraction) data() {}
|
||||
|
||||
// UserSelectInteraction is a user select component's response.
|
||||
type UserSelectInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Values []UserID `json:"values"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (s *UserSelectInteraction) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (s *UserSelectInteraction) Type() ComponentType { return UserSelectComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (s *UserSelectInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (s *UserSelectInteraction) resp() {}
|
||||
func (s *UserSelectInteraction) data() {}
|
||||
|
||||
// MentionableSelectInteraction is a mentionable select component's response.
|
||||
type MentionableSelectInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Values []Snowflake `json:"values"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (s *MentionableSelectInteraction) ID() ComponentID { return s.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (s *MentionableSelectInteraction) Type() ComponentType { return MentionableSelectComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (s *MentionableSelectInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (s *MentionableSelectInteraction) resp() {}
|
||||
func (s *MentionableSelectInteraction) data() {}
|
||||
|
||||
// ButtonInteraction is a button component's response. It is the custom ID of
|
||||
// the button within the component tree.
|
||||
type ButtonInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
}
|
||||
|
||||
// ID implements ComponentInteraction.
|
||||
func (b *ButtonInteraction) ID() ComponentID { return b.CustomID }
|
||||
|
||||
// Type implements ComponentInteraction.
|
||||
func (b *ButtonInteraction) Type() ComponentType { return ButtonComponentType }
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (b *ButtonInteraction) InteractionType() InteractionDataType {
|
||||
return ComponentInteractionType
|
||||
}
|
||||
|
||||
func (b *ButtonInteraction) data() {}
|
||||
func (b *ButtonInteraction) resp() {}
|
||||
|
||||
// ParseComponentInteraction parses the given bytes as a component response.
|
||||
func ParseComponentInteraction(b []byte) (ComponentInteraction, error) {
|
||||
var t struct {
|
||||
Type ComponentType `json:"component_type"`
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, &t); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal component interaction header")
|
||||
}
|
||||
|
||||
var d ComponentInteraction
|
||||
|
||||
switch t.Type {
|
||||
case ButtonComponentType:
|
||||
d = &ButtonInteraction{CustomID: t.CustomID}
|
||||
case StringSelectComponentType:
|
||||
d = &StringSelectInteraction{CustomID: t.CustomID}
|
||||
case ChannelSelectComponentType:
|
||||
d = &ChannelSelectInteraction{CustomID: t.CustomID}
|
||||
case RoleSelectComponentType:
|
||||
d = &RoleSelectInteraction{CustomID: t.CustomID}
|
||||
case UserSelectComponentType:
|
||||
d = &UserSelectInteraction{CustomID: t.CustomID}
|
||||
case MentionableSelectComponentType:
|
||||
d = &MentionableSelectInteraction{CustomID: t.CustomID}
|
||||
default:
|
||||
d = &UnknownComponent{
|
||||
Raw: append(json.Raw(nil), b...),
|
||||
id: t.CustomID,
|
||||
typ: t.Type,
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, d); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal component interaction data")
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// CommandInteractionOptions is a list of interaction options.
|
||||
// Use `Find` to get your named interaction option
|
||||
type CommandInteractionOptions []CommandInteractionOption
|
||||
|
||||
// CommandInteraction is an application command interaction that Discord sends
|
||||
// to us.
|
||||
type CommandInteraction struct {
|
||||
ID CommandID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Options CommandInteractionOptions `json:"options,omitempty"`
|
||||
// GuildID is the id of the guild the command is registered to
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
// TargetID is the id of the user or message targeted by a user or message command.
|
||||
//
|
||||
// See TargetUserID and TargetMessageID
|
||||
TargetID Snowflake `json:"target_id,omitempty"`
|
||||
Resolved struct {
|
||||
// User contains user objects.
|
||||
Users map[UserID]User `json:"users,omitempty"`
|
||||
// Members contains partial member objects (missing User, Deaf and
|
||||
// Mute).
|
||||
Members map[UserID]Member `json:"members,omitempty"`
|
||||
// Role contains role objects.
|
||||
Roles map[RoleID]Role `json:"roles,omitempty"`
|
||||
// Channels contains partial channel objects that only have ID, Name,
|
||||
// Type and Permissions. Threads will also have ThreadMetadata and
|
||||
// ParentID.
|
||||
Channels map[ChannelID]Channel `json:"channels,omitempty"`
|
||||
// Messages contains partial message objects. All fields without
|
||||
// omitempty are presumably present.
|
||||
Messages map[MessageID]Message `json:"messages,omitempty"`
|
||||
// Attachments contains attachments objects.
|
||||
Attachments map[AttachmentID]Attachment `json:"attachments,omitempty"`
|
||||
}
|
||||
}
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (*CommandInteraction) InteractionType() InteractionDataType {
|
||||
return CommandInteractionType
|
||||
}
|
||||
|
||||
// TargetUserID is the id of the user targeted by a user command
|
||||
func (c *CommandInteraction) TargetUserID() UserID {
|
||||
return UserID(c.TargetID)
|
||||
}
|
||||
|
||||
// TargetMessageID is the id of the message targeted by a message command
|
||||
func (c *CommandInteraction) TargetMessageID() MessageID {
|
||||
return MessageID(c.TargetID)
|
||||
}
|
||||
|
||||
func (*CommandInteraction) data() {}
|
||||
|
||||
// CommandInteractionOption is an option for a Command interaction response.
|
||||
type CommandInteractionOption struct {
|
||||
Type CommandOptionType `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Value json.Raw `json:"value,omitempty"`
|
||||
Options CommandInteractionOptions `json:"options,omitempty"`
|
||||
}
|
||||
|
||||
var optionSupportedSnowflakeTypes = map[reflect.Type]CommandOptionType{
|
||||
reflect.TypeOf(ChannelID(0)): ChannelOptionType,
|
||||
reflect.TypeOf(UserID(0)): UserOptionType,
|
||||
reflect.TypeOf(RoleID(0)): RoleOptionType,
|
||||
reflect.TypeOf(Snowflake(0)): MentionableOptionType,
|
||||
}
|
||||
|
||||
var optionKindMap = map[reflect.Kind]CommandOptionType{
|
||||
reflect.Int: NumberOptionType,
|
||||
reflect.Int8: NumberOptionType,
|
||||
reflect.Int16: NumberOptionType,
|
||||
reflect.Int32: NumberOptionType,
|
||||
reflect.Int64: NumberOptionType,
|
||||
reflect.Uint: NumberOptionType,
|
||||
reflect.Uint8: NumberOptionType,
|
||||
reflect.Uint16: NumberOptionType,
|
||||
reflect.Uint32: NumberOptionType,
|
||||
reflect.Uint64: NumberOptionType,
|
||||
reflect.Float32: NumberOptionType,
|
||||
reflect.Float64: NumberOptionType,
|
||||
reflect.String: StringOptionType,
|
||||
reflect.Bool: BooleanOptionType,
|
||||
}
|
||||
|
||||
// Unmarshal unmarshals the options into the struct pointer v. Each struct field
|
||||
// must be exported and is of a supported type.
|
||||
//
|
||||
// Fields that don't satisfy any of the above are ignored. The "discord" struct
|
||||
// tag with a value "-" is ignored. Fields that aren't found in the list of
|
||||
// options and have a "?" at the end of the "discord" struct tag are ignored.
|
||||
//
|
||||
// Supported Types
|
||||
//
|
||||
// The following types are supported:
|
||||
//
|
||||
// - ChannelID (ChannelOptionType)
|
||||
// - UserID (UserOptionType)
|
||||
// - RoleID (RoleOptionType)
|
||||
// - Snowflake (MentionableOptionType)
|
||||
// - string (StringOptionType)
|
||||
// - bool (BooleanOptionType)
|
||||
// - int* (int, int8, int16, int32, int64) (NumberOptionType)
|
||||
// - uint* (uint, uint8, uint16, uint32, uint64) (NumberOptionType)
|
||||
// - float* (float32, float64) (NumberOptionType)
|
||||
// - (any struct and struct pointer) (not Discord-type-checked)
|
||||
//
|
||||
// Any types that are derived from any of the above built-in types are also
|
||||
// supported.
|
||||
//
|
||||
// Pointer types to any of the above types are also supported and will also
|
||||
// implicitly imply optionality.
|
||||
func (o CommandInteractionOptions) Unmarshal(v interface{}) error {
|
||||
return unmarshalOptions(
|
||||
func(name string) unmarshalingOption { return o.Find(name).forUnmarshal() },
|
||||
reflect.ValueOf(v),
|
||||
)
|
||||
}
|
||||
|
||||
func (o CommandInteractionOption) forUnmarshal() unmarshalingOption {
|
||||
return unmarshalingOption{
|
||||
Type: o.Type,
|
||||
Name: o.Name,
|
||||
Value: o.Value,
|
||||
Find: func(name string) unmarshalingOption { return o.Options.Find(name).forUnmarshal() },
|
||||
}
|
||||
}
|
||||
|
||||
type unmarshalingOption struct {
|
||||
Type CommandOptionType
|
||||
Name string
|
||||
Value json.Raw
|
||||
Find func(name string) unmarshalingOption
|
||||
}
|
||||
|
||||
func unmarshalOptions(find func(string) unmarshalingOption, rv reflect.Value) error {
|
||||
rv, rt, err := rfutil.StructRValue(rv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numField := rt.NumField()
|
||||
for i := 0; i < numField; i++ {
|
||||
fieldStruct := rt.Field(i)
|
||||
if !fieldStruct.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := fieldStruct.Tag.Get("discord")
|
||||
switch name {
|
||||
case "-":
|
||||
continue
|
||||
case "?":
|
||||
name = fieldStruct.Name + "?"
|
||||
case "":
|
||||
name = fieldStruct.Name
|
||||
}
|
||||
|
||||
option := find(strings.TrimSuffix(name, "?"))
|
||||
fieldv := rv.Field(i)
|
||||
fieldt := fieldStruct.Type
|
||||
|
||||
if strings.HasSuffix(name, "?") {
|
||||
name = strings.TrimSuffix(name, "?")
|
||||
if option.Type == 0 {
|
||||
// not found
|
||||
continue
|
||||
}
|
||||
} else if fieldStruct.Type.Kind() == reflect.Ptr {
|
||||
fieldt = fieldt.Elem()
|
||||
if option.Type == 0 {
|
||||
// not found
|
||||
fieldv.Set(reflect.NewAt(fieldt, nil))
|
||||
continue
|
||||
}
|
||||
// found, so allocate new value and use that to set
|
||||
newv := reflect.New(fieldt)
|
||||
fieldv.Set(newv)
|
||||
fieldv = newv.Elem()
|
||||
} else if option.Type == 0 {
|
||||
// not found AND the field is not a pointer, so error out
|
||||
return fmt.Errorf("option %q is required but not found", name)
|
||||
}
|
||||
|
||||
if expectType, ok := optionSupportedSnowflakeTypes[fieldt]; ok {
|
||||
if option.Type != expectType {
|
||||
return fmt.Errorf("option %q expecting type %v, got %v", name, expectType, option.Type)
|
||||
}
|
||||
|
||||
var snowflake Snowflake
|
||||
if err := option.Value.UnmarshalTo(&snowflake); err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid snowflake", name)
|
||||
}
|
||||
|
||||
fieldv.Set(reflect.ValueOf(snowflake).Convert(fieldt))
|
||||
continue
|
||||
}
|
||||
|
||||
fieldk := fieldt.Kind()
|
||||
if expectType, ok := optionKindMap[fieldk]; ok {
|
||||
if option.Type != expectType {
|
||||
return fmt.Errorf("option %q expecting type %v, got %v", name, expectType, option.Type)
|
||||
}
|
||||
}
|
||||
|
||||
switch fieldk {
|
||||
case reflect.Struct:
|
||||
if err := unmarshalOptions(option.Find, fieldv.Addr()); err != nil {
|
||||
return errors.Wrapf(err, "option %q has invalid suboptions", name)
|
||||
}
|
||||
|
||||
case reflect.Bool, reflect.String, reflect.Float32, reflect.Float64,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
|
||||
v := reflect.New(fieldt)
|
||||
if err := option.Value.UnmarshalTo(v.Interface()); err != nil {
|
||||
return errors.Wrapf(err, "option %q is not a valid %s", name, fieldt)
|
||||
}
|
||||
fieldv.Set(v.Elem())
|
||||
|
||||
default:
|
||||
return fmt.Errorf("field %s (%q) has unknown type %s", fieldStruct.Name, name, fieldt)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find returns the named command option
|
||||
func (o CommandInteractionOptions) Find(name string) CommandInteractionOption {
|
||||
for _, opt := range o {
|
||||
if strings.EqualFold(opt.Name, name) {
|
||||
return opt
|
||||
}
|
||||
}
|
||||
return CommandInteractionOption{}
|
||||
}
|
||||
|
||||
// String will return the value if the option's value is a valid string.
|
||||
// Otherwise, it will return the raw JSON value of the other type.
|
||||
func (o CommandInteractionOption) String() string {
|
||||
var value string
|
||||
if err := json.Unmarshal(o.Value, &value); err != nil {
|
||||
return string(o.Value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// IntValue reads the option's value as an int.
|
||||
func (o CommandInteractionOption) IntValue() (int64, error) {
|
||||
var i int64
|
||||
err := o.Value.UnmarshalTo(&i)
|
||||
return i, err
|
||||
}
|
||||
|
||||
// BoolValue reads the option's value as a bool.
|
||||
func (o CommandInteractionOption) BoolValue() (bool, error) {
|
||||
var b bool
|
||||
err := o.Value.UnmarshalTo(&b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// SnowflakeValue reads the option's value as a snowflake.
|
||||
func (o CommandInteractionOption) SnowflakeValue() (Snowflake, error) {
|
||||
var id Snowflake
|
||||
err := o.Value.UnmarshalTo(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// FloatValue reads the option's value as a float64.
|
||||
func (o CommandInteractionOption) FloatValue() (float64, error) {
|
||||
var f float64
|
||||
err := o.Value.UnmarshalTo(&f)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// ModalInteraction is the submitted modal form
|
||||
type ModalInteraction struct {
|
||||
CustomID ComponentID `json:"custom_id"`
|
||||
Components ContainerComponents `json:"components"`
|
||||
}
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (m *ModalInteraction) InteractionType() InteractionDataType {
|
||||
return ModalInteractionType
|
||||
}
|
||||
|
||||
func (m *ModalInteraction) data() {}
|
||||
|
||||
// UnknownInteractionData describes an Interaction response with an unknown
|
||||
// type.
|
||||
type UnknownInteractionData struct {
|
||||
json.Raw
|
||||
typ InteractionDataType
|
||||
}
|
||||
|
||||
// InteractionType implements InteractionData.
|
||||
func (u *UnknownInteractionData) InteractionType() InteractionDataType {
|
||||
return u.typ
|
||||
}
|
||||
|
||||
func (u *UnknownInteractionData) data() {}
|
|
@ -1,89 +0,0 @@
|
|||
package discord_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/discord"
|
||||
internaljson "github.com/diamondburned/arikawa/v3/utils/json"
|
||||
)
|
||||
|
||||
func ExampleCommandInteractionOptions_Unmarshal() {
|
||||
options := discord.CommandInteractionOptions{
|
||||
opt(discord.ChannelOptionType, "channel_id", 1),
|
||||
opt(discord.StringOptionType, "string1", "hello"),
|
||||
opt(discord.StringOptionType, "string2", "hello"),
|
||||
opt(discord.StringOptionType, "string3", "hello"),
|
||||
opt(discord.SubcommandOptionType, "sub", discord.CommandInteractionOptions{
|
||||
{
|
||||
Type: discord.RoleOptionType,
|
||||
Name: "role_id",
|
||||
Value: mustJSON("2"),
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
var quickCommand struct {
|
||||
ChannelID discord.ChannelID `discord:"channel_id"`
|
||||
String1 string
|
||||
OptionalString2 *string `discord:"string2"`
|
||||
OptionalString3 string `discord:"string3?"`
|
||||
OptionalString4 *string `discord:"string4"`
|
||||
OptionalString5 string `discord:"string5?"`
|
||||
Suboption struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub"`
|
||||
OptionalSuboption2 *struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub2"`
|
||||
OptionalSuboption3 struct {
|
||||
RoleID discord.RoleID `discord:"role_id"`
|
||||
} `discord:"sub3?"`
|
||||
}
|
||||
|
||||
if err := options.Unmarshal(&quickCommand); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
b, _ := json.MarshalIndent(quickCommand, "", " ")
|
||||
fmt.Println(string(b))
|
||||
|
||||
// Output:
|
||||
// {
|
||||
// "ChannelID": "1",
|
||||
// "String1": "hello",
|
||||
// "OptionalString2": "hello",
|
||||
// "OptionalString3": "hello",
|
||||
// "OptionalString4": null,
|
||||
// "OptionalString5": "",
|
||||
// "Suboption": {
|
||||
// "RoleID": "2"
|
||||
// },
|
||||
// "OptionalSuboption2": null,
|
||||
// "OptionalSuboption3": {
|
||||
// "RoleID": null
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func opt(t discord.CommandOptionType, name string, v interface{}) discord.CommandInteractionOption {
|
||||
o := discord.CommandInteractionOption{
|
||||
Type: t,
|
||||
Name: name,
|
||||
}
|
||||
if opts, ok := v.(discord.CommandInteractionOptions); ok {
|
||||
o.Options = opts
|
||||
} else {
|
||||
o.Value = mustJSON(v)
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func mustJSON(v interface{}) internaljson.Raw {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return internaljson.Raw(b)
|
||||
}
|
|
@ -32,16 +32,6 @@ type Invite struct {
|
|||
InviteMetadata
|
||||
}
|
||||
|
||||
// URL returns a Discord invite URL linking to the invite.
|
||||
func (i Invite) URL() string {
|
||||
return "https://discord.gg/" + i.Code
|
||||
}
|
||||
|
||||
// LongURL returns a long-form Discord invite URL linking to the invite.
|
||||
func (i Invite) LongURL() string {
|
||||
return "https://discord.com/invite/" + i.Code
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/invite#invite-object-target-user-types
|
||||
type InviteUserType uint8
|
||||
|
||||
|
|
|
@ -2,54 +2,16 @@ package discord
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/v3/utils/json/enum"
|
||||
"github.com/diamondburned/arikawa/utils/json/enum"
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object
|
||||
type Message struct {
|
||||
// ID is the id of the message.
|
||||
ID MessageID `json:"id"`
|
||||
// ChannelID is the id of the channel the message was sent in.
|
||||
ChannelID ChannelID `json:"channel_id"`
|
||||
// GuildID is the id of the guild the message was sent in.
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
ID MessageID `json:"id,string"`
|
||||
Type MessageType `json:"type"`
|
||||
ChannelID ChannelID `json:"channel_id,string"`
|
||||
GuildID GuildID `json:"guild_id,string,omitempty"`
|
||||
|
||||
// Type is the type of message.
|
||||
Type MessageType `json:"type"`
|
||||
|
||||
// Flags are the MessageFlags.
|
||||
Flags MessageFlags `json:"flags"`
|
||||
|
||||
// TTS specifies whether the was a TTS message.
|
||||
TTS bool `json:"tts"`
|
||||
// Pinned specifies whether the message is pinned.
|
||||
Pinned bool `json:"pinned"`
|
||||
|
||||
// MentionEveryone specifies whether the message mentions everyone.
|
||||
MentionEveryone bool `json:"mention_everyone"`
|
||||
// Mentions contains the users specifically mentioned in the message.
|
||||
//
|
||||
// The user objects in the mentions array will only have the partial
|
||||
// member field present in MESSAGE_CREATE and MESSAGE_UPDATE events from
|
||||
// text-based guild channels.
|
||||
Mentions []GuildUser `json:"mentions"`
|
||||
// MentionRoleIDs contains the ids of the roles specifically mentioned in
|
||||
// the message.
|
||||
MentionRoleIDs []RoleID `json:"mention_roles"`
|
||||
// MentionChannels are the channels specifically mentioned in the 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, the slice will be empty.
|
||||
MentionChannels []ChannelMention `json:"mention_channels,omitempty"`
|
||||
|
||||
// Author is the author of the message.
|
||||
//
|
||||
// The author object follows the structure of the user object, but is only
|
||||
// a valid user in the case where the message is generated by a user or bot
|
||||
// user. If the message is generated by a webhook, the author object
|
||||
|
@ -58,56 +20,38 @@ type Message struct {
|
|||
// message object.
|
||||
Author User `json:"author"`
|
||||
|
||||
// Content contains the contents of the message.
|
||||
Content string `json:"content"`
|
||||
|
||||
// Timestamp specifies when the message was sent
|
||||
Timestamp Timestamp `json:"timestamp,omitempty"`
|
||||
// EditedTimestamp specifies when this message was edited.
|
||||
//
|
||||
// IsValid() will return false, if the messages hasn't been edited.
|
||||
Timestamp Timestamp `json:"timestamp,omitempty"`
|
||||
EditedTimestamp Timestamp `json:"edited_timestamp,omitempty"`
|
||||
|
||||
// Attachments contains any attached files.
|
||||
TTS bool `json:"tts"`
|
||||
Pinned bool `json:"pinned"`
|
||||
|
||||
// The user objects in the mentions array will only have the partial
|
||||
// member field present in MESSAGE_CREATE and MESSAGE_UPDATE events from
|
||||
// text-based guild channels.
|
||||
Mentions []GuildUser `json:"mentions"`
|
||||
|
||||
MentionRoleIDs []RoleID `json:"mention_roles"`
|
||||
MentionEveryone bool `json:"mention_everyone"`
|
||||
|
||||
// Not all channel mentions in a message will appear in mention_channels.
|
||||
MentionChannels []ChannelMention `json:"mention_channels,omitempty"`
|
||||
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
// Embeds contains any embedded content.
|
||||
Embeds []Embed `json:"embeds"`
|
||||
// Reactions contains any reactions to the message.
|
||||
Embeds []Embed `json:"embeds"`
|
||||
|
||||
Reactions []Reaction `json:"reactions,omitempty"`
|
||||
// Components contains any attached components.
|
||||
Components ContainerComponents `json:"components,omitempty"`
|
||||
|
||||
// Used for validating a message was sent
|
||||
Nonce string `json:"nonce,omitempty"`
|
||||
|
||||
// WebhookID contains the ID of the webhook, if the message was generated
|
||||
// by a webhook.
|
||||
WebhookID WebhookID `json:"webhook_id,omitempty"`
|
||||
|
||||
// Activity is sent with Rich Presence-related chat embeds.
|
||||
Activity *MessageActivity `json:"activity,omitempty"`
|
||||
// Application is sent with Rich Presence-related chat embeds.
|
||||
WebhookID WebhookID `json:"webhook_id,string,omitempty"`
|
||||
Activity *MessageActivity `json:"activity,omitempty"`
|
||||
Application *MessageApplication `json:"application,omitempty"`
|
||||
|
||||
// ApplicationID contains the ID of the application, if this message was
|
||||
// generated by an interaction or an application-owned webhook.
|
||||
ApplicationID AppID `json:"application_id,omitempty"`
|
||||
|
||||
// Reference is the reference data sent with crossposted messages and
|
||||
// inline replies.
|
||||
Reference *MessageReference `json:"message_reference,omitempty"`
|
||||
// ReferencedMessage is the message that was replied to. If not present and
|
||||
// the type is InlinedReplyMessage, the backend couldn't fetch the
|
||||
// replied-to message. If null, the message was deleted. If present and
|
||||
// non-null, it is a message object
|
||||
ReferencedMessage *Message `json:"referenced_message,omitempty"`
|
||||
|
||||
// Interaction is the interaction that the message is in response to.
|
||||
// This is only present if the message is in response to an interaction.
|
||||
Interaction *MessageInteraction `json:"interaction,omitempty"`
|
||||
|
||||
// Stickers contains the sticker "items" sent with the message.
|
||||
Stickers []StickerItem `json:"sticker_items,omitempty"`
|
||||
Reference *MessageReference `json:"message_reference,omitempty"`
|
||||
Flags MessageFlags `json:"flags"`
|
||||
}
|
||||
|
||||
// URL generates a Discord client URL to the message. If the message doesn't
|
||||
|
@ -126,7 +70,6 @@ func (m Message) URL() string {
|
|||
|
||||
type MessageType uint8
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-types
|
||||
const (
|
||||
DefaultMessage MessageType = iota
|
||||
RecipientAddMessage
|
||||
|
@ -141,181 +84,26 @@ const (
|
|||
NitroTier2Message
|
||||
NitroTier3Message
|
||||
ChannelFollowAddMessage
|
||||
_
|
||||
GuildDiscoveryDisqualifiedMessage
|
||||
GuildDiscoveryRequalifiedMessage
|
||||
GuildDiscoveryGracePeriodInitialWarning
|
||||
GuildDiscoveryGracePeriodFinalWarning
|
||||
// ThreadCreatedMessage is a new message sent to the parent GuildText
|
||||
// channel, used to inform users that a thread has been created. It is
|
||||
// currently only sent in one case: when a GuildPublicThread is created
|
||||
// from an older message (older is still TBD, but is currently set to a
|
||||
// very small value). The message contains a message reference with the
|
||||
// GuildID and ChannelID of the thread. The content of the message is the
|
||||
// name of the thread.
|
||||
ThreadCreatedMessage
|
||||
InlinedReplyMessage
|
||||
ChatInputCommandMessage
|
||||
// ThreadStarterMessage is a new message sent as the first message in
|
||||
// threads that are started from an existing message in the parent channel.
|
||||
// It only contains a message reference field that points to the message
|
||||
// from which the thread was started.
|
||||
ThreadStarterMessage
|
||||
GuildInviteReminderMessage
|
||||
ContextMenuCommand
|
||||
AutoModerationActionMessage
|
||||
RoleSubscriptionPurchaseMessage
|
||||
InteractionPremiumUpsellMessage
|
||||
|
||||
StageStartMessage
|
||||
StageEndMessage
|
||||
StageSpeakerMessage
|
||||
_
|
||||
StageTopicMessage
|
||||
|
||||
GuildApplicationPremiumSubscriptionMessage
|
||||
)
|
||||
|
||||
type MessageFlags enum.Enum
|
||||
|
||||
// NullMessage is the JSON null value of MessageFlags.
|
||||
const NullMessage MessageFlags = enum.Null
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-flags
|
||||
const (
|
||||
// CrosspostedMessage specifies whether the message has been published to
|
||||
// subscribed channels (via Channel Following).
|
||||
CrosspostedMessage MessageFlags = 1 << iota
|
||||
// MessageIsCrosspost specifies whether the message originated from a
|
||||
// message in another channel (via Channel Following).
|
||||
MessageIsCrosspost
|
||||
// SuppressEmbeds specifies whether to not include any embeds when
|
||||
// serializing the message.
|
||||
SuppressEmbeds
|
||||
// SourceMessageDeleted specifies whether the source message for the
|
||||
// crosspost has been deleted (via Channel Following).
|
||||
SourceMessageDeleted
|
||||
// UrgentMessage specifies whether the message came from the urgent message
|
||||
// system.
|
||||
UrgentMessage
|
||||
// MessageHasThread specifies whether the message has an associated thread
|
||||
// with the same id as the message
|
||||
MessageHasThread
|
||||
// EphemeralMessage specifies whether the message is only visible to
|
||||
// the user who invoked the Interaction
|
||||
EphemeralMessage
|
||||
// MessageLoading specifies whether the message is an Interaction Response
|
||||
// and the bot is "thinking"
|
||||
MessageLoading
|
||||
// TODO: add FailedToMentionSomeRolesInThread
|
||||
|
||||
// SuppressNotifications specifies whether the message will not trigger push and desktop notifications.
|
||||
SuppressNotifications = 1 << 12
|
||||
var (
|
||||
NullMessage MessageFlags = enum.Null
|
||||
CrosspostedMessage MessageFlags = 1
|
||||
MessageIsCrosspost MessageFlags = 2
|
||||
SuppressEmbeds MessageFlags = 4
|
||||
SourceMessageDeleted MessageFlags = 8
|
||||
UrgentMessage MessageFlags = 16
|
||||
)
|
||||
|
||||
// StickerItem contains partial data of a Sticker.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/sticker#sticker-item-object
|
||||
type StickerItem struct {
|
||||
// ID is the ID of the sticker.
|
||||
ID StickerID `json:"id"`
|
||||
// Name is the name of the sticker.
|
||||
Name string `json:"name"`
|
||||
// FormatType is the type of sticker format.
|
||||
FormatType StickerFormatType `json:"format_type"`
|
||||
}
|
||||
|
||||
// StickerURLWithType returns the URL to the emoji's image.
|
||||
//
|
||||
// Supported ImageTypes: PNG
|
||||
func (s StickerItem) StickerURLWithType(t ImageType) string {
|
||||
return "https://cdn.discordapp.com/stickers/" + t.format(s.ID.String())
|
||||
}
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-sticker-structure
|
||||
type Sticker struct {
|
||||
// ID is the ID of the sticker.
|
||||
ID StickerID `json:"id"`
|
||||
// PackID is the ID of the pack the sticker is from.
|
||||
PackID StickerPackID `json:"pack_id,omitempty"`
|
||||
// Name is the name of the sticker.
|
||||
Name string `json:"name"`
|
||||
// Description is the description of the sticker.
|
||||
Description string `json:"description"`
|
||||
// Tags is a comma-delimited list of tags for the sticker. To get the list
|
||||
// as a slice, use TagList.
|
||||
Tags string `json:"tags"`
|
||||
// Type is the type of sticker.
|
||||
Type StickerType `json:"type"`
|
||||
// FormatType is the type of sticker format.
|
||||
FormatType StickerFormatType `json:"format_type"`
|
||||
// Available specifies whether this guild sticker can be used, may be false due to loss of Server Boosts.
|
||||
Available bool `json:"available,omitempty"`
|
||||
// GuildID is the id of the guild that owns this sticker.
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
// User is the user that uploaded the guild sticker
|
||||
User *User `json:"user,omitempty"`
|
||||
// SortValue is the standard sticker's sort order within its pack.
|
||||
SortValue *int `json:"sort_value,omitempty"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the sticker was created.
|
||||
func (s Sticker) CreatedAt() time.Time {
|
||||
return s.ID.Time()
|
||||
}
|
||||
|
||||
// PackCreatedAt returns a time object representing when the sticker's pack
|
||||
// was created.
|
||||
func (s Sticker) PackCreatedAt() time.Time {
|
||||
return s.PackID.Time()
|
||||
}
|
||||
|
||||
// TagList splits the sticker tags into a slice of strings. Each tag will have
|
||||
// its trailing space trimmed.
|
||||
func (s Sticker) TagList() []string {
|
||||
tags := strings.Split(s.Tags, ",")
|
||||
for i := range tags {
|
||||
tags[i] = strings.TrimSpace(tags[i])
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
// StickerURLWithType returns the URL to the emoji's image.
|
||||
//
|
||||
// Supported ImageTypes: PNG
|
||||
func (s Sticker) StickerURLWithType(t ImageType) string {
|
||||
return "https://cdn.discordapp.com/stickers/" + t.format(s.ID.String())
|
||||
}
|
||||
|
||||
type StickerType int
|
||||
|
||||
// https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types
|
||||
const (
|
||||
// StandardSticker is an official sticker in a pack, part of Nitro or in a removed purchasable pack.
|
||||
StandardSticker StickerType = iota + 1
|
||||
// GuildSticker is a sticker uploaded to a boosted guild for the guild's members.
|
||||
GuildSticker
|
||||
)
|
||||
|
||||
type StickerFormatType uint8
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-sticker-format-types
|
||||
const (
|
||||
StickerFormatPNG = 1
|
||||
StickerFormatAPNG = 2
|
||||
StickerFormatLottie = 3
|
||||
)
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#channel-mention-object
|
||||
type ChannelMention struct {
|
||||
// ChannelID is the ID of the channel.
|
||||
ChannelID ChannelID `json:"id"`
|
||||
// GuildID is the ID of the guild containing the channel.
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
// ChannelType is the type of channel.
|
||||
ChannelID ChannelID `json:"id,string"`
|
||||
GuildID GuildID `json:"guild_id,string"`
|
||||
ChannelType ChannelType `json:"type"`
|
||||
// ChannelName is the name of the channel.
|
||||
ChannelName string `json:"name"`
|
||||
ChannelName string `json:"name"`
|
||||
}
|
||||
|
||||
type GuildUser struct {
|
||||
|
@ -325,17 +113,15 @@ type GuildUser struct {
|
|||
|
||||
//
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-activity-structure
|
||||
type MessageActivity struct {
|
||||
// Type is the type of message activity.
|
||||
Type MessageActivityType `json:"type"`
|
||||
// PartyID is the party_id from a Rich Presence event.
|
||||
|
||||
// From a Rich Presence event
|
||||
PartyID string `json:"party_id,omitempty"`
|
||||
}
|
||||
|
||||
type MessageActivityType uint8
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-activity-types
|
||||
const (
|
||||
JoinMessage MessageActivityType = iota + 1
|
||||
SpectateMessage
|
||||
|
@ -345,123 +131,43 @@ const (
|
|||
|
||||
//
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-application-structure
|
||||
type MessageApplication struct {
|
||||
// ID is the id of the application.
|
||||
ID AppID `json:"id"`
|
||||
// CoverID is the id of the embed's image asset.
|
||||
CoverID string `json:"cover_image,omitempty"`
|
||||
// Description is the application's description.
|
||||
ID AppID `json:"id,string"`
|
||||
CoverID string `json:"cover_image,omitempty"`
|
||||
Description string `json:"description"`
|
||||
// Icon is the id of the application's icon.
|
||||
Icon string `json:"icon"`
|
||||
// Name is the name of the application.
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// CreatedAt returns a time object representing when the message application
|
||||
// was created.
|
||||
func (m MessageApplication) CreatedAt() time.Time {
|
||||
return m.ID.Time()
|
||||
}
|
||||
//
|
||||
|
||||
// MessageReference is used in four situations:
|
||||
//
|
||||
// # Crosspost messages
|
||||
//
|
||||
// Messages that originated from another channel (IS_CROSSPOST flag). These
|
||||
// messages have all three fields, with data of the original message that was
|
||||
// crossposted.
|
||||
//
|
||||
// # Channel Follow Add messages
|
||||
//
|
||||
// Automatic messages sent when a channel is followed into the current channel
|
||||
// (type 12). These messages have the ChannelID and GuildID fields, with data
|
||||
// of the followed announcement channel.
|
||||
//
|
||||
// # Pin messages
|
||||
//
|
||||
// Automatic messages sent when a message is pinned (type 6). These messages
|
||||
// have MessageID and ChannelID, and GuildID if it is in a guild, with data
|
||||
// of the message that was pinned.
|
||||
//
|
||||
// # Replies
|
||||
//
|
||||
// Messages replying to a previous message (type 19). These messages have
|
||||
// MessageID, and ChannelID, and GuildID if it is in a guild, with data of the
|
||||
// message that was replied to. The ChannelID and GuildID will be the
|
||||
// same as the reply.
|
||||
//
|
||||
// Replies are created by including a message_reference when sending a message.
|
||||
// When sending, only MessageID is required.
|
||||
// https://discord.com/developers/docs/resources/channel#message-object-message-reference-structure
|
||||
type MessageReference struct {
|
||||
// MessageID is the id of the originating message.
|
||||
MessageID MessageID `json:"message_id,omitempty"`
|
||||
// ChannelID is the id of the originating message's channel.
|
||||
ChannelID ChannelID `json:"channel_id,omitempty"`
|
||||
// GuildID is the id of the originating message's guild.
|
||||
GuildID GuildID `json:"guild_id,omitempty"`
|
||||
ChannelID ChannelID `json:"channel_id,string"`
|
||||
|
||||
// Field might not be provided
|
||||
MessageID MessageID `json:"message_id,string,omitempty"`
|
||||
GuildID GuildID `json:"guild_id,string,omitempty"`
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// https://discord.com/developers/docs/interactions/receiving-and-responding#message-interaction-object-message-interaction-structure
|
||||
type MessageInteraction struct {
|
||||
// ID is the id of the originating interaction.
|
||||
ID InteractionID `json:"id"`
|
||||
// Type is the type of the originating interaction.
|
||||
Type InteractionDataType `json:"type"`
|
||||
// Name is the name of the application command that was invoked with the
|
||||
// originating interaction.
|
||||
Name string `json:"name"`
|
||||
// User is the user who invoked the originating interaction.
|
||||
User User `json:"user"`
|
||||
// Member is the member who invoked the originating interaction in
|
||||
// the guild.
|
||||
Member *Member `json:"member,omitempty"`
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#attachment-object
|
||||
type Attachment struct {
|
||||
// ID is the attachment id.
|
||||
ID AttachmentID `json:"id"`
|
||||
// Filename is the name of file attached.
|
||||
Filename string `json:"filename"`
|
||||
// Description is the attachment's description. It is a maximum of 1024
|
||||
// characters long.
|
||||
Description string `json:"description,omitempty"`
|
||||
// ContentType is the media type of file.
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
// Size is the size of file in bytes.
|
||||
Size uint64 `json:"size"`
|
||||
ID AttachmentID `json:"id,string"`
|
||||
Filename string `json:"filename"`
|
||||
Size uint64 `json:"size"`
|
||||
|
||||
// URL is the source url of file.
|
||||
URL URL `json:"url"`
|
||||
// Proxy is the a proxied url of file.
|
||||
URL URL `json:"url"`
|
||||
Proxy URL `json:"proxy_url"`
|
||||
|
||||
// Height is the height of the file, if it is an image.
|
||||
// Only if Image
|
||||
Height uint `json:"height,omitempty"`
|
||||
// Width is the width of the file, if it is an image.
|
||||
Width uint `json:"width,omitempty"`
|
||||
// Ephemeral is whether this attachment is ephemeral. Ephemeral attachments
|
||||
// will automatically be removed after a set period of time. Ephemeral
|
||||
// attachments on messages are guaranteed to be available as long as
|
||||
// the message itself exists.
|
||||
Ephemeral bool `json:"ephemeral,omitempty"`
|
||||
Width uint `json:"width,omitempty"`
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// https://discord.com/developers/docs/resources/channel#reaction-object
|
||||
type Reaction struct {
|
||||
// Count is the amount of times the emoji has been used to react.
|
||||
Count int `json:"count"`
|
||||
// Me specifies whether the current user reacted using this emoji.
|
||||
Me bool `json:"me"`
|
||||
// Emoji contains emoji information.
|
||||
Count int `json:"count"`
|
||||
Me bool `json:"me"` // for current user
|
||||
Emoji Emoji `json:"emoji"`
|
||||
}
|
||||
|
|
|
@ -1,30 +1,15 @@
|
|||
package discord
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
import "fmt"
|
||||
|
||||
// Color describes an RGB color (with NO alpha). If a value is -1, then it's
|
||||
// marshaled to JSON as null.
|
||||
type Color int32
|
||||
type Color uint32
|
||||
|
||||
// DefaultEmbedColor is the default color to use for an embed.
|
||||
var DefaultEmbedColor Color = 0x303030
|
||||
|
||||
// NullColor is a Color that's marshaled to null.
|
||||
const NullColor Color = -1
|
||||
|
||||
// Uint32 returns the color as a Uint32. If the color is null, then 0 is
|
||||
// returned.
|
||||
func (c Color) Uint32() uint32 {
|
||||
if c == NullColor {
|
||||
return 0
|
||||
}
|
||||
return uint32(c)
|
||||
}
|
||||
|
||||
// Int converts Color to int.
|
||||
func (c Color) Int() int {
|
||||
return int(c)
|
||||
}
|
||||
|
@ -42,34 +27,6 @@ func (c Color) RGB() (uint8, uint8, uint8) {
|
|||
return r, g, b
|
||||
}
|
||||
|
||||
// String returns the Color in hexadecimal (#FFFFFF) format.
|
||||
func (c Color) String() string {
|
||||
r, g, b := c.RGB()
|
||||
return fmt.Sprintf("#%02X%02X%02X", r, g, b)
|
||||
}
|
||||
|
||||
func (c Color) MarshalJSON() ([]byte, error) {
|
||||
if c < 0 {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
return []byte(strconv.Itoa(c.Int())), nil
|
||||
}
|
||||
|
||||
func (c *Color) UnmarshalJSON(json []byte) error {
|
||||
s := string(json)
|
||||
|
||||
if s == "null" {
|
||||
*c = NullColor
|
||||
return nil
|
||||
}
|
||||
|
||||
v, err := strconv.ParseInt(s, 10, 32)
|
||||
*c = Color(v)
|
||||
return err
|
||||
}
|
||||
|
||||
// Embed describes a box with a left colored border that sometimes appears in
|
||||
// messages.
|
||||
type Embed struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Type EmbedType `json:"type,omitempty"`
|
||||
|
@ -89,7 +46,6 @@ type Embed struct {
|
|||
Fields []EmbedField `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// NewEmbed creates a normal embed with default values.
|
||||
func NewEmbed() *Embed {
|
||||
return &Embed{
|
||||
Type: NormalEmbed,
|
||||
|
@ -97,7 +53,23 @@ func NewEmbed() *Embed {
|
|||
}
|
||||
}
|
||||
|
||||
// Validate validates the embed.
|
||||
type ErrOverbound struct {
|
||||
Count int
|
||||
Max int
|
||||
|
||||
Thing string
|
||||
}
|
||||
|
||||
var _ error = (*ErrOverbound)(nil)
|
||||
|
||||
func (e ErrOverbound) Error() string {
|
||||
if e.Thing == "" {
|
||||
return fmt.Sprintf("Overbound error: %d > %d", e.Count, e.Max)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(e.Thing+" overbound: %d > %d", e.Count, e.Max)
|
||||
}
|
||||
|
||||
func (e *Embed) Validate() error {
|
||||
if e.Type == "" {
|
||||
e.Type = NormalEmbed
|
||||
|
@ -108,73 +80,60 @@ func (e *Embed) Validate() error {
|
|||
}
|
||||
|
||||
if len(e.Title) > 256 {
|
||||
return &OverboundError{len(e.Title), 256, "title"}
|
||||
return &ErrOverbound{len(e.Title), 256, "title"}
|
||||
}
|
||||
|
||||
if len(e.Description) > 4096 {
|
||||
return &OverboundError{len(e.Description), 4096, "description"}
|
||||
if len(e.Description) > 2048 {
|
||||
return &ErrOverbound{len(e.Description), 2048, "description"}
|
||||
}
|
||||
|
||||
if len(e.Fields) > 25 {
|
||||
return &OverboundError{len(e.Fields), 25, "fields"}
|
||||
return &ErrOverbound{len(e.Fields), 25, "fields"}
|
||||
}
|
||||
|
||||
var sum = 0 +
|
||||
len(e.Title) +
|
||||
len(e.Description)
|
||||
|
||||
if e.Footer != nil {
|
||||
if len(e.Footer.Text) > 2048 {
|
||||
return &OverboundError{len(e.Footer.Text), 2048, "footer text"}
|
||||
return &ErrOverbound{len(e.Footer.Text), 2048, "footer text"}
|
||||
}
|
||||
|
||||
sum += len(e.Footer.Text)
|
||||
}
|
||||
|
||||
if e.Author != nil {
|
||||
if len(e.Author.Name) > 256 {
|
||||
return &OverboundError{len(e.Author.Name), 256, "author name"}
|
||||
return &ErrOverbound{len(e.Author.Name), 256, "author name"}
|
||||
}
|
||||
|
||||
sum += len(e.Author.Name)
|
||||
}
|
||||
|
||||
for i, field := range e.Fields {
|
||||
if len(field.Name) > 256 {
|
||||
return &OverboundError{len(field.Name), 256,
|
||||
return &ErrOverbound{len(field.Name), 256,
|
||||
fmt.Sprintf("field %d name", i)}
|
||||
}
|
||||
|
||||
if len(field.Value) > 1024 {
|
||||
return &OverboundError{len(field.Value), 1024,
|
||||
return &ErrOverbound{len(field.Value), 1024,
|
||||
fmt.Sprintf("field %d value", i)}
|
||||
}
|
||||
|
||||
sum += len(field.Name) + len(field.Value)
|
||||
}
|
||||
|
||||
if sum := e.Length(); sum > 6000 {
|
||||
return &OverboundError{sum, 6000, "sum of all characters"}
|
||||
if sum > 6000 {
|
||||
return &ErrOverbound{sum, 6000, "sum of all characters"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Length returns the sum of the lengths of all text in the embed.
|
||||
func (e Embed) Length() int {
|
||||
var sum = 0 +
|
||||
len(e.Title) +
|
||||
len(e.Description)
|
||||
if e.Footer != nil {
|
||||
sum += len(e.Footer.Text)
|
||||
}
|
||||
if e.Author != nil {
|
||||
sum += len(e.Author.Name)
|
||||
}
|
||||
for _, field := range e.Fields {
|
||||
sum += len(field.Name) + len(field.Value)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// EmbedTypes are "loosely defined" and, for the most part, are not used by our
|
||||
// clients for rendering. Embed attributes power what is rendered.
|
||||
//
|
||||
// Deprecated: Embed types should be considered deprecated and might be removed
|
||||
// in a future API version.
|
||||
type EmbedType string
|
||||
|
||||
// Embed type constants.
|
||||
const (
|
||||
NormalEmbed EmbedType = "rich"
|
||||
ImageEmbed EmbedType = "image"
|
||||
|
@ -182,16 +141,15 @@ const (
|
|||
GIFVEmbed EmbedType = "gifv"
|
||||
ArticleEmbed EmbedType = "article"
|
||||
LinkEmbed EmbedType = "link"
|
||||
// Undocumented
|
||||
)
|
||||
|
||||
// EmbedFooter is the footer of an embed.
|
||||
type EmbedFooter struct {
|
||||
Text string `json:"text"`
|
||||
Icon URL `json:"icon_url,omitempty"`
|
||||
ProxyIcon URL `json:"proxy_icon_url,omitempty"`
|
||||
}
|
||||
|
||||
// EmbedImage is the large image of an embed.
|
||||
type EmbedImage struct {
|
||||
URL URL `json:"url"`
|
||||
Proxy URL `json:"proxy_url"`
|
||||
|
@ -199,7 +157,6 @@ type EmbedImage struct {
|
|||
Width uint `json:"width,omitempty"`
|
||||
}
|
||||
|
||||
// EmbedThumbnail is the small image of an embed. It often appears on the right.
|
||||
type EmbedThumbnail struct {
|
||||
URL URL `json:"url,omitempty"`
|
||||
Proxy URL `json:"proxy_url,omitempty"`
|
||||
|
@ -207,10 +164,8 @@ type EmbedThumbnail struct {
|
|||
Width uint `json:"width,omitempty"`
|
||||
}
|
||||
|
||||
// EmbedVideo is the video of an embed.
|
||||
type EmbedVideo struct {
|
||||
URL URL `json:"url"`
|
||||
Proxy URL `json:"proxy_url,omitempty"`
|
||||
Height uint `json:"height"`
|
||||
Width uint `json:"width"`
|
||||
}
|
||||
|
|
|
@ -2,95 +2,72 @@ package discord
|
|||
|
||||
type Permissions uint64
|
||||
|
||||
// https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
|
||||
const (
|
||||
var (
|
||||
// Allows creation of instant invites
|
||||
PermissionCreateInstantInvite Permissions = 1 << iota
|
||||
PermissionCreateInstantInvite Permissions = 1 << 0
|
||||
// Allows kicking members
|
||||
PermissionKickMembers
|
||||
PermissionKickMembers Permissions = 1 << 1
|
||||
// Allows banning members
|
||||
PermissionBanMembers
|
||||
PermissionBanMembers Permissions = 1 << 2
|
||||
// Allows all permissions and bypasses channel permission overwrites
|
||||
PermissionAdministrator
|
||||
PermissionAdministrator Permissions = 1 << 3
|
||||
// Allows management and editing of channels
|
||||
PermissionManageChannels
|
||||
PermissionManageChannels Permissions = 1 << 4
|
||||
// Allows management and editing of the guild
|
||||
PermissionManageGuild
|
||||
PermissionManageGuild Permissions = 1 << 5
|
||||
// Allows for the addition of reactions to messages
|
||||
PermissionAddReactions
|
||||
PermissionAddReactions Permissions = 1 << 6
|
||||
// Allows for viewing of audit logs
|
||||
PermissionViewAuditLog
|
||||
PermissionViewAuditLog Permissions = 1 << 7
|
||||
// Allows for using priority speaker in a voice channel
|
||||
PermissionPrioritySpeaker
|
||||
PermissionPrioritySpeaker Permissions = 1 << 8
|
||||
// Allows the user to go live
|
||||
PermissionStream
|
||||
PermissionStream Permissions = 1 << 9
|
||||
// Allows guild members to view a channel, which includes reading messages
|
||||
// in text channels
|
||||
PermissionViewChannel
|
||||
PermissionViewChannel Permissions = 1 << 10
|
||||
// Allows for sending messages in a channel
|
||||
PermissionSendMessages
|
||||
PermissionSendMessages Permissions = 1 << 11
|
||||
// Allows for sending of /tts messages
|
||||
PermissionSendTTSMessages
|
||||
PermissionSendTTSMessages Permissions = 1 << 12
|
||||
// Allows for deletion of other users messages
|
||||
PermissionManageMessages
|
||||
PermissionManageMessages Permissions = 1 << 13
|
||||
// Links sent by users with this permission will be auto-embedded
|
||||
PermissionEmbedLinks
|
||||
PermissionEmbedLinks Permissions = 1 << 14
|
||||
// Allows for uploading images and files
|
||||
PermissionAttachFiles
|
||||
PermissionAttachFiles Permissions = 1 << 15
|
||||
// Allows for reading of message history
|
||||
PermissionReadMessageHistory
|
||||
PermissionReadMessageHistory Permissions = 1 << 16
|
||||
// Allows for using the @everyone tag to notify all users in a channel,
|
||||
// and the @here tag to notify all online users in a channel
|
||||
PermissionMentionEveryone
|
||||
PermissionMentionEveryone Permissions = 1 << 17
|
||||
// Allows the usage of custom emojis from other servers
|
||||
PermissionUseExternalEmojis
|
||||
// Allows for viewing guild insights
|
||||
PermissionViewGuildInsights
|
||||
PermissionUseExternalEmojis Permissions = 1 << 18
|
||||
|
||||
// ?
|
||||
|
||||
// Allows for joining of a voice channel
|
||||
PermissionConnect
|
||||
PermissionConnect Permissions = 1 << 20
|
||||
// Allows for speaking in a voice channel
|
||||
PermissionSpeak
|
||||
PermissionSpeak Permissions = 1 << 21
|
||||
// Allows for muting members in a voice channel
|
||||
PermissionMuteMembers
|
||||
PermissionMuteMembers Permissions = 1 << 22
|
||||
// Allows for deafening of members in a voice channel
|
||||
PermissionDeafenMembers
|
||||
PermissionDeafenMembers Permissions = 1 << 23
|
||||
// Allows for moving of members between voice channels
|
||||
PermissionMoveMembers
|
||||
PermissionMoveMembers Permissions = 1 << 24
|
||||
// Allows for using voice-activity-detection in a voice channel
|
||||
PermissionUseVAD
|
||||
PermissionUseVAD Permissions = 1 << 25
|
||||
// Allows for modification of own nickname
|
||||
PermissionChangeNickname
|
||||
PermissionChangeNickname Permissions = 1 << 26
|
||||
// Allows for modification of other users nicknames
|
||||
PermissionManageNicknames
|
||||
PermissionManageNicknames Permissions = 1 << 27
|
||||
// Allows management and editing of roles
|
||||
PermissionManageRoles
|
||||
PermissionManageRoles Permissions = 1 << 28
|
||||
// Allows management and editing of webhooks
|
||||
PermissionManageWebhooks
|
||||
// Allows members to use slash commands in text channels
|
||||
PermissionManageEmojisAndStickers
|
||||
// Allows members to use slash commands in text channels
|
||||
PermissionUseSlashCommands
|
||||
// Allows for requesting to speak in stage channels. (This permission is
|
||||
// under active development and may be changed or removed.)
|
||||
PermissionRequestToSpeak
|
||||
// Allows for creating, editing, and deleting scheduled events.
|
||||
PermissionManageEvents
|
||||
// Allows for deleting and archiving threads, and viewing all private
|
||||
// threads
|
||||
PermissionManageThreads
|
||||
// Allows for creating and participating in threads.
|
||||
PermissionCreatePublicThreads
|
||||
// Allows for creating and participating in private threads.
|
||||
PermissionCreatePrivateThreads
|
||||
// Allows the usage of custom stickers from other servers
|
||||
PermissionUseExternalStickers
|
||||
// Allows for sending messages in threads
|
||||
PermissionSendMessagesInThreads
|
||||
// Allows for launching activities (applications with the EMBEDDED flag)
|
||||
// in a voice channel
|
||||
PermissionStartEmbeddedActivities
|
||||
// Allows for timing out users
|
||||
PermissionModerateMembers
|
||||
PermissionManageWebhooks Permissions = 1 << 29
|
||||
// Allows management and editing of emojis
|
||||
PermissionManageEmojis Permissions = 1 << 30
|
||||
|
||||
PermissionAllText = 0 |
|
||||
PermissionViewChannel |
|
||||
|
@ -101,34 +78,25 @@ const (
|
|||
PermissionAttachFiles |
|
||||
PermissionReadMessageHistory |
|
||||
PermissionMentionEveryone |
|
||||
PermissionUseExternalEmojis |
|
||||
PermissionUseSlashCommands |
|
||||
PermissionManageThreads |
|
||||
PermissionCreatePublicThreads |
|
||||
PermissionCreatePrivateThreads |
|
||||
PermissionUseExternalStickers |
|
||||
PermissionAddReactions |
|
||||
PermissionSendMessagesInThreads
|
||||
PermissionUseExternalEmojis
|
||||
|
||||
PermissionAllVoice = 0 |
|
||||
PermissionViewChannel |
|
||||
PermissionConnect |
|
||||
PermissionSpeak |
|
||||
PermissionStream |
|
||||
PermissionMuteMembers |
|
||||
PermissionDeafenMembers |
|
||||
PermissionMoveMembers |
|
||||
PermissionUseVAD |
|
||||
PermissionPrioritySpeaker |
|
||||
PermissionRequestToSpeak |
|
||||
PermissionStartEmbeddedActivities
|
||||
PermissionPrioritySpeaker
|
||||
|
||||
PermissionAllChannel = 0 |
|
||||
PermissionAllText |
|
||||
PermissionAllVoice |
|
||||
PermissionCreateInstantInvite |
|
||||
PermissionManageRoles |
|
||||
PermissionManageChannels
|
||||
PermissionManageChannels |
|
||||
PermissionAddReactions |
|
||||
PermissionViewAuditLog
|
||||
|
||||
PermissionAll = 0 |
|
||||
PermissionAllChannel |
|
||||
|
@ -137,21 +105,11 @@ const (
|
|||
PermissionManageGuild |
|
||||
PermissionAdministrator |
|
||||
PermissionManageWebhooks |
|
||||
PermissionManageEmojisAndStickers |
|
||||
PermissionManageEmojis |
|
||||
PermissionManageNicknames |
|
||||
PermissionChangeNickname |
|
||||
PermissionViewAuditLog |
|
||||
PermissionManageEvents
|
||||
PermissionChangeNickname
|
||||
)
|
||||
|
||||
func NewPermissions(p ...Permissions) *Permissions {
|
||||
var perm Permissions
|
||||
for _, permission := range p {
|
||||
perm |= permission
|
||||
}
|
||||
return &perm
|
||||
}
|
||||
|
||||
func (p Permissions) Has(perm Permissions) bool {
|
||||
return HasFlag(uint64(p), uint64(perm))
|
||||
}
|
||||
|
@ -187,7 +145,7 @@ func CalcOverwrites(guild Guild, channel Channel, member Member) Permissions {
|
|||
return PermissionAll
|
||||
}
|
||||
|
||||
for _, overwrite := range channel.Overwrites {
|
||||
for _, overwrite := range channel.Permissions {
|
||||
if GuildID(overwrite.ID) == guild.ID {
|
||||
perm &= ^overwrite.Deny
|
||||
perm |= overwrite.Allow
|
||||
|
@ -197,9 +155,9 @@ func CalcOverwrites(guild Guild, channel Channel, member Member) Permissions {
|
|||
|
||||
var deny, allow Permissions
|
||||
|
||||
for _, overwrite := range channel.Overwrites {
|
||||
for _, overwrite := range channel.Permissions {
|
||||
for _, id := range member.RoleIDs {
|
||||
if id == RoleID(overwrite.ID) && overwrite.Type == OverwriteRole {
|
||||
if id == RoleID(overwrite.ID) && overwrite.Type == "role" {
|
||||
deny |= overwrite.Deny
|
||||
allow |= overwrite.Allow
|
||||
break
|
||||
|
@ -210,7 +168,7 @@ func CalcOverwrites(guild Guild, channel Channel, member Member) Permissions {
|
|||
perm &= ^deny
|
||||
perm |= allow
|
||||
|
||||
for _, overwrite := range channel.Overwrites {
|
||||
for _, overwrite := range channel.Permissions {
|
||||
if UserID(overwrite.ID) == member.User.ID {
|
||||
perm &= ^overwrite.Deny
|
||||
perm |= overwrite.Allow
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
package discord
|
||||
|
||||
// EventStatus describes the different statuses GuildScheduledEvent can be.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-status
|
||||
type EventStatus int
|
||||
|
||||
const (
|
||||
ScheduledEvent EventStatus = iota + 1
|
||||
ActiveEvent
|
||||
CompletedEvent
|
||||
CancelledEvent
|
||||
)
|
||||
|
||||
// EntityType describes the different types GuildScheduledEvent can be.
|
||||
type EntityType int
|
||||
|
||||
const (
|
||||
StageInstanceEntity EntityType = iota + 1
|
||||
VoiceEntity
|
||||
ExternalEntity
|
||||
)
|
||||
|
||||
// ScheduledEventPrivacy describes the privacy levels of GuildScheduledEvent.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-privacy-level
|
||||
type ScheduledEventPrivacyLevel int
|
||||
|
||||
const (
|
||||
// GuildOnly requires the scheduled event to be only accessible to guild members.
|
||||
GuildOnly ScheduledEventPrivacyLevel = iota + 2
|
||||
)
|
||||
|
||||
// GuildScheduledEvent describes the scheduled event structure.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-structure
|
||||
type GuildScheduledEvent struct {
|
||||
// ID is the id of the scheduled event.
|
||||
ID EventID `json:"id"`
|
||||
// GuildID is the guild id of where the scheduled event belongs to.
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
// ChannelID is the channel id in which the scheduled event will be
|
||||
// hosted at, this may be NullChannelID if the EntityType is set
|
||||
// to ExternalEntity.
|
||||
ChannelID ChannelID `json:"channel_id"`
|
||||
// CreatorID is the user id of who created the scheduled event.
|
||||
CreatorID UserID `json:"creator_id"`
|
||||
// Name is the name of the scheduled event.
|
||||
Name string `json:"name"`
|
||||
// Description is the description of the scheduled event.
|
||||
Description string `json:"description"`
|
||||
// StartTime is when the scheduled event will start at.
|
||||
StartTime Timestamp `json:"scheduled_start_time"`
|
||||
// EndTime is when the scheduled event will end at, if it does.
|
||||
EndTime Timestamp `json:"scheduled_end_time"`
|
||||
// PrivacyLevel is the privacy level of the scheduled event.
|
||||
PrivacyLevel ScheduledEventPrivacyLevel `json:"privacy_level"`
|
||||
// Status is the status of the scheduled event.
|
||||
Status EventStatus `json:"status"`
|
||||
// EntityType describes the type of scheduled event.
|
||||
EntityType EntityType `json:"entity_type"`
|
||||
// EntityID is the id of an entity associated with a scheduled event.
|
||||
EntityID EntityID `json:"entity_id"`
|
||||
// EntityMetadata is additional metadata for the scheduled event.
|
||||
EntityMetadata *EntityMetadata `json:"entity_metadata"`
|
||||
// Creator is the the user responsible for creating the scheduled event. This field
|
||||
// will only be present if CreatorID is
|
||||
Creator *User `json:"creator"`
|
||||
// UserCount is the number of users subscribed to the scheduled event.
|
||||
UserCount int `json:"user_count"`
|
||||
// Image is the cover image hash of the scheduled event.
|
||||
Image Hash `json:"image,omitempty"`
|
||||
}
|
||||
|
||||
// EntityMetadata is the entity metadata of GuildScheduledEvent.
|
||||
type EntityMetadata struct {
|
||||
// Location describes where the event takes place at. This is not
|
||||
// optional when GuildScheduled#EntityType is set as ExternalEntity.
|
||||
Location string `json:"location,omitempty"`
|
||||
}
|
|
@ -15,31 +15,16 @@ func DurationSinceEpoch(t time.Time) time.Duration {
|
|||
return time.Duration(t.UnixNano()) - Epoch
|
||||
}
|
||||
|
||||
//go:generate go run ../utils/cmd/gensnowflake -o snowflake_types.go AppID AttachmentID AuditLogEntryID ChannelID CommandID EmojiID GuildID IntegrationID InteractionID MessageID RoleID StageID StickerID StickerPackID TagID TeamID UserID WebhookID EventID EntityID
|
||||
|
||||
// Mention generates the mention syntax for this channel ID.
|
||||
func (s ChannelID) Mention() string { return "<#" + s.String() + ">" }
|
||||
|
||||
// Mention generates the mention syntax for this role ID.
|
||||
func (s RoleID) Mention() string { return "<@&" + s.String() + ">" }
|
||||
|
||||
// Mention generates the mention syntax for this user ID.
|
||||
func (s UserID) Mention() string { return "<@" + s.String() + ">" }
|
||||
|
||||
// Snowflake is the format of Discord's ID type. It is a format that can be
|
||||
// sorted chronologically.
|
||||
type Snowflake uint64
|
||||
|
||||
// NullSnowflake gets encoded into a null. This is used for
|
||||
// optional and nullable snowflake fields.
|
||||
const NullSnowflake = ^Snowflake(0)
|
||||
|
||||
// NewSnowflake creates a new snowflake from the given time.
|
||||
func NewSnowflake(t time.Time) Snowflake {
|
||||
return Snowflake((DurationSinceEpoch(t) / time.Millisecond) << 22)
|
||||
}
|
||||
|
||||
// ParseSnowflake parses a snowflake.
|
||||
func ParseSnowflake(sf string) (Snowflake, error) {
|
||||
if sf == "null" {
|
||||
return NullSnowflake, nil
|
||||
|
@ -87,8 +72,7 @@ func (s Snowflake) IsValid() bool {
|
|||
return !(int64(s) == 0 || s == NullSnowflake)
|
||||
}
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
// IsNull returns whether or not the snowflake is null.
|
||||
func (s Snowflake) IsNull() bool {
|
||||
return s == NullSnowflake
|
||||
}
|
||||
|
@ -109,3 +93,160 @@ func (s Snowflake) PID() uint8 {
|
|||
func (s Snowflake) Increment() uint16 {
|
||||
return uint16(s & 0xFFF)
|
||||
}
|
||||
|
||||
type AppID Snowflake
|
||||
|
||||
const NullAppID = AppID(NullSnowflake)
|
||||
|
||||
func (s AppID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AppID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s AppID) String() string { return Snowflake(s).String() }
|
||||
func (s AppID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s AppID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s AppID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AppID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AppID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AppID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type AttachmentID Snowflake
|
||||
|
||||
const NullAttachmentID = AttachmentID(NullSnowflake)
|
||||
|
||||
func (s AttachmentID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AttachmentID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s AttachmentID) String() string { return Snowflake(s).String() }
|
||||
func (s AttachmentID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s AttachmentID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s AttachmentID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AttachmentID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AttachmentID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AttachmentID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type AuditLogEntryID Snowflake
|
||||
|
||||
const NullAuditLogEntryID = AuditLogEntryID(NullSnowflake)
|
||||
|
||||
func (s AuditLogEntryID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AuditLogEntryID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s AuditLogEntryID) String() string { return Snowflake(s).String() }
|
||||
func (s AuditLogEntryID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s AuditLogEntryID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s AuditLogEntryID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AuditLogEntryID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AuditLogEntryID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AuditLogEntryID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type ChannelID Snowflake
|
||||
|
||||
const NullChannelID = ChannelID(NullSnowflake)
|
||||
|
||||
func (s ChannelID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *ChannelID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s ChannelID) String() string { return Snowflake(s).String() }
|
||||
func (s ChannelID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s ChannelID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s ChannelID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s ChannelID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s ChannelID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s ChannelID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
func (s ChannelID) Mention() string { return "<#" + s.String() + ">" }
|
||||
|
||||
type EmojiID Snowflake
|
||||
|
||||
const NullEmojiID = EmojiID(NullSnowflake)
|
||||
|
||||
func (s EmojiID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *EmojiID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s EmojiID) String() string { return Snowflake(s).String() }
|
||||
func (s EmojiID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s EmojiID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s EmojiID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s EmojiID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s EmojiID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s EmojiID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type IntegrationID Snowflake
|
||||
|
||||
const NullIntegrationID = IntegrationID(NullSnowflake)
|
||||
|
||||
func (s IntegrationID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *IntegrationID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s IntegrationID) String() string { return Snowflake(s).String() }
|
||||
func (s IntegrationID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s IntegrationID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s IntegrationID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s IntegrationID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s IntegrationID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s IntegrationID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type GuildID Snowflake
|
||||
|
||||
const NullGuildID = GuildID(NullSnowflake)
|
||||
|
||||
func (s GuildID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *GuildID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s GuildID) String() string { return Snowflake(s).String() }
|
||||
func (s GuildID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s GuildID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s GuildID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s GuildID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s GuildID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s GuildID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type MessageID Snowflake
|
||||
|
||||
const NullMessageID = MessageID(NullSnowflake)
|
||||
|
||||
func (s MessageID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *MessageID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s MessageID) String() string { return Snowflake(s).String() }
|
||||
func (s MessageID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s MessageID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s MessageID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s MessageID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s MessageID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s MessageID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
type RoleID Snowflake
|
||||
|
||||
const NullRoleID = RoleID(NullSnowflake)
|
||||
|
||||
func (s RoleID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *RoleID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s RoleID) String() string { return Snowflake(s).String() }
|
||||
func (s RoleID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s RoleID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s RoleID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s RoleID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s RoleID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s RoleID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
func (s RoleID) Mention() string { return "<@&" + s.String() + ">" }
|
||||
|
||||
type UserID Snowflake
|
||||
|
||||
const NullUserID = UserID(NullSnowflake)
|
||||
|
||||
func (s UserID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *UserID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s UserID) String() string { return Snowflake(s).String() }
|
||||
func (s UserID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s UserID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s UserID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s UserID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s UserID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s UserID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
func (s UserID) Mention() string { return "<@" + s.String() + ">" }
|
||||
|
||||
type WebhookID Snowflake
|
||||
|
||||
const NullWebhookID = WebhookID(NullSnowflake)
|
||||
|
||||
func (s WebhookID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *WebhookID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
func (s WebhookID) String() string { return Snowflake(s).String() }
|
||||
func (s WebhookID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
func (s WebhookID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
func (s WebhookID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s WebhookID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s WebhookID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s WebhookID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
|
|
@ -1,485 +0,0 @@
|
|||
// Code generated by gensnowflake. DO NOT EDIT.
|
||||
|
||||
package discord
|
||||
|
||||
import "time"
|
||||
|
||||
// AppID is the snowflake type for a AppID.
|
||||
type AppID Snowflake
|
||||
|
||||
// NullAppID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullAppID = AppID(NullSnowflake)
|
||||
|
||||
func (s AppID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AppID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s AppID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s AppID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s AppID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s AppID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AppID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AppID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AppID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// AttachmentID is the snowflake type for a AttachmentID.
|
||||
type AttachmentID Snowflake
|
||||
|
||||
// NullAttachmentID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullAttachmentID = AttachmentID(NullSnowflake)
|
||||
|
||||
func (s AttachmentID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AttachmentID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s AttachmentID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s AttachmentID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s AttachmentID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s AttachmentID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AttachmentID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AttachmentID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AttachmentID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// AuditLogEntryID is the snowflake type for a AuditLogEntryID.
|
||||
type AuditLogEntryID Snowflake
|
||||
|
||||
// NullAuditLogEntryID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullAuditLogEntryID = AuditLogEntryID(NullSnowflake)
|
||||
|
||||
func (s AuditLogEntryID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *AuditLogEntryID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s AuditLogEntryID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s AuditLogEntryID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s AuditLogEntryID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s AuditLogEntryID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s AuditLogEntryID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s AuditLogEntryID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s AuditLogEntryID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// ChannelID is the snowflake type for a ChannelID.
|
||||
type ChannelID Snowflake
|
||||
|
||||
// NullChannelID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullChannelID = ChannelID(NullSnowflake)
|
||||
|
||||
func (s ChannelID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *ChannelID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s ChannelID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s ChannelID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s ChannelID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s ChannelID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s ChannelID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s ChannelID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s ChannelID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// CommandID is the snowflake type for a CommandID.
|
||||
type CommandID Snowflake
|
||||
|
||||
// NullCommandID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullCommandID = CommandID(NullSnowflake)
|
||||
|
||||
func (s CommandID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *CommandID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s CommandID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s CommandID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s CommandID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s CommandID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s CommandID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s CommandID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s CommandID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// EmojiID is the snowflake type for a EmojiID.
|
||||
type EmojiID Snowflake
|
||||
|
||||
// NullEmojiID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullEmojiID = EmojiID(NullSnowflake)
|
||||
|
||||
func (s EmojiID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *EmojiID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s EmojiID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s EmojiID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s EmojiID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s EmojiID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s EmojiID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s EmojiID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s EmojiID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// GuildID is the snowflake type for a GuildID.
|
||||
type GuildID Snowflake
|
||||
|
||||
// NullGuildID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullGuildID = GuildID(NullSnowflake)
|
||||
|
||||
func (s GuildID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *GuildID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s GuildID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s GuildID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s GuildID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s GuildID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s GuildID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s GuildID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s GuildID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// IntegrationID is the snowflake type for a IntegrationID.
|
||||
type IntegrationID Snowflake
|
||||
|
||||
// NullIntegrationID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullIntegrationID = IntegrationID(NullSnowflake)
|
||||
|
||||
func (s IntegrationID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *IntegrationID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s IntegrationID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s IntegrationID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s IntegrationID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s IntegrationID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s IntegrationID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s IntegrationID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s IntegrationID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// InteractionID is the snowflake type for a InteractionID.
|
||||
type InteractionID Snowflake
|
||||
|
||||
// NullInteractionID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullInteractionID = InteractionID(NullSnowflake)
|
||||
|
||||
func (s InteractionID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *InteractionID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s InteractionID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s InteractionID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s InteractionID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s InteractionID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s InteractionID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s InteractionID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s InteractionID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// MessageID is the snowflake type for a MessageID.
|
||||
type MessageID Snowflake
|
||||
|
||||
// NullMessageID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullMessageID = MessageID(NullSnowflake)
|
||||
|
||||
func (s MessageID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *MessageID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s MessageID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s MessageID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s MessageID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s MessageID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s MessageID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s MessageID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s MessageID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// RoleID is the snowflake type for a RoleID.
|
||||
type RoleID Snowflake
|
||||
|
||||
// NullRoleID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullRoleID = RoleID(NullSnowflake)
|
||||
|
||||
func (s RoleID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *RoleID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s RoleID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s RoleID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s RoleID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s RoleID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s RoleID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s RoleID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s RoleID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// StageID is the snowflake type for a StageID.
|
||||
type StageID Snowflake
|
||||
|
||||
// NullStageID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullStageID = StageID(NullSnowflake)
|
||||
|
||||
func (s StageID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *StageID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s StageID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s StageID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s StageID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s StageID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s StageID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s StageID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s StageID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// StickerID is the snowflake type for a StickerID.
|
||||
type StickerID Snowflake
|
||||
|
||||
// NullStickerID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullStickerID = StickerID(NullSnowflake)
|
||||
|
||||
func (s StickerID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *StickerID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s StickerID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s StickerID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s StickerID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s StickerID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s StickerID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s StickerID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s StickerID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// StickerPackID is the snowflake type for a StickerPackID.
|
||||
type StickerPackID Snowflake
|
||||
|
||||
// NullStickerPackID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullStickerPackID = StickerPackID(NullSnowflake)
|
||||
|
||||
func (s StickerPackID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *StickerPackID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s StickerPackID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s StickerPackID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s StickerPackID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s StickerPackID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s StickerPackID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s StickerPackID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s StickerPackID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// TagID is the snowflake type for a TagID.
|
||||
type TagID Snowflake
|
||||
|
||||
// NullTagID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullTagID = TagID(NullSnowflake)
|
||||
|
||||
func (s TagID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *TagID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s TagID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s TagID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s TagID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s TagID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s TagID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s TagID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s TagID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// TeamID is the snowflake type for a TeamID.
|
||||
type TeamID Snowflake
|
||||
|
||||
// NullTeamID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullTeamID = TeamID(NullSnowflake)
|
||||
|
||||
func (s TeamID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *TeamID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s TeamID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s TeamID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s TeamID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s TeamID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s TeamID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s TeamID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s TeamID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// UserID is the snowflake type for a UserID.
|
||||
type UserID Snowflake
|
||||
|
||||
// NullUserID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullUserID = UserID(NullSnowflake)
|
||||
|
||||
func (s UserID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *UserID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s UserID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s UserID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s UserID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s UserID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s UserID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s UserID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s UserID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// WebhookID is the snowflake type for a WebhookID.
|
||||
type WebhookID Snowflake
|
||||
|
||||
// NullWebhookID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullWebhookID = WebhookID(NullSnowflake)
|
||||
|
||||
func (s WebhookID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *WebhookID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s WebhookID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s WebhookID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s WebhookID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s WebhookID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s WebhookID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s WebhookID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s WebhookID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// EventID is the snowflake type for a EventID.
|
||||
type EventID Snowflake
|
||||
|
||||
// NullEventID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullEventID = EventID(NullSnowflake)
|
||||
|
||||
func (s EventID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *EventID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s EventID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s EventID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s EventID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s EventID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s EventID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s EventID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s EventID) Increment() uint16 { return Snowflake(s).Increment() }
|
||||
|
||||
// EntityID is the snowflake type for a EntityID.
|
||||
type EntityID Snowflake
|
||||
|
||||
// NullEntityID gets encoded into a null. This is used for optional and nullable snowflake fields.
|
||||
const NullEntityID = EntityID(NullSnowflake)
|
||||
|
||||
func (s EntityID) MarshalJSON() ([]byte, error) { return Snowflake(s).MarshalJSON() }
|
||||
func (s *EntityID) UnmarshalJSON(v []byte) error { return (*Snowflake)(s).UnmarshalJSON(v) }
|
||||
|
||||
// String returns the ID, or nothing if the snowflake isn't valid.
|
||||
func (s EntityID) String() string { return Snowflake(s).String() }
|
||||
|
||||
// IsValid returns whether or not the snowflake is valid.
|
||||
func (s EntityID) IsValid() bool { return Snowflake(s).IsValid() }
|
||||
|
||||
// IsNull returns whether or not the snowflake is null. This method is rarely
|
||||
// ever useful; most people should use IsValid instead.
|
||||
func (s EntityID) IsNull() bool { return Snowflake(s).IsNull() }
|
||||
|
||||
func (s EntityID) Time() time.Time { return Snowflake(s).Time() }
|
||||
func (s EntityID) Worker() uint8 { return Snowflake(s).Worker() }
|
||||
func (s EntityID) PID() uint8 { return Snowflake(s).PID() }
|
||||
func (s EntityID) Increment() uint16 { return Snowflake(s).Increment() }
|
|
@ -1,31 +0,0 @@
|
|||
package discord
|
||||
|
||||
// A StageInstance holds information about a live stage instance.
|
||||
//
|
||||
// https://discord.com/developers/docs/resources/stage-instance#stage-instance-object
|
||||
type StageInstance struct {
|
||||
// ID is the id of this Stage instance.
|
||||
ID StageID `json:"id"`
|
||||
// GuildID is the guild id of the associated Stage channel.
|
||||
GuildID GuildID `json:"guild_id"`
|
||||
// ChannelID is the id of the associated Stage channel.
|
||||
ChannelID ChannelID `json:"channel_id"`
|
||||
// Topic is the topic of the Stage instance (1-120 characters).
|
||||
Topic string `json:"topic"`
|
||||
// PrivacyLevel is the privacy level of the Stage instance.
|
||||
PrivacyLevel PrivacyLevel `json:"privacy_level"`
|
||||
// NotDiscoverable defines whether or not Stage discovery is disabled.
|
||||
NotDiscoverable bool `json:"discoverable_disabled"`
|
||||
}
|
||||
|
||||
type PrivacyLevel int
|
||||
|
||||
// https://discord.com/developers/docs/resources/stage-instance#stage-instance-object-privacy-level
|
||||
const (
|
||||
// PublicStage is used if a StageInstance instance is visible publicly, such as on
|
||||
// StageInstance discovery.
|
||||
PublicStage PrivacyLevel = iota + 1
|
||||
// GuildOnlyStage is used if a StageInstance instance is visible to only guild
|
||||
// members.
|
||||
GuildOnlyStage
|
||||
)
|
|
@ -30,7 +30,6 @@ func NowTimestamp() Timestamp {
|
|||
func (t *Timestamp) UnmarshalJSON(v []byte) error {
|
||||
str := strings.Trim(string(v), `"`)
|
||||
if str == "null" {
|
||||
*t = Timestamp{}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -108,18 +107,9 @@ func DurationToSeconds(dura time.Duration) Seconds {
|
|||
func (s Seconds) MarshalJSON() ([]byte, error) {
|
||||
if s < 1 {
|
||||
return []byte("null"), nil
|
||||
} else {
|
||||
return []byte(strconv.Itoa(int(s))), nil
|
||||
}
|
||||
|
||||
return []byte(strconv.Itoa(int(s))), nil
|
||||
}
|
||||
|
||||
func (s *Seconds) UnmarshalJSON(data []byte) error {
|
||||
if string(data) == "null" {
|
||||
*s = NullSecond
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, (*int)(s))
|
||||
}
|
||||
|
||||
func (s Seconds) String() string {
|
||||
|
@ -132,18 +122,6 @@ func (s Seconds) Duration() time.Duration {
|
|||
|
||||
//
|
||||
|
||||
// OptionalSeconds is the option type for Seconds.
|
||||
type OptionalSeconds = *Seconds
|
||||
|
||||
// ZeroOptionalSeconds are 0 OptionalSeconds.
|
||||
var ZeroOptionalSeconds = NewOptionalSeconds(0)
|
||||
|
||||
// NewOptionalSeconds creates a new OptionalSeconds using the value of the
|
||||
// passed Seconds.
|
||||
func NewOptionalSeconds(s Seconds) OptionalSeconds { return &s }
|
||||
|
||||
//
|
||||
|
||||
// Milliseconds is in float64 because some Discord events return time with a
|
||||
// trailing decimal.
|
||||
type Milliseconds float64
|
||||
|
@ -160,34 +138,3 @@ func (ms Milliseconds) Duration() time.Duration {
|
|||
const f64ms = Milliseconds(time.Millisecond)
|
||||
return time.Duration(ms * f64ms)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
// ArchiveDuration is the duration after which a thread without activity will
|
||||
// be archived.
|
||||
//
|
||||
// The duration's unit is minutes.
|
||||
type ArchiveDuration int
|
||||
|
||||
const (
|
||||
OneHourArchive ArchiveDuration = 60
|
||||
OneDayArchive ArchiveDuration = 24 * OneHourArchive
|
||||
// ThreeDaysArchive archives a thread after three days.
|
||||
//
|
||||
// This duration is only available to nitro boosted guilds. The Features
|
||||
// field of a Guild will indicate whether this is the case.
|
||||
ThreeDaysArchive ArchiveDuration = 3 * OneDayArchive
|
||||
// SevenDaysArchive archives a thread after seven days.
|
||||
//
|
||||
// This duration is only available to nitro boosted guilds. The Features
|
||||
// field of a Guild will indicate whether this is the case.
|
||||
SevenDaysArchive ArchiveDuration = 7 * OneDayArchive
|
||||
)
|
||||
|
||||
func (m ArchiveDuration) String() string {
|
||||
return m.Duration().String()
|
||||
}
|
||||
|
||||
func (m ArchiveDuration) Duration() time.Duration {
|
||||
return time.Duration(m) * time.Minute
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue