Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Slash Commands, Message Components, and Interactions #31

Open
wants to merge 47 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a93903d
Add support for allowed mentions
Sep 24, 2021
b585b97
Add missing fields for Channel
Sep 24, 2021
7a50618
Fix up
Sep 24, 2021
2be4803
Add support for interaction related features
Sep 21, 2021
51a3e8f
Add an example for interactions
Sep 24, 2021
570053d
Add extra fields related to interactions and components
Sep 24, 2021
aa9aa15
Add new `channel_types` field for `ApplicationCommandOption`
Sep 29, 2021
71c9e6a
Add missing fields and object
Oct 3, 2021
bc851e1
Fix mapping error
Oct 3, 2021
51d6735
Split `InteractionData` for Application Command and Message Component
Oct 5, 2021
9f49b6a
Convert String value to Snowflake if the ApplicationCommandOptionType…
Oct 7, 2021
f082168
Fix mapping error
Oct 13, 2021
9d0257e
Merge branch 'master' into interactions
Oct 13, 2021
6249620
Remove unnecessary comment
Oct 13, 2021
a08c73a
Merge branch 'interactions' of https://github.com/soya-daizu/discordc…
Oct 13, 2021
c4b3e3b
Merge branch 'master' into interactions
Oct 19, 2021
a6b4389
Rewrite `ApplicationCommandInteractionDataOption` json serialization
Oct 20, 2021
c8e7cff
Merge branch 'interactions' of https://github.com/soya-daizu/discordc…
Oct 20, 2021
4b56412
Add autocomplete support
Oct 30, 2021
1aaac82
Fix mapping error
Nov 2, 2021
b2709bf
`min_value` and `max_value` for ApplicationCommandOption
Nov 2, 2021
9607356
add locale fields in Interactions
Jan 13, 2022
199edd4
Text Input support
Feb 9, 2022
6f765d4
Modal support
Feb 9, 2022
c644bcf
Attachment support
Feb 9, 2022
207150a
Update sample
Feb 9, 2022
c6eec22
Add missing field
Feb 11, 2022
5ca416e
Add missing interaction api responses
Feb 15, 2022
5c85082
Merge branch 'shardlab:master' into interactions
Feb 24, 2022
322042d
Format code
Feb 24, 2022
fbc51c1
Fix up
Mar 2, 2022
1a3b593
Support command localization
Mar 26, 2022
9c73852
Fixes for deferring and following up interactions
Apr 11, 2022
4849247
Merge branch 'master' into interactions
Apr 28, 2022
59eaf81
Add partial support for permissions v2
Apr 28, 2022
dc91cb7
Accept Bearer token when editing command perms
May 12, 2022
8b4c8ab
Update examples/interactions.cr
May 16, 2022
bc1b02b
Support min and max length for command options
Jul 8, 2022
f940216
Merge branch 'shardlab:master' into interactions
Aug 26, 2022
c818e74
Add support for new select menu types
Oct 22, 2022
88ea56f
Format code
Oct 22, 2022
15da126
Rename SelectOption to StringSelectOption
soya-daizu Feb 5, 2023
66d4f52
Add focused field to interaction data option
soya-daizu Feb 5, 2023
4020386
Add specs
soya-daizu Feb 5, 2023
2a0da6a
Format code
soya-daizu Feb 5, 2023
24102e1
style: use combined assignment
soya-daizu Mar 26, 2023
daa4a78
Merge branch 'shardlab:master' into interactions
soya-daizu Apr 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions examples/interactions.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# This example bot demonstrates interaction-related features
# such as Application Commands and Message Components.
#
# For more information on interactions in general, see
# https://discord.com/developers/docs/interactions/receiving-and-responding#interactions-and-bot-users

require "../src/discordcr"

# Make sure to replace this fake data with actual data when running.
client = Discord::Client.new(token: "Bot MjI5NDU5NjgxOTU1NjUyMzM3.Cpnz31.GQ7K9xwZtvC40y8MPY3eTqjEIXm", client_id: 229459681955652337_u64)

# Making an array of commands with `PartialApplicationCommand`
# to register multiple commands all together.

commands = [] of Discord::PartialApplicationCommand

commands.push(
Discord::PartialApplicationCommand.new(
name: "animal",
description: "Reacts with the type of animal you select",
options: [
Discord::ApplicationCommandOption.string(
name: "animal_type",
description: "The type of animal you want to hear from",
required: true,
choices: [
Discord::ApplicationCommandOptionChoice.new(
"dog", "woof!"
),
Discord::ApplicationCommandOptionChoice.new(
"cat", "meow"
),
]
),
]
)
)

commands.push(
Discord::PartialApplicationCommand.new(
name: "counter",
description: "Show a step-up/step-down counter",
options: [
Discord::ApplicationCommandOption.integer(
name: "step",
description: "Increase/decrease per step (Default: 1)",
min_value: 1,
max_value: 10
),
]
)
)

commands.push(
Discord::PartialApplicationCommand.new(
name: "Greet",
description: "",
type: Discord::ApplicationCommandType::User
)
)

commands.push(
Discord::PartialApplicationCommand.new(
name: "Upcase",
description: "",
type: Discord::ApplicationCommandType::Message
)
)

commands.push(
Discord::PartialApplicationCommand.new(
name: "modal_test",
description: "Show a sample modal"
)
)

# You can also register one by one with `#create_global_application_command`
client.bulk_overwrite_global_application_commands(commands)

# Handle interactions
client.on_interaction_create do |interaction|
if interaction.type.application_command?
data = interaction.data.as(Discord::ApplicationCommandInteractionData)

case data.name
when "animal"
response = Discord::InteractionResponse.message(
data.options.not_nil!.first.value.to_s
)
client.create_interaction_response(interaction.id, interaction.token, response)
when "counter"
step = data.options.try(&.first.value) || 1
response = Discord::InteractionResponse.message(
"0",
components: [
Discord::ActionRow.new(
Discord::Button.new(Discord::ButtonStyle::Primary, "-", custom_id: "sub:#{step}"),
Discord::Button.new(Discord::ButtonStyle::Primary, "+", custom_id: "add:#{step}")
),
]
)
client.create_interaction_response(interaction.id, interaction.token, response)
when "Greet"
response = begin
user = data.resolved.not_nil!.users.not_nil![data.target_id.not_nil!]
Discord::InteractionResponse.message(
":wave: #{user.mention}"
)
rescue
Discord::InteractionResponse.message(
"Who am I supposed to greet!?"
)
end
client.create_interaction_response(interaction.id, interaction.token, response)
when "Upcase"
response = begin
message = data.resolved.not_nil!.messages.not_nil![data.target_id.not_nil!]
Discord::InteractionResponse.message(
message.content.upcase
)
rescue
Discord::InteractionResponse.message(
"What am I supposed to upcase!?"
)
end
client.create_interaction_response(interaction.id, interaction.token, response)
when "modal_test"
response = Discord::InteractionResponse.modal(
custom_id: "sample_modal",
title: "Sample Modal",
components: [
Discord::ActionRow.new(
Discord::TextInput.new("short", Discord::TextInputStyle::Short, "Short text field")
),
Discord::ActionRow.new(
Discord::TextInput.new("paragraph", Discord::TextInputStyle::Paragraph, "Long text field")
),
]
)
client.create_interaction_response(interaction.id, interaction.token, response)
end
elsif interaction.type.message_component?
data = interaction.data.as(Discord::MessageComponentInteractionData)

key, value = data.custom_id.split(":")
count = interaction.message.not_nil!.content.to_i
case key
when "add"
count += value.to_i
when "sub"
count -= value.to_i
end

response = Discord::InteractionResponse.update_message(
count.to_s
)
client.create_interaction_response(interaction.id, interaction.token, response)
elsif interaction.type.modal_submit?
data = interaction.data.as(Discord::ModalSubmitInteractionData)

response_text = String.build do |str|
data.components.each do |row|
row.components.each do |component|
str << "#{component.custom_id}: #{component.value}\n" if component.is_a?(Discord::TextInput)
end
end
end
response = Discord::InteractionResponse.message(
response_text
)
client.create_interaction_response(interaction.id, interaction.token, response)
end
end

client.run
62 changes: 62 additions & 0 deletions spec/application_commands_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require "./spec_helper"

describe Discord::ApplicationCommandInteractionDataOption do
it "parses options" do
opt_json = %({"name":"cat_name","type":3,"value":"tama"})
json = %({"name":"subcommand","type":1,"options":[#{opt_json}]})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
opt_obj = Discord::ApplicationCommandInteractionDataOption.from_json(opt_json)
obj.options.should eq [opt_obj]
end

it "parses value as String" do
json = %({"name":"animal_type","type":3,"value":"meow"})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a String
end

it "parses value as Int64" do
json = %({"name":"animal_type","type":4,"value":1})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Int64
end

it "parses value as Bool" do
json = %({"name":"is_cat","type":5,"value":true})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Bool
end

it "parses value as Snowflake" do
json = %({"name":"user","type":6,"value":"1234567890"})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Discord::Snowflake

json = %({"name":"channel","type":7,"value":"1234567890"})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Discord::Snowflake

json = %({"name":"role","type":8,"value":"1234567890"})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Discord::Snowflake

json = %({"name":"mentionable","type":9,"value":"1234567890"})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Discord::Snowflake
end

it "parses value as Float64" do
json = %({"name":"catfulness","type":10,"value":1.0})

obj = Discord::ApplicationCommandInteractionDataOption.from_json(json)
obj.value.should be_a Float64
end
end
62 changes: 62 additions & 0 deletions spec/components_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
require "./spec_helper"

describe Discord::Component do
it "parses ActionRow" do
btn_json = %({"type":2,"label":"Click me!","style":1,"custom_id":"button_id"})
json = %({"type":1,"components":[#{btn_json}]})

obj = Discord::Component.from_json(json)
btn_obj = Discord::Component.from_json(btn_json)
obj.should be_a Discord::ActionRow
obj.as(Discord::ActionRow).components.should eq [btn_obj]
end

it "parses Button" do
json = %({"type":2,"label":"Click me!","style":1,"custom_id":"button_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::Button
end

it "parses StringSelect" do
json = %({"type":3,"options":[{"label":"Option 1","value":"1"},{"label":"Option 2","value":"2"}],"custom_id":"select_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::SelectMenu
end

it "parses TextInput" do
json = %({"type":4,"label":"Input Label","style":1,"custom_id":"input_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::TextInput
end

it "parses UserSelect" do
json = %({"type":5,"custom_id":"select_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::SelectMenu
end

it "parses RoleSelect" do
json = %({"type":6,"custom_id":"select_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::SelectMenu
end

it "parses MentionableSelect" do
json = %({"type":7,"custom_id":"select_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::SelectMenu
end

it "parses ChannelSelect" do
json = %({"type":8,"custom_id":"select_id"})

obj = Discord::Component.from_json(json)
obj.should be_a Discord::SelectMenu
end
end
24 changes: 24 additions & 0 deletions spec/interactions_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require "./spec_helper"

describe Discord::InteractionData do
it "parses ApplicationCommandInteractionData" do
json = %({"type":1,"options":[{"value":"woof!","type":3,"name":"animal_type"}],"name":"animal","id":"1071437155282464818"})

obj = Discord::InteractionData.from_json(json)
obj.should be_a Discord::ApplicationCommandInteractionData
end

it "parses MessageComponentInteractionData" do
json = %({"custom_id":"add:1","component_type":2})

obj = Discord::InteractionData.from_json(json)
obj.should be_a Discord::MessageComponentInteractionData
end

it "parses ModalSubmitInteractionData" do
json = %({"custom_id":"sample_modal","components":[{"type":1,"components":[{"value":"short test","type":4,"custom_id":"short"}]},{"type":1,"components":[{"value":"long test","type":4,"custom_id":"paragraph"}]}]})

obj = Discord::InteractionData.from_json(json)
obj.should be_a Discord::ModalSubmitInteractionData
end
end
18 changes: 18 additions & 0 deletions src/discordcr/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,10 @@ module Discord

payload = Gateway::ResumedPayload.from_json(data)
call_event resumed, payload
when "APPLICATION_COMMAND_PERMISSIONS_UPDATE"
payload = ApplicationCommandPermissions.from_json(data)

call_event application_command_permissions_update, payload
when "CHANNEL_CREATE"
payload = Channel.from_json(data)

Expand Down Expand Up @@ -596,6 +600,10 @@ module Discord
@cache.try &.remove_guild_role(payload.guild_id, payload.role_id)

call_event guild_role_delete, payload
when "INTERACTION_CREATE"
payload = Interaction.from_json(data)

call_event interaction_create, payload
when "INVITE_CREATE"
payload = Gateway::InviteCreatePayload.from_json(data)

Expand Down Expand Up @@ -794,6 +802,11 @@ module Discord
# [API docs for this event](https://discord.com/developers/docs/topics/gateway#resumed)
event resumed, Gateway::ResumedPayload

# Called when an application command permission has been updated.
#
# [API docs for this event](https://discord.com/developers/docs/topics/gateway#application-commands-permissions-update)
event application_command_permissions_update, ApplicationCommandPermissions

# Called when a channel has been created on a server the bot has access to,
# or when somebody has started a DM channel with the bot.
#
Expand Down Expand Up @@ -899,6 +912,11 @@ module Discord
# [API docs for this event](https://discord.com/developers/docs/topics/gateway#guild-role-delete)
event guild_role_delete, Gateway::GuildRoleDeletePayload

# Called when a user in a guild uses an Application Command.
#
# [API docs for this event](https://discord.com/developers/docs/topics/gateway#interaction-create)
event interaction_create, Interaction

# Called when an invite is created on a guild.
#
# [API docs for this event](https://discordapp.com/developers/docs/topics/gateway#invite-create)
Expand Down
Loading