From df37552486a120125cc4c216d5482078829eb270 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Fri, 27 Jun 2025 23:30:06 +0200 Subject: [PATCH] wow that's a lot of components json --- src/grom/channel/guild/forum.gleam | 59 ++++- src/grom/guild/audit_log.gleam | 6 +- .../application_command/command_option.gleam | 34 ++- src/grom/interaction/resolved.gleam | 59 ++++- src/grom/internal/time_rfc3339.gleam | 11 + src/grom/invite.gleam | 10 +- src/grom/message.gleam | 1 - src/grom/message/allowed_mentions.gleam | 6 +- src/grom/message/component/action_row.gleam | 30 ++- src/grom/message/component/button.gleam | 114 ++++++++- .../message/component/channel_select.gleam | 80 +++++- .../component/mentionable_select.gleam | 75 +++++- src/grom/message/component/role_select.gleam | 57 ++++- src/grom/message/component/section.gleam | 63 ++++- .../message/component/string_select.gleam | 117 ++++++++- src/grom/message/component/text_display.gleam | 24 +- src/grom/message/component/text_input.gleam | 78 +++++- src/grom/message/component/user_select.gleam | 57 ++++- src/grom/message/embed.gleam | 228 +++++++++++++++++- src/grom/message/resolved.gleam | 76 ------ src/grom/user/current_user.gleam | 24 +- 21 files changed, 1045 insertions(+), 164 deletions(-) delete mode 100644 src/grom/message/resolved.gleam diff --git a/src/grom/channel/guild/forum.gleam b/src/grom/channel/guild/forum.gleam index 4b1cf0e..e266981 100644 --- a/src/grom/channel/guild/forum.gleam +++ b/src/grom/channel/guild/forum.gleam @@ -1,8 +1,12 @@ import gleam/dynamic/decode import gleam/int import gleam/json.{type Json} +import gleam/list import gleam/option.{type Option, None, Some} import gleam/time/duration.{type Duration} +import grom/client +import grom/file.{type File} +import grom/internal/time_duration import grom/message import grom/message/allowed_mentions.{type AllowedMentions} import grom/message/attachment @@ -24,8 +28,11 @@ pub type Tag { pub opaque type StartThread { StartThread( name: String, - auto_archive_duration: Duration, + auto_archive_duration: Option(Duration), rate_limit_per_user: Option(Duration), + message: StartThreadMessage, + applied_tags: Option(List(String)), + files: Option(List(File)), ) } @@ -154,3 +161,53 @@ pub fn tag_to_json(tag: Tag) -> Json { [id, name, is_moderated, emoji_id, emoji_name] |> json.object } + +@internal +pub fn start_thread_to_json(start_thread: StartThread) -> Json { + let name = [#("name", json.string(start_thread.name))] + + let auto_archive_duration = case start_thread.auto_archive_duration { + Some(duration) -> [ + #("auto_archive_duration", time_duration.to_int_seconds_encode(duration)), + ] + None -> [] + } + + let rate_limit_per_user = case start_thread.rate_limit_per_user { + Some(limit) -> [ + #("rate_limit_per_user", time_duration.to_int_seconds_encode(limit)), + ] + None -> [] + } + + [name, auto_archive_duration, rate_limit_per_user] + |> list.flatten + |> json.object +} + +@internal +pub fn start_thread_message_to_json(message: StartThreadMessage) -> Json { + let content = case message.content { + Some(content) -> [#("content", json.string(content))] + None -> [] + } + + let embeds = case message.embeds { + Some(embeds) -> [#("embeds", json.array(embeds, embed.to_json))] + None -> [] + } + + let allowed_mentions = case message.allowed_mentions { + Some(allowed_mentions) -> [ + #("allowed_mentions", allowed_mentions.to_json(allowed_mentions)), + ] + None -> [] + } + + todo as "Components and the rest" + + [content, embeds, allowed_mentions] + |> list.flatten + |> json.object +} +// PUBLIC API FUNCTIONS -------------------------------------------------------- diff --git a/src/grom/guild/audit_log.gleam b/src/grom/guild/audit_log.gleam index 0fab0d3..61e122a 100644 --- a/src/grom/guild/audit_log.gleam +++ b/src/grom/guild/audit_log.gleam @@ -7,7 +7,7 @@ import gleam/list import gleam/result import grom/channel.{type Channel} import grom/client.{type Client} -import grom/error +import grom/error.{type Error} import grom/guild/audit_log/entry.{type Entry} import grom/guild/auto_moderation/rule.{type Rule} import grom/guild/scheduled_event.{type ScheduledEvent} @@ -118,7 +118,7 @@ pub fn get( client: Client, for guild_id: String, with query: List(GetQuery), -) -> Result(AuditLog, error.FlybycordError) { +) -> Result(AuditLog, Error) { let query = query |> list.map(fn(parameter) { @@ -145,5 +145,5 @@ pub fn get( response.body |> json.parse(using: decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } diff --git a/src/grom/interaction/application_command/command_option.gleam b/src/grom/interaction/application_command/command_option.gleam index cd13a18..9b11ae8 100644 --- a/src/grom/interaction/application_command/command_option.gleam +++ b/src/grom/interaction/application_command/command_option.gleam @@ -5,7 +5,7 @@ import gleam/dict.{type Dict} import gleam/dynamic/decode import gleam/option.{type Option, None} -import grom/channel +import grom/guild import grom/interaction/application_command/choice.{type Choice} // TYPES ----------------------------------------------------------------------- @@ -66,7 +66,7 @@ pub type CommandOption { name_localizations: Option(Dict(String, String)), description: String, description_localizations: Option(Dict(String, String)), - channel_types: List(channel.Type), + channel_types: List(ChannelType), is_required: Bool, ) Role( @@ -103,6 +103,17 @@ pub type CommandOption { ) } +pub type ChannelType { + TextChannel + DmChannel + VoiceChannel + CategoryChannel + AnnouncementChannel + StageChannel + ForumChannel + MediaChannel +} + // DECODERS -------------------------------------------------------------------- @internal @@ -337,7 +348,7 @@ fn channel_decoder() -> decode.Decoder(CommandOption) { ) use channel_types <- decode.field( "channel_types", - decode.list(channel.type_decoder()), + decode.list(channel_type_decoder()), ) use is_required <- decode.field("required", decode.bool) @@ -425,3 +436,20 @@ fn attachment_decoder() -> decode.Decoder(CommandOption) { is_required:, )) } + +@internal +pub fn channel_type_decoder() -> decode.Decoder(ChannelType) { + use type_ <- decode.then(decode.int) + + case type_ { + 0 -> decode.success(TextChannel) + 1 -> decode.success(DmChannel) + 2 -> decode.success(VoiceChannel) + 4 -> decode.success(CategoryChannel) + 5 -> decode.success(AnnouncementChannel) + 13 -> decode.success(StageChannel) + 15 -> decode.success(ForumChannel) + 16 -> decode.success(MediaChannel) + _ -> decode.failure(TextChannel, "ChannelType") + } +} diff --git a/src/grom/interaction/resolved.gleam b/src/grom/interaction/resolved.gleam index 170e3b7..2a7880a 100644 --- a/src/grom/interaction/resolved.gleam +++ b/src/grom/interaction/resolved.gleam @@ -1,5 +1,4 @@ import gleam/option.{type Option} -import grom/channel import grom/channel/guild/thread import grom/guild/member.{type Member} import grom/guild/role.{type Role} @@ -13,25 +12,67 @@ pub type Resolved { users: Option(List(#(String, User))), members: Option(List(#(String, Member))), roles: Option(List(#(String, Role))), - channels: Option(List(#(String, PartialChannel))), + channels: Option(List(#(String, Channel))), messages: Option(List(#(String, Message))), attachments: Option(List(#(String, Attachment))), ) } -pub type PartialChannel { - PartialChannel( +pub type Channel { + TextChannel( id: String, name: String, - type_: channel.Type, - permissions: List(Permission), + current_user_permissions: List(Permission), ) - PartialThread( + VoiceChannel( id: String, name: String, - type_: channel.Type, - permissions: List(Permission), + current_user_permissions: List(Permission), + ) + CategoryChannel( + id: String, + name: String, + current_user_permissions: List(Permission), + ) + AnnouncementChannel( + id: String, + name: String, + current_user_permissions: List(Permission), + ) + AnnouncementThread( + id: String, + name: String, + current_user_permissions: List(Permission), + metadata: thread.Metadata, + parent_id: String, + ) + PublicThread( + id: String, + name: String, + current_user_permissions: List(Permission), metadata: thread.Metadata, parent_id: String, ) + PrivateThread( + id: String, + name: String, + current_user_permissions: List(Permission), + metadata: thread.Metadata, + parent_id: String, + ) + StageChannel( + id: String, + name: String, + current_user_permissions: List(Permission), + ) + ForumChannel( + id: String, + name: String, + current_user_permissions: List(Permission), + ) + MediaChannel( + id: String, + name: String, + current_user_permissions: List(Permission), + ) } diff --git a/src/grom/internal/time_rfc3339.gleam b/src/grom/internal/time_rfc3339.gleam index 8549055..8dd7aa7 100644 --- a/src/grom/internal/time_rfc3339.gleam +++ b/src/grom/internal/time_rfc3339.gleam @@ -1,4 +1,6 @@ import gleam/dynamic/decode +import gleam/json.{type Json} +import gleam/time/duration import gleam/time/timestamp.{type Timestamp} // DECODERS -------------------------------------------------------------------- @@ -15,3 +17,12 @@ pub fn decoder() -> decode.Decoder(Timestamp) { } } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(timestamp: Timestamp) -> Json { + timestamp + |> timestamp.to_rfc3339(duration.seconds(0)) + |> json.string +} diff --git a/src/grom/invite.gleam b/src/grom/invite.gleam index 6bf1fbd..36f927a 100644 --- a/src/grom/invite.gleam +++ b/src/grom/invite.gleam @@ -9,7 +9,7 @@ import gleam/time/timestamp.{type Timestamp} import grom/application.{type Application} import grom/channel.{type Channel} import grom/client.{type Client} -import grom/error +import grom/error.{type Error} import grom/guild.{type Guild} import grom/internal/rest import grom/internal/time_duration @@ -288,7 +288,7 @@ pub fn create_encode(create: Create) -> Json { pub fn get_many( client: Client, for channel_id: String, -) -> Result(List(WithMetadata), error.FlybycordError) { +) -> Result(List(WithMetadata), Error) { use response <- result.try( client |> rest.new_request(http.Get, "/channels/" <> channel_id <> "/invites") @@ -297,7 +297,7 @@ pub fn get_many( response.body |> json.parse(using: decode.list(with_metadata_decoder())) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } pub fn create( @@ -305,7 +305,7 @@ pub fn create( for channel_id: String, with create: Create, reason reason: Option(String), -) -> Result(WithoutMetadata, error.FlybycordError) { +) -> Result(WithoutMetadata, Error) { let json = create |> create_encode use response <- result.try( @@ -318,5 +318,5 @@ pub fn create( response.body |> json.parse(using: without_metadata_decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } diff --git a/src/grom/message.gleam b/src/grom/message.gleam index 3942d84..0d32c37 100644 --- a/src/grom/message.gleam +++ b/src/grom/message.gleam @@ -7,7 +7,6 @@ import gleam/option.{type Option, None} import gleam/result import gleam/time/timestamp.{type Timestamp} import grom/application.{type Application} -import grom/application/current_application import grom/channel/guild/thread.{type Thread} import grom/client.{type Client} import grom/error.{type Error} diff --git a/src/grom/message/allowed_mentions.gleam b/src/grom/message/allowed_mentions.gleam index eb9f7a9..7f357e4 100644 --- a/src/grom/message/allowed_mentions.gleam +++ b/src/grom/message/allowed_mentions.gleam @@ -63,9 +63,9 @@ pub fn type_decoder() -> decode.Decoder(Type) { // ENCODERS -------------------------------------------------------------------- @internal -pub fn encode(allowed_mentions: AllowedMentions) -> Json { +pub fn to_json(allowed_mentions: AllowedMentions) -> Json { let types = case allowed_mentions.types { - Some(types) -> [#("parse", json.array(types, type_encode))] + Some(types) -> [#("parse", json.array(types, type_to_json))] None -> [] } @@ -90,7 +90,7 @@ pub fn encode(allowed_mentions: AllowedMentions) -> Json { } @internal -pub fn type_encode(type_: Type) -> Json { +pub fn type_to_json(type_: Type) -> Json { case type_ { Users -> "users" Roles -> "roles" diff --git a/src/grom/message/component/action_row.gleam b/src/grom/message/component/action_row.gleam index 5babb3b..c12ac0c 100644 --- a/src/grom/message/component/action_row.gleam +++ b/src/grom/message/component/action_row.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} import grom/message/component/button.{type Button} import grom/message/component/channel_select.{type ChannelSelect} import grom/message/component/mentionable_select.{type MentionableSelect} @@ -68,8 +70,32 @@ pub fn component_decoder() -> decode.Decoder(Component) { } _ -> decode.failure( - Button(button.Regular(None, button.Primary, None, None, "", False)), + Button(button.Regular(None, False, button.Primary, None, None, "")), "Component", ) } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(action_row: ActionRow) -> Json { + let type_ = [#("type", json.int(1))] + + let id = case action_row.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + todo as "components" + + [type_, id] + |> list.flatten + |> json.object +} + +pub fn component_to_json(component: Component) -> Json { + case component { + _ -> todo as "come back when the rest of components is done" + } +} diff --git a/src/grom/message/component/button.gleam b/src/grom/message/component/button.gleam index 942918e..00ba3ae 100644 --- a/src/grom/message/component/button.gleam +++ b/src/grom/message/component/button.gleam @@ -1,26 +1,27 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} -import grom/emoji.{type Emoji} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- pub type Button { Regular( id: Option(Int), + is_disabled: Bool, style: Style, label: Option(String), emoji: Option(Emoji), custom_id: String, - is_disabled: Bool, ) Link( id: Option(Int), + is_disabled: Bool, label: Option(String), emoji: Option(Emoji), url: String, - is_disabled: Bool, ) - Premium(id: Option(Int), sku_id: String, is_disabled: Bool) + Premium(id: Option(Int), is_disabled: Bool, sku_id: String) } pub type Style { @@ -30,6 +31,10 @@ pub type Style { Danger } +pub type Emoji { + Emoji(id: Option(String), name: String, is_animated: Bool) +} + // DECODERS -------------------------------------------------------------------- @internal @@ -39,7 +44,7 @@ pub fn decoder() -> decode.Decoder(Button) { 1 | 2 | 3 | 4 -> regular_decoder() 5 -> link_decoder() 6 -> premium_decoder() - _ -> decode.failure(Regular(None, Primary, None, None, "", False), "Button") + _ -> decode.failure(Regular(None, False, Primary, None, None, ""), "Button") } } @@ -54,7 +59,7 @@ fn regular_decoder() -> decode.Decoder(Button) { use emoji <- decode.optional_field( "emoji", None, - decode.optional(emoji.decoder()), + decode.optional(emoji_decoder()), ) use custom_id <- decode.field("custom_id", decode.string) use is_disabled <- decode.optional_field("disabled", False, decode.bool) @@ -72,7 +77,7 @@ fn link_decoder() -> decode.Decoder(Button) { use emoji <- decode.optional_field( "emoji", None, - decode.optional(emoji.decoder()), + decode.optional(emoji_decoder()), ) use url <- decode.field("url", decode.string) use is_disabled <- decode.optional_field("disabled", False, decode.bool) @@ -99,3 +104,96 @@ pub fn style_decoder() -> decode.Decoder(Style) { _ -> decode.failure(Primary, "Style") } } + +@internal +pub fn emoji_decoder() -> decode.Decoder(Emoji) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.string) + use is_animated <- decode.optional_field("animated", False, decode.bool) + + decode.success(Emoji(id:, name:, is_animated:)) +} + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(button: Button) -> Json { + let type_ = [#("type", json.int(2))] + + let id = case button.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let style = case button { + Regular(style:, ..) -> [#("style", style_to_json(style))] + Link(..) -> [#("style", json.int(5))] + Premium(..) -> [#("style", json.int(6))] + } + + let label = case button { + Regular(label:, ..) | Link(label:, ..) -> + case label { + Some(label) -> [#("label", json.string(label))] + None -> [] + } + _ -> [] + } + + let emoji = case button { + Regular(emoji:, ..) | Link(emoji:, ..) -> + case emoji { + Some(emoji) -> [#("emoji", emoji_to_json(emoji))] + None -> [] + } + _ -> [] + } + + let custom_id = case button { + Regular(custom_id:, ..) -> [#("custom_id", json.string(custom_id))] + _ -> [] + } + + let sku_id = case button { + Premium(sku_id:, ..) -> [#("sku_id", json.string(sku_id))] + _ -> [] + } + + let url = case button { + Link(url:, ..) -> [#("url", json.string(url))] + _ -> [] + } + + let is_disabled = [#("disabled", json.bool(button.is_disabled))] + + [type_, id, style, label, emoji, custom_id, sku_id, url, is_disabled] + |> list.flatten + |> json.object +} + +@internal +pub fn style_to_json(style: Style) -> Json { + case style { + Primary -> 1 + Secondary -> 2 + Success -> 3 + Danger -> 4 + } + |> json.int +} + +@internal +pub fn emoji_to_json(emoji: Emoji) -> Json { + let id = case emoji.id { + Some(id) -> [#("id", json.string(id))] + None -> [] + } + + let name = [#("name", json.string(emoji.name))] + + let is_animated = [#("animated", json.bool(emoji.is_animated))] + + [id, name, is_animated] + |> list.flatten + |> json.object +} diff --git a/src/grom/message/component/channel_select.gleam b/src/grom/message/component/channel_select.gleam index af1a2cf..5a7b9a0 100644 --- a/src/grom/message/component/channel_select.gleam +++ b/src/grom/message/component/channel_select.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} pub type ChannelSelect { ChannelSelect( @@ -16,7 +18,7 @@ pub type ChannelSelect { pub type ChannelType { Text - DM + Dm Category Announcement Forum @@ -73,7 +75,7 @@ pub fn channel_type_decoder() -> decode.Decoder(ChannelType) { use variant <- decode.then(decode.int) case variant { 0 -> decode.success(Text) - 1 -> decode.success(DM) + 1 -> decode.success(Dm) 4 -> decode.success(Category) 5 -> decode.success(Announcement) 15 -> decode.success(Forum) @@ -81,3 +83,75 @@ pub fn channel_type_decoder() -> decode.Decoder(ChannelType) { _ -> decode.failure(Text, "ChannelType") } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(channel_select: ChannelSelect) -> Json { + let type_ = [#("type", json.int(8))] + + let id = case channel_select.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(channel_select.custom_id))] + + let channel_types = case channel_select.channel_types { + Some(types) -> [#("channel_types", json.array(types, channel_type_to_json))] + None -> [] + } + + let placeholder = case channel_select.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + let default_values = case channel_select.default_values { + Some(values) -> [ + #("default_values", json.array(values, default_value_to_json)), + ] + None -> [] + } + + let min_values = [#("min_values", json.int(channel_select.min_values))] + + let max_values = [#("max_values", json.int(channel_select.max_values))] + + let is_disabled = [#("disabled", json.bool(channel_select.is_disabled))] + + [ + type_, + id, + custom_id, + channel_types, + placeholder, + default_values, + min_values, + max_values, + is_disabled, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn channel_type_to_json(channel_type: ChannelType) -> Json { + case channel_type { + Text -> 0 + Dm -> 1 + Category -> 4 + Announcement -> 5 + Forum -> 15 + Media -> 16 + } + |> json.int +} + +@internal +pub fn default_value_to_json(default_value: DefaultValue) -> Json { + json.object([ + #("id", json.string(default_value.id)), + #("type", json.string("channel")), + ]) +} diff --git a/src/grom/message/component/mentionable_select.gleam b/src/grom/message/component/mentionable_select.gleam index 9ba500b..eab93f8 100644 --- a/src/grom/message/component/mentionable_select.gleam +++ b/src/grom/message/component/mentionable_select.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- @@ -16,9 +18,8 @@ pub type MentionableSelect { } pub type DefaultValue { - User(id: String) - Role(id: String) - Channel(id: String) + UserValue(id: String) + RoleValue(id: String) } // DECODERS -------------------------------------------------------------------- @@ -56,9 +57,67 @@ pub fn default_value_decoder() -> decode.Decoder(DefaultValue) { use id <- decode.field("id", decode.string) case type_ { - "user" -> decode.success(User(id:)) - "role" -> decode.success(Role(id:)) - "channel" -> decode.success(Channel(id:)) - _ -> decode.failure(User(""), "DefaultValue") + "user" -> decode.success(UserValue(id:)) + "role" -> decode.success(RoleValue(id:)) + _ -> decode.failure(UserValue(""), "DefaultValue") } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(mentionable_select: MentionableSelect) -> Json { + let type_ = [#("type", json.int(7))] + + let id = case mentionable_select.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(mentionable_select.custom_id))] + + let placeholder = case mentionable_select.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + let default_values = case mentionable_select.default_values { + Some(values) -> [ + #("default_values", json.array(values, default_value_to_json)), + ] + None -> [] + } + + let min_values = [#("min_values", json.int(mentionable_select.min_values))] + + let max_values = [#("max_values", json.int(mentionable_select.max_values))] + + let is_disabled = [#("disabled", json.bool(mentionable_select.is_disabled))] + + [ + type_, + id, + custom_id, + placeholder, + default_values, + min_values, + max_values, + is_disabled, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn default_value_to_json(default_value: DefaultValue) -> Json { + json.object([ + #("id", json.string(default_value.id)), + #( + "type", + json.string(case default_value { + UserValue(..) -> "user" + RoleValue(..) -> "role" + }), + ), + ]) +} diff --git a/src/grom/message/component/role_select.gleam b/src/grom/message/component/role_select.gleam index 43ac97c..c891ccb 100644 --- a/src/grom/message/component/role_select.gleam +++ b/src/grom/message/component/role_select.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- @@ -54,3 +56,56 @@ pub fn default_value_decoder() -> decode.Decoder(DefaultValue) { use id <- decode.field("id", decode.string) decode.success(DefaultValue(id:)) } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(role_select: RoleSelect) -> Json { + let type_ = [#("type", json.int(6))] + + let id = case role_select.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(role_select.custom_id))] + + let placeholder = case role_select.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + let default_values = case role_select.default_values { + Some(values) -> [ + #("default_values", json.array(values, default_value_to_json)), + ] + None -> [] + } + + let min_values = [#("min_values", json.int(role_select.min_values))] + + let max_values = [#("max_values", json.int(role_select.max_values))] + + let is_disabled = [#("disabled", json.bool(role_select.is_disabled))] + + [ + type_, + id, + custom_id, + placeholder, + default_values, + min_values, + max_values, + is_disabled, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn default_value_to_json(default_value: DefaultValue) -> Json { + json.object([ + #("id", json.string(default_value.id)), + #("type", json.string("role")), + ]) +} diff --git a/src/grom/message/component/section.gleam b/src/grom/message/component/section.gleam index 4055975..9facaa4 100644 --- a/src/grom/message/component/section.gleam +++ b/src/grom/message/component/section.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} import grom/message/component/button.{type Button} import grom/message/component/text_display.{type TextDisplay} import grom/message/component/unfurled_media_item.{type UnfurledMediaItem} @@ -71,8 +73,65 @@ pub fn accessory_decoder() { } _ -> decode.failure( - Button(button.Regular(None, button.Primary, None, None, "", False)), + Button(button.Regular(None, False, button.Primary, None, None, "")), "Accessory", ) } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(section: Section) -> Json { + let type_ = [#("type", json.int(9))] + + let id = case section.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let components = [ + #("components", json.array(section.components, component_to_json)), + ] + + let accessory = [#("accessory", accessory_to_json(section.accessory))] + + [type_, id, components, accessory] + |> list.flatten + |> json.object +} + +@internal +pub fn component_to_json(component: Component) -> Json { + case component { + TextDisplay(text_display) -> text_display.to_json(text_display) + } +} + +@internal +pub fn accessory_to_json(accessory: Accessory) -> Json { + case accessory { + Button(button) -> button.to_json(button) + Thumbnail(..) -> { + let type_ = [#("type", json.int(11))] + + let id = case accessory.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let media = todo as "unfurled media item to json" + + let description = case accessory.description { + Some(description) -> [#("description", json.string(description))] + None -> [] + } + + let is_spoiler = [#("spoiler", json.bool(accessory.is_spoiler))] + + [type_, id, media, description, is_spoiler] + |> list.flatten + |> json.object + } + } +} diff --git a/src/grom/message/component/string_select.gleam b/src/grom/message/component/string_select.gleam index 133c4f1..b4a578a 100644 --- a/src/grom/message/component/string_select.gleam +++ b/src/grom/message/component/string_select.gleam @@ -1,6 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option as GOption, None} -import grom/emoji.{type Emoji} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option as GOption, None, Some} // TYPES ----------------------------------------------------------------------- @@ -22,10 +23,14 @@ pub type Option { value: String, description: GOption(String), emoji: GOption(Emoji), - is_default: GOption(Bool), + is_default: Bool, ) } +pub type Emoji { + Emoji(id: GOption(String), name: String, is_animated: Bool) +} + // DECODERS -------------------------------------------------------------------- @internal @@ -40,7 +45,7 @@ pub fn decoder() -> decode.Decoder(StringSelect) { ) use min_values <- decode.optional_field("min_values", 1, decode.int) use max_values <- decode.optional_field("max_values", 1, decode.int) - use is_disabled <- decode.optional_field("is_disabled", False, decode.bool) + use is_disabled <- decode.optional_field("disabled", False, decode.bool) decode.success(StringSelect( id:, custom_id:, @@ -56,8 +61,106 @@ pub fn decoder() -> decode.Decoder(StringSelect) { pub fn option_decoder() -> decode.Decoder(Option) { use label <- decode.field("label", decode.string) use value <- decode.field("value", decode.string) - use description <- decode.field("description", decode.optional(decode.string)) - use emoji <- decode.field("emoji", decode.optional(emoji.decoder())) - use is_default <- decode.field("is_default", decode.optional(decode.bool)) + use description <- decode.optional_field( + "description", + None, + decode.optional(decode.string), + ) + use emoji <- decode.optional_field( + "emoji", + None, + decode.optional(emoji_decoder()), + ) + use is_default <- decode.optional_field("default", False, decode.bool) decode.success(Option(label:, value:, description:, emoji:, is_default:)) } + +@internal +pub fn emoji_decoder() -> decode.Decoder(Emoji) { + use id <- decode.field("id", decode.optional(decode.string)) + use name <- decode.field("name", decode.string) + use is_animated <- decode.optional_field("animated", False, decode.bool) + + decode.success(Emoji(id:, name:, is_animated:)) +} + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(string_select: StringSelect) -> Json { + let type_ = [#("type", json.int(3))] + + let id = case string_select.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(string_select.custom_id))] + + let options = [ + #("options", json.array(string_select.options, option_to_json)), + ] + + let placeholder = case string_select.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + let min_values = [#("min_values", json.int(string_select.min_values))] + + let max_values = [#("max_values", json.int(string_select.max_values))] + + let is_disabled = [#("disabled", json.bool(string_select.is_disabled))] + + [ + type_, + id, + custom_id, + options, + placeholder, + min_values, + max_values, + is_disabled, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn option_to_json(option: Option) -> Json { + let label = [#("label", json.string(option.label))] + + let value = [#("value", json.string(option.value))] + + let description = case option.description { + Some(description) -> [#("description", json.string(description))] + None -> [] + } + + let emoji = case option.emoji { + Some(emoji) -> [#("emoji", emoji_to_json(emoji))] + None -> [] + } + + let is_default = [#("default", json.bool(option.is_default))] + + [label, value, description, emoji, is_default] + |> list.flatten + |> json.object +} + +@internal +pub fn emoji_to_json(emoji: Emoji) -> Json { + let id = case emoji.id { + Some(id) -> [#("id", json.string(id))] + None -> [] + } + + let name = [#("name", json.string(emoji.name))] + + let is_animated = [#("animated", json.bool(emoji.is_animated))] + + [id, name, is_animated] + |> list.flatten + |> json.object +} diff --git a/src/grom/message/component/text_display.gleam b/src/grom/message/component/text_display.gleam index 7fa5cde..f7014a0 100644 --- a/src/grom/message/component/text_display.gleam +++ b/src/grom/message/component/text_display.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- @@ -11,7 +13,25 @@ pub type TextDisplay { @internal pub fn decoder() -> decode.Decoder(TextDisplay) { - use id <- decode.field("id", decode.optional(decode.int)) + use id <- decode.optional_field("id", None, decode.optional(decode.int)) use content <- decode.field("content", decode.string) decode.success(TextDisplay(id:, content:)) } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(text_display: TextDisplay) -> Json { + let type_ = [#("type", json.int(10))] + + let id = case text_display.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let content = [#("content", json.string(text_display.content))] + + [type_, id, content] + |> list.flatten + |> json.object +} diff --git a/src/grom/message/component/text_input.gleam b/src/grom/message/component/text_input.gleam index b8a2479..387f02c 100644 --- a/src/grom/message/component/text_input.gleam +++ b/src/grom/message/component/text_input.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- @@ -9,8 +11,8 @@ pub type TextInput { custom_id: String, style: Style, label: String, - min_length: Option(Int), - max_length: Option(Int), + min_length: Int, + max_length: Int, is_required: Bool, value: Option(String), placeholder: Option(String), @@ -30,16 +32,8 @@ pub fn decoder() -> decode.Decoder(TextInput) { use custom_id <- decode.field("custom_id", decode.string) use style <- decode.field("style", style_decoder()) use label <- decode.field("label", decode.string) - use min_length <- decode.optional_field( - "min_length", - None, - decode.optional(decode.int), - ) - use max_length <- decode.optional_field( - "max_length", - None, - decode.optional(decode.int), - ) + use min_length <- decode.optional_field("min_length", 0, decode.int) + use max_length <- decode.optional_field("max_length", 4000, decode.int) use is_required <- decode.optional_field("required", True, decode.bool) use value <- decode.optional_field( "value", @@ -74,3 +68,61 @@ pub fn style_decoder() -> decode.Decoder(Style) { _ -> decode.failure(Short, "Style") } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(text_input: TextInput) -> Json { + let type_ = [#("type", json.int(4))] + + let id = case text_input.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(text_input.custom_id))] + + let style = [#("style", style_to_json(text_input.style))] + + let label = [#("label", json.string(text_input.label))] + + let min_length = [#("min_length", json.int(text_input.min_length))] + + let max_length = [#("max_length", json.int(text_input.max_length))] + + let is_required = [#("required", json.bool(text_input.is_required))] + + let value = case text_input.value { + Some(value) -> [#("value", json.string(value))] + None -> [] + } + + let placeholder = case text_input.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + [ + type_, + id, + custom_id, + style, + label, + min_length, + max_length, + is_required, + value, + placeholder, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn style_to_json(style: Style) -> Json { + case style { + Short -> 1 + Paragraph -> 2 + } + |> json.int +} diff --git a/src/grom/message/component/user_select.gleam b/src/grom/message/component/user_select.gleam index 4c1cad6..821bebb 100644 --- a/src/grom/message/component/user_select.gleam +++ b/src/grom/message/component/user_select.gleam @@ -1,5 +1,7 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} // TYPES ----------------------------------------------------------------------- @@ -54,3 +56,56 @@ pub fn default_value_decoder() -> decode.Decoder(DefaultValue) { use id <- decode.field("id", decode.string) decode.success(DefaultValue(id:)) } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(user_select: UserSelect) -> Json { + let type_ = [#("type", json.int(5))] + + let id = case user_select.id { + Some(id) -> [#("id", json.int(id))] + None -> [] + } + + let custom_id = [#("custom_id", json.string(user_select.custom_id))] + + let placeholder = case user_select.placeholder { + Some(placeholder) -> [#("placeholder", json.string(placeholder))] + None -> [] + } + + let default_values = case user_select.default_values { + Some(values) -> [ + #("default_values", json.array(values, default_value_to_json)), + ] + None -> [] + } + + let min_values = [#("min_values", json.int(user_select.min_values))] + + let max_values = [#("max_values", json.int(user_select.max_values))] + + let is_disabled = [#("disabled", json.bool(user_select.is_disabled))] + + [ + type_, + id, + custom_id, + placeholder, + default_values, + min_values, + max_values, + is_disabled, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn default_value_to_json(default_value: DefaultValue) { + json.object([ + #("id", json.string(default_value.id)), + #("type", json.string("user")), + ]) +} diff --git a/src/grom/message/embed.gleam b/src/grom/message/embed.gleam index f4cf932..bd7d013 100644 --- a/src/grom/message/embed.gleam +++ b/src/grom/message/embed.gleam @@ -1,6 +1,10 @@ import gleam/dynamic/decode -import gleam/option.{type Option, None} +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/time/duration import gleam/time/timestamp.{type Timestamp} +import grom/guild import grom/internal/time_rfc3339 // TYPES ----------------------------------------------------------------------- @@ -254,3 +258,225 @@ pub fn field_decoder() -> decode.Decoder(Field) { ) decode.success(Field(name:, value:, is_inline:)) } + +// ENCODERS -------------------------------------------------------------------- + +pub fn to_json(embed: Embed) -> Json { + let title = case embed.title { + Some(title) -> [#("title", json.string(title))] + None -> [] + } + + let type_ = case embed.type_ { + Some(type_) -> [#("type", type_to_json(type_))] + None -> [] + } + + let description = case embed.description { + Some(description) -> [#("description", json.string(description))] + None -> [] + } + + let url = case embed.url { + Some(url) -> [#("url", json.string(url))] + None -> [] + } + + let timestamp = case embed.timestamp { + Some(timestamp) -> [#("timestamp", time_rfc3339.to_json(timestamp))] + None -> [] + } + + let color = case embed.color { + Some(color) -> [#("color", json.int(color))] + None -> [] + } + + let footer = case embed.footer { + Some(footer) -> [#("footer", footer_to_json(footer))] + None -> [] + } + + let image = case embed.image { + Some(image) -> [#("image", image_to_json(image))] + None -> [] + } + + let video = case embed.video { + Some(video) -> [#("video", video_to_json(video))] + None -> [] + } + + let provider = case embed.provider { + Some(provider) -> [#("provider", provider_to_json(provider))] + None -> [] + } + + let author = case embed.author { + Some(author) -> [#("author", author_to_json(author))] + None -> [] + } + + let fields = case embed.fields { + Some(fields) -> [#("fields", json.array(fields, field_to_json))] + None -> [] + } + + [ + title, + type_, + description, + url, + timestamp, + color, + footer, + image, + video, + provider, + author, + fields, + ] + |> list.flatten + |> json.object +} + +@internal +pub fn type_to_json(type_: Type) -> Json { + case type_ { + Rich -> "rich" + ImageEmbed -> "image" + VideoEmbed -> "video" + Gifv -> "gifv" + Article -> "article" + Link -> "link" + PollResult -> "poll_result" + } + |> json.string +} + +@internal +pub fn footer_to_json(footer: Footer) -> Json { + let text = [#("text", json.string(footer.text))] + + let icon_url = case footer.icon_url { + Some(url) -> [#("icon_url", json.string(url))] + None -> [] + } + + let proxy_icon_url = case footer.proxy_icon_url { + Some(url) -> [#("proxy_icon_url", json.string(url))] + None -> [] + } + + [text, icon_url, proxy_icon_url] + |> list.flatten + |> json.object +} + +@internal +pub fn image_to_json(image: Image) -> Json { + let url = [#("url", json.string(image.url))] + + let proxy_url = case image.proxy_url { + Some(url) -> [#("proxy_url", json.string(url))] + None -> [] + } + + let height = case image.height { + Some(height) -> [#("height", json.int(height))] + None -> [] + } + + let width = case image.width { + Some(width) -> [#("width", json.int(width))] + None -> [] + } + + [url, proxy_url, height, width] + |> list.flatten + |> json.object +} + +@internal +pub fn video_to_json(video: Video) -> Json { + let url = case video.url { + Some(url) -> [#("url", json.string(url))] + None -> [] + } + + let proxy_url = case video.proxy_url { + Some(url) -> [#("proxy_url", json.string(url))] + None -> [] + } + + let height = case video.height { + Some(height) -> [#("height", json.int(height))] + None -> [] + } + + let width = case video.width { + Some(width) -> [#("width", json.int(width))] + None -> [] + } + + [url, proxy_url, height, width] + |> list.flatten + |> json.object +} + +@internal +pub fn provider_to_json(provider: Provider) -> Json { + let name = case provider.name { + Some(name) -> [#("name", json.string(name))] + None -> [] + } + + let url = case provider.url { + Some(url) -> [#("url", json.string(url))] + None -> [] + } + + [name, url] + |> list.flatten + |> json.object +} + +@internal +pub fn author_to_json(author: Author) -> Json { + let name = [#("name", json.string(author.name))] + + let url = case author.url { + Some(url) -> [#("url", json.string(url))] + None -> [] + } + + let icon_url = case author.icon_url { + Some(url) -> [#("icon_url", json.string(url))] + None -> [] + } + + let proxy_icon_url = case author.proxy_icon_url { + Some(url) -> [#("proxy_icon_url", json.string(url))] + None -> [] + } + + [name, url, icon_url, proxy_icon_url] + |> list.flatten + |> json.object +} + +@internal +pub fn field_to_json(field: Field) -> Json { + let name = [#("name", json.string(field.name))] + + let value = [#("value", json.string(field.value))] + + let is_inline = case field.is_inline { + Some(inline) -> [#("inline", json.bool(inline))] + None -> [] + } + + [name, value, is_inline] + |> list.flatten + |> json.object +} diff --git a/src/grom/message/resolved.gleam b/src/grom/message/resolved.gleam deleted file mode 100644 index feacba4..0000000 --- a/src/grom/message/resolved.gleam +++ /dev/null @@ -1,76 +0,0 @@ -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import grom/channel -import grom/channel/guild/thread -import grom/guild/member.{type Member} -import grom/guild/role.{type Role} -import grom/message/attachment.{type Attachment} -import grom/permission.{type Permission} -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Resolved) { - use users <- decode.optional_field( - "users", - None, - decode.optional(decode.dict(decode.string, user.decoder())), - ) - use members <- decode.optional_field( - "members", - None, - decode.optional(decode.dict(decode.string, member.decoder())), - ) - use roles <- decode.optional_field( - "roles", - None, - decode.optional(decode.dict(decode.string, role.decoder())), - ) - use channels <- decode.optional_field( - "channels", - None, - decode.optional(decode.dict(decode.string, partial_channel_decoder())), - ) - use attachments <- decode.optional_field( - "attachments", - None, - decode.optional(decode.dict(decode.string, attachment.decoder())), - ) - decode.success(Resolved(users:, members:, roles:, channels:, attachments:)) -} - -@internal -pub fn partial_channel_decoder() -> decode.Decoder(PartialChannel) { - use variant <- decode.field("type", channel.type_decoder()) - case variant { - channel.AnnouncementThreadChannel - | channel.PublicThreadChannel - | channel.PrivateThreadChannel -> { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", channel.type_decoder()) - use permissions <- decode.field("permissions", permission.decoder()) - use metadata <- decode.field("metadata", thread.metadata_decoder()) - use parent_id <- decode.field("parent_id", decode.string) - decode.success(PartialThread( - id:, - name:, - type_:, - permissions:, - metadata:, - parent_id:, - )) - } - _ -> { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", channel.type_decoder()) - use permissions <- decode.field("permissions", permission.decoder()) - decode.success(PartialChannel(id:, name:, type_:, permissions:)) - } - } -} diff --git a/src/grom/user/current_user.gleam b/src/grom/user/current_user.gleam index bedc3b4..4ee8fa8 100644 --- a/src/grom/user/current_user.gleam +++ b/src/grom/user/current_user.gleam @@ -8,7 +8,7 @@ import gleam/option.{type Option, None, Some} import gleam/result import gleam/string import grom/client.{type Client} -import grom/error +import grom/error.{type Error} import grom/guild/member.{type Member} import grom/image import grom/internal/rest @@ -58,7 +58,7 @@ pub fn modify_encode(modify: Modify) -> Json { // PUBLIC API FUNCTIONS -------------------------------------------------------- -pub fn get(client: Client) -> Result(User, error.FlybycordError) { +pub fn get(client: Client) -> Result(User, Error) { use response <- result.try( client |> rest.new_request(http.Get, "/users/@me") @@ -67,13 +67,10 @@ pub fn get(client: Client) -> Result(User, error.FlybycordError) { response.body |> json.parse(using: user.decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } -pub fn modify( - client: Client, - with data: Modify, -) -> Result(User, error.FlybycordError) { +pub fn modify(client: Client, with data: Modify) -> Result(User, Error) { let json = data |> modify_encode use response <- result.try( client @@ -84,7 +81,7 @@ pub fn modify( response.body |> json.parse(using: user.decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } pub fn new_modify() -> Modify { @@ -106,7 +103,7 @@ pub fn modify_banner(modify: Modify, banner: image.Data) -> Modify { pub fn get_guilds( client: Client, with query: List(GetGuildsQuery), -) -> Result(User, error.FlybycordError) { +) -> Result(User, Error) { let query = list.map(query, fn(parameter) { case parameter { @@ -131,13 +128,10 @@ pub fn get_guilds( response.body |> json.parse(using: user.decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } -pub fn get_as_member( - client: Client, - guild_id: String, -) -> Result(Member, error.FlybycordError) { +pub fn get_as_member(client: Client, guild_id: String) -> Result(Member, Error) { use response <- result.try( client |> rest.new_request(http.Get, "/users/@me/guilds/" <> guild_id <> "/member") @@ -146,5 +140,5 @@ pub fn get_as_member( response.body |> json.parse(using: member.decoder()) - |> result.map_error(error.DecodeError) + |> result.map_error(error.CouldNotDecode) } -- 2.51.2