diff --git a/src/grom/channel/guild/forum.gleam b/src/grom/channel/guild/forum.gleam index e266981..86bebaa 100644 --- a/src/grom/channel/guild/forum.gleam +++ b/src/grom/channel/guild/forum.gleam @@ -1,11 +1,18 @@ import gleam/dynamic/decode +import gleam/http +import gleam/http/request import gleam/int import gleam/json.{type Json} import gleam/list import gleam/option.{type Option, None, Some} +import gleam/result import gleam/time/duration.{type Duration} -import grom/client +import grom/channel/guild/thread.{type Thread} +import grom/client.{type Client} +import grom/error.{type Error} import grom/file.{type File} +import grom/internal/flags +import grom/internal/rest import grom/internal/time_duration import grom/message import grom/message/allowed_mentions.{type AllowedMentions} @@ -31,7 +38,7 @@ pub opaque type StartThread { auto_archive_duration: Option(Duration), rate_limit_per_user: Option(Duration), message: StartThreadMessage, - applied_tags: Option(List(String)), + applied_tags_ids: Option(List(String)), files: Option(List(File)), ) } @@ -180,7 +187,16 @@ pub fn start_thread_to_json(start_thread: StartThread) -> Json { None -> [] } - [name, auto_archive_duration, rate_limit_per_user] + let message = [ + #("message", start_thread_message_to_json(start_thread.message)), + ] + + let applied_tags_ids = case start_thread.applied_tags_ids { + Some(ids) -> [#("applied_tags", json.array(ids, json.string))] + None -> [] + } + + [name, auto_archive_duration, rate_limit_per_user, message, applied_tags_ids] |> list.flatten |> json.object } @@ -204,10 +220,82 @@ pub fn start_thread_message_to_json(message: StartThreadMessage) -> Json { None -> [] } - todo as "Components and the rest" + let components = case message.components { + Some(components) -> [ + #("components", json.array(components, component.to_json)), + ] + None -> [] + } + + let sticker_ids = case message.sticker_ids { + Some(ids) -> [#("sticker_ids", json.array(ids, json.string))] + None -> [] + } - [content, embeds, allowed_mentions] + let attachments = case message.attachments { + Some(attachments) -> [ + #("attachments", json.array(attachments, attachment.create_to_json)), + ] + None -> [] + } + + let flags = case message.flags { + Some(flags) -> [ + #( + "flags", + flags + |> flags.to_int(message.bits_flags()) + |> json.int, + ), + ] + None -> [] + } + + [ + content, + embeds, + allowed_mentions, + components, + sticker_ids, + attachments, + flags, + ] |> list.flatten |> json.object } + // PUBLIC API FUNCTIONS -------------------------------------------------------- + +pub fn start_thread( + client: Client, + in channel_id: String, + with start_thread: StartThread, + because reason: Option(String), +) -> Result(Thread, Error) { + use response <- result.try(case start_thread.files { + Some(files) -> { + client + |> rest.new_multipart_request( + http.Post, + "/channels/" <> channel_id <> "/threads", + start_thread_to_json(start_thread), + files, + ) + |> rest.with_reason(reason) + |> rest.execute_multipart + } + None -> { + let json = start_thread |> start_thread_to_json + + client + |> rest.new_request(http.Post, "/channels/" <> channel_id <> "/threads") + |> request.set_body(json |> json.to_string) + |> rest.with_reason(reason) + |> rest.execute + } + }) + + response.body + |> json.parse(using: thread.decoder()) + |> result.map_error(error.CouldNotDecode) +} diff --git a/src/grom/error.gleam b/src/grom/error.gleam index e27a1cf..7a17218 100644 --- a/src/grom/error.gleam +++ b/src/grom/error.gleam @@ -8,4 +8,5 @@ pub type Error { HttpError(httpc.HttpError) CouldNotDecode(json.DecodeError) StatusCodeUnsuccessful(Response(String)) + BodyNotValidUtf8(BitArray) } diff --git a/src/grom/internal/rest.gleam b/src/grom/internal/rest.gleam index 83a7beb..1cd8ee3 100644 --- a/src/grom/internal/rest.gleam +++ b/src/grom/internal/rest.gleam @@ -1,6 +1,7 @@ +import gleam/bit_array import gleam/http import gleam/http/request.{type Request} -import gleam/http/response.{type Response} +import gleam/http/response.{type Response, Response} import gleam/httpc import gleam/int import gleam/json.{type Json} @@ -8,7 +9,7 @@ import gleam/list import gleam/option.{type Option, None, Some} import gleam/result import grom/client.{type Client} -import grom/error +import grom/error.{type Error} import grom/file.{type File} import multipart_form import multipart_form/field @@ -22,9 +23,7 @@ const discord_api_path = "api/v10" // INTERNAL FUNCTIONS ---------------------------------------------------------- @internal -pub fn execute( - request: Request(String), -) -> Result(Response(String), error.Error) { +pub fn execute(request: Request(String)) -> Result(Response(String), Error) { use response <- result.try( request |> httpc.send @@ -35,6 +34,28 @@ pub fn execute( |> ensure_status_code_success } +@internal +pub fn execute_multipart( + request: Request(BitArray), +) -> Result(Response(String), Error) { + use response <- result.try( + request + |> httpc.send_bits + |> result.map_error(error.HttpError), + ) + + use body <- result.try( + response.body + |> bit_array.to_string + |> result.replace_error(error.BodyNotValidUtf8(response.body)), + ) + + let response = Response(..response, body:) + + response + |> ensure_status_code_success +} + @internal pub fn new_request( client: Client, @@ -61,7 +82,7 @@ pub fn new_multipart_request( path: String, payload_json: Json, files: List(File), -) { +) -> Request(BitArray) { let #(_, file_parts) = files |> list.map_fold(0, fn(acc, file) { @@ -103,7 +124,7 @@ pub fn with_reason(request: Request(a), reason: Option(String)) -> Request(a) { fn ensure_status_code_success( response: Response(String), -) -> Result(Response(String), error.Error) { +) -> Result(Response(String), Error) { case response.status { status if status >= 200 && status < 300 -> Ok(response) _ -> Error(error.StatusCodeUnsuccessful(response)) diff --git a/src/grom/message/attachment.gleam b/src/grom/message/attachment.gleam index 4dd90f4..e65d017 100644 --- a/src/grom/message/attachment.gleam +++ b/src/grom/message/attachment.gleam @@ -1,6 +1,8 @@ import gleam/dynamic/decode import gleam/int -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.{type Duration} import grom/internal/base64 import grom/internal/flags @@ -103,3 +105,19 @@ pub fn decoder() -> decode.Decoder(Attachment) { flags:, )) } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn create_to_json(create: Create) -> Json { + let filename = [#("filename", json.string(create.filename))] + + let description = case create.description { + Some(description) -> [#("description", json.string(description))] + None -> [] + } + + [filename, description] + |> list.flatten + |> json.object +} diff --git a/src/grom/message/component.gleam b/src/grom/message/component.gleam index 9e57c5d..7d7df99 100644 --- a/src/grom/message/component.gleam +++ b/src/grom/message/component.gleam @@ -1,31 +1,18 @@ import gleam/dynamic/decode +import gleam/json.{type Json} import gleam/option.{None} import grom/message/component/action_row.{type ActionRow} -import grom/message/component/button.{type Button} -import grom/message/component/channel_select.{type ChannelSelect} import grom/message/component/container.{type Container} import grom/message/component/file.{type File} import grom/message/component/media_gallery.{type MediaGallery} -import grom/message/component/mentionable_select.{type MentionableSelect} -import grom/message/component/role_select.{type RoleSelect} import grom/message/component/section.{type Section} import grom/message/component/separator.{type Separator} -import grom/message/component/string_select.{type StringSelect} import grom/message/component/text_display.{type TextDisplay} -import grom/message/component/text_input.{type TextInput} -import grom/message/component/user_select.{type UserSelect} // TYPES ----------------------------------------------------------------------- pub type Component { ActionRow(ActionRow) - Button(Button) - StringSelect(StringSelect) - TextInput(TextInput) - UserSelect(UserSelect) - RoleSelect(RoleSelect) - MentionableSelect(MentionableSelect) - ChannelSelect(ChannelSelect) Section(Section) TextDisplay(TextDisplay) MediaGallery(MediaGallery) @@ -44,34 +31,6 @@ pub fn decoder() -> decode.Decoder(Component) { use action_row <- decode.then(action_row.decoder()) decode.success(ActionRow(action_row)) } - 2 -> { - use button <- decode.then(button.decoder()) - decode.success(Button(button)) - } - 3 -> { - use string_select <- decode.then(string_select.decoder()) - decode.success(StringSelect(string_select)) - } - 4 -> { - use text_input <- decode.then(text_input.decoder()) - decode.success(TextInput(text_input)) - } - 5 -> { - use user_select <- decode.then(user_select.decoder()) - decode.success(UserSelect(user_select)) - } - 6 -> { - use role_select <- decode.then(role_select.decoder()) - decode.success(RoleSelect(role_select)) - } - 7 -> { - use mentionable_select <- decode.then(mentionable_select.decoder()) - decode.success(MentionableSelect(mentionable_select)) - } - 8 -> { - use channel_select <- decode.then(channel_select.decoder()) - decode.success(ChannelSelect(channel_select)) - } 9 -> { use section <- decode.then(section.decoder()) decode.success(Section(section)) @@ -99,3 +58,18 @@ pub fn decoder() -> decode.Decoder(Component) { _ -> decode.failure(ActionRow(action_row.ActionRow(None, [])), "Component") } } + +// ENCODERS -------------------------------------------------------------------- + +@internal +pub fn to_json(component: Component) -> Json { + case component { + ActionRow(action_row) -> action_row.to_json(action_row) + Section(section) -> section.to_json(section) + TextDisplay(text_display) -> text_display.to_json(text_display) + MediaGallery(media_gallery) -> media_gallery.to_json(media_gallery) + File(file) -> file.to_json(file) + Separator(separator) -> separator.to_json(separator) + Container(container) -> container.to_json(container) + } +}