diff --git a/src/grom.gleam b/src/grom.gleam index 38f58c1..5f4a3cd 100644 --- a/src/grom.gleam +++ b/src/grom.gleam @@ -3594,15 +3594,32 @@ fn sticker_item_decoder() -> Decoder(StickerItem) { decode.success(StickerItem(id:, name:, format_type:)) } +pub type MessageReferenceType { + MessageReferenceIsReply + MessageReferenceIsForward +} + +fn message_reference_type_to_json(type_: MessageReferenceType) -> Json { + case type_ { + MessageReferenceIsReply -> json.int(0) + MessageReferenceIsForward -> json.int(1) + } +} + +fn message_reference_type_decoder() -> Decoder(MessageReferenceType) { + use variant <- decode.then(decode.int) + case variant { + 0 -> decode.success(MessageReferenceIsReply) + 1 -> decode.success(MessageReferenceIsForward) + _ -> decode.failure(MessageReferenceIsReply, "MessageReferenceType") + } +} + /// Information regarding a crossposted message, channel follow add notification, /// pin notification reply, etc. pub type MessageReference { - ReplyMessageReference( - original_message_id: Snowflake(Message), - original_channel_id: Snowflake(Channel), - original_guild_id: Option(Snowflake(Guild)), - ) - ForwardMessageReference( + MessageReference( + type_: MessageReferenceType, original_message_id: Snowflake(Message), original_channel_id: Snowflake(Channel), original_guild_id: Option(Snowflake(Guild)), @@ -3610,7 +3627,11 @@ pub type MessageReference { } fn message_reference_decoder() -> Decoder(MessageReference) { - use type_ <- decode.optional_field("type", None, decode.map(decode.int, Some)) + use type_ <- decode.optional_field( + "type", + MessageReferenceIsReply, + message_reference_type_decoder(), + ) use original_message_id <- decode.field("message_id", snowflake_decoder()) use original_channel_id <- decode.field("channel_id", snowflake_decoder()) use original_guild_id <- decode.optional_field( @@ -3619,29 +3640,12 @@ fn message_reference_decoder() -> Decoder(MessageReference) { decode.map(snowflake_decoder(), Some), ) - case type_ { - Some(0) | None -> - decode.success(ReplyMessageReference( - original_message_id:, - original_channel_id:, - original_guild_id:, - )) - Some(1) -> - decode.success(ForwardMessageReference( - original_message_id:, - original_channel_id:, - original_guild_id:, - )) - _ -> - decode.failure( - ReplyMessageReference( - original_message_id: Snowflake(0), - original_channel_id: Snowflake(0), - original_guild_id: None, - ), - "MessageReference", - ) - } + decode.success(MessageReference( + type_:, + original_message_id:, + original_channel_id:, + original_guild_id:, + )) } pub type MessageFlag { @@ -3812,6 +3816,13 @@ pub type MessageNonce { StringMessageNonce(String) } +fn message_nonce_to_json(nonce: MessageNonce) -> Json { + case nonce { + IntMessageNonce(int) -> json.int(int) + StringMessageNonce(string) -> json.string(string) + } +} + fn message_nonce_decoder() -> Decoder(MessageNonce) { decode.one_of(decode.map(decode.int, IntMessageNonce), or: [ decode.map(decode.string, StringMessageNonce), @@ -12860,9 +12871,7 @@ fn create_forum_like_thread_message_to_json( Ok(#( "attachments", attachments - |> list.index_map(fn(attachment, idx) { - create_message_attachment_to_json(idx, attachment) - }) + |> list.index_map(create_message_attachment_to_json) |> json.preprocessed_array, )) None -> Error(Nil) @@ -12919,6 +12928,7 @@ pub fn new_forum_like_thread_message_with_embeds( CreateForumLikeThreadMessage(..create, embeds: Some(embeds)) } +/// Please read the [AllowedMessageMention](#AllowedMessageMention) documentation beforehand. pub fn new_forum_like_thread_message_with_allowed_mentions( create: CreateForumLikeThreadMessage, allowed_mentions: List(AllowedMessageMention), @@ -12990,8 +13000,8 @@ pub opaque type CreateMessageAttachment { } fn create_message_attachment_to_json( - idx: Int, create: CreateMessageAttachment, + idx: Int, ) -> Json { [ Ok(#("id", json.int(idx))), @@ -13056,7 +13066,8 @@ pub type File { /// In that case, the more specific allowed mention will be given priority. /// If multiple specific mentions are passed, only the first will be used. /// -/// If an empty list is passed, contrary to default Discord behavior, no mentions will be allowed to notify users, to prevent abuse. +/// If an empty list is passed (which is the default Grom behavior), +/// contrary to default Discord behavior, no mentions will be allowed to notify users, to prevent abuse. pub type AllowedMessageMention { AllowMentioningRoles(AllowedRoleMentions) AllowMentioningUsers(AllowedUserMentions) @@ -13371,14 +13382,6 @@ fn create_embed_image_to_json(create: CreateEmbedImage) -> Json { |> list_to_json_object } -fn create_embed_video_to_json(create: CreateEmbedVideo) -> Json { - [ - Ok(#("url", json.string(create.url))), - optional_to_json(create.description, "description", json.string), - ] - |> list_to_json_object -} - pub fn new_embed_image(url url: String) -> CreateEmbedImage { CreateEmbedImage(url:, description: None) } @@ -13390,21 +13393,6 @@ pub fn new_embed_image_with_description( CreateEmbedImage(..create, description: Some(description)) } -pub opaque type CreateEmbedVideo { - CreateEmbedVideo(url: String, description: Option(String)) -} - -pub fn new_embed_video(url url: String) -> CreateEmbedVideo { - CreateEmbedVideo(url:, description: None) -} - -pub fn new_embed_video_with_description( - create: CreateEmbedVideo, - description: String, -) -> CreateEmbedVideo { - CreateEmbedVideo(..create, description: Some(description)) -} - pub opaque type CreateEmbedAuthor { CreateEmbedAuthor(name: String, url: Option(String), icon_url: Option(String)) } @@ -15042,3 +15030,512 @@ pub fn get_message_response( ) -> Result(Message, RestError) { handle_response(response, decode_with: message_decoder()) } + +pub opaque type CreateMessage { + CreateMessage( + content: Option(String), + nonce: Option(MessageNonce), + is_tts: Option(Bool), + embeds: Option(List(CreateMessageEmbed)), + allowed_mentions: List(AllowedMessageMention), + reference: Option(CreateMessageReference), + components: Option(List(CreateMessageComponent)), + sticker_ids: Option(List(Snowflake(Sticker))), + attachments: Option(List(CreateMessageAttachment)), + flags: Option(List(CreateMessageFlag)), + enforce_nonce: Option(Bool), + poll: Option(CreatePoll), + ) +} + +/// You must set at least one of `content`, `embeds`, `sticker_ids`, `components`, `attachments`, or `poll`. +/// +/// If you're forwarding a message, only `reference` is required. +/// +/// By default, no mentions will be allowed. +pub fn new_message() -> CreateMessage { + CreateMessage( + content: None, + nonce: None, + is_tts: None, + embeds: None, + allowed_mentions: [], + reference: None, + components: None, + sticker_ids: None, + attachments: None, + flags: None, + enforce_nonce: None, + poll: None, + ) +} + +fn create_message_to_json(create: CreateMessage) -> Json { + let attachments = case create.attachments { + Some(attachments) -> + Ok(#( + "attachments", + attachments + |> list.index_map(create_message_attachment_to_json) + |> json.preprocessed_array, + )) + None -> Error(Nil) + } + + [ + optional_to_json(create.content, "content", json.string), + optional_to_json(create.nonce, "nonce", message_nonce_to_json), + optional_to_json(create.is_tts, "tts", json.bool), + optional_to_json(create.embeds, "embeds", json.array( + _, + create_message_embed_to_json, + )), + Ok(#( + "allowed_mentions", + allowed_message_mentions_to_json(create.allowed_mentions), + )), + optional_to_json( + create.reference, + "message_reference", + create_message_reference_to_json, + ), + optional_to_json(create.components, "components", json.array( + _, + create_message_component_to_json, + )), + attachments, + optional_to_json(create.enforce_nonce, "enforce_nonce", json.bool), + optional_to_json(create.poll, "poll", create_poll_to_json), + ] + |> list_to_json_object +} + +/// Content must be maximum 2000 characters. +pub fn new_message_with_content( + create: CreateMessage, + content: String, +) -> CreateMessage { + CreateMessage(..create, content: Some(content)) +} + +/// A nonce is used to verify a message was sent through the gateway. +/// +/// Verify message create events for that nonce to make sure a message was created. +pub fn new_message_with_nonce( + create: CreateMessage, + nonce: MessageNonce, +) -> CreateMessage { + CreateMessage(..create, nonce: Some(nonce)) +} + +/// With an enforced nonce, Discord will verify whether a message with the same nonce was sent by the bot in the past couple minutes. +/// +/// If that is the case, the response will contain the older message and no new message will be created. +pub fn new_message_with_enforced_nonce( + create: CreateMessage, + nonce: MessageNonce, +) -> CreateMessage { + CreateMessage(..create, nonce: Some(nonce), enforce_nonce: Some(True)) +} + +/// A text-to-speech message will be read out-loud to all readers. +/// +/// Requires the `AllowSendingTtsMessages` permission. +pub fn new_tts_message(create: CreateMessage) -> CreateMessage { + CreateMessage(..create, is_tts: Some(True)) +} + +pub fn new_message_with_embeds( + create: CreateMessage, + embeds: List(CreateMessageEmbed), +) -> CreateMessage { + CreateMessage(..create, embeds: Some(embeds)) +} + +/// Please read the [AllowedMessageMention](#AllowedMessageMention) documentation beforehand. +pub fn new_message_with_allowed_mentions( + create: CreateMessage, + allowed_mentions: List(AllowedMessageMention), +) -> CreateMessage { + CreateMessage(..create, allowed_mentions:) +} + +pub fn new_message_with_reference( + create: CreateMessage, + reference: CreateMessageReference, +) -> CreateMessage { + CreateMessage(..create, reference: Some(reference)) +} + +/// If using Components V2, make sure to set the `CreateMessageUsingComponentsV2` flag. +pub fn new_message_with_components( + create: CreateMessage, + components: List(CreateMessageComponent), +) -> CreateMessage { + CreateMessage(..create, components: Some(components)) +} + +pub fn new_message_with_stickers( + create: CreateMessage, + ids sticker_ids: List(Snowflake(Sticker)), +) -> CreateMessage { + CreateMessage(..create, sticker_ids: Some(sticker_ids)) +} + +pub fn new_message_with_attachments( + create: CreateMessage, + attachments: List(CreateMessageAttachment), +) -> CreateMessage { + CreateMessage(..create, attachments: Some(attachments)) +} + +pub fn new_message_with_flags( + create: CreateMessage, + flags: List(CreateMessageFlag), +) -> CreateMessage { + CreateMessage(..create, flags: Some(flags)) +} + +pub fn new_message_with_poll( + create: CreateMessage, + poll: CreatePoll, +) -> CreateMessage { + CreateMessage(..create, poll: Some(poll)) +} + +pub type CreateMessageFlag { + /// Makes the links in the message not create an embed. + CreateMessageWithSuppressedEmbeds + CreateMessageWithSuppressedNotifications + /// If this flag is set, you cannot set `content`, `embeds`, `sticker_ids`, or `poll`. + CreateMessageUsingComponentsV2 +} + +pub opaque type CreatePoll { + CreatePoll( + question: String, + answers: List(CreatePollAnswer), + duration: Option(Duration), + allows_multiselect: Option(Bool), + ) +} + +/// Defaults: +/// - duration - 24 hours +/// - doesn't allow multiselect +pub fn new_poll( + asking question: String, + answers answers: List(CreatePollAnswer), +) -> CreatePoll { + CreatePoll(question:, answers:, duration: None, allows_multiselect: None) +} + +/// Duration must be max 32 days. Duration is rounded to the nearest hour. +pub fn new_poll_with_duration( + create: CreatePoll, + duration: Duration, +) -> CreatePoll { + CreatePoll(..create, duration: Some(duration)) +} + +pub fn new_poll_allowing_multiselect(create: CreatePoll) -> CreatePoll { + CreatePoll(..create, allows_multiselect: Some(True)) +} + +fn create_poll_to_json(create: CreatePoll) -> Json { + [ + Ok(#("question", json.object([#("text", json.string(create.question))]))), + Ok(#("answers", json.array(create.answers, create_poll_answer_to_json))), + optional_to_json(create.duration, "duration", duration_to_json_hours), + optional_to_json(create.allows_multiselect, "allow_multiselect", json.bool), + ] + |> list_to_json_object +} + +fn duration_to_json_hours(duration: Duration) -> Json { + let seconds = + duration + |> duration.to_seconds + |> float.round + + let hours = seconds / 3600 + + json.int(hours) +} + +pub opaque type CreatePollAnswer { + CreatePollAnswer(text: String, emoji: Option(CreatePollAnswerEmoji)) +} + +fn create_poll_answer_to_json(create: CreatePollAnswer) -> Json { + json.object([ + #( + "poll_media", + [ + Ok(#("text", json.string(create.text))), + optional_to_json( + create.emoji, + "emoji", + create_poll_answer_emoji_to_json, + ), + ] + |> list_to_json_object, + ), + ]) +} + +/// Text must be maximum 55 characters. +pub fn new_poll_answer(text text: String) -> CreatePollAnswer { + CreatePollAnswer(text:, emoji: None) +} + +pub fn new_poll_answer_with_emoji( + create: CreatePollAnswer, + emoji: CreatePollAnswerEmoji, +) -> CreatePollAnswer { + CreatePollAnswer(..create, emoji: Some(emoji)) +} + +pub type CreatePollAnswerEmoji { + CreatePollAnswerUnicodeEmoji(character: String) + CreatePollAnswerCustomEmoji(id: Snowflake(Emoji)) +} + +fn create_poll_answer_emoji_to_json(create: CreatePollAnswerEmoji) -> Json { + case create { + CreatePollAnswerUnicodeEmoji(..) -> [ + #("name", json.string(create.character)), + ] + CreatePollAnswerCustomEmoji(..) -> [#("id", snowflake_to_json(create.id))] + } + |> json.object +} + +pub opaque type CreateMessageReference { + CreateMessageReference( + type_: MessageReferenceType, + original_message_id: Snowflake(Message), + original_channel_id: Option(Snowflake(Channel)), + original_guild_id: Option(Snowflake(Guild)), + fail_if_not_exists: Bool, + ) +} + +fn create_message_reference_to_json(create: CreateMessageReference) -> Json { + [ + Ok(#("type", message_reference_type_to_json(create.type_))), + Ok(#("message_id", snowflake_to_json(create.original_message_id))), + optional_to_json( + create.original_channel_id, + "channel_id", + snowflake_to_json, + ), + optional_to_json(create.original_guild_id, "guild_id", snowflake_to_json), + Ok(#("fail_if_not_exists", json.bool(create.fail_if_not_exists))), + ] + |> list_to_json_object +} + +/// Allows you to make a new message be a reply to another message. +/// +/// You MUST include the original guild ID unless the message was sent in a DM. +/// +/// Requires the `AllowReadingMessageHistory` permission. +pub fn new_reply_message_reference( + original_message_id original_message_id: Snowflake(Message), + original_guild_id original_guild_id: Option(Snowflake(Guild)), +) -> CreateMessageReference { + CreateMessageReference( + type_: MessageReferenceIsReply, + original_message_id:, + original_channel_id: None, + original_guild_id:, + fail_if_not_exists: True, + ) +} + +/// Allows you to make a new message be a forward of another message. +/// +/// You MUST include the original guild ID unless the message was sent in a DM. +pub fn new_forward_message_reference( + original_message_id original_message_id: Snowflake(Message), + original_channel_id original_channel_id: Snowflake(Channel), + original_guild_id original_guild_id: Option(Snowflake(Guild)), +) -> CreateMessageReference { + CreateMessageReference( + type_: MessageReferenceIsForward, + original_message_id:, + original_channel_id: Some(original_channel_id), + original_guild_id:, + fail_if_not_exists: True, + ) +} + +/// Makes it so that the message will send as a regular message instead of failing if the original message doesn't exist. +pub fn new_message_reference_with_no_fail_if_original_not_exists( + create: CreateMessageReference, +) -> CreateMessageReference { + CreateMessageReference(..create, fail_if_not_exists: False) +} + +pub opaque type CreateMessageEmbed { + CreateMessageEmbed( + title: Option(String), + description: Option(String), + url: Option(String), + timestamp: Option(Timestamp), + colour: Option(Colour), + footer: Option(CreateEmbedFooter), + image: Option(CreateEmbedImage), + thumbnail: Option(CreateEmbedImage), + author: Option(CreateEmbedAuthor), + fields: Option(List(CreateEmbedField)), + ) +} + +fn create_message_embed_to_json(create: CreateMessageEmbed) -> Json { + [ + Ok(#("type", json.string("rich"))), + optional_to_json(create.title, "title", json.string), + optional_to_json(create.description, "description", json.string), + optional_to_json(create.url, "url", json.string), + optional_to_json(create.timestamp, "timestamp", timestamp_to_json), + optional_to_json(create.colour, "color", colour_to_json), + optional_to_json(create.footer, "footer", create_embed_footer_to_json), + optional_to_json(create.image, "image", create_embed_image_to_json), + optional_to_json(create.thumbnail, "thumbnail", create_embed_image_to_json), + optional_to_json(create.author, "author", create_embed_author_to_json), + optional_to_json(create.fields, "fields", json.array( + _, + create_embed_field_to_json, + )), + ] + |> list_to_json_object +} + +pub fn new_message_embed() { + CreateMessageEmbed( + title: None, + description: None, + url: None, + timestamp: None, + colour: None, + footer: None, + image: None, + thumbnail: None, + author: None, + fields: None, + ) +} + +pub fn new_message_embed_with_title( + create: CreateMessageEmbed, + title: String, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, title: Some(title)) +} + +pub fn new_message_embed_with_description( + create: CreateMessageEmbed, + description: String, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, description: Some(description)) +} + +pub fn new_message_embed_with_url( + create: CreateMessageEmbed, + url: String, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, url: Some(url)) +} + +pub fn new_message_embed_with_timestamp( + create: CreateMessageEmbed, + timestamp: Timestamp, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, timestamp: Some(timestamp)) +} + +pub fn new_message_embed_with_colour( + create: CreateMessageEmbed, + colour: Colour, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, colour: Some(colour)) +} + +pub fn new_message_embed_with_image( + create: CreateMessageEmbed, + image: CreateEmbedImage, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, image: Some(image)) +} + +pub fn new_message_embed_with_thumbnail( + create: CreateMessageEmbed, + thumbnail: CreateEmbedImage, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, thumbnail: Some(thumbnail)) +} + +pub fn new_message_embed_with_author( + create: CreateMessageEmbed, + author: CreateEmbedAuthor, +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, author: Some(author)) +} + +pub fn new_message_embed_with_fields( + create: CreateMessageEmbed, + fields: List(CreateEmbedField), +) -> CreateMessageEmbed { + CreateMessageEmbed(..create, fields: Some(fields)) +} + +/// If sending in a guild channel, requires the `AllowSendingMessages` permission. +pub fn new_message_request( + token token: Token, + in_channel_with_id channel_id: Snowflake(Channel), + using create: CreateMessage, +) -> Request(BitArray) { + let json = create |> create_message_to_json |> json.to_string + + let request = + new_request( + token:, + to: "/channels/" <> snowflake_to_string(channel_id) <> "/messages", + method: http.Post, + ) + |> request.set_body(json) + |> request.map(bit_array.from_string) + + case create.attachments { + Some(attachments) -> { + let body = + attachments + |> list.index_map(fn(attachment, idx) { + #( + "files[" <> int.to_string(idx) <> "]", + field.File( + name: attachment.file.name, + content_type: attachment.file.content_type, + content: attachment.file.content, + ), + ) + }) + |> list.prepend(#( + "payload_json", + field.StringWithType(content: json, content_type: "application/json"), + )) + + request + |> multipart_form.to_request(body) + } + None -> request + } +} + +pub fn new_message_response( + response: Response(BitArray), +) -> Result(Message, RestError) { + handle_response_bits(response, decode_with: message_decoder()) +}