1
0
Fork 0
mirror of https://github.com/diamondburned/arikawa.git synced 2025-03-22 18:09:21 +00:00

api: Add missing slash command endpoints (#252)

* api: add missing slash command endpoints
* fix examples
* api: Remove Original prefix from Client.OriginalInteractionResponse,
  EditOriginalResponse, and DeleteOriginalResponse
This commit is contained in:
Maximilian von Lindern 2021-08-03 20:44:20 +02:00 committed by GitHub
parent 127c6776ac
commit 101fe46313
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 409 additions and 34 deletions

View file

@ -9,7 +9,6 @@ import (
"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 `APP_ID="APP ID" GUILD_ID="GUILD ID" BOT_TOKEN="TOKEN HERE" go run .`
@ -35,8 +34,8 @@ func main() {
data := api.InteractionResponse{
Type: api.MessageInteractionWithSource,
Data: &api.InteractionResponseData{
Content: option.NewNullableString("This is a message with a button!"),
Components: &[]discord.Component{
Content: "This is a message with a button!",
Components: []discord.Component{
discord.ActionRowComponent{
Components: []discord.Component{
discord.ButtonComponent{
@ -85,7 +84,7 @@ func main() {
data := api.InteractionResponse{
Type: api.UpdateMessage,
Data: &api.InteractionResponseData{
Content: option.NewNullableString("Custom ID: " + e.Data.CustomID),
Content: "Custom ID: " + e.Data.CustomID,
},
}

View file

@ -9,7 +9,6 @@ import (
"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 `APP_ID="APP ID" GUILD_ID="GUILD ID" BOT_TOKEN="TOKEN HERE" go run .`
@ -33,7 +32,7 @@ func main() {
data := api.InteractionResponse{
Type: api.MessageInteractionWithSource,
Data: &api.InteractionResponseData{
Content: option.NewNullableString("Pong!"),
Content: "Pong!",
},
}

View file

@ -7,6 +7,7 @@ import (
var EndpointApplications = Endpoint + "applications/"
// https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command-json-params
type CreateCommandData struct {
Name string `json:"name"`
Description string `json:"description"`
@ -61,6 +62,22 @@ func (c *Client) DeleteCommand(appID discord.AppID, commandID discord.CommandID)
)
}
// 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 []discord.Command) ([]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) {
@ -122,3 +139,92 @@ func (c *Client) DeleteGuildCommand(
"/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 []discord.Command) ([]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),
)
}

View file

@ -2,6 +2,9 @@ package api
import (
"mime/multipart"
"strconv"
"github.com/pkg/errors"
"github.com/diamondburned/arikawa/v3/discord"
"github.com/diamondburned/arikawa/v3/utils/json/option"
@ -12,7 +15,7 @@ var EndpointInteractions = Endpoint + "interactions/"
type InteractionResponseType uint
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactioncallbacktype
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-callback-type
const (
PongInteraction InteractionResponseType = iota + 1
_
@ -23,42 +26,226 @@ const (
UpdateMessage
)
// InteractionResponseFlags implements flags for an
// InteractionApplicationCommandCallbackData.
//
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-object-interaction-application-command-callback-data-flags
type InteractionResponseFlags uint
const EphemeralResponse InteractionResponseFlags = 64
type InteractionResponse struct {
Type InteractionResponseType `json:"type"`
Data *InteractionResponseData `json:"data,omitempty"`
}
// InteractionResponseFlags implements flags for an InteractionApplicationCommandCallbackData.
// https://discord.com/developers/docs/interactions/slash-commands#interaction-response-interactionapplicationcommandcallbackdata
type InteractionResponseFlags uint
// NeedsMultipart returns true if the InteractionResponse has files.
func (resp InteractionResponse) NeedsMultipart() bool {
return resp.Data != nil && resp.Data.NeedsMultipart()
}
const EphemeralResponse InteractionResponseFlags = 64
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 {
TTS option.NullableBool `json:"tts,omitempty"`
Content option.NullableString `json:"content,omitempty"`
Components *[]discord.Component `json:"components,omitempty"`
Embeds *[]discord.Embed `json:"embeds,omitempty"`
AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"`
Flags InteractionResponseFlags `json:"flags,omitempty"`
Files []sendpart.File `json:"-"`
// Content are the message contents (up to 2000 characters).
//
// Required: one of content, file, embeds
Content string `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.Component `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.
Flags InteractionResponseFlags `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:"-"`
}
// 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)
}
// 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.Content == "" && len(resp.Data.Embeds) == 0 && 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")
}
}
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"}
}
}
var URL = EndpointInteractions + id.String() + "/" + token + "/callback"
return sendpart.POST(c.Client, resp, nil, URL)
}
// NeedsMultipart returns true if the InteractionResponse has files.
func (resp InteractionResponse) NeedsMultipart() bool {
return resp.Data != nil && len(resp.Data.Files) > 0
// 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")
}
func (resp InteractionResponse) WriteMultipart(body *multipart.Writer) error {
return sendpart.Write(body, resp, resp.Data.Files)
type EditInteractionResponseData 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.Component `json:"components,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"`
// 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 _, 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"}
}
}
}
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.
func (c *Client) CreateInteractionFollowup(
appID discord.AppID, token string, data InteractionResponseData) (*discord.Message, error) {
if data.Content == "" && 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")
}
}
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"}
}
}
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 _, 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"}
}
}
}
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())
}

View file

@ -129,6 +129,10 @@ type ExecuteData struct {
// 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.
@ -145,8 +149,8 @@ type ExecuteData struct {
// the message.
Components []discord.Component `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 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.
@ -200,9 +204,12 @@ func (c *Client) execute(data ExecuteData, wait bool) (*discord.Message, error)
}
}
var param url.Values
param := make(url.Values, 2)
if wait {
param = url.Values{"wait": {"true"}}
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()
@ -216,8 +223,15 @@ func (c *Client) execute(data ExecuteData, wait bool) (*discord.Message, error)
return msg, sendpart.POST(c.Client, data, ptr, URL)
}
// https://discord.com/developers/docs/resources/webhook#edit-webhook-message-jsonform-params
// 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"`

View file

@ -1,13 +1,60 @@
package discord
import "time"
import (
"time"
"github.com/diamondburned/arikawa/v3/utils/json"
)
// 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/slash-commands#application-command-object
type Command struct {
ID CommandID `json:"id"`
AppID AppID `json:"application_id"`
Name string `json:"name"`
Description string `json:"description"`
Options []CommandOption `json:"options,omitempty"`
// ID is the unique id of the command.
ID CommandID `json:"id"`
// 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"`
// Description is the 1-100 character description.
Description string `json:"description"`
// Options are the parameters for the command.
//
// Note that required options must be listed before optional options, and
// a command, or each individual subcommand, can have a maximum of 25
// options.
Options []CommandOption `json:"options,omitempty"`
// NoDefaultPermissions defines whether the command is NOT enabled by
// default when the app is added to a guild.
NoDefaultPermission bool `json:"default_permission"`
}
func (c Command) MarshalJSON() ([]byte, error) {
type RawCommand Command
// 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 = !c.NoDefaultPermission
return json.Marshal((RawCommand)(c))
}
func (c *Command) UnmarshalJSON(data []byte) error {
type RawCommand Command
if err := json.Unmarshal(data, (*RawCommand)(c)); 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 = !c.NoDefaultPermission
return nil
}
// CreatedAt returns a time object representing when the command was created.
@ -43,3 +90,26 @@ type CommandOptionChoice struct {
Name string `json:"name"`
Value string `json:"value"`
}
// 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
)