From 87787d06d316b67c38173c49daf4abdec41c723d Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Tue, 11 Aug 2026 02:20:22 +0200 Subject: [PATCH] commands: types & decoders --- src/grom.gleam | 1077 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1069 insertions(+), 8 deletions(-) diff --git a/src/grom.gleam b/src/grom.gleam index ffc610a..63595dd 100644 --- a/src/grom.gleam +++ b/src/grom.gleam @@ -3492,7 +3492,7 @@ fn message_channel_select_decoder() -> Decoder(MessageChannelSelect) { use channel_types <- decode.optional_field( "channel_types", [], - decode.list(of: channel_select_type_decoder()), + decode.list(of: select_channel_type_decoder()), ) use min_values <- decode.optional_field("min_values", 1, decode.int) use max_values <- decode.optional_field("max_values", 1, decode.int) @@ -3540,7 +3540,7 @@ pub type MessageChannelSelect { custom_id: String, placeholder: Option(String), default_channels: List(Snowflake(Channel)), - channel_types: List(ChannelSelectType), + channel_types: List(SelectChannelType), min_values: Int, max_values: Int, is_required: Bool, @@ -14470,7 +14470,7 @@ pub opaque type CreateMessageChannelSelect { /// Which channels will be selected by default. default_channels: List(Snowflake(Channel)), /// Which channel types to include in the list. - channel_types: Option(List(ChannelSelectType)), + channel_types: Option(List(SelectChannelType)), /// Minimum amount of selected values, default 1. /// /// Must be between 0 and 25. @@ -14498,7 +14498,7 @@ fn create_message_channel_select_to_json( )), optional_to_json(create.channel_types, "channel_types", json.array( _, - channel_select_type_to_json, + select_channel_type_to_json, )), Ok(#("min_values", json.int(create.min_values))), Ok(#("max_values", json.int(create.max_values))), @@ -14553,7 +14553,7 @@ pub fn new_message_channel_select_with_default_channels( /// Which channel types are to be included in the select window. pub fn new_message_channel_select_with_channel_types( create: CreateMessageChannelSelect, - channel_types: List(ChannelSelectType), + channel_types: List(SelectChannelType), ) -> CreateMessageChannelSelect { CreateMessageChannelSelect(..create, channel_types: Some(channel_types)) } @@ -14584,7 +14584,7 @@ pub fn new_disabled_message_channel_select( CreateMessageChannelSelect(..create, is_disabled: True) } -pub type ChannelSelectType { +pub type SelectChannelType { SelectTextChannels SelectDmChannels SelectVoiceChannels @@ -14598,7 +14598,7 @@ pub type ChannelSelectType { SelectMediaChannels } -fn channel_select_type_decoder() -> Decoder(ChannelSelectType) { +fn select_channel_type_decoder() -> Decoder(SelectChannelType) { use variant <- decode.then(decode.int) case variant { 0 -> decode.success(SelectTextChannels) @@ -14616,7 +14616,7 @@ fn channel_select_type_decoder() -> Decoder(ChannelSelectType) { } } -fn channel_select_type_to_json(type_: ChannelSelectType) -> Json { +fn select_channel_type_to_json(type_: SelectChannelType) -> Json { case type_ { SelectTextChannels -> 0 SelectDmChannels -> 1 @@ -18248,3 +18248,1064 @@ pub fn delete_invite_response( ) -> Result(Invite, RestError) { handle_response(response, decode_with: invite_decoder()) } + +pub type Command { + CommandSlash(SlashCommand) + CommandUser(UserCommand) + CommandMessage(MessageCommand) +} + +pub fn command_decoder() -> Decoder(Command) { + use type_ <- decode.optional_field("type", 1, decode.int) + + case type_ { + 1 -> decode.map(slash_command_decoder(), CommandSlash) + 2 -> decode.map(user_command_decoder(), CommandUser) + 3 -> decode.map(message_command_decoder(), CommandMessage) + _ -> + decode.failure( + CommandUser(UserCommand( + Snowflake(0), + Snowflake(0), + Snowflake(0), + None, + "", + None, + None, + False, + False, + False, + [], + Snowflake(0), + )), + "Command", + ) + } +} + +pub type CommandVersion + +pub type SlashCommand { + SlashCommand( + id: Snowflake(SlashCommand), + command_id: Snowflake(Command), + application_id: Snowflake(Application), + /// Is present for guild-specific commands. + guild_id: Option(Snowflake(Guild)), + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + options: List(SlashCommandOption), + /// None - no permissions required + /// Some([]) - disabled for everyone by default + required_permissions: Option(List(Permission)), + is_nsfw: Bool, + /// Whether the command can be used in guild-installed environments. + is_guild_installed: Bool, + /// Whether the command can be used in user-installed environments. + is_user_installed: Bool, + /// Describes where the command is usable. + /// + /// Is empty for guild-specific commands. + contexts: List(CommandContext), + version: Snowflake(CommandVersion), + ) +} + +pub fn slash_command_decoder() -> Decoder(SlashCommand) { + use id <- decode.field("id", snowflake_decoder()) + use command_id <- decode.field("id", snowflake_decoder()) + use application_id <- decode.field("application_id", snowflake_decoder()) + use guild_id <- decode.optional_field( + "guild_id", + None, + decode.map(snowflake_decoder(), Some), + ) + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use options <- decode.optional_field( + "options", + [], + decode.list(of: slash_command_option_decoder()), + ) + use required_permissions <- decode.field( + "default_member_permissions", + decode.optional(permissions_decoder()), + ) + use is_nsfw <- decode.optional_field("nsfw", False, decode.bool) + use integration_types <- decode.optional_field( + "integration_types", + [], + decode.list( + decode.one_of(decode.int, or: [ + { + use string <- decode.then(decode.string) + case int.parse(string) { + Ok(int) -> decode.success(int) + Error(_) -> decode.failure(0, "Int") + } + }, + ]), + ), + ) + + let is_guild_installed = integration_types |> list.contains(0) + let is_user_installed = integration_types |> list.contains(1) + + use contexts <- decode.optional_field("contexts", [], { + use option <- decode.then( + decode.optional(decode.list(of: command_context_decoder())), + ) + case option { + Some(list) -> decode.success(list) + None -> decode.success([]) + } + }) + use version <- decode.field("version", snowflake_decoder()) + + decode.success(SlashCommand( + id:, + command_id:, + application_id:, + guild_id:, + name:, + name_localizations:, + description:, + description_localizations:, + options:, + required_permissions:, + is_nsfw:, + is_guild_installed:, + is_user_installed:, + contexts:, + version:, + )) +} + +pub type UserCommand { + UserCommand( + id: Snowflake(UserCommand), + command_id: Snowflake(Command), + application_id: Snowflake(Application), + /// Is present for guild-specific commands. + guild_id: Option(Snowflake(Guild)), + name: String, + name_localizations: Option(Dict(Locale, String)), + /// None - no permissions required + /// Some([]) - disabled for everyone by default + required_permissions: Option(List(Permission)), + is_nsfw: Bool, + /// Whether the command can be used in guild-installed environments. + is_guild_installed: Bool, + /// Whether the command can be used in user-installed environments. + is_user_installed: Bool, + /// Is empty for guild-specific commands. + contexts: List(CommandContext), + version: Snowflake(CommandVersion), + ) +} + +pub fn user_command_decoder() -> Decoder(UserCommand) { + use id <- decode.field("id", snowflake_decoder()) + use command_id <- decode.field("id", snowflake_decoder()) + use application_id <- decode.field("application_id", snowflake_decoder()) + use guild_id <- decode.optional_field( + "guild_id", + None, + decode.map(snowflake_decoder(), Some), + ) + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use required_permissions <- decode.field( + "default_member_permissions", + decode.optional(permissions_decoder()), + ) + use is_nsfw <- decode.optional_field("nsfw", False, decode.bool) + use integration_types <- decode.optional_field( + "integration_types", + [], + decode.list( + decode.one_of(decode.int, or: [ + { + use string <- decode.then(decode.string) + case int.parse(string) { + Ok(int) -> decode.success(int) + Error(_) -> decode.failure(0, "Int") + } + }, + ]), + ), + ) + + let is_guild_installed = integration_types |> list.contains(0) + let is_user_installed = integration_types |> list.contains(1) + + use contexts <- decode.optional_field( + "contexts", + [], + decode.list(of: command_context_decoder()), + ) + use version <- decode.field("version", snowflake_decoder()) + + decode.success(UserCommand( + id:, + command_id:, + application_id:, + guild_id:, + name:, + name_localizations:, + required_permissions:, + is_nsfw:, + is_guild_installed:, + is_user_installed:, + contexts:, + version:, + )) +} + +pub type MessageCommand { + MessageCommand( + id: Snowflake(MessageCommand), + command_id: Snowflake(Command), + application_id: Snowflake(Application), + /// Is present for guild-specific commands. + guild_id: Option(Snowflake(Guild)), + name: String, + name_localizations: Option(Dict(Locale, String)), + /// None - no permissions required + /// Some([]) - disabled for everyone by default + required_permissions: Option(List(Permission)), + is_nsfw: Bool, + /// Whether the command can be used in guild-installed environments. + is_guild_installed: Bool, + /// Whether the command can be used in user-installed environments. + is_user_installed: Bool, + /// Is empty for guild-specific commands. + contexts: List(CommandContext), + version: Snowflake(CommandVersion), + ) +} + +pub fn message_command_decoder() -> Decoder(MessageCommand) { + use id <- decode.field("id", snowflake_decoder()) + use command_id <- decode.field("id", snowflake_decoder()) + use application_id <- decode.field("application_id", snowflake_decoder()) + use guild_id <- decode.optional_field( + "guild_id", + None, + decode.map(snowflake_decoder(), Some), + ) + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use required_permissions <- decode.field( + "default_member_permissions", + decode.optional(permissions_decoder()), + ) + use is_nsfw <- decode.optional_field("nsfw", False, decode.bool) + use integration_types <- decode.optional_field( + "integration_types", + [], + decode.list( + decode.one_of(decode.int, or: [ + { + use string <- decode.then(decode.string) + case int.parse(string) { + Ok(int) -> decode.success(int) + Error(_) -> decode.failure(0, "Int") + } + }, + ]), + ), + ) + + let is_guild_installed = integration_types |> list.contains(0) + let is_user_installed = integration_types |> list.contains(1) + + use contexts <- decode.optional_field( + "contexts", + [], + decode.list(of: command_context_decoder()), + ) + use version <- decode.field("version", snowflake_decoder()) + + decode.success(MessageCommand( + id:, + command_id:, + application_id:, + guild_id:, + name:, + name_localizations:, + required_permissions:, + is_nsfw:, + is_guild_installed:, + is_user_installed:, + contexts:, + version:, + )) +} + +pub type CommandContext { + CommandUsableInGuilds + CommandUsableInBotDms + /// The command is usable in DMs between different users. + CommandUsableInDms +} + +pub fn command_context_decoder() -> Decoder(CommandContext) { + use variant <- decode.then(decode.int) + + case variant { + 0 -> decode.success(CommandUsableInGuilds) + 1 -> decode.success(CommandUsableInBotDms) + 2 -> decode.success(CommandUsableInDms) + _ -> decode.failure(CommandUsableInGuilds, "CommandContext") + } +} + +pub type SlashCommandOption { + SlashCommandOptionSubCommand(SubCommandSlashCommandOption) + SlashCommandOptionSubCommandGroup(SubCommandGroupSlashCommandOption) + SlashCommandOptionString(StringSlashCommandOption) + SlashCommandOptionInt(IntSlashCommandOption) + SlashCommandOptionFloat(FloatSlashCommandOption) + SlashCommandOptionBool(BoolSlashCommandOption) + SlashCommandOptionUser(UserSlashCommandOption) + SlashCommandOptionChannel(ChannelSlashCommandOption) + SlashCommandOptionRole(RoleSlashCommandOption) + SlashCommandOptionMentionable(MentionableSlashCommandOption) + SlashCommandOptionAttachment(AttachmentSlashCommandOption) +} + +pub fn slash_command_option_decoder() -> Decoder(SlashCommandOption) { + use type_ <- decode.field("type", decode.int) + case type_ { + 1 -> + decode.map( + sub_command_slash_command_option_decoder(), + SlashCommandOptionSubCommand, + ) + 2 -> + decode.map( + sub_command_group_slash_command_option_decoder(), + SlashCommandOptionSubCommandGroup, + ) + + 3 -> + decode.map( + string_slash_command_option_decoder(), + SlashCommandOptionString, + ) + 4 -> decode.map(int_slash_command_option_decoder(), SlashCommandOptionInt) + 5 -> decode.map(bool_slash_command_option_decoder(), SlashCommandOptionBool) + 6 -> decode.map(user_slash_command_option_decoder(), SlashCommandOptionUser) + 7 -> + decode.map( + channel_slash_command_option_decoder(), + SlashCommandOptionChannel, + ) + 8 -> decode.map(role_slash_command_option_decoder(), SlashCommandOptionRole) + 9 -> + decode.map( + mentionable_slash_command_option_decoder(), + SlashCommandOptionMentionable, + ) + 10 -> + decode.map(float_slash_command_option_decoder(), SlashCommandOptionFloat) + 11 -> + decode.map( + attachment_slash_command_option_decoder(), + SlashCommandOptionAttachment, + ) + _ -> + decode.failure( + SlashCommandOptionBool(BoolSlashCommandOption("", None, "", None, False)), + "SlashCommandOption", + ) + } +} + +pub type SubCommandSlashCommandOption { + SubCommandSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + options: List(SubCommandOption), + ) +} + +pub fn sub_command_slash_command_option_decoder() -> Decoder( + SubCommandSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use options <- decode.optional_field( + "options", + [], + decode.list(of: sub_command_option_decoder()), + ) + + decode.success(SubCommandSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + options:, + )) +} + +pub type SubCommandGroupSlashCommandOption { + SubCommandGroupSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + options: List(SubCommandGroupOption), + ) +} + +pub fn sub_command_group_slash_command_option_decoder() -> Decoder( + SubCommandGroupSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use options <- decode.optional_field( + "options", + [], + decode.list(of: sub_command_group_option_decoder()), + ) + + decode.success(SubCommandGroupSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + options:, + )) +} + +pub type StringSlashCommandOption { + StringSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + choices: List(StringSlashCommandOptionChoice), + min_length: Option(Int), + max_length: Option(Int), + is_autocomplete_enabled: Bool, + ) +} + +pub fn string_slash_command_option_decoder() -> Decoder( + StringSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + use choices <- decode.optional_field( + "choices", + [], + decode.list(of: string_slash_command_option_choice_decoder()), + ) + use min_length <- decode.optional_field( + "min_length", + None, + decode.map(decode.int, Some), + ) + use max_length <- decode.optional_field( + "max_length", + None, + decode.map(decode.int, Some), + ) + use is_autocomplete_enabled <- decode.optional_field( + "autocomplete", + False, + decode.bool, + ) + + decode.success(StringSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + choices:, + min_length:, + max_length:, + is_autocomplete_enabled:, + )) +} + +pub type StringSlashCommandOptionChoice { + StringSlashCommandOptionChoice( + name: String, + name_localizations: Option(Dict(Locale, String)), + value: String, + ) +} + +fn string_slash_command_option_choice_decoder() -> Decoder( + StringSlashCommandOptionChoice, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use value <- decode.field("value", decode.string) + decode.success(StringSlashCommandOptionChoice( + name:, + name_localizations:, + value:, + )) +} + +pub type IntSlashCommandOption { + IntSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + choices: List(IntSlashCommandOptionChoice), + min_value: Option(Int), + max_value: Option(Int), + is_autocomplete_enabled: Bool, + ) +} + +pub fn int_slash_command_option_decoder() -> Decoder(IntSlashCommandOption) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + use choices <- decode.optional_field( + "choices", + [], + decode.list(of: int_slash_command_option_choice_decoder()), + ) + use min_value <- decode.optional_field( + "min_value", + None, + decode.map(decode.int, Some), + ) + use max_value <- decode.optional_field( + "max_value", + None, + decode.map(decode.int, Some), + ) + use is_autocomplete_enabled <- decode.optional_field( + "autocomplete", + False, + decode.bool, + ) + + decode.success(IntSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + choices:, + min_value:, + max_value:, + is_autocomplete_enabled:, + )) +} + +pub type IntSlashCommandOptionChoice { + IntSlashCommandOptionChoice( + name: String, + name_localizations: Option(Dict(Locale, String)), + value: Int, + ) +} + +fn int_slash_command_option_choice_decoder() -> Decoder( + IntSlashCommandOptionChoice, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use value <- decode.field("value", decode.int) + + decode.success(IntSlashCommandOptionChoice(name:, name_localizations:, value:)) +} + +pub type FloatSlashCommandOption { + FloatSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + choices: List(FloatSlashCommandOptionChoice), + min_value: Option(Float), + max_value: Option(Float), + is_autocomplete_enabled: Bool, + ) +} + +pub fn float_slash_command_option_decoder() -> Decoder(FloatSlashCommandOption) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + use choices <- decode.optional_field( + "choices", + [], + decode.list(of: float_slash_command_option_choice_decoder()), + ) + use min_value <- decode.optional_field( + "min_value", + None, + decode.map(decode.float, Some), + ) + use max_value <- decode.optional_field( + "max_value", + None, + decode.map(decode.float, Some), + ) + use is_autocomplete_enabled <- decode.optional_field( + "autocomplete", + False, + decode.bool, + ) + + decode.success(FloatSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + choices:, + min_value:, + max_value:, + is_autocomplete_enabled:, + )) +} + +pub type FloatSlashCommandOptionChoice { + FloatSlashCommandOptionChoice( + name: String, + name_localizations: Option(Dict(Locale, String)), + value: Float, + ) +} + +fn float_slash_command_option_choice_decoder() -> Decoder( + FloatSlashCommandOptionChoice, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use value <- decode.field("value", decode.float) + + decode.success(FloatSlashCommandOptionChoice( + name:, + name_localizations:, + value:, + )) +} + +pub type BoolSlashCommandOption { + BoolSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + ) +} + +pub fn bool_slash_command_option_decoder() -> Decoder(BoolSlashCommandOption) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + decode.success(BoolSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + )) +} + +pub type UserSlashCommandOption { + UserSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + ) +} + +pub fn user_slash_command_option_decoder() -> Decoder(UserSlashCommandOption) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + + decode.success(UserSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + )) +} + +pub type ChannelSlashCommandOption { + ChannelSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + /// An empty list allows all channel types to be selected. + channel_types: List(SelectChannelType), + ) +} + +pub fn channel_slash_command_option_decoder() -> Decoder( + ChannelSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + use channel_types <- decode.optional_field( + "channel_types", + [], + decode.list(of: select_channel_type_decoder()), + ) + + decode.success(ChannelSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + channel_types:, + )) +} + +pub type RoleSlashCommandOption { + RoleSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + ) +} + +pub fn role_slash_command_option_decoder() -> Decoder(RoleSlashCommandOption) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + + decode.success(RoleSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + )) +} + +pub type MentionableSlashCommandOption { + MentionableSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + ) +} + +pub fn mentionable_slash_command_option_decoder() -> Decoder( + MentionableSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + + decode.success(MentionableSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + )) +} + +pub type AttachmentSlashCommandOption { + AttachmentSlashCommandOption( + name: String, + name_localizations: Option(Dict(Locale, String)), + description: String, + description_localizations: Option(Dict(Locale, String)), + is_required: Bool, + /// An empty list accepts all file types. + file_types: List(FileUploadFilter), + ) +} + +pub fn attachment_slash_command_option_decoder() -> Decoder( + AttachmentSlashCommandOption, +) { + use name <- decode.field("name", decode.string) + use name_localizations <- decode.optional_field( + "name_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use description <- decode.field("description", decode.string) + use description_localizations <- decode.optional_field( + "description_localizations", + None, + decode.optional(decode.dict(locale_decoder(), decode.string)), + ) + use is_required <- decode.optional_field("required", False, decode.bool) + use file_types <- decode.optional_field( + "file_types", + [], + decode.list(of: file_upload_filter_decoder()), + ) + + decode.success(AttachmentSlashCommandOption( + name:, + name_localizations:, + description:, + description_localizations:, + is_required:, + file_types:, + )) +} + +/// CAUTION: The filter doesn't check file contents, only the file's extension! +/// +/// You're still expected to verify file contents. +pub type FileUploadFilter { + /// Default set: `.png`, `.gif`, `.jpg`, `.jpeg`, `.jfif`, `.webp`, `.avif` + AcceptUploadingImages + /// Default set: `.mp4`, `.mov`, `.qt`, `.webm` + AcceptUploadingVideos + /// Default set: `.mp3`, `.m4a`, `.wav`, `.ogg`, `.opus`, `.flac` + AcceptUploadingAudios + /// Include the `.` prefix. + AcceptUploadingFilesWithExtension(extension: String) +} + +pub fn file_upload_filter_decoder() -> Decoder(FileUploadFilter) { + use variant <- decode.then(decode.string) + + case variant { + "image" -> decode.success(AcceptUploadingImages) + "video" -> decode.success(AcceptUploadingVideos) + "audio" -> decode.success(AcceptUploadingAudios) + "." <> extension -> + decode.success(AcceptUploadingFilesWithExtension("." <> extension)) + _ -> decode.failure(AcceptUploadingImages, "FileUploadFilter") + } +} + +pub type SubCommandOption { + SubCommandOptionString(StringSlashCommandOption) + SubCommandOptionInt(IntSlashCommandOption) + SubCommandOptionFloat(FloatSlashCommandOption) + SubCommandOptionBool(BoolSlashCommandOption) + SubCommandOptionUser(UserSlashCommandOption) + SubCommandOptionChannel(ChannelSlashCommandOption) + SubCommandOptionRole(RoleSlashCommandOption) + SubCommandOptionMentionable(MentionableSlashCommandOption) + SubCommandOptionAttachment(AttachmentSlashCommandOption) +} + +pub fn sub_command_option_decoder() -> Decoder(SubCommandOption) { + use type_ <- decode.field("type", decode.int) + case type_ { + 3 -> + decode.map(string_slash_command_option_decoder(), SubCommandOptionString) + 4 -> decode.map(int_slash_command_option_decoder(), SubCommandOptionInt) + 5 -> decode.map(bool_slash_command_option_decoder(), SubCommandOptionBool) + 6 -> decode.map(user_slash_command_option_decoder(), SubCommandOptionUser) + 7 -> + decode.map( + channel_slash_command_option_decoder(), + SubCommandOptionChannel, + ) + 8 -> decode.map(role_slash_command_option_decoder(), SubCommandOptionRole) + 9 -> + decode.map( + mentionable_slash_command_option_decoder(), + SubCommandOptionMentionable, + ) + 10 -> + decode.map(float_slash_command_option_decoder(), SubCommandOptionFloat) + 11 -> + decode.map( + attachment_slash_command_option_decoder(), + SubCommandOptionAttachment, + ) + _ -> + decode.failure( + SubCommandOptionBool(BoolSlashCommandOption("", None, "", None, False)), + "SubCommandOption", + ) + } +} + +pub type SubCommandGroupOption { + SubCommandGroupOptionSubCommand(SubCommandSlashCommandOption) + SubCommandGroupOptionSubCommandGroup(SubCommandGroupSlashCommandOption) +} + +pub fn sub_command_group_option_decoder() -> Decoder(SubCommandGroupOption) { + use type_ <- decode.field("type", decode.int) + case type_ { + 1 -> + decode.map( + sub_command_slash_command_option_decoder(), + SubCommandGroupOptionSubCommand, + ) + 2 -> + decode.map( + sub_command_group_slash_command_option_decoder(), + SubCommandGroupOptionSubCommandGroup, + ) + _ -> + decode.failure( + SubCommandGroupOptionSubCommand( + SubCommandSlashCommandOption("", None, "", None, []), + ), + "SubCommandGroupOption", + ) + } +} -- 2.51.2