diff --git a/examples/interactions.cr b/examples/interactions.cr new file mode 100644 index 00000000..77251c7b --- /dev/null +++ b/examples/interactions.cr @@ -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 diff --git a/spec/application_commands_spec.cr b/spec/application_commands_spec.cr new file mode 100644 index 00000000..fe67b4ad --- /dev/null +++ b/spec/application_commands_spec.cr @@ -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 diff --git a/spec/components_spec.cr b/spec/components_spec.cr new file mode 100644 index 00000000..e1ad2d59 --- /dev/null +++ b/spec/components_spec.cr @@ -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 diff --git a/spec/interactions_spec.cr b/spec/interactions_spec.cr new file mode 100644 index 00000000..6fa264c1 --- /dev/null +++ b/spec/interactions_spec.cr @@ -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 diff --git a/src/discordcr/client.cr b/src/discordcr/client.cr index 5b876c50..b43cce25 100644 --- a/src/discordcr/client.cr +++ b/src/discordcr/client.cr @@ -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) @@ -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) @@ -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. # @@ -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) diff --git a/src/discordcr/mappings/application_commands.cr b/src/discordcr/mappings/application_commands.cr new file mode 100644 index 00000000..c8a5d826 --- /dev/null +++ b/src/discordcr/mappings/application_commands.cr @@ -0,0 +1,361 @@ +require "./converters" + +module Discord + enum ApplicationCommandType : UInt8 + ChatInput = 1 + User = 2 + Message = 3 + + def to_json(json : JSON::Builder) + json.number(value) + end + end + + struct ApplicationCommand + include JSON::Serializable + + property id : Snowflake + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandType))] + property type : ApplicationCommandType? + property application_id : Snowflake + property guild_id : Snowflake? + property name : String + property name_localizations : Hash(String, String)? + property description : String + property description_localizations : Hash(String, String)? + property options : Array(ApplicationCommandOption)? + property default_member_permissions : Permissions? + property dm_permission : Bool? + property version : Snowflake + property name_localized : String? + property description_localized : String? + end + + # `ApplicationCommand` object used for bulk overwriting commands + struct PartialApplicationCommand + include JSON::Serializable + + property name : String + property name_localizations : Hash(String, String)? + property description : String + property description_localizations : Hash(String, String)? + property options : Array(ApplicationCommandOption)? + property default_member_permissions : Permissions? + property dm_permission : Bool? + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandType))] + property type : ApplicationCommandType? + + def initialize(@name, @description = "", @name_localizations = nil, @description_localizations = nil, @options = nil, @default_member_permissions = nil, @dm_permission = nil, @type = nil) + end + end + + enum ApplicationCommandOptionType : UInt8 + SubCommand = 1 + SubCommandGroup = 2 + String = 3 + Integer = 4 + Boolean = 5 + User = 6 + Channel = 7 + Role = 8 + Mentionable = 9 + Number = 10 + Attachment = 11 + end + + struct ApplicationCommandOption + include JSON::Serializable + + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandOptionType))] + property type : ApplicationCommandOptionType + property name : String + property name_localizations : Hash(String, String)? + property description : String + property description_localizations : Hash(String, String)? + property required : Bool? + property choices : Array(ApplicationCommandOptionChoice)? + property options : Array(ApplicationCommandOption)? + property channel_types : Array(ChannelType)? + property min_value : Int64 | Float64? + property max_value : Int64 | Float64? + property autocomplete : Bool? + property name_localized : String? + property description_localized : String? + + def initialize(@type, @name, @description, @name_localizations = nil, + @description_localizations = nil, @required = nil, + @choices = nil, @options = nil, @channel_types = nil, + @min_value = nil, @max_value = nil, @autocomplete = nil) + end + + def self.sub_command(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + options : Array(ApplicationCommandOption)? = nil) + self.new( + ApplicationCommandOptionType::SubCommand, + name, + description, + name_localizations, + description_localizations, + required, + options: options + ) + end + + def self.sub_command_group(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + options : Array(ApplicationCommandOption)? = nil) + self.new( + ApplicationCommandOptionType::SubCommandGroup, + name, + description, + name_localizations, + description_localizations, + required, + options: options + ) + end + + def self.string(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + choices : Array(ApplicationCommandOptionChoice)? = nil, + min_value : Int64? = nil, max_value : Int64? = nil, autocomplete : Bool? = nil) + self.new( + ApplicationCommandOptionType::String, + name, + description, + name_localizations, + description_localizations, + required, + choices: choices, + min_value: min_value, + max_value: max_value, + autocomplete: autocomplete + ) + end + + def self.integer(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + choices : Array(ApplicationCommandOptionChoice)? = nil, + min_value : Int64? = nil, max_value : Int64? = nil, autocomplete : Bool? = nil) + self.new( + ApplicationCommandOptionType::Integer, + name, + description, + name_localizations, + description_localizations, + required, + choices: choices, + min_value: min_value, + max_value: max_value, + autocomplete: autocomplete + ) + end + + def self.boolean(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil) + self.new( + ApplicationCommandOptionType::Boolean, + name, + description, + name_localizations, + description_localizations, + required + ) + end + + def self.user(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil) + self.new( + ApplicationCommandOptionType::User, + name, + description, + name_localizations, + description_localizations, + required + ) + end + + def self.channel(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + channel_types : Array(ChannelType)? = nil) + self.new( + ApplicationCommandOptionType::Channel, + name, + description, + name_localizations, + description_localizations, + required, + channel_types: channel_types + ) + end + + def self.role(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil) + self.new( + ApplicationCommandOptionType::Role, + name, + description, + name_localizations, + description_localizations, + required + ) + end + + def self.mentionable(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil) + self.new( + ApplicationCommandOptionType::Mentionable, + name, + description, + name_localizations, + description_localizations, + required + ) + end + + def self.number(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil, + choices : Array(ApplicationCommandOptionChoice)? = nil, + min_value : Float64? = nil, max_value : Float64? = nil, + autocomplete : Bool? = nil) + self.new( + ApplicationCommandOptionType::Number, + name, + description, + name_localizations, + description_localizations, + required, + choices: choices, + min_value: min_value, + max_value: max_value, + autocomplete: autocomplete + ) + end + + def self.attachment(name : String, description : String, name_localizations : Hash(String, String)? = nil, + description_localizations : Hash(String, String)? = nil, required : Bool? = nil) + self.new( + ApplicationCommandOptionType::Attachment, + name, + description, + name_localizations, + description_localizations, + required + ) + end + end + + struct ApplicationCommandOptionChoice + include JSON::Serializable + + property name : String + property name_localizations : Hash(String, String)? + property value : String | Int64 | Float64 + property name_localized : String? + + def initialize(@name, @value, @name_localizations = nil) + end + end + + struct ApplicationCommandInteractionDataOption + include JSON::Serializable + + property name : String + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandOptionType))] + property type : ApplicationCommandOptionType + property value : String | Int64 | Float64 | Bool | Snowflake? + property options : Array(ApplicationCommandInteractionDataOption)? + property focused : Bool? + + def initialize(@name, @type, @value = nil, @options = nil, @focused = nil) + end + + def self.new(pull : JSON::PullParser) + name = nil + type = nil + value_raw = "" + options = nil + focused = nil + + pull.read_object do |key| + case key + when "name" + name = pull.read_string + when "type" + type = ApplicationCommandOptionType.new(pull.read_int.to_u8) + when "value" + value_raw = pull.read_raw + when "options" + options = Array(self).new + pull.read_array do + options.push(self.new(pull)) + end + when "focused" + focused = pull.read_bool + end + end + + value = case type + when ApplicationCommandOptionType::String + String.from_json(value_raw) + when ApplicationCommandOptionType::Integer + Int64.from_json(value_raw) + when ApplicationCommandOptionType::Boolean + Bool.from_json(value_raw) + when ApplicationCommandOptionType::User, + ApplicationCommandOptionType::Channel, + ApplicationCommandOptionType::Role, + ApplicationCommandOptionType::Mentionable + Snowflake.from_json(value_raw) + when ApplicationCommandOptionType::Number + Float64.from_json(value_raw) + end + + self.new(name || "", type || ApplicationCommandOptionType.new(0), value, options, focused) + end + end + + struct GuildApplicationCommandPermissions + include JSON::Serializable + + property id : Snowflake + property application_id : Snowflake + property guild_id : Snowflake + property permissions : Array(ApplicationCommandPermissions) + end + + enum ApplicationCommandPermissionType : UInt8 + Role = 1 + User = 2 + Channel = 3 + end + + struct ApplicationCommandPermissions + include JSON::Serializable + + # `guild_id` responds to `@everyone` and `guild_id - 1` responds to all channels in the guild. + property id : Snowflake + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandPermissionType))] + property type : ApplicationCommandPermissionType + property permission : Bool + + def initialize(@id, @type, @permission) + end + + def self.role(id : UInt64 | Snowflake, permissions : Bool) + id = Snowflake.new(id) unless id.is_a?(Snowflake) + self.new(id, ApplicationCommandPermissionType::Role, permissions) + end + + def self.user(id : UInt64 | Snowflake, permissions : Bool) + id = Snowflake.new(id) unless id.is_a?(Snowflake) + self.new(id, ApplicationCommandPermissionType::User, permissions) + end + + def self.channel(id : UInt64 | Snowflake, permissions : Bool) + id = Snowflake.new(id) unless id.is_a?(Snowflake) + self.new(id, ApplicationCommandPermissionType::Channel, permissions) + end + end +end diff --git a/src/discordcr/mappings/channel.cr b/src/discordcr/mappings/channel.cr index dd18e6ab..dea4e82e 100644 --- a/src/discordcr/mappings/channel.cr +++ b/src/discordcr/mappings/channel.cr @@ -88,10 +88,13 @@ module Discord property nonce : String | Int64? property activity : Activity? property application : OAuth2Application? + property application_id : Snowflake? property webhook_id : Snowflake? property flags : MessageFlags? property thread : Channel? property referenced_message : Message? + property interaction : MessageInteraction? + property components : Array(Component)? def message_reference : MessageReference MessageReference.new(@id, @channel_id, @guild_id) @@ -200,6 +203,7 @@ module Discord @[JSON::Field(converter: Discord::MaybeTimestampConverter)] property last_pin_timestamp : Time? property rtc_region : String? + @[JSON::Field(converter: Enum::ValueConverter(Discord::VideoQualityMode))] property video_quality_mode : VideoQualityMode? property thread_metadata : ThreadMetaData? property message_count : UInt32? @@ -207,6 +211,7 @@ module Discord property member : ThreadMember? property default_auto_archive_duration : AutoArchiveDuration? property last_message_id : Snowflake? + property permissions : String? # :nodoc: def initialize(private_channel : PrivateChannel) diff --git a/src/discordcr/mappings/components.cr b/src/discordcr/mappings/components.cr new file mode 100644 index 00000000..2744fdcd --- /dev/null +++ b/src/discordcr/mappings/components.cr @@ -0,0 +1,133 @@ +require "./converters" + +module Discord + enum ComponentType : UInt8 + ActionRow = 1 + Button = 2 + StringSelect = 3 + TextInput = 4 + UserSelect = 5 + RoleSelect = 6 + MentionableSelect = 7 + ChannelSelect = 8 + end + + enum ButtonStyle : UInt8 + Primary = 1 + Secondary = 2 + Success = 3 + Danger = 4 + Link = 5 + end + + enum TextInputStyle : UInt8 + Short = 1 + Paragraph = 2 + end + + abstract struct Component + include JSON::Serializable + + use_json_discriminator "type", { + ComponentType::ActionRow => ActionRow, + ComponentType::Button => Button, + ComponentType::StringSelect => SelectMenu, + ComponentType::TextInput => TextInput, + ComponentType::UserSelect => SelectMenu, + ComponentType::RoleSelect => SelectMenu, + ComponentType::MentionableSelect => SelectMenu, + ComponentType::ChannelSelect => SelectMenu, + } + + @[JSON::Field(converter: Enum::ValueConverter(Discord::ComponentType))] + property type : ComponentType + end + + struct ActionRow < Component + @type : ComponentType = ComponentType::ActionRow + + property components : Array(Button | SelectMenu | TextInput) + + def initialize(*components : Button | SelectMenu | TextInput) + @components = [*components] of Button | SelectMenu | TextInput + end + end + + struct Button < Component + @type : ComponentType = ComponentType::Button + + @[JSON::Field(converter: Enum::ValueConverter(Discord::ButtonStyle))] + property style : ButtonStyle + property label : String? + property emoji : Emoji? + property custom_id : String? + property url : String? + property disabled : Bool? + + def initialize(@style, @label = nil, @emoji = nil, @custom_id = nil, @url = nil, @disabled = nil) + end + end + + struct SelectMenu < Component + property custom_id : String + property options : Array(StringSelectOption)? + property channel_types : Array(ChannelType)? + property placeholder : String? + property min_values : UInt8? + property max_values : UInt8? + property disabled : Bool? + + def initialize(@type, @custom_id, @options = nil, @channel_types = nil, @placeholder = nil, @min_values = nil, @max_values = nil, @disabled = nil) + end + + def self.string(custom_id, options, placeholder = nil, min_values = nil, max_values = nil, disabled = nil) + self.new(ComponentType::StringSelect, custom_id, options, nil, placeholder, min_values, max_values, disabled) + end + + def self.user(custom_id, placeholder = nil, min_values = nil, max_values = nil, disabled = nil) + self.new(ComponentType::UserSelect, custom_id, nil, nil, placeholder, min_values, max_values, disabled) + end + + def self.role(custom_id, placeholder = nil, min_values = nil, max_values = nil, disabled = nil) + self.new(ComponentType::RoleSelect, custom_id, nil, nil, placeholder, min_values, max_values, disabled) + end + + def self.mentionable(custom_id, placeholder = nil, min_values = nil, max_values = nil, disabled = nil) + self.new(ComponentType::MentionableSelect, custom_id, nil, nil, placeholder, min_values, max_values, disabled) + end + + def self.channel(custom_id, channel_types, placeholder = nil, min_values = nil, max_values = nil, disabled = nil) + self.new(ComponentType::ChannelSelect, custom_id, nil, channel_types, placeholder, min_values, max_values, disabled) + end + end + + struct TextInput < Component + @type : ComponentType = ComponentType::TextInput + + property custom_id : String + @[JSON::Field(converter: Enum::ValueConverter(Discord::TextInputStyle))] + property style : TextInputStyle? + property label : String? + property min_length : UInt16? + property max_length : UInt16? + property required : Bool? + property value : String? + property placeholder : String? + + def initialize(@custom_id, @style, @label, @min_length = nil, @max_length = nil, @required = nil, @value = nil, @placeholder = nil) + end + end + + struct StringSelectOption + include JSON::Serializable + + property label : String + property value : String + property description : String? + property emoji : Emoji? + property default : Bool? + + def initialize(@label, @value, @description = nil, @emoji = nil, @default = nil) + end + end +end diff --git a/src/discordcr/mappings/guild.cr b/src/discordcr/mappings/guild.cr index e9e9a314..76a1e546 100644 --- a/src/discordcr/mappings/guild.cr +++ b/src/discordcr/mappings/guild.cr @@ -158,8 +158,8 @@ module Discord property joined_at : Time @[JSON::Field(converter: Discord::MaybeTimestampConverter)] property premium_since : Time? - property deaf : Bool - property mute : Bool + property deaf : Bool? + property mute : Bool? property communication_disabled_until : Time? end diff --git a/src/discordcr/mappings/interactions.cr b/src/discordcr/mappings/interactions.cr new file mode 100644 index 00000000..a407786e --- /dev/null +++ b/src/discordcr/mappings/interactions.cr @@ -0,0 +1,218 @@ +require "./converters" + +module Discord + enum InteractionType : UInt8 + Ping = 1 + ApplicationCommand = 2 + MessageComponent = 3 + ApplicationCommandAutocomplete = 4 + ModalSubmit = 5 + end + + struct Interaction + include JSON::Serializable + + property id : Snowflake + property application_id : Snowflake + @[JSON::Field(converter: Enum::ValueConverter(Discord::InteractionType))] + property type : InteractionType + property data : InteractionData? + property guild_id : Snowflake? + property channel_id : Snowflake? + property member : GuildMember? + property user : User? + property token : String + property version : Int32 + property message : Message? + property locale : String? + property guild_locale : String? + end + + abstract struct InteractionData + include JSON::Serializable + + def self.new(pull : JSON::PullParser) + type = self + json = String.build do |io| + JSON.build(io) do |builder| + builder.start_object + pull.read_object do |key| + if key == "id" + type = ApplicationCommandInteractionData + elsif key == "custom_id" + type = MessageComponentInteractionData + elsif key == "components" + type = ModalSubmitInteractionData + end + builder.field(key) { pull.read_raw(builder) } + end + builder.end_object + end + end + + type.from_json(json) + end + end + + struct ApplicationCommandInteractionData < InteractionData + property id : Snowflake + property name : String + @[JSON::Field(converter: Enum::ValueConverter(Discord::ApplicationCommandType))] + property type : ApplicationCommandType + property resolved : ResolvedInteractionData? + property options : Array(ApplicationCommandInteractionDataOption)? + property target_id : Snowflake? + end + + struct MessageComponentInteractionData < InteractionData + property custom_id : String + @[JSON::Field(converter: Enum::ValueConverter(Discord::ComponentType))] + property component_type : ComponentType + property values : Array(String)? + end + + struct ModalSubmitInteractionData < InteractionData + property custom_id : String + property components : Array(ActionRow) + end + + struct ResolvedInteractionData + include JSON::Serializable + + property users : Hash(Snowflake, User)? + property members : Hash(Snowflake, PartialGuildMember)? + property roles : Hash(Snowflake, Role)? + property channels : Hash(Snowflake, Channel)? + property messages : Hash(Snowflake, Message)? + property attachments : Hash(Snowflake, Attachment)? + end + + struct MessageInteraction + include JSON::Serializable + + property id : Snowflake + @[JSON::Field(converter: Enum::ValueConverter(Discord::InteractionType))] + property type : InteractionType + property name : String + property user : User + end + + enum InteractionCallbackType : UInt8 + Pong = 1 + ChannelMessageWithSource = 4 + DeferredChannelMessageWithSource = 5 + DeferredUpdateMessage = 6 + UpdateMessage = 7 + ApplicationCommandAutocompleteResult = 8 + Modal = 9 + end + + struct InteractionResponse + include JSON::Serializable + + @[JSON::Field(converter: Enum::ValueConverter(Discord::InteractionCallbackType))] + property type : InteractionCallbackType + property data : (InteractionCallbackMessageData | InteractionCallbackAutocompleteData | InteractionCallbackModalData)? + + def initialize(@type, @data = nil) + end + + def self.pong + self.new(InteractionCallbackType::Pong) + end + + def self.message(data : InteractionCallbackMessageData) + self.new(InteractionCallbackType::ChannelMessageWithSource, data) + end + + def self.message(content : String? = nil, embeds : Array(Embed)? = nil, + components : Array(ActionRow)? = nil, + flags : InteractionCallbackDataFlags? = nil, tts : Bool? = nil) + data = InteractionCallbackMessageData.new(content, embeds, components, flags, tts) + self.message(data) + end + + def self.deferred_message(flags : InteractionCallbackDataFlags? = nil) + data = InteractionCallbackMessageData.new(flags: flags) + self.new(InteractionCallbackType::DeferredChannelMessageWithSource, data) + end + + def self.deferred_update_message + self.new(InteractionCallbackType::DeferredUpdateMessage) + end + + def self.update_message(data : InteractionCallbackMessageData) + self.new(InteractionCallbackType::UpdateMessage, data) + end + + def self.update_message(content : String? = nil, embeds : Array(Embed)? = nil, + components : Array(ActionRow)? = nil, + flags : InteractionCallbackDataFlags? = nil, tts : Bool? = nil) + data = InteractionCallbackMessageData.new(content, embeds, components, flags, tts) + self.update_message(data) + end + + def self.autocomplete_result(data : InteractionCallbackAutocompleteData) + self.new(InteractionCallbackType::ApplicationCommandAutocompleteResult, data) + end + + def self.autocomplete_result(choices : Array(ApplicationCommandOptionChoice)) + data = InteractionCallbackAutocompleteData.new(choices) + self.new(InteractionCallbackType::ApplicationCommandAutocompleteResult, data) + end + + def self.modal(data : InteractionCallbackModalData) + self.new(InteractionCallbackType::Modal, data) + end + + def self.modal(custom_id : String, title : String, components : Array(ActionRow)) + data = InteractionCallbackModalData.new(custom_id, title, components) + self.new(InteractionCallbackType::Modal, data) + end + end + + @[Flags] + enum InteractionCallbackDataFlags + SuppressEmbeds = 1 << 2 + Ephemeral = 1 << 6 + + def to_json(json : JSON::Builder) + json.number(value) + end + end + + struct InteractionCallbackMessageData + include JSON::Serializable + + property tts : Bool? + property content : String? + property embeds : Array(Embed)? + property allowed_mentions : AllowedMentions? + @[JSON::Field(converter: Enum::ValueConverter(Discord::InteractionCallbackDataFlags))] + property flags : InteractionCallbackDataFlags? + property components : Array(ActionRow)? + + def initialize(@content = nil, @embeds = nil, @components = nil, @flags = nil, @tts = nil) + end + end + + struct InteractionCallbackAutocompleteData + include JSON::Serializable + + property choices : Array(ApplicationCommandOptionChoice) + + def initialize(@choices) + end + end + + struct InteractionCallbackModalData + include JSON::Serializable + + property custom_id : String + property title : String + property components : Array(ActionRow) + + def initialize(@custom_id, @title, @components) + end + end +end diff --git a/src/discordcr/rest.cr b/src/discordcr/rest.cr index 5fcb123d..ffce48d8 100644 --- a/src/discordcr/rest.cr +++ b/src/discordcr/rest.cr @@ -22,7 +22,7 @@ module Discord mutexes = (@mutexes ||= Hash(RateLimitKey, Mutex).new) global_mutex = (@global_mutex ||= Mutex.new) - headers["Authorization"] = @token + headers["Authorization"] ||= @token headers["User-Agent"] = USER_AGENT headers["X-RateLimit-Precision"] = "millisecond" @@ -315,14 +315,16 @@ module Discord # For more details on the format of the `embed` object, look at the # [relevant documentation](https://discord.com/developers/docs/resources/channel#embed-object). def create_message(channel_id : UInt64 | Snowflake, content : String, embed : Embed? = nil, tts : Bool = false, - nonce : Int64 | String? = nil, allowed_mentions : AllowedMentions? = nil, message_reference : MessageReference? = nil) + nonce : Int64 | String? = nil, allowed_mentions : AllowedMentions? = nil, + message_reference : MessageReference? = nil, components : Array(Component)? = nil) json = encode_tuple( content: content, embed: embed, tts: tts, nonce: nonce, allowed_mentions: allowed_mentions, - message_reference: message_reference + message_reference: message_reference, + components: components ) response = request( @@ -2247,5 +2249,515 @@ module Discord # Expecting response Message.from_json(response.body) if wait end + + # Fetch all of the global commands for your application. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-global-application-commands) + def get_global_application_commands(locale : String? = nil, with_localizations : Bool? = false) + application_id = client_id + path = "/applications/#{application_id}/commands" + path += "?with_localizations=#{with_localizations}" if with_localizations + headers = HTTP::Headers.new + headers["X-Discord-Locale"] = locale if locale + + response = request( + :applications_aid_commands, + application_id, + "GET", + path, + headers, + nil + ) + + Array(ApplicationCommand).from_json(response.body) + end + + # Create a new global command. New global commands will be available in all guilds after 1 hour. + # + # NOTE: Creating a command with the same name as an existing command for your application will overwrite the old command. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#create-global-application-command) + def create_global_application_command(name : String, description : String, + name_localizations : Hash(String, String)?, description_localizations : Hash(String, String)?, + options : Array(ApplicationCommandOption)? = nil, default_member_permissions : Permissions? = nil, + dm_permission : Bool? = nil, type : ApplicationCommandType? = ApplicationCommandType::ChatInput) + application_id = client_id + + json = encode_tuple( + name: name, + description: description, + name_localizations: name_localizations, + description_localizations: description_localizations, + options: options, + default_member_permissions: default_member_permissions, + dm_permission: dm_permission, + type: type + ) + + response = request( + :applications_aid_commands, + application_id, + "POST", + "/applications/#{application_id}/commands", + HTTP::Headers{"Content-Type" => "application/json"}, + json + ) + + ApplicationCommand.from_json(response.body) + end + + # Fetch a global command for your application. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-global-application-command) + def get_global_application_command(command_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_commands, + application_id, + "GET", + "/applications/#{application_id}/commands/#{command_id}", + HTTP::Headers.new, + nil + ) + + ApplicationCommand.from_json(response.body) + end + + # Edit a global command. Updates will be available in all guilds after 1 hour. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-global-application-command) + def edit_global_application_command(name : String, description : String, + name_localizations : Hash(String, String)?, description_localizations : Hash(String, String)?, + options : Array(ApplicationCommandOption)? = nil, default_member_permissions : Permissions? = nil, + dm_permission : Bool? = nil) + application_id = client_id + + json = encode_tuple( + name: name, + description: description, + name_localizations: name_localizations, + description_localizations: description_localizations, + options: options, + default_member_permissions: default_member_permissions, + dm_permission: dm_permission + ) + + response = request( + :applications_aid_commands, + application_id, + "PATCH", + "/applications/#{application_id}/commands/#{command_id}", + HTTP::Headers{"Content-Type" => "application/json"}, + json + ) + + ApplicationCommand.from_json(response.body) + end + + # Deletes a global command. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#delete-global-application-command) + def delete_global_application_command(command_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_commands, + application_id, + "DELETE", + "/applications/#{application_id}/commands/#{command_id}", + HTTP::Headers.new, + nil + ) + end + + # Takes a list of application commands, overwriting the existing global command list 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. + # + # NOTE: This will overwrite all types of application commands: slash commands, user commands, and message commands. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#bulk-overwrite-global-application-commands) + def bulk_overwrite_global_application_commands(commands : Array(PartialApplicationCommand)) + application_id = client_id + + response = request( + :applications_aid_commands, + application_id, + "PUT", + "/applications/#{application_id}/commands", + HTTP::Headers{"Content-Type" => "application/json"}, + commands.to_json + ) + + Array(ApplicationCommand).from_json(response.body) + end + + # Fetch all of the guild commands for your application for a specific guild. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-commands) + def get_guild_application_commands(guild_id : UInt64 | Snowflake, locale : String? = nil, with_localizations : Bool? = false) + application_id = client_id + path = "/applications/#{application_id}/guilds/#{guild_id}/commands" + path += "?with_localizations=#{with_localizations}" if with_localizations + headers = HTTP::Headers.new + headers["X-Discord-Locale"] = locale if locale + + response = request( + :applications_aid_guilds_gid_commands, + application_id, + "GET", + path, + headers, + nil + ) + + Array(ApplicationCommand).from_json(response.body) + end + + # Create a new guild command. New guild commands will be available in the guild immediately. If the command did not already exist, it will count toward daily application command create limits. + # + # NOTE: Creating a command with the same name as an existing command for your application will overwrite the old command. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#create-guild-application-commands) + def create_guild_application_command(guild_id : UInt64 | Snowflake, name : String, description : String, + name_localizations : Hash(String, String)?, description_localizations : Hash(String, String)?, + options : Array(ApplicationCommandOption)? = nil, default_member_permissions : Permissions? = nil, + type : ApplicationCommandType? = ApplicationCommandType::ChatInput) + application_id = client_id + + json = encode_tuple( + name: name, + description: description, + name_localizations: name_localizations, + description_localizations: description_localizations, + options: options, + default_member_permission: default_member_permission, + type: type + ) + + response = request( + :applications_aid_guilds_gid_commands, + application_id, + "POST", + "/applications/#{application_id}/guilds/#{guild_id}/commands", + HTTP::Headers{"Content-Type" => "application/json"}, + json + ) + + ApplicationCommand.from_json(response.body) + end + + # Fetch a guild command for your application. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command) + def get_global_application_command(guild_id : UInt64 | Snowflake, command_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands_cid, + application_id, + "GET", + "/applications/#{application_id}/guilds/#{guild_id}/commands/#{command_id}", + HTTP::Headers.new, + nil + ) + + ApplicationCommand.from_json(response.body) + end + + # Edit a guild command. Updates for guild commands will be available immediately. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/slash-commands#edit-guild-application-command) + def edit_guild_application_command(guild_id : UInt64 | Snowflake, name : String, description : String, + name_localizations : Hash(String, String)?, description_localizations : Hash(String, String)?, + options : Array(ApplicationCommandOption)? = nil, default_member_permissions : Permissions? = nil) + application_id = client_id + + json = encode_tuple( + name: name, + description: description, + name_localizations: name_localizations, + description_localizations: description_localizations, + options: options, + default_member_permission: default_member_permission + ) + + response = request( + :applications_aid_guilds_gid_commands_cid, + application_id, + "PATCH", + "/applications/#{application_id}/guilds/#{guild_id}/commands/#{command_id}", + HTTP::Headers{"Content-Type" => "application/json"}, + json + ) + + ApplicationCommand.from_json(response.body) + end + + # Delete a guild command. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/slash-commands#delete-guild-application-command) + def delete_guild_application_command(guild_id : UInt64 | Snowflake, command_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands_cid, + application_id, + "DELETE", + "/applications/#{application_id}/guilds/#{guild_id}/commands/#{command_id}", + HTTP::Headers.new, + nil + ) + end + + # Takes a list of application commands, overwriting existing commands for the guild. + # + # NOTE: This will overwrite all types of application commands: slash commands, user commands, and message commands. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/slash-commands#bulk-overwrite-guild-application-commands) + def bulk_overwrite_guild_application_commands(guild_id : UInt64 | Snowflake, commands : Array(PartialApplicationCommand)) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands, + application_id, + "PUT", + "/applications/#{application_id}/guilds/#{guild_id}/commands", + HTTP::Headers{"Content-Type" => "application/json"}, + commands.to_json + ) + + Array(ApplicationCommand).from_json(response.body) + end + + # Fetches command permissions for all commands for your application in a guild. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-guild-application-command-permissions) + def get_guild_application_command_permissions(guild_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands_permissions, + application_id, + "GET", + "/applications/#{application_id}/guilds/#{guild_id}/commands/permissions", + HTTP::Headers.new, + nil + ) + + Array(GuildApplicationCommandPermissions).from_json(response.body) + end + + # Fetches command permissions for a specific command for your application in a guild. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#get-application-command-permissions) + def get_application_command_permissions(guild_id : UInt64 | Snowflake, command_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands_cid_permissions, + application_id, + "GET", + "/applications/#{application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions", + HTTP::Headers.new, + nil + ) + + GuildApplicationCommandPermissions.from_json(response.body) + end + + # Edits command permissions for a specific command for your application in a guild. You can only add up to 10 permission overwrites for a command. + # + # NOTE: This endpoint requires authentication with a Bearer token that has permission to manage the guild and its roles. For more information, read above about [application command permissions](https://discord.com/developers/docs/interactions/application-commands#permissions). + # NOTE: This endpoint will overwrite existing permissions for the command in that guild + # + # [API docs for this method](https://discord.com/developers/docs/interactions/application-commands#edit-application-command-permissions) + def edit_application_command_permissions(guild_id : UInt64 | Snowflake, command_id : UInt64 | Snowflake, + permissions : Array(ApplicationCommandPermissions), token : String) + application_id = client_id + + response = request( + :applications_aid_guilds_gid_commands_cid_permissions, + application_id, + "PUT", + "/applications/#{application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions", + HTTP::Headers{"Content-Type" => "application/json", "Authorization" => token}, + {permissions: permissions}.to_json + ) + + GuildApplicationCommandPermissions.from_json(response.body) + end + + # Create a response to an Interaction from the gateway. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response) + def create_interaction_response(interaction_id : UInt64 | Snowflake, interaction_token : String, + response : InteractionResponse) + response = request( + :interactions_iid_itk_callback, + interaction_id, + "POST", + "/interactions/#{interaction_id}/#{interaction_token}/callback", + HTTP::Headers{"Content-Type" => "application/json"}, + response.to_json + ) + end + + # Returns the initial Interaction response. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#get-original-interaction-response) + def get_original_interaction_response(interaction_token : String) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_original, + application_id, + "GET", + "/webhooks/#{application_id}/#{interaction_token}/messages/@original", + HTTP::Headers.new, + nil + ) + + Message.from_json(response.body) + end + + # Edits the initial Interaction response. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#edit-original-interaction-response) + def edit_original_interaction_response(interaction_token : String, content : String? = nil, + embeds : Array(Embed)? = nil) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_original, + application_id, + "PATCH", + "/webhooks/#{application_id}/#{interaction_token}/messages/@original", + HTTP::Headers{"Content-Type" => "application/json"}, + {content: content, embeds: embeds}.to_json + ) + + Message.from_json(response.body) + end + + # Deletes the initial Interaction response. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#delete-original-interaction-response) + def delete_original_interaction_response(interaction_token : String) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_original, + application_id, + "DELETE", + "/webhooks/#{application_id}/#{interaction_token}/messages/@original", + HTTP::Headers.new, + nil + ) + end + + # Create a followup message for an Interaction. Functions the same as `#execute_webhook`, but `wait` is always true. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#create-followup-message) + def create_followup_message(interaction_token : String, content : String? = nil, + file : IO? = nil, filename : String? = nil, + embeds : Array(Embed)? = nil, tts : Bool? = nil, + avatar_url : String? = nil, username : String? = nil, + flags : MessageFlags? = nil) + application_id = client_id + + json = encode_tuple( + content: content, + embeds: embeds, + tts: tts, + avatar_url: avatar_url, + username: username, + flags: flags + ) + + body, content_type = if file + io = IO::Memory.new + + unless filename + if file.is_a? File + filename = File.basename(file.path) + else + filename = "" + end + end + + builder = HTTP::FormData::Builder.new(io) + builder.file("file", file, HTTP::FormData::FileMetadata.new(filename: filename)) + builder.field("payload_json", json) + builder.finish + + {io.to_s, builder.content_type} + else + {json, "application/json"} + end + + response = request( + :webhooks_aid_itk, + application_id, + "POST", + "/webhooks/#{application_id}/#{interaction_token}", + HTTP::Headers{"Content-Type" => content_type}, + body + ) + + Message.from_json(response.body) + end + + # Returns a followup message for an Interaction. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#get-followup-message) + def get_followup_message(interaction_token : String, message_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_mid, + application_id, + "GET", + "/webhooks/#{application_id}/#{interaction_token}/messages/#{message_id}", + HTTP::Headers.new, + nil + ) + + Message.from_json(response.body) + end + + # Edits a followup message for an Interaction. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#edit-followup-message) + def edit_followup_message(interaction_token : String, message_id : UInt64 | Snowflake, + content : String? = nil, embeds : Array(Embed)? = nil) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_original, + application_id, + "PATCH", + "/webhooks/#{application_id}/#{interaction_token}/messages/#{message_id}", + HTTP::Headers{"Content-Type" => "application/json"}, + {content: content, embeds: embeds}.to_json + ) + + Message.from_json(response.body) + end + + # Deletes a followup message for an Interaction. + # + # [API docs for this method](https://discord.com/developers/docs/interactions/receiving-and-responding#delete-followup-message) + def delete_followup_message(interaction_token : String, message_id : UInt64 | Snowflake) + application_id = client_id + + response = request( + :webhooks_aid_itk_messages_original, + application_id, + "DELETE", + "/webhooks/#{application_id}/#{interaction_token}/messages/#{message_id}", + HTTP::Headers.new, + nil + ) + end end end diff --git a/src/discordcr/snowflake.cr b/src/discordcr/snowflake.cr index 821b9a12..99001cbe 100644 --- a/src/discordcr/snowflake.cr +++ b/src/discordcr/snowflake.cr @@ -24,6 +24,11 @@ module Discord new(value) end + # Allows serialization of snowflake as an object key + def self.from_json_object_key?(key : String) + Snowflake.new(key) + end + def initialize(@value : UInt64) end