From d7e7e8eab0b96bbc38d9eea37bb065dda2d86451 Mon Sep 17 00:00:00 2001 From: Filip Hoffmann Date: Tue, 24 Feb 2026 02:02:25 +0100 Subject: [PATCH] start from the ground up --- src/grom.gleam | 413 +- src/grom/activity.gleam | 635 --- src/grom/application.gleam | 596 --- .../application/current_application.gleam | 194 - .../role_connection_metadata.gleam | 171 - src/grom/application/team.gleam | 107 - src/grom/channel.gleam | 2429 ----------- src/grom/channel/forum.gleam | 131 - src/grom/channel/media.gleam | 18 - src/grom/channel/permission_overwrite.gleam | 122 - src/grom/channel/thread.gleam | 588 --- src/grom/command.gleam | 2726 ------------ src/grom/component/action_row.gleam | 117 - src/grom/component/button.gleam | 217 - src/grom/component/channel_select.gleam | 167 - src/grom/component/checkbox.gleam | 36 - src/grom/component/checkbox_group.gleam | 91 - src/grom/component/container.gleam | 126 - src/grom/component/file.gleam | 56 - src/grom/component/file_upload.gleam | 70 - src/grom/component/label.gleam | 85 - src/grom/component/media_gallery.gleam | 80 - src/grom/component/mentionable_select.gleam | 129 - src/grom/component/radio_group.gleam | 75 - src/grom/component/role_select.gleam | 125 - src/grom/component/section.gleam | 150 - src/grom/component/separator.gleam | 75 - src/grom/component/string_select.gleam | 179 - src/grom/component/text_display.gleam | 43 - src/grom/component/text_input.gleam | 128 - src/grom/component/unfurled_media_item.gleam | 58 - src/grom/component/user_select.gleam | 125 - src/grom/emoji.gleam | 110 - src/grom/entitlement.gleam | 210 - src/grom/file.gleam | 3 - src/grom/gateway.gleam | 3636 ----------------- src/grom/gateway/intent.gleam | 103 - src/grom/guild.gleam | 2206 ---------- src/grom/guild/audit_log.gleam | 568 --- src/grom/guild/auto_moderation.gleam | 520 --- src/grom/guild/integration.gleam | 199 - src/grom/guild/onboarding.gleam | 320 -- src/grom/guild/role.gleam | 418 -- src/grom/guild/scheduled_event.gleam | 366 -- .../scheduled_event/recurrence_rule.gleam | 259 -- src/grom/guild_member.gleam | 442 -- src/grom/image.gleam | 43 - src/grom/interaction.gleam | 1883 --------- src/grom/internal/base64.gleam | 13 - src/grom/internal/flags.gleam | 44 - src/grom/internal/rest.gleam | 147 - src/grom/internal/time_duration.gleam | 76 - src/grom/internal/time_rfc3339.gleam | 28 - src/grom/internal/time_timestamp.gleam | 22 - src/grom/invite.gleam | 594 --- src/grom/message.gleam | 1499 ------- src/grom/message/activity.gleam | 31 - src/grom/message/allowed_mentions.gleam | 106 - src/grom/message/attachment.gleam | 142 - src/grom/message/call.gleam | 23 - src/grom/message/embed.gleam | 522 --- src/grom/message/interaction_metadata.gleam | 83 - src/grom/message/message_reference.gleam | 105 - src/grom/message/poll.gleam | 322 -- src/grom/message/reaction.gleam | 149 - src/grom/modal.gleam | 20 - src/grom/modification.gleam | 50 - src/grom/permission.gleam | 149 - src/grom/sku.gleam | 133 - src/grom/snowflake.gleam | 18 - src/grom/soundboard.gleam | 152 - src/grom/stage_instance.gleam | 204 - src/grom/sticker.gleam | 246 -- src/grom/subscription.gleam | 85 - src/grom/user.gleam | 504 --- src/grom/user/current_user.gleam | 191 - src/grom/voice.gleam | 101 - src/grom/webhook.gleam | 96 - src/grom/webhook_event.gleam | 143 - 79 files changed, 390 insertions(+), 27156 deletions(-) delete mode 100644 src/grom/activity.gleam delete mode 100644 src/grom/application.gleam delete mode 100644 src/grom/application/current_application.gleam delete mode 100644 src/grom/application/role_connection_metadata.gleam delete mode 100644 src/grom/application/team.gleam delete mode 100644 src/grom/channel.gleam delete mode 100644 src/grom/channel/forum.gleam delete mode 100644 src/grom/channel/media.gleam delete mode 100644 src/grom/channel/permission_overwrite.gleam delete mode 100644 src/grom/channel/thread.gleam delete mode 100644 src/grom/command.gleam delete mode 100644 src/grom/component/action_row.gleam delete mode 100644 src/grom/component/button.gleam delete mode 100644 src/grom/component/channel_select.gleam delete mode 100644 src/grom/component/checkbox.gleam delete mode 100644 src/grom/component/checkbox_group.gleam delete mode 100644 src/grom/component/container.gleam delete mode 100644 src/grom/component/file.gleam delete mode 100644 src/grom/component/file_upload.gleam delete mode 100644 src/grom/component/label.gleam delete mode 100644 src/grom/component/media_gallery.gleam delete mode 100644 src/grom/component/mentionable_select.gleam delete mode 100644 src/grom/component/radio_group.gleam delete mode 100644 src/grom/component/role_select.gleam delete mode 100644 src/grom/component/section.gleam delete mode 100644 src/grom/component/separator.gleam delete mode 100644 src/grom/component/string_select.gleam delete mode 100644 src/grom/component/text_display.gleam delete mode 100644 src/grom/component/text_input.gleam delete mode 100644 src/grom/component/unfurled_media_item.gleam delete mode 100644 src/grom/component/user_select.gleam delete mode 100644 src/grom/emoji.gleam delete mode 100644 src/grom/entitlement.gleam delete mode 100644 src/grom/file.gleam delete mode 100644 src/grom/gateway.gleam delete mode 100644 src/grom/gateway/intent.gleam delete mode 100644 src/grom/guild.gleam delete mode 100644 src/grom/guild/audit_log.gleam delete mode 100644 src/grom/guild/auto_moderation.gleam delete mode 100644 src/grom/guild/integration.gleam delete mode 100644 src/grom/guild/onboarding.gleam delete mode 100644 src/grom/guild/role.gleam delete mode 100644 src/grom/guild/scheduled_event.gleam delete mode 100644 src/grom/guild/scheduled_event/recurrence_rule.gleam delete mode 100644 src/grom/guild_member.gleam delete mode 100644 src/grom/image.gleam delete mode 100644 src/grom/interaction.gleam delete mode 100644 src/grom/internal/base64.gleam delete mode 100644 src/grom/internal/flags.gleam delete mode 100644 src/grom/internal/rest.gleam delete mode 100644 src/grom/internal/time_duration.gleam delete mode 100644 src/grom/internal/time_rfc3339.gleam delete mode 100644 src/grom/internal/time_timestamp.gleam delete mode 100644 src/grom/invite.gleam delete mode 100644 src/grom/message.gleam delete mode 100644 src/grom/message/activity.gleam delete mode 100644 src/grom/message/allowed_mentions.gleam delete mode 100644 src/grom/message/attachment.gleam delete mode 100644 src/grom/message/call.gleam delete mode 100644 src/grom/message/embed.gleam delete mode 100644 src/grom/message/interaction_metadata.gleam delete mode 100644 src/grom/message/message_reference.gleam delete mode 100644 src/grom/message/poll.gleam delete mode 100644 src/grom/message/reaction.gleam delete mode 100644 src/grom/modal.gleam delete mode 100644 src/grom/modification.gleam delete mode 100644 src/grom/permission.gleam delete mode 100644 src/grom/sku.gleam delete mode 100644 src/grom/snowflake.gleam delete mode 100644 src/grom/soundboard.gleam delete mode 100644 src/grom/stage_instance.gleam delete mode 100644 src/grom/sticker.gleam delete mode 100644 src/grom/subscription.gleam delete mode 100644 src/grom/user.gleam delete mode 100644 src/grom/user/current_user.gleam delete mode 100644 src/grom/voice.gleam delete mode 100644 src/grom/webhook.gleam delete mode 100644 src/grom/webhook_event.gleam diff --git a/src/grom.gleam b/src/grom.gleam index beff6a0..ee44122 100644 --- a/src/grom.gleam +++ b/src/grom.gleam @@ -1,31 +1,398 @@ -import gleam/http/response.{type Response} -import gleam/httpc -import gleam/json -import gleam/otp/actor -import stratus +import gleam/dynamic/decode.{type Decoder} +import gleam/int +import gleam/json.{type Json} +import gleam/list +import gleam/option.{type Option, None, Some} -// TYPES ----------------------------------------------------------------------- +/// A snowflake is another name for an ID. +/// It is possible to retrieve an object's creation date from its snowflake. +pub opaque type Snowflake(a) { + Snowflake(id: Int) +} + +/// An image hash is used to download images from Discord. +/// Every non-attachment image (avatars, banners, even guild tag badges) is hashed by Discord and is able to be downloaded through a link. +pub opaque type ImageHash { + ImageHash(hash: String) +} + +/// Returns the image hash in string form. +/// Generally not needed. +pub fn image_hash_to_string(image_hash: ImageHash) -> String { + image_hash.hash +} + +fn image_hash_decoder() -> Decoder(ImageHash) { + decode.map(decode.string, ImageHash) +} + +fn snowflake_to_json(snowflake: Snowflake(a)) -> Json { + snowflake.id + |> int.to_string + |> json.string +} + +pub fn snowflake_to_string(snowflake: Snowflake(a)) -> String { + snowflake.id + |> int.to_string +} + +pub fn snowflake_to_int(snowflake: Snowflake(a)) -> Int { + snowflake.id +} + +fn snowflake_decoder() -> Decoder(Snowflake(a)) { + use id <- decode.then(decode.string) + case int.parse(id) { + Ok(id) -> decode.success(Snowflake(id)) + Error(_) -> decode.failure(Snowflake(0), "Snowflake") + } +} + +pub fn new_snowflake(from id: Int) -> Snowflake(a) { + Snowflake(id) +} + +fn flags_decoder(bits_flags: List(#(Int, flag))) -> Decoder(List(flag)) { + use bits <- decode.then(decode.int) + + bits_flags + |> list.filter_map(fn(item) { + let #(bit, flag) = item + case int.bitwise_and(bits, bit) != 0 { + True -> Ok(flag) + False -> Error(Nil) + } + }) + |> decode.success +} + +fn flags_to_int(flags: List(flag), bits_flags: List(#(Int, flag))) -> Int { + bits_flags + |> list.filter_map(fn(item) { + let #(bit, flag) = item + let is_in_flags = list.any(flags, fn(curr) { curr == flag }) + case is_in_flags { + True -> Ok(bit) + False -> Error(Nil) + } + }) + |> int.sum +} + +fn flags_to_json(flags: List(flag), bits_flags: List(#(Int, flag))) -> Json { + json.int(flags |> flags_to_int(bits_flags)) +} + +pub type User { + User( + id: Snowflake(User), + username: String, + /// Mostly deprecated. Only bots have discriminators nowadays. + /// Users will very likely have their discriminator set to `0`. + /// Used in the past when usernames weren't user-specific. + /// Doesn't include the `#` prefix. + discriminator: String, + /// Also called a display name. + /// Is `None` when the user doesn't have a global name, and rather uses their username as their display name. + global_name: Option(String), + /// Is `None` when the user uses a default avatar. + avatar_hash: Option(ImageHash), + /// Whether the user is a bot user. + is_bot: Bool, + /// Whether the user is Discord's official system account. + is_system: Bool, + /// Is `None` if it isn't known whether the user has enabled MFA. + /// MFA = multi-factor authentication. + has_mfa_enabled: Option(Bool), + /// Is `None` when the user doesn't use a custom banner. + banner_hash: Option(ImageHash), + /// The user's banner accent color in RGB hexadecimal format. + /// Is `None` when the user uses a default color based on avatar. + accent_color: Option(Int), + /// The user's chosen locale. + /// Full list of locales: [link](https://docs.discord.com/developers/reference#locales) + /// Discord hasn't disclosed when this field could be `None`. + locale: Option(String), + flags: List(UserFlag), + /// Is `None` if the user's premium type isn't known. + premium_type: Option(UserPremiumType), + /// Publicly visible flags - aka Discord badges. + /// Visible in the top right corner of a person's profile in the apps. + public_flags: List(UserFlag), + /// Is `None` when the user has no avatar decorations. + avatar_decoration: Option(UserAvatarDecoration), + /// Is `None` when the user has no collectibles. + collectibles: Option(UserCollectibles), + /// Is `None` if the user never had a primary guild. + primary_guild: Option(UserPrimaryGuild), + ) +} + +fn user_decoder() -> Decoder(User) { + use id <- decode.field("id", snowflake_decoder()) + use username <- decode.field("username", decode.string) + use discriminator <- decode.field("discriminator", decode.string) + use global_name <- decode.field("global_name", decode.optional(decode.string)) + use avatar_hash <- decode.field( + "avatar", + decode.optional(image_hash_decoder()), + ) + use is_bot <- decode.optional_field("bot", False, decode.bool) + use is_system <- decode.optional_field("system", False, decode.bool) + use has_mfa_enabled <- decode.optional_field( + "mfa_enabled", + None, + decode.optional(decode.bool), + ) + use banner_hash <- decode.optional_field( + "banner", + None, + decode.optional(image_hash_decoder()), + ) + use accent_color <- decode.optional_field( + "accent_color", + None, + decode.optional(decode.int), + ) + use locale <- decode.optional_field( + "locale", + None, + decode.optional(decode.string), + ) + use flags <- decode.optional_field( + "flags", + [], + flags_decoder(bits_user_flags()), + ) + use premium_type <- decode.optional_field( + "premium_type", + None, + decode.optional(user_premium_type_decoder()), + ) + use public_flags <- decode.optional_field( + "public_flags", + [], + flags_decoder(bits_user_flags()), + ) + use avatar_decoration <- decode.optional_field( + "avatar_decoration_data", + None, + decode.optional(user_avatar_decoration_decoder()), + ) + use collectibles <- decode.optional_field( + "collectibles", + None, + decode.optional(user_collectibles_decoder()), + ) + use primary_guild <- decode.optional_field( + "primary_guild", + None, + decode.optional(user_primary_guild_decoder()), + ) + decode.success(User( + id:, + username:, + discriminator:, + global_name:, + avatar_hash:, + is_bot:, + is_system:, + has_mfa_enabled:, + banner_hash:, + accent_color:, + locale:, + flags:, + premium_type:, + public_flags:, + avatar_decoration:, + collectibles:, + primary_guild:, + )) +} + +fn user_primary_guild_decoder() -> Decoder(UserPrimaryGuild) { + use is_enabled <- decode.field( + "identity_enabled", + decode.optional(decode.bool), + ) + case is_enabled { + Some(True) -> { + use id <- decode.field("identity_guild_id", snowflake_decoder()) + use tag <- decode.field("tag", decode.string) + use badge_hash <- decode.field("badge", image_hash_decoder()) + + decode.success(ActiveUserPrimaryGuild(id:, tag:, badge_hash:)) + } + Some(False) -> decode.success(ManuallyClearedUserPrimaryGuild) + None -> decode.success(AutomaticallyClearedUserPrimaryGuild) + } +} + +fn user_avatar_decoration_decoder() -> Decoder(UserAvatarDecoration) { + use hash <- decode.field("asset", image_hash_decoder()) + use sku_id <- decode.field("sku_id", snowflake_decoder()) + decode.success(UserAvatarDecoration(hash:, sku_id:)) +} + +fn user_premium_type_decoder() -> Decoder(UserPremiumType) { + use variant <- decode.then(decode.int) + case variant { + 0 -> decode.success(UserHasNoPremiumSubscription) + 1 -> decode.success(UserHasNitroClassic) + 2 -> decode.success(UserHasNitro) + 3 -> decode.success(UserHasNitroBasic) + _ -> decode.failure(UserHasNoPremiumSubscription, "PremiumType") + } +} + +pub type UserPrimaryGuild { + /// A user actively has a primary guild tag. + ActiveUserPrimaryGuild( + id: Snowflake(Guild), + tag: String, + badge_hash: ImageHash, + ) + /// The user's primary guild was cleared by Discord because the guild became ineligible for guild tags. + AutomaticallyClearedUserPrimaryGuild + /// The user manually removed their guild tag. + ManuallyClearedUserPrimaryGuild +} + +// TODO: GET RID OF ME, ACTUALLY USE SKUs +pub type Sku + +// TODO: GET RID OF ME, ACTUALLY USE GUILDS +pub type Guild + +pub type UserAvatarDecoration { + UserAvatarDecoration(hash: ImageHash, sku_id: Snowflake(Sku)) +} -pub type Client { - Client(token: String) +pub type UserCollectibles { + UserCollectibles( + /// Is `None` when the user doesn't have a nameplate. + nameplate: Option(UserNameplate), + ) } -pub type Error { - HttpError(httpc.HttpError) - CouldNotDecode(json.DecodeError) - StatusCodeUnsuccessful(Response(String)) - ResponseNotValidUtf8(BitArray) - InvalidGatewayUrl(String) - NoConnectionFound - CouldNotInitializeWebsocketConnection(actor.StartError) - CouldNotStartActor(actor.StartError) - CouldNotSendEvent(stratus.SocketReason) - CouldNotCloseWebsocketConnection(stratus.SocketReason) - CouldNotStartHeartbeatCycle(Error) +/// A nameplate is a decoration around a user's Direct Messages / online member list view tab. +pub type UserNameplate { + UserNameplate( + sku_id: Snowflake(Sku), + hash: ImageHash, + /// Currently unused. + label: String, + palette: UserNameplatePalette, + ) +} + +pub type UserNameplatePalette { + CrimsonUserNameplate + BerryUserNameplate + SkyUserNameplate + TealUserNameplate + ForestUserNameplate + BubbleGumUserNameplate + VioletUserNameplate + CobaltUserNameplate + CloverUserNameplate + LemonUserNameplate + WhiteUserNameplate +} + +pub type UserFlag { + /// Given to official Discord staff. + UserIsStaff + /// Given to partnered guild owners. + UserIsDiscordPartner + /// Given to participants of Hypesquad events. + UserIsHypesquadEventsMember + /// Given to Discord Bug Hunters, first level of the badge. + UserIsBugHunterLevel1 + UserBelongsToHypesquadBravery + UserBelongsToHypesquadBrilliance + UserBelongsToHypesquadBalance + /// Given to "early Nitro supporters". + UserIsPremiumEarlySupporter + /// The user is actually a development team. + UserIsTeamPseudoUser + /// Given to Discord Bug Hunters, second level of the badge. + UserIsBugHunterLevel2 + /// Given to verified bot users. + UserIsVerifiedBot + /// Given to early verified bot developers. No longer obtainable. + UserIsVerifiedDeveloper + /// Given to people who passed the Discord Certified Moderator course. + UserIsCertifiedModerator + /// The bot only uses HTTP webhook interaction and is shown in the online member list. + BotUsesHttpInteractions + /// Given to [active developers](https://support-dev.discord.com/hc/articles/10113997751447). + /// No longer obtainable. + UserIsActiveDeveloper +} + +fn bits_user_flags() -> List(#(Int, UserFlag)) { + [ + #(int.bitwise_shift_left(1, 0), UserIsStaff), + #(int.bitwise_shift_left(1, 1), UserIsDiscordPartner), + #(int.bitwise_shift_left(1, 2), UserIsHypesquadEventsMember), + #(int.bitwise_shift_left(1, 3), UserIsBugHunterLevel1), + #(int.bitwise_shift_left(1, 6), UserBelongsToHypesquadBravery), + #(int.bitwise_shift_left(1, 7), UserBelongsToHypesquadBrilliance), + #(int.bitwise_shift_left(1, 8), UserBelongsToHypesquadBalance), + #(int.bitwise_shift_left(1, 9), UserIsPremiumEarlySupporter), + #(int.bitwise_shift_left(1, 10), UserIsTeamPseudoUser), + #(int.bitwise_shift_left(1, 14), UserIsBugHunterLevel2), + #(int.bitwise_shift_left(1, 16), UserIsVerifiedBot), + #(int.bitwise_shift_left(1, 17), UserIsVerifiedDeveloper), + #(int.bitwise_shift_left(1, 18), UserIsCertifiedModerator), + #(int.bitwise_shift_left(1, 19), BotUsesHttpInteractions), + #(int.bitwise_shift_left(1, 22), UserIsActiveDeveloper), + ] +} + +/// Describes what level of a Nitro subscription a user has. +pub type UserPremiumType { + UserHasNoPremiumSubscription + UserHasNitroClassic + UserHasNitro + UserHasNitroBasic +} + +fn user_collectibles_decoder() -> Decoder(UserCollectibles) { + use nameplate <- decode.optional_field( + "nameplate", + None, + decode.optional(user_nameplate_decoder()), + ) + decode.success(UserCollectibles(nameplate:)) +} + +fn user_nameplate_decoder() -> Decoder(UserNameplate) { + use sku_id <- decode.field("sku_id", snowflake_decoder()) + use hash <- decode.field("asset", image_hash_decoder()) + use label <- decode.field("label", decode.string) + use palette <- decode.field("palette", user_nameplate_palette_decoder()) + + decode.success(UserNameplate(sku_id, hash, label, palette)) } -// FUNCTIONS ------------------------------------------------------------------- +fn user_nameplate_palette_decoder() -> Decoder(UserNameplatePalette) { + use variant <- decode.then(decode.string) -pub fn version() -> String { - "v6.0.0" + case variant { + "crimson" -> decode.success(CrimsonUserNameplate) + "berry" -> decode.success(BerryUserNameplate) + "sky" -> decode.success(SkyUserNameplate) + "teal" -> decode.success(TealUserNameplate) + "forest" -> decode.success(ForestUserNameplate) + "bubble_gum" -> decode.success(BubbleGumUserNameplate) + "violet" -> decode.success(VioletUserNameplate) + "cobalt" -> decode.success(CobaltUserNameplate) + "clover" -> decode.success(CloverUserNameplate) + "lemon" -> decode.success(LemonUserNameplate) + "white" -> decode.success(WhiteUserNameplate) + _ -> decode.failure(CrimsonUserNameplate, "UserNameplatePalette") + } } diff --git a/src/grom/activity.gleam b/src/grom/activity.gleam deleted file mode 100644 index 211628e..0000000 --- a/src/grom/activity.gleam +++ /dev/null @@ -1,635 +0,0 @@ -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/timestamp.{type Timestamp} -import grom/internal/flags -import grom/internal/time_timestamp - -// TYPES ---------------------------------------------------------------------- - -/// Important documents: https://discord.com/developers/docs/events/gateway-events#activity-object -pub type Activity { - Activity( - name: String, - type_: Type, - url: Option(String), - created_at: Timestamp, - timestamps: Option(Timestamps), - application_id: Option(String), - status_display_type: Option(DisplayType), - details: Option(String), - details_url: Option(String), - state: Option(String), - state_url: Option(String), - emoji: Option(Emoji), - party: Option(Party), - assets: Option(Assets), - secrets: Option(Secrets), - flags: Option(List(Flag)), - buttons: Option(List(Button)), - ) -} - -/// Important documents: https://discord.com/developers/docs/events/gateway-events#activity-object-activity-types -pub type Type { - Playing - Streaming - Listening - Watching - Custom - Competing -} - -pub type Timestamps { - Timestamps(start: Option(Timestamp), end: Option(Timestamp)) -} - -/// Important documents: https://discord.com/developers/docs/events/gateway-events#activity-object-status-display-types -pub type DisplayType { - DisplayName - DisplayState - DisplayDetails -} - -pub type Emoji { - Emoji(name: String, id: Option(String), is_animated: Option(Bool)) -} - -pub type Party { - Party(id: Option(String), size: Option(PartySize)) -} - -pub type PartySize { - PartySize(current_size: Int, max_size: Int) -} - -pub type Assets { - Assets( - large_image: Option(String), - large_text: Option(String), - large_url: Option(String), - small_image: Option(String), - small_text: Option(String), - small_url: Option(String), - ) -} - -pub type Secrets { - Secrets(join: Option(String), spectate: Option(String), match: Option(String)) -} - -pub type Flag { - Instance - Join - Spectate - JoinRequest - Sync - Play - PartyPrivacyFriends - PartyPrivacyVoiceChannel - Embedded -} - -pub type Button { - Button(label: String, url: String) -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 0), Instance), - #(int.bitwise_shift_left(1, 1), Join), - #(int.bitwise_shift_left(1, 2), Spectate), - #(int.bitwise_shift_left(1, 3), JoinRequest), - #(int.bitwise_shift_left(1, 4), Sync), - #(int.bitwise_shift_left(1, 5), Play), - #(int.bitwise_shift_left(1, 6), PartyPrivacyFriends), - #(int.bitwise_shift_left(1, 7), PartyPrivacyVoiceChannel), - #(int.bitwise_shift_left(1, 8), Embedded), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Activity) { - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", type_decoder()) - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - use created_at <- decode.field( - "created_at", - time_timestamp.from_unix_milliseconds_decoder(), - ) - use timestamps <- decode.optional_field( - "timestamps", - None, - decode.optional(timestamps_decoder()), - ) - use application_id <- decode.optional_field( - "application_id", - None, - decode.optional(decode.string), - ) - use status_display_type <- decode.optional_field( - "status_display_type", - None, - decode.optional(display_type_decoder()), - ) - use details <- decode.optional_field( - "details", - None, - decode.optional(decode.string), - ) - use details_url <- decode.optional_field( - "details_url", - None, - decode.optional(decode.string), - ) - use state <- decode.optional_field( - "state", - None, - decode.optional(decode.string), - ) - use state_url <- decode.optional_field( - "state_url", - None, - decode.optional(decode.string), - ) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji_decoder()), - ) - use party <- decode.optional_field( - "party", - None, - decode.optional(party_decoder()), - ) - use assets <- decode.optional_field( - "assets", - None, - decode.optional(assets_decoder()), - ) - use secrets <- decode.optional_field( - "secrets", - None, - decode.optional(secrets_decoder()), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use buttons <- decode.optional_field("buttons", None, decode.success(None)) - decode.success(Activity( - name:, - type_:, - url:, - created_at:, - timestamps:, - application_id:, - status_display_type:, - details:, - details_url:, - state:, - state_url:, - emoji:, - party:, - assets:, - secrets:, - flags:, - buttons:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Playing) - 1 -> decode.success(Streaming) - 2 -> decode.success(Listening) - 3 -> decode.success(Watching) - 4 -> decode.success(Custom) - 5 -> decode.success(Competing) - _ -> decode.failure(Playing, "Type") - } -} - -@internal -pub fn timestamps_decoder() -> decode.Decoder(Timestamps) { - use start <- decode.optional_field( - "start", - None, - decode.optional(time_timestamp.from_unix_milliseconds_decoder()), - ) - use end <- decode.optional_field( - "end", - None, - decode.optional(time_timestamp.from_unix_milliseconds_decoder()), - ) - - decode.success(Timestamps(start:, end:)) -} - -@internal -pub fn display_type_decoder() -> decode.Decoder(DisplayType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(DisplayName) - 1 -> decode.success(DisplayState) - 2 -> decode.success(DisplayDetails) - _ -> decode.failure(DisplayName, "DisplayType") - } -} - -@internal -pub fn emoji_decoder() -> decode.Decoder(Emoji) { - use name <- decode.field("name", decode.string) - use id <- decode.optional_field("id", None, decode.optional(decode.string)) - use is_animated <- decode.optional_field( - "animated", - None, - decode.optional(decode.bool), - ) - - decode.success(Emoji(name:, id:, is_animated:)) -} - -@internal -pub fn party_decoder() -> decode.Decoder(Party) { - use id <- decode.optional_field("id", None, decode.optional(decode.string)) - use size <- decode.optional_field( - "size", - None, - decode.optional(party_size_decoder()), - ) - - decode.success(Party(id:, size:)) -} - -@internal -pub fn party_size_decoder() -> decode.Decoder(PartySize) { - use current_size <- decode.field(0, decode.int) - use max_size <- decode.field(1, decode.int) - - decode.success(PartySize(current_size:, max_size:)) -} - -@internal -pub fn assets_decoder() -> decode.Decoder(Assets) { - use large_image <- decode.optional_field( - "large_image", - None, - decode.optional(decode.string), - ) - use large_text <- decode.optional_field( - "large_text", - None, - decode.optional(decode.string), - ) - use large_url <- decode.optional_field( - "large_url", - None, - decode.optional(decode.string), - ) - use small_image <- decode.optional_field( - "small_image", - None, - decode.optional(decode.string), - ) - use small_text <- decode.optional_field( - "small_text", - None, - decode.optional(decode.string), - ) - use small_url <- decode.optional_field( - "small_url", - None, - decode.optional(decode.string), - ) - - decode.success(Assets( - large_image:, - large_text:, - large_url:, - small_image:, - small_text:, - small_url:, - )) -} - -@internal -pub fn secrets_decoder() -> decode.Decoder(Secrets) { - use join <- decode.optional_field( - "join", - None, - decode.optional(decode.string), - ) - use spectate <- decode.optional_field( - "spectate", - None, - decode.optional(decode.string), - ) - use match <- decode.optional_field( - "match", - None, - decode.optional(decode.string), - ) - - decode.success(Secrets(join:, spectate:, match:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(activity: Activity) -> Json { - let name = [#("name", json.string(activity.name))] - - let type_ = [#("type", type_to_json(activity.type_))] - - let url = case activity.url { - Some(url) -> [#("url", json.string(url))] - None -> [] - } - - let created_at = [ - #( - "created_at", - json.int(time_timestamp.to_unix_milliseconds(activity.created_at)), - ), - ] - - let timestamps = case activity.timestamps { - Some(timestamps) -> [#("timestamps", timestamps_to_json(timestamps))] - None -> [] - } - - let application_id = case activity.application_id { - Some(id) -> [#("application_id", json.string(id))] - None -> [] - } - - let status_display_type = case activity.status_display_type { - Some(type_) -> [#("status_display_type", display_type_to_json(type_))] - None -> [] - } - - let details = case activity.details { - Some(details) -> [#("details", json.string(details))] - None -> [] - } - - let details_url = case activity.details_url { - Some(url) -> [#("details_url", json.string(url))] - None -> [] - } - - let state = case activity.state { - Some(state) -> [#("state", json.string(state))] - None -> [] - } - - let state_url = case activity.state_url { - Some(url) -> [#("state_url", json.string(url))] - None -> [] - } - - let emoji = case activity.emoji { - Some(emoji) -> [#("emoji", emoji_to_json(emoji))] - None -> [] - } - - let party = case activity.party { - Some(party) -> [#("party", party_to_json(party))] - None -> [] - } - - let assets = case activity.assets { - Some(assets) -> [#("assets", assets_to_json(assets))] - None -> [] - } - - let secrets = case activity.secrets { - Some(secrets) -> [#("secrets", secrets_to_json(secrets))] - None -> [] - } - - let flags = case activity.flags { - Some(flags) -> [#("flags", flags.to_json(flags, bits_flags()))] - None -> [] - } - - let buttons = case activity.buttons { - Some(buttons) -> [#("buttons", json.array(buttons, button_to_json))] - None -> [] - } - - [ - name, - type_, - url, - created_at, - timestamps, - application_id, - status_display_type, - details, - details_url, - state, - state_url, - emoji, - party, - assets, - secrets, - flags, - buttons, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn type_to_json(type_: Type) -> Json { - case type_ { - Playing -> 0 - Streaming -> 1 - Listening -> 2 - Watching -> 3 - Custom -> 4 - Competing -> 5 - } - |> json.int -} - -@internal -pub fn timestamps_to_json(timestamps: Timestamps) -> Json { - let start = case timestamps.start { - Some(timestamp) -> [ - #("start", json.int(time_timestamp.to_unix_milliseconds(timestamp))), - ] - None -> [] - } - - let end = case timestamps.end { - Some(timestamp) -> [ - #("end", json.int(time_timestamp.to_unix_milliseconds(timestamp))), - ] - None -> [] - } - - [start, end] - |> list.flatten - |> json.object -} - -@internal -pub fn display_type_to_json(display_type: DisplayType) -> Json { - case display_type { - DisplayName -> 0 - DisplayState -> 1 - DisplayDetails -> 2 - } - |> json.int -} - -@internal -pub fn emoji_to_json(emoji: Emoji) -> Json { - let name = [#("name", json.string(emoji.name))] - let id = case emoji.id { - Some(id) -> [#("id", json.string(id))] - None -> [] - } - let is_animated = case emoji.is_animated { - Some(animated) -> [#("animated", json.bool(animated))] - None -> [] - } - - [name, id, is_animated] - |> list.flatten - |> json.object -} - -@internal -pub fn party_to_json(party: Party) -> Json { - let id = case party.id { - Some(id) -> [#("id", json.string(id))] - None -> [] - } - - let size = case party.size { - Some(size) -> [#("size", party_size_to_json(size))] - _ -> [] - } - - [id, size] - |> list.flatten - |> json.object -} - -@internal -pub fn party_size_to_json(party_size: PartySize) -> Json { - json.array([party_size.current_size, party_size.max_size], json.int) -} - -@internal -pub fn assets_to_json(assets: Assets) -> Json { - let large_image = case assets.large_image { - Some(image) -> [#("large_image", json.string(image))] - None -> [] - } - - let large_text = case assets.large_text { - Some(text) -> [#("large_text", json.string(text))] - None -> [] - } - - let large_url = case assets.large_url { - Some(url) -> [#("large_url", json.string(url))] - None -> [] - } - - let small_image = case assets.small_image { - Some(image) -> [#("small_image", json.string(image))] - None -> [] - } - - let small_text = case assets.small_text { - Some(text) -> [#("small_text", json.string(text))] - None -> [] - } - - let small_url = case assets.small_url { - Some(url) -> [#("small_url", json.string(url))] - None -> [] - } - - [large_image, large_text, large_url, small_image, small_text, small_url] - |> list.flatten - |> json.object -} - -@internal -pub fn secrets_to_json(secrets: Secrets) -> Json { - let join = case secrets.join { - Some(secret) -> [#("join", json.string(secret))] - None -> [] - } - - let spectate = case secrets.spectate { - Some(secret) -> [#("spectate", json.string(secret))] - None -> [] - } - - let match = case secrets.match { - Some(secret) -> [#("match", json.string(secret))] - None -> [] - } - - [join, spectate, match] - |> list.flatten - |> json.object -} - -@internal -pub fn button_to_json(button: Button) -> Json { - [#("label", json.string(button.label)), #("url", json.string(button.url))] - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -/// Example usage: -/// ```gleam -/// let activity = activity.new(named: "with your mom", type_: activity.Playing) -/// -/// gateway_connection -/// |> gateway.update_presence(gateway.UpdatePresenceMessage( -/// ..gateway.new_update_presence_message(), -/// activities: [activity], -/// )) -/// ``` -pub fn new(named name: String, type_ type_: Type) -> Activity { - Activity( - name:, - type_:, - url: None, - created_at: timestamp.system_time(), - timestamps: None, - application_id: None, - status_display_type: None, - details: None, - details_url: None, - state: None, - state_url: None, - emoji: None, - party: None, - assets: None, - secrets: None, - flags: None, - buttons: None, - ) -} diff --git a/src/grom/application.gleam b/src/grom/application.gleam deleted file mode 100644 index 284d1e5..0000000 --- a/src/grom/application.gleam +++ /dev/null @@ -1,596 +0,0 @@ -import gleam/bool -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/option.{type Option, None} -import gleam/result -import gleam/set.{type Set} -import gleam/string -import grom -import grom/application/team.{type Team} -import grom/emoji.{type Emoji} -import grom/entitlement.{type Entitlement} -import grom/guild.{type Guild} -import grom/image -import grom/internal/flags -import grom/internal/rest -import grom/permission.{type Permission} -import grom/sku.{type Sku} -import grom/user.{type User} -import grom/webhook_event - -// TYPES ----------------------------------------------------------------------- - -pub type Application { - Application( - id: String, - name: String, - icon_hash: Option(String), - description: String, - rpc_origins: Option(List(String)), - is_bot_public: Bool, - does_bot_require_code_grant: Bool, - bot: Option(User), - terms_of_service_url: Option(String), - privacy_policy_url: Option(String), - owner: Option(User), - team: Option(Team), - guild_id: Option(String), - guild: Option(Guild), - primary_sku_id: Option(String), - slug: Option(String), - cover_image_hash: Option(String), - flags: Option(List(Flag)), - approximate_guild_count: Option(Int), - approximate_user_install_count: Option(Int), - redirect_uris: Option(List(String)), - interaction_endpoint_url: Option(String), - role_connections_verification_url: Option(String), - event_webhooks_url: Option(String), - event_webhooks_status: Option(EventWebhookStatus), - event_webhooks_types: Option(List(webhook_event.Type)), - tags: Option(List(String)), - install_params: Option(InstallParams), - installation_context_config: Option( - Dict(InstallationContext, InstallationContextConfig), - ), - custom_install_url: Option(String), - ) -} - -pub type Flag { - ApplicationAutoModerationRuleCreateBadge - GatewayPresence - GatewayPresenceLimited - GatewayGuildMembers - GatewayGuildMembersLimited - VerificationPendingGuildLimit - Embedded - GatewayMessageContent - GatewayMessageContentLimited - ApplicationCommandBadge -} - -pub type EventWebhookStatus { - Disabled - Enabled - DisabledByDiscord -} - -pub type InstallParams { - InstallParams(scopes: List(String), permissions: List(Permission)) -} - -pub type InstallationContext { - GuildInstall - UserInstall -} - -pub type InstallationContextConfig { - InstallationContextConfig(oauth2_install_params: Option(InstallParams)) -} - -pub type GetEntitlementsQuery { - UserId(String) - SkuIds(Set(String)) - BeforeId(String) - AfterId(String) - Limit(Int) - GuildId(String) - ExcludeEnded(Bool) - ExcludeDeleted(Bool) -} - -/// Unlike `user` or `guild`, all `application` images have the same possible formats. -pub type ImageFormat { - PngImage - JpegImage - WebpImage -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 6), ApplicationAutoModerationRuleCreateBadge), - #(int.bitwise_shift_left(1, 12), GatewayPresence), - #(int.bitwise_shift_left(1, 13), GatewayPresenceLimited), - #(int.bitwise_shift_left(1, 14), GatewayGuildMembers), - #(int.bitwise_shift_left(1, 15), GatewayGuildMembersLimited), - #(int.bitwise_shift_left(1, 16), VerificationPendingGuildLimit), - #(int.bitwise_shift_left(1, 17), Embedded), - #(int.bitwise_shift_left(1, 18), GatewayMessageContent), - #(int.bitwise_shift_left(1, 19), GatewayMessageContentLimited), - #(int.bitwise_shift_left(1, 23), ApplicationCommandBadge), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Application) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use icon_hash <- decode.field("icon", decode.optional(decode.string)) - use description <- decode.field("description", decode.string) - use rpc_origins <- decode.optional_field( - "rpc_origins", - None, - decode.optional(decode.list(decode.string)), - ) - use is_bot_public <- decode.field("bot_public", decode.bool) - use does_bot_require_code_grant <- decode.field( - "bot_require_code_grant", - decode.bool, - ) - use bot <- decode.optional_field("bot", None, decode.optional(user.decoder())) - use terms_of_service_url <- decode.optional_field( - "terms_of_service_url", - None, - decode.optional(decode.string), - ) - use privacy_policy_url <- decode.optional_field( - "privacy_policy_url", - None, - decode.optional(decode.string), - ) - use owner <- decode.optional_field( - "owner", - None, - decode.optional(user.decoder()), - ) - use team <- decode.field("team", decode.optional(team.decoder())) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use guild <- decode.optional_field( - "guild", - None, - decode.optional(guild.decoder()), - ) - use primary_sku_id <- decode.optional_field( - "primary_sku_id", - None, - decode.optional(decode.string), - ) - use slug <- decode.optional_field( - "slug", - None, - decode.optional(decode.string), - ) - use cover_image_hash <- decode.optional_field( - "cover_image", - None, - decode.optional(decode.string), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use approximate_guild_count <- decode.optional_field( - "approximate_guild_count", - None, - decode.optional(decode.int), - ) - use approximate_user_install_count <- decode.optional_field( - "approximate_user_install_count", - None, - decode.optional(decode.int), - ) - use redirect_uris <- decode.optional_field( - "redirect_uris", - None, - decode.optional(decode.list(decode.string)), - ) - use interaction_endpoint_url <- decode.optional_field( - "interaction_endpoint_url", - None, - decode.optional(decode.string), - ) - use role_connections_verification_url <- decode.optional_field( - "role_connections_verification_url", - None, - decode.optional(decode.string), - ) - use event_webhooks_url <- decode.optional_field( - "event_webhooks_url", - None, - decode.optional(decode.string), - ) - use event_webhooks_status <- decode.optional_field( - "event_webhooks_status", - None, - decode.optional(event_webhook_status_decoder()), - ) - use event_webhooks_types <- decode.optional_field( - "event_webhooks_types", - None, - decode.optional(decode.list(webhook_event.type_decoder())), - ) - use tags <- decode.optional_field( - "tags", - None, - decode.optional(decode.list(decode.string)), - ) - use install_params <- decode.optional_field( - "install_params", - None, - decode.optional(install_params_decoder()), - ) - use installation_context_config <- decode.optional_field( - "integration_types_config", - None, - decode.optional(decode.dict( - installation_context_string_decoder(), - installation_context_config_decoder(), - )), - ) - use custom_install_url <- decode.optional_field( - "custom_install_url", - None, - decode.optional(decode.string), - ) - decode.success(Application( - id:, - name:, - icon_hash:, - description:, - rpc_origins:, - is_bot_public:, - does_bot_require_code_grant:, - bot:, - terms_of_service_url:, - privacy_policy_url:, - owner:, - team:, - guild_id:, - guild:, - primary_sku_id:, - slug:, - cover_image_hash:, - flags:, - approximate_guild_count:, - approximate_user_install_count:, - redirect_uris:, - interaction_endpoint_url:, - role_connections_verification_url:, - event_webhooks_url:, - event_webhooks_status:, - event_webhooks_types:, - tags:, - install_params:, - installation_context_config:, - custom_install_url:, - )) -} - -@internal -pub fn install_params_decoder() -> decode.Decoder(InstallParams) { - use scopes <- decode.field("scopes", decode.list(decode.string)) - use permissions <- decode.field("permissions", permission.decoder()) - decode.success(InstallParams(scopes:, permissions:)) -} - -@internal -pub fn installation_context_string_decoder() -> decode.Decoder( - InstallationContext, -) { - use variant <- decode.then(decode.string) - case variant { - "0" -> decode.success(GuildInstall) - "1" -> decode.success(UserInstall) - _ -> decode.failure(GuildInstall, "IntegrationType") - } -} - -@internal -pub fn installation_context_int_decoder() -> decode.Decoder(InstallationContext) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(GuildInstall) - 1 -> decode.success(UserInstall) - _ -> decode.failure(GuildInstall, "IntegrationType") - } -} - -@internal -pub fn installation_context_config_decoder() -> decode.Decoder( - InstallationContextConfig, -) { - use oauth2_install_params <- decode.optional_field( - "oauth2_install_params", - None, - decode.optional(install_params_decoder()), - ) - decode.success(InstallationContextConfig(oauth2_install_params:)) -} - -@internal -pub fn event_webhook_status_decoder() -> decode.Decoder(EventWebhookStatus) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Disabled) - 2 -> decode.success(Enabled) - 3 -> decode.success(DisabledByDiscord) - _ -> decode.failure(Disabled, "EventWebhookStatus") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn install_params_to_json(install_params: InstallParams) -> Json { - let InstallParams(scopes:, permissions:) = install_params - json.object([ - #("scopes", json.array(scopes, json.string)), - #("permissions", permission.to_json(permissions)), - ]) -} - -@internal -pub fn installation_context_to_string(context: InstallationContext) -> String { - case context { - GuildInstall -> 0 - UserInstall -> 1 - } - |> int.to_string -} - -@internal -pub fn installation_context_to_json(context: InstallationContext) -> Json { - case context { - GuildInstall -> "0" - UserInstall -> "1" - } - |> json.string -} - -@internal -pub fn installation_context_config_to_json( - installation_context_config: InstallationContextConfig, -) -> Json { - let InstallationContextConfig(oauth2_install_params:) = - installation_context_config - json.object([ - #("oauth2_install_params", case oauth2_install_params { - None -> json.null() - option.Some(params) -> install_params_to_json(params) - }), - ]) -} - -@internal -pub fn event_webhook_status_to_json( - event_webhook_status: EventWebhookStatus, -) -> Json { - case event_webhook_status { - Disabled -> json.int(1) - Enabled -> json.int(2) - DisabledByDiscord -> json.int(3) - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_emojis( - client: grom.Client, - for application_id: String, -) -> Result(List(Emoji), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/emojis", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.at(["items"], decode.list(emoji.decoder()))) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_emoji( - client: grom.Client, - for application_id: String, - id emoji_id: String, -) -> Result(Emoji, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/emojis/" <> emoji_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_emoji( - client: grom.Client, - for application_id: String, - named name: String, - bytes image: image.Data, -) -> Result(Emoji, grom.Error) { - let json = - json.object([ - #("name", json.string(name)), - #("image", json.string(image |> image.to_base64)), - ]) - - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" <> application_id <> "/emojis", - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn rename_emoji( - client: grom.Client, - for application_id: String, - id emoji_id: String, - to name: String, -) -> Result(Emoji, grom.Error) { - let json = json.object([#("name", json.string(name))]) - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/applications/" <> application_id <> "/emojis/" <> emoji_id, - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_emoji( - client: grom.Client, - from application_id: String, - id emoji_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/applications/" <> application_id <> "/emojis/" <> emoji_id, - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn get_entitlements( - client: grom.Client, - for application_id: String, - with query: Set(GetEntitlementsQuery), -) -> Result(List(Entitlement), grom.Error) { - let query = - set.map(query, fn(item) { - case item { - UserId(id) -> #("user_id", id) - SkuIds(ids) -> #( - "sku_ids", - ids - |> set.to_list - |> string.join(","), - ) - BeforeId(id) -> #("before", id) - AfterId(id) -> #("after", id) - Limit(limit) -> #("limit", limit |> int.to_string) - GuildId(id) -> #("guild_id", id) - ExcludeEnded(exclude) -> #( - "exclude_ended", - exclude - |> bool.to_string - |> string.lowercase, - ) - ExcludeDeleted(exclude) -> #( - "exclude_deleted", - exclude - |> bool.to_string - |> string.lowercase, - ) - } - }) - - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/entitlements", - ) - |> request.set_query(query |> set.to_list) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(entitlement.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_skus( - client: grom.Client, - for application_id: String, -) -> Result(List(Sku), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/applications/" <> application_id <> "/skus") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: sku.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn icon_request( - id id: String, - hash icon: String, - format format: ImageFormat, -) -> Request(String) { - let extension = case format { - PngImage -> ".png" - JpegImage -> ".jpg" - WebpImage -> ".webp" - } - - rest.new_cdn_request( - to: "/app-icons/" <> id <> "/" <> icon <> extension, - query: [], - ) -} - -pub fn cover_image_request( - id id: String, - hash cover_image: String, - format format: ImageFormat, -) -> Request(String) { - // Yes, it uses the exact same endpoint - // - // Don't blame me, hate the game not the player 🤷 - icon_request(id, cover_image, format) -} diff --git a/src/grom/application/current_application.gleam b/src/grom/application/current_application.gleam deleted file mode 100644 index a2a0865..0000000 --- a/src/grom/application/current_application.gleam +++ /dev/null @@ -1,194 +0,0 @@ -import gleam/dict.{type Dict} -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/application.{type Application} -import grom/image -import grom/internal/flags -import grom/internal/rest -import grom/modification.{type Modification, Skip} -import grom/webhook_event - -// TYPES ----------------------------------------------------------------------- - -pub type Modify { - Modify( - custom_install_url: Option(String), - description: Option(String), - role_connections_verification_url: Option(String), - install_params: Option(application.InstallParams), - installation_contexts_config: Option( - Dict( - application.InstallationContext, - application.InstallationContextConfig, - ), - ), - flags: Option(List(application.Flag)), - icon: Modification(image.Data), - cover_image: Modification(image.Data), - interactions_endpoint_url: Option(String), - tags: Option(List(String)), - event_webhooks_url: Option(String), - event_webhooks_status: Option(application.EventWebhookStatus), - event_webhooks_types: Option(List(webhook_event.Type)), - ) -} - -// ENCODERS -------------------------------------------------------------------- - -fn modify_to_json(modify: Modify) -> Json { - let custom_install_url = case modify.custom_install_url { - Some(url) -> [#("custom_install_url", json.string(url))] - None -> [] - } - - let description = case modify.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let role_connections_verification_url = case - modify.role_connections_verification_url - { - Some(url) -> [#("role_connections_verification_url", json.string(url))] - None -> [] - } - - let install_params = case modify.install_params { - Some(params) -> [ - #("install_params", application.install_params_to_json(params)), - ] - None -> [] - } - - let installation_contexts_config = case modify.installation_contexts_config { - Some(config) -> [ - #( - "integration_types_config", - json.dict( - config, - application.installation_context_to_string, - application.installation_context_config_to_json, - ), - ), - ] - None -> [] - } - - let flags = case modify.flags { - Some(flags) -> [#("flags", flags.to_json(flags, application.bits_flags()))] - None -> [] - } - - let icon = - modify.icon - |> modification.to_json("icon", image.to_json) - - let cover_image = - modify.cover_image - |> modification.to_json("cover_image", image.to_json) - - let interactions_endpoint_url = case modify.interactions_endpoint_url { - Some(url) -> [#("interactions_endpoint_url", json.string(url))] - None -> [] - } - - let tags = case modify.tags { - Some(tags) -> [#("tags", json.array(tags, json.string))] - None -> [] - } - - let event_webhooks_url = case modify.event_webhooks_url { - Some(url) -> [#("event_webhooks_url", json.string(url))] - None -> [] - } - - let event_webhooks_status = case modify.event_webhooks_status { - Some(status) -> [ - #( - "event_webhooks_status", - application.event_webhook_status_to_json(status), - ), - ] - None -> [] - } - - let event_webhooks_types = case modify.event_webhooks_types { - Some(types) -> [ - #("event_webhooks_types", json.array(types, webhook_event.type_encode)), - ] - None -> [] - } - - [ - custom_install_url, - description, - role_connections_verification_url, - install_params, - installation_contexts_config, - flags, - icon, - cover_image, - interactions_endpoint_url, - tags, - event_webhooks_url, - event_webhooks_status, - event_webhooks_types, - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get(client: grom.Client) -> Result(Application, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/applications/@me") - |> rest.execute, - ) - - response.body - |> json.parse(using: application.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - with modify: Modify, -) -> Result(Application, grom.Error) { - let json = modify |> modify_to_json - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/applications/@me") - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: application.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify( - custom_install_url: None, - description: None, - role_connections_verification_url: None, - install_params: None, - installation_contexts_config: None, - flags: None, - icon: Skip, - cover_image: Skip, - interactions_endpoint_url: None, - tags: None, - event_webhooks_url: None, - event_webhooks_status: None, - event_webhooks_types: None, - ) -} diff --git a/src/grom/application/role_connection_metadata.gleam b/src/grom/application/role_connection_metadata.gleam deleted file mode 100644 index c283d06..0000000 --- a/src/grom/application/role_connection_metadata.gleam +++ /dev/null @@ -1,171 +0,0 @@ -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/function -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/internal/rest - -// TYPES ----------------------------------------------------------------------- - -pub type RoleConnectionMetadata { - RoleConnectionMetadata( - type_: Type, - key: String, - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - ) -} - -pub type Type { - IntegerLessThanOrEqual - IntegerGreaterThanOrEqual - IntegerEqual - IntegerNotEqual - DatetimeLessThanOrEqual - DatetimeGreaterThanOrEqual - BooleanEqual - BooleanNotEqual -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(RoleConnectionMetadata) { - use type_ <- decode.field("type", type_decoder()) - use key <- decode.field("key", decode.string) - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - decode.success(RoleConnectionMetadata( - type_:, - key:, - name:, - name_localizations:, - description:, - description_localizations:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(IntegerLessThanOrEqual) - 2 -> decode.success(IntegerGreaterThanOrEqual) - 3 -> decode.success(IntegerEqual) - 4 -> decode.success(IntegerNotEqual) - 5 -> decode.success(DatetimeLessThanOrEqual) - 6 -> decode.success(DatetimeGreaterThanOrEqual) - 7 -> decode.success(BooleanEqual) - 8 -> decode.success(BooleanNotEqual) - _ -> decode.failure(IntegerLessThanOrEqual, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -fn encode(metadata: RoleConnectionMetadata) -> Json { - let name_localizations = case metadata.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description_localizations = case metadata.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - [ - [ - #("type", type_encode(metadata.type_)), - #("key", json.string(metadata.key)), - #("name", json.string(metadata.name)), - ], - name_localizations, - description_localizations, - ] - |> list.flatten - |> json.object -} - -fn type_encode(type_: Type) -> Json { - case type_ { - IntegerLessThanOrEqual -> 1 - IntegerGreaterThanOrEqual -> 2 - IntegerEqual -> 3 - IntegerNotEqual -> 4 - DatetimeLessThanOrEqual -> 5 - DatetimeGreaterThanOrEqual -> 6 - BooleanEqual -> 7 - BooleanNotEqual -> 8 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for application_id: String, -) -> Result(List(RoleConnectionMetadata), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/role-connections/metadata", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - for application_id: String, - new metadata: List(RoleConnectionMetadata), -) -> Result(List(RoleConnectionMetadata), grom.Error) { - let json = json.array(metadata, encode) - - use response <- result.try( - client - |> rest.new_request( - http.Put, - "/applications/" <> application_id <> "/role-connections/metadata", - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(decoder())) - |> result.map_error(grom.CouldNotDecode) -} diff --git a/src/grom/application/team.gleam b/src/grom/application/team.gleam deleted file mode 100644 index 9609e36..0000000 --- a/src/grom/application/team.gleam +++ /dev/null @@ -1,107 +0,0 @@ -import gleam/dynamic/decode -import gleam/http/request.{type Request} -import gleam/option.{type Option} -import grom/internal/rest -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Team { - Team( - id: String, - icon_hash: Option(String), - members: List(Member), - name: String, - owner_user_id: String, - ) -} - -pub type Member { - Member( - user: User, - role: MemberRole, - team_id: String, - membership_state: MembershipState, - ) -} - -pub type MemberRole { - Admin - Developer - ReadOnly -} - -pub type MembershipState { - Invited - Accepted -} - -pub type IconFormat { - PngIcon - JpegIcon - WebpIcon -} - -// DECODER --------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Team) { - use id <- decode.field("id", decode.string) - use icon_hash <- decode.field("icon_hash", decode.optional(decode.string)) - use members <- decode.field("members", decode.list(member_decoder())) - use name <- decode.field("name", decode.string) - use owner_user_id <- decode.field("owner_user_id", decode.string) - decode.success(Team(id:, icon_hash:, members:, name:, owner_user_id:)) -} - -@internal -pub fn member_decoder() -> decode.Decoder(Member) { - use user <- decode.field("user", user.decoder()) - use role <- decode.field("role", member_role_decoder()) - use team_id <- decode.field("team_id", decode.string) - use membership_state <- decode.field( - "membership_state", - membership_state_decoder(), - ) - decode.success(Member(user:, role:, team_id:, membership_state:)) -} - -@internal -pub fn member_role_decoder() -> decode.Decoder(MemberRole) { - use variant <- decode.then(decode.string) - case variant { - "admin" -> decode.success(Admin) - "developer" -> decode.success(Developer) - "read_only" -> decode.success(ReadOnly) - _ -> decode.failure(ReadOnly, "MemberRole") - } -} - -@internal -pub fn membership_state_decoder() -> decode.Decoder(MembershipState) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Invited) - 2 -> decode.success(Accepted) - _ -> decode.failure(Invited, "MembershipState") - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn icon_request( - id id: String, - hash icon: String, - format format: IconFormat, -) -> Request(String) { - let extension = case format { - PngIcon -> ".png" - JpegIcon -> ".jpeg" - WebpIcon -> ".webp" - } - - rest.new_cdn_request( - to: "/team-icons/" <> id <> "/" <> icon <> extension, - query: [], - ) -} diff --git a/src/grom/channel.gleam b/src/grom/channel.gleam deleted file mode 100644 index 1861ed9..0000000 --- a/src/grom/channel.gleam +++ /dev/null @@ -1,2429 +0,0 @@ -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 gleam/time/timestamp.{type Timestamp} -import grom -import grom/channel/forum -import grom/channel/media -import grom/channel/permission_overwrite.{type PermissionOverwrite} -import grom/channel/thread.{type Thread} -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/message.{type Message} -import grom/modification.{type Modification, Skip} -import grom/permission.{type Permission} -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Channel { - Text( - id: String, - guild_id: Option(String), - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - topic: Option(String), - is_nsfw: Bool, - last_message_id: Option(String), - rate_limit_per_user: Duration, - parent_id: Option(String), - last_pin_timestamp: Option(Timestamp), - current_user_permissions: Option(List(Permission)), - default_auto_archive_duration: Option(Duration), - ) - Dm( - id: String, - last_message_id: Option(String), - recipients: List(User), - current_user_permissions: Option(List(Permission)), - ) - Voice( - id: String, - guild_id: String, - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - is_nsfw: Bool, - last_message_id: Option(String), - bitrate: Int, - user_limit: Int, - rate_limit_per_user: Duration, - parent_id: Option(String), - rtc_region_id: Option(String), - video_quality_mode: VideoQualityMode, - current_user_permissions: Option(List(Permission)), - ) - Category( - id: String, - guild_id: Option(String), - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - current_user_permissions: Option(List(Permission)), - ) - Announcement( - id: String, - guild_id: Option(String), - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - topic: Option(String), - is_nsfw: Bool, - last_message_id: Option(String), - parent_id: Option(String), - last_pin_timestamp: Option(Timestamp), - current_user_permissions: Option(List(Permission)), - default_auto_archive_duration: Duration, - ) - Thread(Thread) - Stage( - id: String, - guild_id: String, - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - is_nsfw: Bool, - last_message_id: Option(String), - bitrate: Int, - user_limit: Int, - rate_limit_per_user: Duration, - parent_id: Option(String), - rtc_region_id: Option(String), - video_quality_mode: VideoQualityMode, - current_user_permissions: Option(List(Permission)), - ) - Forum( - id: String, - guild_id: Option(String), - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - topic: Option(String), - is_nsfw: Bool, - last_thread_id: Option(String), - rate_limit_per_user: Duration, - parent_id: Option(String), - default_auto_archive_duration: Option(Duration), - current_user_permissions: Option(List(Permission)), - flags: List(forum.Flag), - available_tags: List(forum.Tag), - default_reaction_emoji: Option(forum.DefaultReaction), - default_thread_rate_limit_per_user: Duration, - default_sort_order: Option(forum.SortOrder), - default_layout: forum.Layout, - ) - Media( - id: String, - guild_id: Option(String), - position: Int, - permission_overwrites: List(PermissionOverwrite), - name: String, - topic: Option(String), - is_nsfw: Bool, - last_thread_id: Option(String), - rate_limit_per_user: Duration, - parent_id: Option(String), - default_auto_archive_duration: Option(Duration), - current_user_permissions: Option(List(Permission)), - flags: List(media.Flag), - available_tags: List(forum.Tag), - default_reaction_emoji: Option(forum.DefaultReaction), - default_thread_rate_limit_per_user: Option(Duration), - default_sort_order: Option(forum.SortOrder), - ) -} - -pub opaque type Modify { - ModifyText( - name: Option(String), - convert_to_announcement: Bool, - position: Modification(Int), - topic: Modification(String), - is_nsfw: Option(Bool), - rate_limit_per_user: Modification(Duration), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - default_auto_archive_duration: Modification(Duration), - default_thread_rate_limit_per_user: Option(Duration), - ) - ModifyVoice( - name: Option(String), - position: Modification(Int), - is_nsfw: Option(Bool), - rate_limit_per_user: Modification(Duration), - bitrate: Modification(Int), - user_limit: Modification(Int), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - rtc_region_id: Modification(String), - video_quality_mode: Modification(VideoQualityMode), - ) - ModifyCategory( - name: Option(String), - position: Modification(Int), - permission_overwrites: Modification(List(permission_overwrite.Create)), - ) - ModifyAnnouncement( - name: Option(String), - convert_to_text: Bool, - position: Modification(Int), - topic: Modification(String), - is_nsfw: Option(Bool), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - default_auto_archive_duration: Modification(Duration), - ) - ModifyThread( - name: Option(String), - is_archived: Option(Bool), - auto_archive_duration: Option(Duration), - is_locked: Option(Bool), - is_invitable: Option(Bool), - rate_limit_per_user: Modification(Duration), - flags: Option(List(thread.Flag)), - applied_tags_ids: Option(List(String)), - ) - ModifyStage( - name: Option(String), - position: Modification(Int), - is_nsfw: Option(Bool), - rate_limit_per_user: Modification(Duration), - bitrate: Modification(Int), - user_limit: Modification(Int), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - rtc_region_id: Modification(String), - video_quality_mode: Modification(VideoQualityMode), - ) - ModifyForum( - name: Option(String), - position: Modification(Int), - topic: Modification(String), - is_nsfw: Option(Bool), - rate_limit_per_user: Modification(Duration), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - default_auto_archive_duration: Modification(Duration), - flags: Option(List(forum.Flag)), - available_tags: Option(List(forum.Tag)), - default_reaction_emoji: Modification(forum.DefaultReaction), - default_thread_rate_limit_per_user: Option(Duration), - default_sort_order: Modification(forum.SortOrder), - default_layout: Option(forum.Layout), - ) - ModifyMedia( - name: Option(String), - position: Modification(Int), - topic: Modification(String), - is_nsfw: Option(Bool), - rate_limit_per_user: Modification(Duration), - permission_overwrites: Modification(List(permission_overwrite.Create)), - parent_id: Modification(String), - default_auto_archive_duration: Modification(Duration), - flags: Option(List(media.Flag)), - available_tags: Option(List(forum.Tag)), - default_reaction_emoji: Modification(forum.DefaultReaction), - default_thread_rate_limit_per_user: Option(Duration), - default_sort_order: Modification(forum.SortOrder), - ) -} - -pub type Create { - CreateTextChannel(CreateText) - CreateDmChannel(CreateDm) - CreateVoiceChannel(CreateVoice) - CreateCategoryChannel(CreateCategory) - CreateAnnouncementChannel(CreateAnnouncement) - CreateStageChannel(CreateStage) - CreateForumChannel(CreateForum) - CreateMediaChannel(CreateMedia) -} - -pub type CreateText { - CreateText( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - topic: Option(String), - rate_limit_per_user: Option(Duration), - parent_id: Option(String), - is_nsfw: Bool, - default_auto_archive_duration: Option(Duration), - default_thread_rate_limit_per_user: Option(Duration), - ) -} - -pub type CreateDm { - CreateDm(recipient_id: String) -} - -pub type CreateVoice { - CreateVoice( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - bitrate: Option(Int), - user_limit: Option(Int), - rate_limit_per_user: Option(Duration), - parent_id: Option(String), - is_nsfw: Bool, - rtc_region: Option(String), - video_quality_mode: Option(VideoQualityMode), - ) -} - -pub type CreateCategory { - CreateCategory( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - ) -} - -pub type CreateAnnouncement { - CreateAnnouncement( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - topic: Option(String), - parent_id: Option(String), - is_nsfw: Bool, - default_auto_archive_duration: Option(Duration), - default_thread_rate_limit_per_user: Option(Duration), - ) -} - -pub type CreateStage { - CreateStage( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - bitrate: Option(Int), - user_limit: Option(Int), - rate_limit_per_user: Option(Duration), - parent_id: Option(String), - is_nsfw: Bool, - rtc_region: Option(String), - video_quality_mode: Option(VideoQualityMode), - ) -} - -pub type CreateForum { - CreateForum( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - topic: Option(String), - rate_limit_per_user: Option(Duration), - parent_id: Option(String), - is_nsfw: Bool, - default_auto_archive_duration: Option(Duration), - default_reaction_emoji: Option(forum.DefaultReaction), - available_tags: Option(List(forum.Tag)), - default_sort_order: Option(forum.SortOrder), - default_forum_layout: Option(forum.Layout), - default_thread_rate_limit_per_user: Option(Duration), - ) -} - -pub type CreateMedia { - CreateMedia( - guild_id: String, - name: String, - position: Option(Int), - permission_overwrites: Option(List(permission_overwrite.Create)), - topic: Option(String), - rate_limit_per_user: Option(Duration), - parent_id: Option(String), - default_auto_archive_duration: Option(Duration), - default_reaction_emoji: Option(forum.DefaultReaction), - available_tags: Option(List(forum.Tag)), - default_sort_order: Option(forum.SortOrder), - default_thread_rate_limit_per_user: Option(Duration), - ) -} - -pub type FollowedChannel { - FollowedChannel(channel_id: String, webhook_id: String) -} - -pub type VideoQualityMode { - AutomaticVideoQuality - HDVideoQuality -} - -pub type ReceivedThreads { - ReceivedThreads( - threads: List(Thread), - // TODO: Rename to current_members or something - members: List(thread.Member), - has_more: Bool, - ) -} - -pub type GetMessagesQuery { - BeforeId(String) - AroundId(String) - AfterId(String) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Channel) { - use type_ <- decode.field("type", decode.int) - case type_ { - 0 -> text_decoder() - 1 -> dm_decoder() - 2 -> voice_decoder() - 4 -> category_decoder() - 5 -> announcement_decoder() - 10 | 11 | 12 -> { - use thread <- decode.then(thread.decoder()) - decode.success(Thread(thread)) - } - 13 -> stage_decoder() - 15 -> forum_decoder() - 16 -> media_decoder() - _ -> - decode.failure( - Text( - "", - None, - 0, - [], - "", - None, - False, - None, - duration.seconds(0), - None, - None, - None, - None, - ), - "Channel", - ) - } -} - -@internal -pub fn text_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use topic <- decode.field("topic", decode.optional(decode.string)) - use is_nsfw <- decode.field("nsfw", decode.bool) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_minutes_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use last_pin_timestamp <- decode.optional_field( - "last_pin_timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - use default_auto_archive_duration <- decode.optional_field( - "default_auto_archive_duration", - None, - decode.optional(time_duration.from_int_seconds_decoder()), - ) - decode.success(Text( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - topic:, - is_nsfw:, - last_message_id:, - rate_limit_per_user:, - parent_id:, - last_pin_timestamp:, - current_user_permissions:, - default_auto_archive_duration:, - )) -} - -@internal -pub fn dm_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use recipients <- decode.field("recipients", decode.list(user.decoder())) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - decode.success(Dm( - id:, - last_message_id:, - recipients:, - current_user_permissions:, - )) -} - -@internal -pub fn voice_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use is_nsfw <- decode.field("nsfw", decode.bool) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use bitrate <- decode.field("bitrate", decode.int) - use user_limit <- decode.field("user_limit", decode.int) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_int_seconds_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use rtc_region_id <- decode.field( - "rtc_region", - decode.optional(decode.string), - ) - use video_quality_mode <- decode.optional_field( - "video_quality_mode", - AutomaticVideoQuality, - video_quality_mode_decoder(), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - decode.success(Voice( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - is_nsfw:, - last_message_id:, - bitrate:, - user_limit:, - rate_limit_per_user:, - parent_id:, - rtc_region_id:, - video_quality_mode:, - current_user_permissions:, - )) -} - -@internal -pub fn category_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - decode.success(Category( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - current_user_permissions:, - )) -} - -@internal -pub fn announcement_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use topic <- decode.field("topic", decode.optional(decode.string)) - use is_nsfw <- decode.field("nsfw", decode.bool) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use last_pin_timestamp <- decode.optional_field( - "last_pin_timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - use default_auto_archive_duration <- decode.field( - "default_auto_archive_duration", - time_duration.from_int_seconds_decoder(), - ) - decode.success(Announcement( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - topic:, - is_nsfw:, - last_message_id:, - parent_id:, - last_pin_timestamp:, - current_user_permissions:, - default_auto_archive_duration:, - )) -} - -@internal -pub fn stage_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use is_nsfw <- decode.field("nsfw", decode.bool) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use bitrate <- decode.field("bitrate", decode.int) - use user_limit <- decode.field("user_limit", decode.int) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_int_seconds_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use rtc_region_id <- decode.field( - "rtc_region", - decode.optional(decode.string), - ) - use video_quality_mode <- decode.optional_field( - "video_quality_mode", - AutomaticVideoQuality, - video_quality_mode_decoder(), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - decode.success(Stage( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - is_nsfw:, - last_message_id:, - bitrate:, - user_limit:, - rate_limit_per_user:, - parent_id:, - rtc_region_id:, - video_quality_mode:, - current_user_permissions:, - )) -} - -@internal -pub fn forum_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use topic <- decode.field("topic", decode.optional(decode.string)) - use is_nsfw <- decode.field("is_nsfw", decode.bool) - use last_thread_id <- decode.field( - "last_thread_id", - decode.optional(decode.string), - ) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_minutes_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use default_auto_archive_duration <- decode.optional_field( - "default_auto_archive_duration", - None, - decode.optional(time_duration.from_minutes_decoder()), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - use flags <- decode.field("flags", flags.decoder(forum.bits_flags())) - use available_tags <- decode.field( - "available_tags", - decode.list(forum.tag_decoder()), - ) - use default_reaction_emoji <- decode.field( - "default_reaction_emoji", - decode.optional(forum.default_reaction_decoder()), - ) - use default_thread_rate_limit_per_user <- decode.field( - "default_thread_rate_limit_per_user", - time_duration.from_int_seconds_decoder(), - ) - use default_sort_order <- decode.field( - "default_sort_order", - decode.optional(forum.sort_order_type_decoder()), - ) - use default_layout <- decode.field( - "default_layout", - forum.layout_type_decoder(), - ) - decode.success(Forum( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - topic:, - is_nsfw:, - last_thread_id:, - rate_limit_per_user:, - parent_id:, - default_auto_archive_duration:, - current_user_permissions:, - available_tags:, - flags:, - default_reaction_emoji:, - default_thread_rate_limit_per_user:, - default_sort_order:, - default_layout:, - )) -} - -@internal -pub fn media_decoder() -> decode.Decoder(Channel) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permission_overwrites <- decode.field( - "permission_overwrites", - decode.list(permission_overwrite.decoder()), - ) - use name <- decode.field("name", decode.string) - use topic <- decode.field("topic", decode.optional(decode.string)) - use is_nsfw <- decode.field("is_nsfw", decode.bool) - use last_thread_id <- decode.field( - "last_thread_id", - decode.optional(decode.string), - ) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_minutes_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.optional(decode.string)) - use default_auto_archive_duration <- decode.optional_field( - "default_auto_archive_duration", - None, - decode.optional(time_duration.from_minutes_decoder()), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - use flags <- decode.field("flags", flags.decoder(media.bits_flags())) - use available_tags <- decode.field( - "available_tags", - decode.list(forum.tag_decoder()), - ) - use default_reaction_emoji <- decode.field( - "default_reaction_emoji", - decode.optional(forum.default_reaction_decoder()), - ) - use default_thread_rate_limit_per_user <- decode.optional_field( - "default_thread_rate_limit_per_user", - None, - decode.optional(time_duration.from_minutes_decoder()), - ) - use default_sort_order <- decode.field( - "default_sort_order", - decode.optional(forum.sort_order_type_decoder()), - ) - decode.success(Media( - id:, - guild_id:, - position:, - permission_overwrites:, - name:, - topic:, - is_nsfw:, - last_thread_id:, - rate_limit_per_user:, - parent_id:, - default_auto_archive_duration:, - current_user_permissions:, - flags:, - available_tags:, - default_reaction_emoji:, - default_thread_rate_limit_per_user:, - default_sort_order:, - )) -} - -@internal -pub fn video_quality_mode_decoder() -> decode.Decoder(VideoQualityMode) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(AutomaticVideoQuality) - 2 -> decode.success(HDVideoQuality) - _ -> decode.failure(AutomaticVideoQuality, "VideoQualityMode") - } -} - -@internal -pub fn followed_channel_decoder() -> decode.Decoder(FollowedChannel) { - use channel_id <- decode.field("channel_id", decode.string) - use webhook_id <- decode.field("webhook_id", decode.string) - decode.success(FollowedChannel(channel_id:, webhook_id:)) -} - -@internal -pub fn received_threads_decoder() -> decode.Decoder(ReceivedThreads) { - use threads <- decode.field("threads", decode.list(thread.decoder())) - use members <- decode.field("members", decode.list(thread.member_decoder())) - use has_more <- decode.field("has_more", decode.bool) - - decode.success(ReceivedThreads(threads:, members:, has_more:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - case modify { - ModifyText(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let type_ = case modify.convert_to_announcement { - True -> [#("type", json.int(5))] - False -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let topic = - modify.topic - |> modification.to_json("topic", json.string) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let parent_id = - modify.parent_id - |> modification.to_json("parent_id", json.string) - - let default_auto_archive_duration = - modify.default_auto_archive_duration - |> modification.to_json( - "default_auto_archive_duration", - // FIXME: THIS IS SUPPOSED TO BE MINUTES!!! - time_duration.to_int_seconds_json, - ) - - let default_thread_rate_limit_per_user = case - modify.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - [ - name, - type_, - position, - topic, - is_nsfw, - rate_limit_per_user, - permission_overwrites, - parent_id, - default_auto_archive_duration, - default_thread_rate_limit_per_user, - ] - |> list.flatten - |> json.object - } - ModifyVoice(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let bitrate = - modify.bitrate - |> modification.to_json("bitrate", json.int) - - let user_limit = - modify.user_limit - |> modification.to_json("user_limit", json.int) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let parent_id = - modify.parent_id - |> modification.to_json("parent_id", json.string) - - let rtc_region_id = - modify.rtc_region_id - |> modification.to_json("rtc_region", json.string) - - let video_quality_mode = - modify.video_quality_mode - |> modification.to_json( - "video_quality_mode", - video_quality_mode_to_json, - ) - - [ - name, - position, - is_nsfw, - rate_limit_per_user, - bitrate, - user_limit, - permission_overwrites, - parent_id, - rtc_region_id, - video_quality_mode, - ] - |> list.flatten - |> json.object - } - ModifyCategory(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - [name, position, permission_overwrites] - |> list.flatten - |> json.object - } - ModifyAnnouncement(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let type_ = case modify.convert_to_text { - True -> [#("type", json.int(0))] - False -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let topic = - modify.topic - |> modification.to_json("topic", json.string) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let parent_id = - modify.parent_id - |> modification.to_json("parent_id", json.string) - - let default_auto_archive_duration = - modify.default_auto_archive_duration - |> modification.to_json( - "default_auto_archive_duration", - time_duration.to_int_seconds_json, - ) - - [ - name, - type_, - position, - topic, - is_nsfw, - permission_overwrites, - parent_id, - default_auto_archive_duration, - ] - |> list.flatten - |> json.object - } - ModifyThread(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let is_archived = case modify.is_archived { - Some(archived) -> [#("archived", json.bool(archived))] - None -> [] - } - - let auto_archive_duration = case modify.auto_archive_duration { - Some(duration) -> [ - #( - "auto_archive_duration", - time_duration.to_int_seconds_json(duration), - ), - ] - None -> [] - } - - let is_locked = case modify.is_locked { - Some(locked) -> [#("locked", json.bool(locked))] - None -> [] - } - - let is_invitable = case modify.is_invitable { - Some(invitable) -> [#("invitable", json.bool(invitable))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let flags = case modify.flags { - Some(flags) -> [#("flags", flags.to_json(flags, thread.bits_flags()))] - None -> [] - } - - let applied_tags_ids = case modify.applied_tags_ids { - Some(tags) -> [#("applied_tags", json.array(tags, json.string))] - None -> [] - } - - [ - name, - is_archived, - auto_archive_duration, - is_locked, - is_invitable, - rate_limit_per_user, - flags, - applied_tags_ids, - ] - |> list.flatten - |> json.object - } - ModifyStage(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let bitrate = - modify.bitrate - |> modification.to_json("bitrate", json.int) - - let user_limit = - modify.user_limit - |> modification.to_json("user_limit", json.int) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let parent_id = - modify.parent_id - |> modification.to_json("parent_id", json.string) - - let rtc_region_id = - modify.rtc_region_id - |> modification.to_json("rtc_region", json.string) - - let video_quality_mode = - modify.video_quality_mode - |> modification.to_json( - "video_quality_mode", - video_quality_mode_to_json, - ) - - [ - name, - position, - is_nsfw, - rate_limit_per_user, - bitrate, - user_limit, - permission_overwrites, - parent_id, - rtc_region_id, - video_quality_mode, - ] - |> list.flatten - |> json.object - } - ModifyForum(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let topic = - modify.topic - |> modification.to_json("topic", json.string) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let parent_id = - modify.parent_id - |> modification.to_json("parent_id", json.string) - - let default_auto_archive_duration = - modify.default_auto_archive_duration - |> modification.to_json( - "default_auto_archive_duration", - time_duration.to_int_seconds_json, - ) - - let flags = case modify.flags { - Some(flags) -> [#("flags", flags.to_json(flags, forum.bits_flags()))] - None -> [] - } - - let available_tags = case modify.available_tags { - Some(tags) -> [#("available_tags", json.array(tags, forum.tag_to_json))] - None -> [] - } - - let default_reaction_emoji = - modify.default_reaction_emoji - |> modification.to_json( - "default_reaction_emoji", - forum.default_reaction_encode, - ) - - let default_thread_rate_limit_per_user = case - modify.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - let default_sort_order = - modify.default_sort_order - |> modification.to_json( - "default_sort_order", - forum.sort_order_type_encode, - ) - - [ - name, - position, - topic, - is_nsfw, - rate_limit_per_user, - permission_overwrites, - parent_id, - default_auto_archive_duration, - flags, - available_tags, - default_reaction_emoji, - default_thread_rate_limit_per_user, - default_sort_order, - ] - |> list.flatten - |> json.object - } - ModifyMedia(..) -> { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let position = - modify.position - |> modification.to_json("position", json.int) - - let topic = - modify.topic - |> modification.to_json("topic", json.string) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let permission_overwrites = - modify.permission_overwrites - |> modification.to_json("permission_overwrites", json.array( - _, - permission_overwrite.create_to_json, - )) - - let default_auto_archive_duration = - modify.default_auto_archive_duration - |> modification.to_json( - "default_auto_archive_duration", - time_duration.to_int_seconds_json, - ) - - let flags = case modify.flags { - Some(flags) -> [#("flags", flags.to_json(flags, media.bits_flags()))] - None -> [] - } - - let available_tags = case modify.available_tags { - Some(tags) -> [#("available_tags", json.array(tags, forum.tag_to_json))] - None -> [] - } - - let default_reaction_emoji = - modify.default_reaction_emoji - |> modification.to_json( - "default_reaction_emoji", - forum.default_reaction_encode, - ) - - let default_thread_rate_limit_per_user = case - modify.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - let default_sort_order = - modify.default_sort_order - |> modification.to_json( - "default_sort_order", - forum.sort_order_type_encode, - ) - - [ - name, - position, - topic, - is_nsfw, - rate_limit_per_user, - permission_overwrites, - default_auto_archive_duration, - flags, - available_tags, - default_reaction_emoji, - default_thread_rate_limit_per_user, - default_sort_order, - ] - |> list.flatten - |> json.object - } - } -} - -@internal -pub fn video_quality_mode_to_json(video_quality_mode: VideoQualityMode) -> Json { - case video_quality_mode { - AutomaticVideoQuality -> 1 - HDVideoQuality -> 2 - } - |> json.int -} - -@internal -pub fn create_to_json(create: Create) -> Json { - case create { - CreateTextChannel(inner) -> create_text_to_json(inner) - CreateDmChannel(inner) -> create_dm_to_json(inner) - CreateVoiceChannel(inner) -> create_voice_to_json(inner) - CreateCategoryChannel(inner) -> create_category_to_json(inner) - CreateAnnouncementChannel(inner) -> create_announcement_to_json(inner) - CreateStageChannel(inner) -> create_stage_to_json(inner) - CreateForumChannel(inner) -> create_forum_to_json(inner) - CreateMediaChannel(inner) -> create_media_to_json(inner) - } -} - -@internal -pub fn create_text_to_json(create_text: CreateText) -> Json { - let name = [#("name", json.string(create_text.name))] - - let position = case create_text.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_text.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let topic = case create_text.topic { - Some(topic) -> [#("topic", json.string(topic))] - None -> [] - } - - let rate_limit_per_user = case create_text.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - let parent_id = case create_text.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create_text.is_nsfw))] - - let default_auto_archive_duration = case - create_text.default_auto_archive_duration - { - Some(duration) -> [ - #( - "default_auto_archive_duration", - json.int(time_duration.to_int_seconds(duration) / 60), - ), - ] - None -> [] - } - - let default_thread_rate_limit_per_user = case - create_text.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - [ - name, - position, - permission_overwrites, - topic, - rate_limit_per_user, - parent_id, - is_nsfw, - default_auto_archive_duration, - default_thread_rate_limit_per_user, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_dm_to_json(create_dm: CreateDm) -> Json { - json.object([#("recipient_id", json.string(create_dm.recipient_id))]) -} - -@internal -pub fn create_voice_to_json(create_voice: CreateVoice) -> Json { - let name = [#("name", json.string(create_voice.name))] - - let position = case create_voice.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_voice.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let bitrate = case create_voice.bitrate { - Some(bitrate) -> [#("bitrate", json.int(bitrate))] - None -> [] - } - - let user_limit = case create_voice.user_limit { - Some(limit) -> [#("user_limit", json.int(limit))] - None -> [] - } - - let rate_limit_per_user = case create_voice.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - let parent_id = case create_voice.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create_voice.is_nsfw))] - - let rtc_region = case create_voice.rtc_region { - Some(region) -> [#("rtc_region", json.string(region))] - None -> [] - } - - let video_quality_mode = case create_voice.video_quality_mode { - Some(mode) -> [#("video_quality_mode", video_quality_mode_to_json(mode))] - None -> [] - } - - [ - name, - position, - permission_overwrites, - bitrate, - user_limit, - rate_limit_per_user, - parent_id, - is_nsfw, - rtc_region, - video_quality_mode, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_category_to_json(create_category: CreateCategory) -> Json { - let name = [#("name", json.string(create_category.name))] - - let position = case create_category.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_category.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - [name, position, permission_overwrites] - |> list.flatten - |> json.object -} - -@internal -pub fn create_announcement_to_json( - create_announcement: CreateAnnouncement, -) -> Json { - let name = [#("name", json.string(create_announcement.name))] - - let position = case create_announcement.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_announcement.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let topic = case create_announcement.topic { - Some(topic) -> [#("topic", json.string(topic))] - None -> [] - } - - let parent_id = case create_announcement.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create_announcement.is_nsfw))] - - let default_auto_archive_duration = case - create_announcement.default_auto_archive_duration - { - Some(duration) -> [ - #( - "default_auto_archive_duration", - time_duration.to_int_seconds(duration) / 60 - |> json.int, - ), - ] - None -> [] - } - - let default_thread_rate_limit_per_user = case - create_announcement.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - [ - name, - position, - permission_overwrites, - topic, - parent_id, - is_nsfw, - default_auto_archive_duration, - default_thread_rate_limit_per_user, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_stage_to_json(create_stage: CreateStage) -> Json { - let name = [#("name", json.string(create_stage.name))] - - let position = case create_stage.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_stage.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let bitrate = case create_stage.bitrate { - Some(bitrate) -> [#("bitrate", json.int(bitrate))] - None -> [] - } - - let user_limit = case create_stage.user_limit { - Some(limit) -> [#("user_limit", json.int(limit))] - None -> [] - } - - let rate_limit_per_user = case create_stage.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - let parent_id = case create_stage.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create_stage.is_nsfw))] - - let rtc_region = case create_stage.rtc_region { - Some(region) -> [#("rtc_region", json.string(region))] - None -> [] - } - - let video_quality_mode = case create_stage.video_quality_mode { - Some(mode) -> [#("video_quality_mode", video_quality_mode_to_json(mode))] - None -> [] - } - - [ - name, - position, - permission_overwrites, - bitrate, - user_limit, - rate_limit_per_user, - parent_id, - is_nsfw, - rtc_region, - video_quality_mode, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_forum_to_json(create_forum: CreateForum) -> Json { - let name = [#("name", json.string(create_forum.name))] - - let position = case create_forum.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_forum.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let topic = case create_forum.topic { - Some(topic) -> [#("topic", json.string(topic))] - None -> [] - } - - let rate_limit_per_user = case create_forum.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - let parent_id = case create_forum.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create_forum.is_nsfw))] - - let default_auto_archive_duration = case - create_forum.default_auto_archive_duration - { - Some(duration) -> [ - #( - "default_auto_archive_duration", - time_duration.to_int_seconds(duration) / 60 - |> json.int, - ), - ] - None -> [] - } - - let default_reaction_emoji = case create_forum.default_reaction_emoji { - Some(emoji) -> [ - #("default_reaction_emoji", forum.default_reaction_encode(emoji)), - ] - None -> [] - } - - let available_tags = case create_forum.available_tags { - Some(tags) -> [#("available_tags", json.array(tags, forum.tag_to_json))] - None -> [] - } - - let default_sort_order = case create_forum.default_sort_order { - Some(order) -> [ - #("default_sort_order", forum.sort_order_type_encode(order)), - ] - None -> [] - } - - let default_forum_layout = case create_forum.default_forum_layout { - Some(layout) -> [#("default_forum_layout", forum.layout_to_json(layout))] - None -> [] - } - - let default_thread_rate_limit_per_user = case - create_forum.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - [ - name, - position, - permission_overwrites, - topic, - rate_limit_per_user, - parent_id, - is_nsfw, - default_auto_archive_duration, - default_reaction_emoji, - available_tags, - default_sort_order, - default_forum_layout, - default_thread_rate_limit_per_user, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_media_to_json(create_media: CreateMedia) -> Json { - let name = [#("name", json.string(create_media.name))] - - let position = case create_media.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - let permission_overwrites = case create_media.permission_overwrites { - Some(overwrites) -> [ - #( - "permission_overwrites", - json.array(overwrites, permission_overwrite.create_to_json), - ), - ] - None -> [] - } - - let topic = case create_media.topic { - Some(topic) -> [#("topic", json.string(topic))] - None -> [] - } - - let rate_limit_per_user = case create_media.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - let parent_id = case create_media.parent_id { - Some(id) -> [#("parent_id", json.string(id))] - None -> [] - } - - let default_auto_archive_duration = case - create_media.default_auto_archive_duration - { - Some(duration) -> [ - #( - "default_auto_archive_duration", - time_duration.to_int_seconds(duration) / 60 - |> json.int, - ), - ] - None -> [] - } - - let default_reaction_emoji = case create_media.default_reaction_emoji { - Some(emoji) -> [ - #("default_reaction_emoji", forum.default_reaction_encode(emoji)), - ] - None -> [] - } - - let available_tags = case create_media.available_tags { - Some(tags) -> [#("available_tags", json.array(tags, forum.tag_to_json))] - None -> [] - } - - let default_sort_order = case create_media.default_sort_order { - Some(order) -> [ - #("default_sort_order", forum.sort_order_type_encode(order)), - ] - None -> [] - } - - let default_thread_rate_limit_per_user = case - create_media.default_thread_rate_limit_per_user - { - Some(limit) -> [ - #( - "default_thread_rate_limit_per_user", - time_duration.to_int_seconds_json(limit), - ), - ] - None -> [] - } - - [ - name, - position, - permission_overwrites, - topic, - rate_limit_per_user, - parent_id, - default_auto_archive_duration, - default_reaction_emoji, - available_tags, - default_sort_order, - default_thread_rate_limit_per_user, - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - id channel_id: String, -) -> Result(Channel, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/channels/" <> channel_id) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - id channel_id: String, - with modify: Modify, - because reason: Option(String), -) -> Result(Channel, grom.Error) { - let json = modify |> modify_to_json - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/channels/" <> channel_id) - |> rest.with_reason(reason) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_text() -> Modify { - ModifyText(None, False, Skip, Skip, None, Skip, Skip, Skip, Skip, None) -} - -pub fn new_modify_voice() -> Modify { - ModifyVoice(None, Skip, None, Skip, Skip, Skip, Skip, Skip, Skip, Skip) -} - -pub fn new_modify_category() -> Modify { - ModifyCategory(None, Skip, Skip) -} - -pub fn new_modify_announcement() -> Modify { - ModifyAnnouncement(None, False, Skip, Skip, None, Skip, Skip, Skip) -} - -pub fn new_modify_thread() -> Modify { - ModifyThread(None, None, None, None, None, Skip, None, None) -} - -pub fn new_modify_stage() -> Modify { - ModifyStage(None, Skip, None, Skip, Skip, Skip, Skip, Skip, Skip, Skip) -} - -pub fn new_modify_forum() -> Modify { - ModifyForum( - None, - Skip, - Skip, - None, - Skip, - Skip, - Skip, - Skip, - None, - None, - Skip, - None, - Skip, - None, - ) -} - -pub fn new_modify_media() -> Modify { - ModifyMedia( - None, - Skip, - Skip, - None, - Skip, - Skip, - Skip, - Skip, - None, - None, - Skip, - None, - Skip, - ) -} - -pub fn delete( - client: grom.Client, - id channel_id: String, - because reason: Option(String), -) -> Result(Channel, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Delete, "/channels/" <> channel_id) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn announcement_follow( - client: grom.Client, - from channel_id: String, - to webhook_channel_id: String, - because reason: Option(String), -) -> Result(FollowedChannel, grom.Error) { - let json = - json.object([#("webhook_channel_id", json.string(webhook_channel_id))]) - - use response <- result.try( - client - |> rest.new_request(http.Post, "/channels/" <> channel_id <> "/followers") - |> rest.with_reason(reason) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: followed_channel_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn trigger_typing_indicator( - client: grom.Client, - in channel_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request(http.Post, "/channels/" <> channel_id <> "/typing") - |> rest.execute, - ) - - Ok(Nil) -} - -// TODO: add pagination stuff!! -pub fn get_pinned_messages( - client: grom.Client, - in channel_id: String, -) -> Result(List(Message), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> channel_id <> "/messages/pins", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(message.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_public_archived_threads( - client: grom.Client, - in channel_id: String, - earlier_than before: Option(Timestamp), - maximum limit: Option(Int), -) -> Result(ReceivedThreads, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> channel_id <> "/threads/archived/public", - ) - |> request.set_query( - [ - case before { - Some(timestamp) -> [ - #("before", timestamp.to_rfc3339(timestamp, duration.seconds(0))), - ] - None -> [] - }, - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - ] - |> list.flatten, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: received_threads_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_private_archived_threads( - client: grom.Client, - in channel_id: String, - earlier_than before: Option(Timestamp), - maximum limit: Option(Int), -) -> Result(ReceivedThreads, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> channel_id <> "/threads/archived/private", - ) - |> request.set_query( - [ - case before { - Some(timestamp) -> [ - #("before", timestamp.to_rfc3339(timestamp, duration.seconds(0))), - ] - None -> [] - }, - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - ] - |> list.flatten, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: received_threads_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_joined_private_archived_threads( - client: grom.Client, - in channel_id: String, - earlier_than before: Option(Timestamp), - maximum limit: Option(Int), -) -> Result(ReceivedThreads, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> channel_id <> "/users/@me/threads/archived/private", - ) - |> request.set_query( - [ - case before { - Some(timestamp) -> [ - #("before", timestamp.to_rfc3339(timestamp, duration.seconds(0))), - ] - None -> [] - }, - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - ] - |> list.flatten, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: received_threads_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_all_from_guild( - client: grom.Client, - id guild_id: String, -) -> Result(List(Channel), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/channels") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -/// `because` has no effect if creating a DM channel. -pub fn create( - client: grom.Client, - using create: Create, - because reason: Option(String), -) -> Result(Channel, grom.Error) { - let endpoint = case create { - CreateDmChannel(_) -> "/users/@me/channels" - CreateTextChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - CreateVoiceChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - CreateCategoryChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - CreateAnnouncementChannel(inner) -> - "/guilds/" <> inner.guild_id <> "/channels" - CreateStageChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - CreateForumChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - CreateMediaChannel(inner) -> "/guilds/" <> inner.guild_id <> "/channels" - } - - let json = - create - |> create_to_json - |> json.to_string - - let reason = case create { - CreateDmChannel(_) -> None - _ -> reason - } - - use response <- result.try( - client - |> rest.new_request(http.Post, endpoint) - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create_text(named name: String, in guild_id: String) -> CreateText { - CreateText(guild_id, name, None, None, None, None, None, False, None, None) -} - -pub fn new_create_dm(to recipient_id: String) -> CreateDm { - CreateDm(recipient_id:) -} - -pub fn new_create_voice(named name: String, in guild_id: String) -> CreateVoice { - CreateVoice( - guild_id, - name, - None, - None, - None, - None, - None, - None, - False, - None, - None, - ) -} - -pub fn new_create_category( - named name: String, - in guild_id: String, -) -> CreateCategory { - CreateCategory(guild_id, name, None, None) -} - -pub fn new_create_announcement( - named name: String, - in guild_id: String, -) -> CreateAnnouncement { - CreateAnnouncement(guild_id, name, None, None, None, None, False, None, None) -} - -pub fn new_create_stage(named name: String, in guild_id: String) -> CreateStage { - CreateStage( - guild_id, - name, - None, - None, - None, - None, - None, - None, - False, - None, - None, - ) -} - -pub fn new_create_forum(named name: String, in guild_id: String) -> CreateForum { - CreateForum( - guild_id, - name, - None, - None, - None, - None, - None, - False, - None, - None, - None, - None, - None, - None, - ) -} - -pub fn new_create_media(named name: String, in guild_id: String) -> CreateMedia { - CreateMedia( - guild_id, - name, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) -} - -/// `maximum` defaults to 50 if not provided. -pub fn get_messages( - client: grom.Client, - in channel_id: String, - time query: Option(GetMessagesQuery), - maximum limit: Option(Int), -) -> Result(List(Message), grom.Error) { - let query = - case query { - Some(AroundId(id)) -> [#("around", id)] - Some(BeforeId(id)) -> [#("before", id)] - Some(AfterId(id)) -> [#("after", id)] - None -> [] - } - |> list.append(case limit { - Some(limit) -> [#("limit", int.to_string(int.clamp(limit, 1, 100)))] - None -> [] - }) - - use response <- result.try( - client - |> rest.new_request(http.Get, "/channels/" <> channel_id <> "/messages") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: message.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn bulk_delete_messages( - client: grom.Client, - in channel_id: String, - ids message_ids: List(String), - because reason: Option(String), -) -> Result(Nil, grom.Error) { - let json = - json.object([#("messages", json.array(message_ids, json.string))]) - |> json.to_string - - client - |> rest.new_request( - http.Post, - "/channels/" <> channel_id <> "/messages/bulk-delete", - ) - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute - |> result.replace(Nil) -} diff --git a/src/grom/channel/forum.gleam b/src/grom/channel/forum.gleam deleted file mode 100644 index baff8de..0000000 --- a/src/grom/channel/forum.gleam +++ /dev/null @@ -1,131 +0,0 @@ -import gleam/dynamic/decode -import gleam/int -import gleam/json.{type Json} -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type Tag { - Tag( - id: String, - name: String, - is_moderated: Bool, - emoji_id: Option(String), - emoji_name: Option(String), - ) -} - -pub type Flag { - RequiresTag -} - -pub type DefaultReaction { - DefaultReaction(emoji_id: Option(String), emoji_name: Option(String)) -} - -pub type SortOrder { - SortByLatestActivity - SortByCreationDate -} - -pub type Layout { - LayoutNotSet - ListLayout - GalleryLayout -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [#(int.bitwise_shift_left(1, 4), RequiresTag)] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn default_reaction_decoder() -> decode.Decoder(DefaultReaction) { - use emoji_id <- decode.field("emoji_id", decode.optional(decode.string)) - use emoji_name <- decode.field("emoji_name", decode.optional(decode.string)) - decode.success(DefaultReaction(emoji_id:, emoji_name:)) -} - -@internal -pub fn sort_order_type_decoder() -> decode.Decoder(SortOrder) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(SortByLatestActivity) - 1 -> decode.success(SortByCreationDate) - _ -> decode.failure(SortByLatestActivity, "SortOrderType") - } -} - -@internal -pub fn layout_type_decoder() -> decode.Decoder(Layout) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(LayoutNotSet) - 1 -> decode.success(ListLayout) - 2 -> decode.success(GalleryLayout) - _ -> decode.failure(LayoutNotSet, "LayoutType") - } -} - -@internal -pub fn tag_decoder() -> decode.Decoder(Tag) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use is_moderated <- decode.field("moderated", decode.bool) - use emoji_id <- decode.field("emoji_id", decode.optional(decode.string)) - use emoji_name <- decode.field("emoji_name", decode.optional(decode.string)) - decode.success(Tag(id:, name:, is_moderated:, emoji_id:, emoji_name:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn default_reaction_encode(default_reaction: DefaultReaction) -> Json { - let DefaultReaction(emoji_id:, emoji_name:) = default_reaction - json.object([ - #("emoji_id", case emoji_id { - None -> json.null() - Some(value) -> json.string(value) - }), - #("emoji_name", case emoji_name { - None -> json.null() - Some(value) -> json.string(value) - }), - ]) -} - -@internal -pub fn sort_order_type_encode(sort_order_type: SortOrder) -> Json { - case sort_order_type { - SortByLatestActivity -> 0 - SortByCreationDate -> 1 - } - |> json.int -} - -@internal -pub fn layout_to_json(layout_type: Layout) -> Json { - case layout_type { - LayoutNotSet -> 0 - ListLayout -> 1 - GalleryLayout -> 2 - } - |> json.int -} - -@internal -pub fn tag_to_json(tag: Tag) -> Json { - let id = #("id", json.string(tag.id)) - let name = #("name", json.string(tag.name)) - let is_moderated = #("moderated", json.bool(tag.is_moderated)) - let emoji_id = #("emoji_id", json.nullable(tag.emoji_id, json.string)) - let emoji_name = #("emoji_name", json.nullable(tag.emoji_name, json.string)) - - [id, name, is_moderated, emoji_id, emoji_name] - |> json.object -} -// PUBLIC API FUNCTIONS -------------------------------------------------------- diff --git a/src/grom/channel/media.gleam b/src/grom/channel/media.gleam deleted file mode 100644 index c41c2cb..0000000 --- a/src/grom/channel/media.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/int - -// TYPES ----------------------------------------------------------------------- - -pub type Flag { - RequiresTag - HideMediaDownloadOptions -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 4), RequiresTag), - #(int.bitwise_shift_left(1, 15), HideMediaDownloadOptions), - ] -} diff --git a/src/grom/channel/permission_overwrite.gleam b/src/grom/channel/permission_overwrite.gleam deleted file mode 100644 index 15aaf4e..0000000 --- a/src/grom/channel/permission_overwrite.gleam +++ /dev/null @@ -1,122 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/option.{type Option, None} -import gleam/result -import grom -import grom/internal/rest -import grom/permission.{type Permission} - -// TYPES ----------------------------------------------------------------------- - -pub type PermissionOverwrite { - PermissionOverwrite( - id: String, - type_: Type, - allow: List(Permission), - deny: List(Permission), - ) -} - -pub type Create { - Create( - type_: Type, - allow: Option(List(Permission)), - deny: Option(List(Permission)), - ) -} - -pub type Type { - ForRole - ForMember -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(PermissionOverwrite) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", type_decoder()) - use allow <- decode.field("allow", permission.decoder()) - use deny <- decode.field("deny", permission.decoder()) - decode.success(PermissionOverwrite(id:, type_:, allow:, deny:)) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(ForRole) - 1 -> decode.success(ForMember) - _ -> decode.failure(ForRole, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - json.object([ - #("type", type_to_json(create.type_)), - #("allow", json.nullable(create.allow, permission.to_json)), - #("deny", json.nullable(create.deny, permission.to_json)), - ]) -} - -@internal -pub fn type_to_json(type_: Type) -> Json { - case type_ { - ForRole -> 0 - ForMember -> 1 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn modify( - client: grom.Client, - for channel_id: String, - id overwrite_id: String, - new overwrite: Create, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - let json = overwrite |> create_to_json - - use _response <- result.try( - client - |> rest.new_request( - http.Put, - "/channels/" <> channel_id <> "/permissions/" <> overwrite_id, - ) - |> rest.with_reason(reason) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn delete( - client: grom.Client, - for channel_id: String, - id overwrite_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/channels/" <> channel_id <> "/permissions/" <> overwrite_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn new_create(type_: Type) -> Create { - Create(type_, None, None) -} diff --git a/src/grom/channel/thread.gleam b/src/grom/channel/thread.gleam deleted file mode 100644 index 6aa0a1d..0000000 --- a/src/grom/channel/thread.gleam +++ /dev/null @@ -1,588 +0,0 @@ -import gleam/bool -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 gleam/time/timestamp.{type Timestamp} -import grom -import grom/guild_member.{type GuildMember} -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/modification.{type Modification, Skip} -import grom/permission.{type Permission} - -// TYPES ----------------------------------------------------------------------- - -pub type Thread { - Thread( - id: String, - type_: Type, - guild_id: Option(String), - name: String, - last_message_id: Option(String), - rate_limit_per_user: Duration, - parent_id: String, - message_count: Int, - member_count: Int, - metadata: Option(Metadata), - current_member: Option(Member), - current_user_permissions: Option(List(Permission)), - flags: List(Flag), - total_message_sent: Int, - applied_tags_ids: Option(List(String)), - ) -} - -pub type Modify { - Modify( - name: Option(String), - is_archived: Option(Bool), - auto_archive_duration: Option(Duration), - is_locked: Option(Bool), - is_invitable: Option(Bool), - rate_limit_per_user: Modification(Duration), - flags: Modification(List(Flag)), - applied_tags_ids: Modification(List(String)), - ) -} - -pub type StartFromMessage { - StartFromMessage( - name: String, - auto_archive_duration: Option(Duration), - rate_limit_per_user: Option(Duration), - ) -} - -pub type StartWithoutMessage { - StartWithoutMessage( - name: String, - auto_archive_duration: Option(Duration), - type_: Option(Type), - is_invitable: Option(Bool), - rate_limit_per_user: Option(Duration), - ) -} - -pub type Metadata { - Metadata( - is_archived: Bool, - auto_archive_duration: Int, - archive_timestamp: Timestamp, - is_locked: Bool, - is_invitable: Option(Bool), - create_timestamp: Option(Timestamp), - ) -} - -pub type Member { - Member( - thread_id: Option(String), - user_id: Option(String), - join_timestamp: Timestamp, - guild_member: Option(GuildMember), - ) -} - -pub type Flag { - Pinned -} - -pub type Type { - Announcement - Public - Private -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [#(int.bitwise_shift_left(1, 1), Pinned)] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Thread) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", type_decoder()) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use name <- decode.field("name", decode.string) - use last_message_id <- decode.field( - "last_message_id", - decode.optional(decode.string), - ) - use rate_limit_per_user <- decode.field( - "rate_limit_per_user", - time_duration.from_minutes_decoder(), - ) - use parent_id <- decode.field("parent_id", decode.string) - use message_count <- decode.field("message_count", decode.int) - use member_count <- decode.field("member_count", decode.int) - use metadata <- decode.optional_field( - "metadata", - None, - decode.optional(metadata_decoder()), - ) - use current_member <- decode.optional_field( - "current_member", - None, - decode.optional(member_decoder()), - ) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(permission.decoder()), - ) - use flags <- decode.field("flags", flags.decoder(bits_flags())) - use total_message_sent <- decode.field("total_message_sent", decode.int) - use applied_tags_ids <- decode.optional_field( - "applied_tags_ids", - None, - decode.optional(decode.list(decode.string)), - ) - decode.success(Thread( - id:, - type_:, - guild_id:, - name:, - last_message_id:, - rate_limit_per_user:, - parent_id:, - message_count:, - member_count:, - metadata:, - current_member:, - current_user_permissions:, - flags:, - total_message_sent:, - applied_tags_ids:, - )) -} - -@internal -pub fn metadata_decoder() -> decode.Decoder(Metadata) { - use is_archived <- decode.field("archived", decode.bool) - use auto_archive_duration <- decode.field("auto_archive_duration", decode.int) - use archive_timestamp <- decode.field( - "archive_timestamp", - time_rfc3339.decoder(), - ) - use is_locked <- decode.field("locked", decode.bool) - use is_invitable <- decode.optional_field( - "invitable", - None, - decode.optional(decode.bool), - ) - use create_timestamp <- decode.optional_field( - "create_timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - decode.success(Metadata( - is_archived:, - auto_archive_duration:, - archive_timestamp:, - is_locked:, - is_invitable:, - create_timestamp:, - )) -} - -@internal -pub fn member_decoder() -> decode.Decoder(Member) { - use thread_id <- decode.optional_field( - "id", - None, - decode.optional(decode.string), - ) - use user_id <- decode.optional_field( - "user_id", - None, - decode.optional(decode.string), - ) - use join_timestamp <- decode.field("join_timestamp", time_rfc3339.decoder()) - use guild_member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - decode.success(Member(thread_id:, user_id:, join_timestamp:, guild_member:)) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 10 -> decode.success(Announcement) - 11 -> decode.success(Public) - 12 -> decode.success(Private) - _ -> decode.failure(Announcement, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn type_to_json(type_: Type) -> Json { - case type_ { - Announcement -> 10 - Public -> 11 - Private -> 12 - } - |> json.int -} - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let is_archived = case modify.is_archived { - Some(archived) -> [#("archived", json.bool(archived))] - None -> [] - } - - let auto_archive_duration = case modify.auto_archive_duration { - Some(duration) -> [ - #("auto_archive_duration", time_duration.to_int_seconds_json(duration)), - ] - None -> [] - } - - let is_locked = case modify.is_locked { - Some(locked) -> [#("locked", json.bool(locked))] - None -> [] - } - - let is_invitable = case modify.is_invitable { - Some(invitable) -> [#("invitable", json.bool(invitable))] - None -> [] - } - - let rate_limit_per_user = - modify.rate_limit_per_user - |> modification.to_json( - "rate_limit_per_user", - time_duration.to_int_seconds_json, - ) - - let flags = - modify.flags - |> modification.to_json("flags", fn(flags) { - flags.to_json(flags, bits_flags()) - }) - - let applied_tags_ids = - modify.applied_tags_ids - |> modification.to_json("applied_tags", fn(tags) { - json.array(tags, json.string) - }) - - [ - name, - is_archived, - auto_archive_duration, - is_locked, - is_invitable, - rate_limit_per_user, - flags, - applied_tags_ids, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn start_from_message_to_json(start_from_message: StartFromMessage) -> Json { - let name = [#("name", json.string(start_from_message.name))] - - let auto_archive_duration = case start_from_message.auto_archive_duration { - Some(duration) -> [ - #("auto_archive_duration", time_duration.to_int_seconds_json(duration)), - ] - None -> [] - } - - let rate_limit_per_user = case start_from_message.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - [name, auto_archive_duration, rate_limit_per_user] - |> list.flatten - |> json.object -} - -@internal -pub fn start_without_message_to_json( - start_without_message: StartWithoutMessage, -) -> Json { - let name = [#("name", json.string(start_without_message.name))] - - let auto_archive_duration = case start_without_message.auto_archive_duration { - Some(duration) -> [ - #("auto_archive_duration", time_duration.to_int_seconds_json(duration)), - ] - None -> [] - } - - let type_ = case start_without_message.type_ { - Some(type_) -> [#("type", type_to_json(type_))] - None -> [] - } - - let is_invitable = case start_without_message.is_invitable { - Some(invitable) -> [#("invitable", json.bool(invitable))] - None -> [] - } - - let rate_limit_per_user = case start_without_message.rate_limit_per_user { - Some(limit) -> [ - #("rate_limit_per_user", time_duration.to_int_seconds_json(limit)), - ] - None -> [] - } - - [name, auto_archive_duration, type_, is_invitable, rate_limit_per_user] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn modify( - client: grom.Client, - id channel_id: String, - with modify: Modify, - because reason: Option(String), -) -> Result(Thread, grom.Error) { - let json = modify |> modify_to_json - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/channels/" <> channel_id) - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify( - name: None, - is_archived: None, - auto_archive_duration: None, - is_locked: None, - is_invitable: None, - rate_limit_per_user: Skip, - flags: Skip, - applied_tags_ids: Skip, - ) -} - -pub fn start_from_message( - client: grom.Client, - in channel_id: String, - from message_id: String, - with start_from_message: StartFromMessage, - because reason: Option(String), -) -> Result(Thread, grom.Error) { - let json = start_from_message |> start_from_message_to_json - - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/channels/" <> channel_id <> "/messages/" <> message_id <> "/threads", - ) - |> rest.with_reason(reason) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_start_from_message(name: String) -> StartFromMessage { - StartFromMessage( - name:, - auto_archive_duration: None, - rate_limit_per_user: None, - ) -} - -pub fn start_without_message( - client: grom.Client, - in channel_id: String, - with start_without_message: StartWithoutMessage, - reason reason: Option(String), -) -> Result(Thread, grom.Error) { - let json = start_without_message |> start_without_message_to_json - - use response <- result.try( - client - |> rest.new_request(http.Post, "/channels/" <> channel_id <> "/threads") - |> rest.with_reason(reason) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_start_without_message(name: String) -> StartWithoutMessage { - StartWithoutMessage( - name:, - auto_archive_duration: None, - type_: None, - is_invitable: None, - rate_limit_per_user: None, - ) -} - -pub fn join( - client: grom.Client, - id thread_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Put, - "/channels/" <> thread_id <> "/thread-members/@me", - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn add_member( - client: grom.Client, - to thread_id: String, - id user_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Put, - "/channels/" <> thread_id <> "/thread-members/" <> user_id, - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn leave( - client: grom.Client, - id thread_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/channels/" <> thread_id <> "/thread-members/@me", - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn remove_member( - client: grom.Client, - from thread_id: String, - id user_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/channels/" <> thread_id <> "/thread-members/" <> user_id, - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn get_member( - client: grom.Client, - from thread_id: String, - id user_id: String, - with_guild_member with_guild_member: Bool, -) -> Result(Member, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> thread_id <> "/thread-members/" <> user_id, - ) - |> request.set_query([#("with_member", bool.to_string(with_guild_member))]) - |> rest.execute, - ) - - response.body - |> json.parse(using: member_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_members( - client: grom.Client, - from thread_id: String, - with_guild_member with_guild_member: Bool, - older_than_id after: Option(String), - maximum limit: Option(Int), -) -> Result(List(Member), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> thread_id <> "/thread-members", - ) - |> request.set_query( - [ - [#("with_member", bool.to_string(with_guild_member))], - case after { - Some(id) -> [#("after", id)] - None -> [] - }, - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - ] - |> list.flatten, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(member_decoder())) - |> result.map_error(grom.CouldNotDecode) -} diff --git a/src/grom/command.gleam b/src/grom/command.gleam deleted file mode 100644 index 4f24965..0000000 --- a/src/grom/command.gleam +++ /dev/null @@ -1,2726 +0,0 @@ -//// This module defines commands. Commands are the things you create and make available to your users. -//// This module will not help you with *receiving* slash commands, only with *defining* them and registering with Discord. -//// Use the `grom/interaction` and `grom/gateway` modules to receive interactions. - -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/function -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/application -import grom/internal/rest -import grom/modification.{type Modification, Skip} -import grom/permission.{type Permission as UserPermission} - -// TYPES ----------------------------------------------------------------------- - -pub type Command { - Slash(SlashCommand) - User(UserCommand) - Message(MessageCommand) -} - -pub type SlashCommand { - SlashCommand( - id: String, - application_id: String, - guild_id: Option(String), - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - parameters: Option(List(Parameter)), - /// None -> All members can use it. - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - /// None -> Your application's installation contexts for the scope in which the command is ran. - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - ) -} - -pub type UserCommand { - UserCommand( - id: String, - application_id: String, - guild_id: Option(String), - name: String, - name_localizations: Option(Dict(String, String)), - /// None -> All members can use it. - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - /// None -> Your application's installation contexts for the scope in which the command is ran. - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - ) -} - -pub type MessageCommand { - MessageCommand( - id: String, - application_id: String, - guild_id: Option(String), - name: String, - name_localizations: Option(Dict(String, String)), - /// None -> All members can use it. - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - /// None -> Your application's installation contexts for the scope in which the command is ran. - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - ) -} - -pub type Context { - AllowedInGuilds - AllowedInBotDms - AllowedInPrivateChannels -} - -pub type Parameter { - SubCommandParameter(ParameterSubCommand) - SubCommandGroupParameter(ParameterSubCommandGroup) - TextParameter(ParameterText) - IntegerParameter(ParameterInteger) - BooleanParameter(ParameterBoolean) - UserParameter(ParameterUser) - ChannelParameter(ParameterChannel) - RoleParameter(ParameterRole) - MentionableParameter(ParameterMentionable) - NumberParameter(ParameterNumber) - AttachmentParameter(ParameterAttachment) -} - -pub type ParameterSubCommand { - ParameterSubCommand( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - parameters: Option(List(Parameter)), - ) -} - -pub type ParameterSubCommandGroup { - ParameterSubCommandGroup( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - parameters: Option(List(Parameter)), - ) -} - -pub type ParameterText { - ParameterText( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - choices: Option(List(TextChoice)), - min_length: Option(Int), - max_length: Option(Int), - is_autocomplete: Bool, - ) -} - -pub type TextChoice { - TextChoice( - name: String, - name_localizations: Option(Dict(String, String)), - value: String, - ) -} - -pub type ParameterInteger { - ParameterInteger( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - choices: Option(List(IntegerChoice)), - min_value: Option(Int), - max_value: Option(Int), - is_autocomplete: Bool, - ) -} - -pub type IntegerChoice { - IntegerChoice( - name: String, - name_localizations: Option(Dict(String, String)), - value: Int, - ) -} - -pub type ParameterBoolean { - ParameterBoolean( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - ) -} - -pub type ParameterUser { - ParameterUser( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - ) -} - -pub type ParameterChannel { - ParameterChannel( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - /// Defaults to every channel type. - allowed_channel_types: Option(List(AllowedChannelType)), - ) -} - -pub type AllowedChannelType { - AllowTextChannels - AllowDmChannels - AllowVoiceChannels - AllowCategoryChannels - AllowAnnouncementChannels - AllowAnnouncementThreads - AllowPublicThreads - AllowPrivateThreads - AllowStageChannels - AllowForumChannels - AllowMediaChannels -} - -pub type ParameterRole { - ParameterRole( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - ) -} - -pub type ParameterMentionable { - ParameterMentionable( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - ) -} - -pub type ParameterNumber { - ParameterNumber( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - choices: Option(List(NumberChoice)), - min_value: Option(Float), - max_value: Option(Float), - is_autocomplete: Bool, - ) -} - -pub type NumberChoice { - NumberChoice( - name: String, - name_localizations: Option(Dict(String, String)), - value: Float, - ) -} - -pub type ParameterAttachment { - ParameterAttachment( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - is_required: Bool, - ) -} - -pub type CreateGlobal { - CreateGlobalSlash(CreateGlobalSlashCommand) - CreateGlobalUser(CreateGlobalUserCommand) - CreateGlobalMessage(CreateGlobalMessageCommand) -} - -pub type CreateGlobalSlashCommand { - CreateGlobalSlashCommand( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - parameters: Option(List(Parameter)), - default_member_permissions: Option(List(UserPermission)), - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - is_nsfw: Bool, - ) -} - -pub type CreateGlobalUserCommand { - CreateGlobalUserCommand( - name: String, - name_localizations: Option(Dict(String, String)), - default_member_permissions: Option(List(UserPermission)), - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - is_nsfw: Bool, - ) -} - -pub type CreateGlobalMessageCommand { - CreateGlobalMessageCommand( - name: String, - name_localizations: Option(Dict(String, String)), - default_member_permissions: Option(List(UserPermission)), - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - is_nsfw: Bool, - ) -} - -pub type CreateForGuild { - CreateSlashForGuild(CreateSlashCommandForGuild) - CreateUserForGuild(CreateUserCommandForGuild) - CreateMessageForGuild(CreateMessageCommandForGuild) -} - -pub type CreateSlashCommandForGuild { - CreateSlashCommandForGuild( - name: String, - name_localizations: Option(Dict(String, String)), - description: String, - description_localizations: Option(Dict(String, String)), - parameters: Option(List(Parameter)), - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - ) -} - -pub type CreateUserCommandForGuild { - CreateUserCommandForGuild( - name: String, - name_localizations: Option(Dict(String, String)), - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - ) -} - -pub type CreateMessageCommandForGuild { - CreateMessageCommandForGuild( - name: String, - name_localizations: Option(Dict(String, String)), - default_member_permissions: Option(List(UserPermission)), - is_nsfw: Bool, - ) -} - -pub type ModifyForGuild { - ModifySlashForGuild(ModifySlashCommandForGuild) - ModifyUserForGuild(ModifyUserCommandForGuild) - ModifyMessageForGuild(ModifyMessageCommandForGuild) -} - -pub type ModifySlashCommandForGuild { - ModifySlashCommandForGuild( - name: Option(String), - name_localizations: Modification(Dict(String, String)), - description: Option(String), - description_localizations: Modification(Dict(String, String)), - parameters: Option(List(Parameter)), - default_member_permissions: Modification(List(UserPermission)), - is_nsfw: Option(Bool), - ) -} - -pub type ModifyUserCommandForGuild { - ModifyUserCommandForGuild( - name: Option(String), - name_localizations: Modification(Dict(String, String)), - default_member_permissions: Modification(List(UserPermission)), - is_nsfw: Option(Bool), - ) -} - -pub type ModifyMessageCommandForGuild { - ModifyMessageCommandForGuild( - name: Option(String), - name_localizations: Modification(Dict(String, String)), - default_member_permissions: Modification(List(UserPermission)), - is_nsfw: Option(Bool), - ) -} - -pub type ModifyGlobal { - ModifyGlobal( - name: Option(String), - name_localizations: Modification(Dict(String, String)), - description: Option(String), - description_localizations: Modification(Dict(String, String)), - /// Please don't use on anything other than slash commands. - /// I will find you and do unspeakable violence. - parameters: Option(List(Parameter)), - default_member_permissions: Modification(List(UserPermission)), - integration_types: Option(List(application.InstallationContext)), - contexts: Option(List(Context)), - is_nsfw: Option(Bool), - ) -} - -pub type GuildPermissions { - GuildPermissions( - command_id: String, - application_id: String, - guild_id: String, - permissions: List(Permission), - ) -} - -/// In this case, permission means whether a user is allowed to use a command. -/// A denied role permission means that all users with this role are not allowed to use the command. -pub type Permission { - RolePermission(role_id: String, is_permission_granted: Bool) - /// If `user_id == guild_id`, `@everyone` gets the permission status (granted/denied). - UserPermission(user_id: String, is_permission_granted: Bool) - /// If `channel_id == { guild_id - 1 }`, all channels get the permission status (granted/denied). - ChannelPermission(channel_id: String, is_permission_granted: Bool) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Command) { - use type_ <- decode.optional_field("type", 1, decode.int) - case type_ { - 1 -> decode.map(slash_command_decoder(), Slash) - _ -> - decode.failure( - Slash(SlashCommand( - "", - "", - None, - "", - None, - "", - None, - None, - None, - False, - None, - None, - )), - "Command", - ) - } -} - -@internal -pub fn slash_command_decoder() -> decode.Decoder(SlashCommand) { - use id <- decode.field("id", decode.string) - use application_id <- decode.field("application_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use parameters <- decode.optional_field( - "options", - None, - decode.optional(decode.list(of: parameter_decoder())), - ) - use default_member_permissions <- decode.optional_field( - "default_member_permissions", - None, - decode.optional(permission.decoder()), - ) - use is_nsfw <- decode.optional_field("nsfw", False, decode.bool) - use integration_types <- decode.optional_field( - "integration_types", - None, - decode.optional( - decode.list(of: application.installation_context_int_decoder()), - ), - ) - use contexts <- decode.optional_field( - "contexts", - None, - decode.optional(decode.list(of: context_decoder())), - ) - - decode.success(SlashCommand( - id:, - application_id:, - guild_id:, - name:, - name_localizations:, - description:, - description_localizations:, - parameters:, - default_member_permissions:, - is_nsfw:, - integration_types:, - contexts:, - )) -} - -@internal -pub fn user_command_decoder() -> decode.Decoder(UserCommand) { - use id <- decode.field("id", decode.string) - use application_id <- decode.field("application_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use default_member_permissions <- decode.optional_field( - "default_member_permissions", - None, - decode.optional(permission.decoder()), - ) - use is_nsfw <- decode.optional_field("nsfw", False, decode.bool) - use integration_types <- decode.optional_field( - "integration_types", - None, - decode.optional( - decode.list(of: application.installation_context_int_decoder()), - ), - ) - use contexts <- decode.optional_field( - "contexts", - None, - decode.optional(decode.list(of: context_decoder())), - ) - - decode.success(UserCommand( - id:, - application_id:, - guild_id:, - name:, - name_localizations:, - default_member_permissions:, - is_nsfw:, - integration_types:, - contexts:, - )) -} - -@internal -pub fn context_decoder() -> decode.Decoder(Context) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(AllowedInGuilds) - 1 -> decode.success(AllowedInBotDms) - 2 -> decode.success(AllowedInPrivateChannels) - _ -> decode.failure(AllowedInGuilds, "Context") - } -} - -@internal -pub fn parameter_decoder() -> decode.Decoder(Parameter) { - use type_ <- decode.field("type", decode.int) - case type_ { - 1 -> decode.map(parameter_sub_command_decoder(), SubCommandParameter) - 2 -> - decode.map( - parameter_sub_command_group_decoder(), - SubCommandGroupParameter, - ) - 3 -> decode.map(parameter_text_decoder(), TextParameter) - 4 -> decode.map(parameter_integer_decoder(), IntegerParameter) - 5 -> decode.map(parameter_boolean_decoder(), BooleanParameter) - 6 -> decode.map(parameter_user_decoder(), UserParameter) - 7 -> decode.map(parameter_channel_decoder(), ChannelParameter) - 8 -> decode.map(parameter_role_decoder(), RoleParameter) - 9 -> decode.map(parameter_mentionable_decoder(), MentionableParameter) - 10 -> decode.map(parameter_number_decoder(), NumberParameter) - 11 -> decode.map(parameter_attachment_decoder(), AttachmentParameter) - _ -> - decode.failure( - SubCommandParameter(ParameterSubCommand("", None, "", None, None)), - "Parameter", - ) - } -} - -@internal -pub fn parameter_sub_command_decoder() -> decode.Decoder(ParameterSubCommand) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use parameters <- decode.optional_field( - "options", - None, - decode.optional(decode.list(of: parameter_decoder())), - ) - - decode.success(ParameterSubCommand( - name:, - name_localizations:, - description:, - description_localizations:, - parameters:, - )) -} - -@internal -pub fn parameter_sub_command_group_decoder() -> decode.Decoder( - ParameterSubCommandGroup, -) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use parameters <- decode.optional_field( - "options", - None, - decode.optional(decode.list(of: parameter_decoder())), - ) - - decode.success(ParameterSubCommandGroup( - name:, - name_localizations:, - description:, - description_localizations:, - parameters:, - )) -} - -@internal -pub fn parameter_text_decoder() -> decode.Decoder(ParameterText) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - use choices <- decode.optional_field( - "choices", - None, - decode.optional(decode.list(of: text_choice_decoder())), - ) - 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 is_autocomplete <- decode.optional_field( - "autocomplete", - False, - decode.bool, - ) - - decode.success(ParameterText( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - choices:, - min_length:, - max_length:, - is_autocomplete:, - )) -} - -@internal -pub fn text_choice_decoder() -> decode.Decoder(TextChoice) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use value <- decode.field("value", decode.string) - - decode.success(TextChoice(name:, name_localizations:, value:)) -} - -@internal -pub fn parameter_integer_decoder() -> decode.Decoder(ParameterInteger) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - use choices <- decode.optional_field( - "choices", - None, - decode.optional(decode.list(of: integer_choice_decoder())), - ) - use min_value <- decode.optional_field( - "min_value", - None, - decode.optional(decode.int), - ) - use max_value <- decode.optional_field( - "max_value", - None, - decode.optional(decode.int), - ) - use is_autocomplete <- decode.optional_field( - "autocomplete", - False, - decode.bool, - ) - - decode.success(ParameterInteger( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - choices:, - min_value:, - max_value:, - is_autocomplete:, - )) -} - -@internal -pub fn integer_choice_decoder() -> decode.Decoder(IntegerChoice) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use value <- decode.field("value", decode.int) - - decode.success(IntegerChoice(name:, name_localizations:, value:)) -} - -@internal -pub fn parameter_boolean_decoder() -> decode.Decoder(ParameterBoolean) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - - decode.success(ParameterBoolean( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - )) -} - -@internal -pub fn parameter_user_decoder() -> decode.Decoder(ParameterUser) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - - decode.success(ParameterUser( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - )) -} - -@internal -pub fn parameter_channel_decoder() -> decode.Decoder(ParameterChannel) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - use allowed_channel_types <- decode.optional_field( - "channel_types", - None, - decode.optional(decode.list(allowed_channel_type_decoder())), - ) - - decode.success(ParameterChannel( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - allowed_channel_types:, - )) -} - -@internal -pub fn allowed_channel_type_decoder() -> decode.Decoder(AllowedChannelType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(AllowTextChannels) - 1 -> decode.success(AllowDmChannels) - 2 -> decode.success(AllowVoiceChannels) - 4 -> decode.success(AllowCategoryChannels) - 5 -> decode.success(AllowAnnouncementChannels) - 10 -> decode.success(AllowAnnouncementThreads) - 11 -> decode.success(AllowPublicThreads) - 12 -> decode.success(AllowPrivateThreads) - 13 -> decode.success(AllowStageChannels) - 15 -> decode.success(AllowForumChannels) - 16 -> decode.success(AllowMediaChannels) - _ -> decode.failure(AllowTextChannels, "AllowedChannelType") - } -} - -@internal -pub fn parameter_role_decoder() -> decode.Decoder(ParameterRole) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - - decode.success(ParameterRole( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - )) -} - -@internal -pub fn parameter_mentionable_decoder() -> decode.Decoder(ParameterMentionable) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - - decode.success(ParameterMentionable( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - )) -} - -@internal -pub fn parameter_number_decoder() -> decode.Decoder(ParameterNumber) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - use choices <- decode.optional_field( - "choices", - None, - decode.optional(decode.list(of: number_choice_decoder())), - ) - use min_value <- decode.optional_field( - "min_value", - None, - decode.optional(decode.float), - ) - use max_value <- decode.optional_field( - "max_value", - None, - decode.optional(decode.float), - ) - use is_autocomplete <- decode.optional_field( - "autocomplete", - False, - decode.bool, - ) - - decode.success(ParameterNumber( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - choices:, - min_value:, - max_value:, - is_autocomplete:, - )) -} - -@internal -pub fn number_choice_decoder() -> decode.Decoder(NumberChoice) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use value <- decode.field("value", decode.float) - - decode.success(NumberChoice(name:, name_localizations:, value:)) -} - -@internal -pub fn parameter_attachment_decoder() -> decode.Decoder(ParameterAttachment) { - use name <- decode.field("name", decode.string) - use name_localizations <- decode.optional_field( - "name_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use description <- decode.field("description", decode.string) - use description_localizations <- decode.optional_field( - "description_localizations", - None, - decode.optional(decode.dict(decode.string, decode.string)), - ) - use is_required <- decode.optional_field("required", False, decode.bool) - - decode.success(ParameterAttachment( - name:, - name_localizations:, - description:, - description_localizations:, - is_required:, - )) -} - -@internal -pub fn guild_permissions_decoder() -> decode.Decoder(GuildPermissions) { - use command_id <- decode.field("id", decode.string) - use application_id <- decode.field("application_id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use permissions <- decode.field( - "permissions", - decode.list(of: permission_decoder()), - ) - - decode.success(GuildPermissions( - command_id:, - application_id:, - guild_id:, - permissions:, - )) -} - -@internal -pub fn permission_decoder() -> decode.Decoder(Permission) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", decode.int) - use is_permission_granted <- decode.field("permission", decode.bool) - - case type_ { - 1 -> decode.success(RolePermission(role_id: id, is_permission_granted:)) - 2 -> decode.success(UserPermission(user_id: id, is_permission_granted:)) - 3 -> - decode.success(ChannelPermission(channel_id: id, is_permission_granted:)) - _ -> decode.failure(RolePermission("", False), "Permission") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_global_to_json(create: CreateGlobal) -> Json { - case create { - CreateGlobalSlash(create) -> create_global_slash_command_to_json(create) - CreateGlobalUser(create) -> create_global_user_command_to_json(create) - CreateGlobalMessage(create) -> create_global_message_command_to_json(create) - } -} - -@internal -pub fn create_global_slash_command_to_json( - create: CreateGlobalSlashCommand, -) -> Json { - let type_ = [#("type", json.int(1))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(create.description))] - - let description_localizations = case create.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let parameters = case create.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - let integration_types = case create.integration_types { - Some(types) -> [ - #( - "integration_types", - json.array(types, application.installation_context_to_json), - ), - ] - None -> [] - } - - let contexts = case create.contexts { - Some(contexts) -> [#("contexts", json.array(contexts, context_to_json))] - None -> [] - } - - [ - type_, - name, - name_localizations, - description, - description_localizations, - parameters, - default_member_permissions, - is_nsfw, - integration_types, - contexts, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_global_user_command_to_json( - create: CreateGlobalUserCommand, -) -> Json { - let type_ = [#("type", json.int(2))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - let integration_types = case create.integration_types { - Some(types) -> [ - #( - "integration_types", - json.array(types, application.installation_context_to_json), - ), - ] - None -> [] - } - - let contexts = case create.contexts { - Some(contexts) -> [#("contexts", json.array(contexts, context_to_json))] - None -> [] - } - - [ - type_, - name, - name_localizations, - default_member_permissions, - is_nsfw, - integration_types, - contexts, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_global_message_command_to_json( - create: CreateGlobalMessageCommand, -) -> Json { - let type_ = [#("type", json.int(3))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - let integration_types = case create.integration_types { - Some(types) -> [ - #( - "integration_types", - json.array(types, application.installation_context_to_json), - ), - ] - None -> [] - } - - let contexts = case create.contexts { - Some(contexts) -> [#("contexts", json.array(contexts, context_to_json))] - None -> [] - } - - [ - type_, - name, - name_localizations, - default_member_permissions, - is_nsfw, - integration_types, - contexts, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn context_to_json(context: Context) -> Json { - case context { - AllowedInGuilds -> 0 - AllowedInBotDms -> 1 - AllowedInPrivateChannels -> 2 - } - |> json.int -} - -@internal -pub fn parameter_to_json(parameter: Parameter) -> Json { - case parameter { - SubCommandParameter(parameter) -> parameter_sub_command_to_json(parameter) - SubCommandGroupParameter(parameter) -> - parameter_sub_command_group_to_json(parameter) - TextParameter(parameter) -> parameter_text_to_json(parameter) - IntegerParameter(parameter) -> parameter_integer_to_json(parameter) - BooleanParameter(parameter) -> parameter_boolean_to_json(parameter) - UserParameter(parameter) -> parameter_user_to_json(parameter) - ChannelParameter(parameter) -> parameter_channel_to_json(parameter) - RoleParameter(parameter) -> parameter_role_to_json(parameter) - MentionableParameter(parameter) -> parameter_mentionable_to_json(parameter) - NumberParameter(parameter) -> parameter_number_to_json(parameter) - AttachmentParameter(parameter) -> parameter_attachment_to_json(parameter) - } -} - -@internal -pub fn parameter_sub_command_to_json(parameter: ParameterSubCommand) -> Json { - let type_ = [#("type", json.int(1))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let parameters = case parameter.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - [ - type_, - name, - name_localizations, - description, - description_localizations, - parameters, - ] - |> list.flatten - |> json.object -} - -pub fn parameter_sub_command_group_to_json( - parameter: ParameterSubCommandGroup, -) -> Json { - let type_ = [#("type", json.int(2))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let parameters = case parameter.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - [ - type_, - name, - name_localizations, - description, - description_localizations, - parameters, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_text_to_json(parameter: ParameterText) -> Json { - let type_ = [#("type", json.int(3))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - let choices = case parameter.choices { - Some(choices) -> [#("choices", json.array(choices, text_choice_to_json))] - None -> [] - } - - let min_length = case parameter.min_length { - Some(min_length) -> [#("min_length", json.int(min_length))] - None -> [] - } - - let max_length = case parameter.max_length { - Some(max_length) -> [#("max_length", json.int(max_length))] - None -> [] - } - - let is_autocomplete = [ - #("autocomplete", json.bool(parameter.is_autocomplete)), - ] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - choices, - min_length, - max_length, - is_autocomplete, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn text_choice_to_json(choice: TextChoice) -> Json { - let name = [#("name", json.string(choice.name))] - - let name_localizations = case choice.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let value = [#("value", json.string(choice.value))] - - [name, name_localizations, value] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_integer_to_json(parameter: ParameterInteger) -> Json { - let type_ = [#("type", json.int(4))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - let choices = case parameter.choices { - Some(choices) -> [#("choices", json.array(choices, integer_choice_to_json))] - None -> [] - } - - let min_value = case parameter.min_value { - Some(min_value) -> [#("min_value", json.int(min_value))] - None -> [] - } - - let max_value = case parameter.max_value { - Some(max_value) -> [#("max_value", json.int(max_value))] - None -> [] - } - - let is_autocomplete = [ - #("autocomplete", json.bool(parameter.is_autocomplete)), - ] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - choices, - min_value, - max_value, - is_autocomplete, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn integer_choice_to_json(choice: IntegerChoice) -> Json { - let name = [#("name", json.string(choice.name))] - - let name_localizations = case choice.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let value = [#("value", json.int(choice.value))] - - [name, name_localizations, value] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_boolean_to_json(parameter: ParameterBoolean) -> Json { - let type_ = [#("type", json.int(5))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_user_to_json(parameter: ParameterUser) -> Json { - let type_ = [#("type", json.int(6))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_channel_to_json(parameter: ParameterChannel) -> Json { - let type_ = [#("type", json.int(7))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - let allowed_channel_types = case parameter.allowed_channel_types { - Some(types) -> [ - #("channel_types", json.array(types, allowed_channel_type_to_json)), - ] - None -> [] - } - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - allowed_channel_types, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn allowed_channel_type_to_json(type_: AllowedChannelType) -> Json { - case type_ { - AllowTextChannels -> 0 - AllowDmChannels -> 1 - AllowVoiceChannels -> 2 - AllowCategoryChannels -> 4 - AllowAnnouncementChannels -> 5 - AllowAnnouncementThreads -> 10 - AllowPublicThreads -> 11 - AllowPrivateThreads -> 12 - AllowStageChannels -> 13 - AllowForumChannels -> 15 - AllowMediaChannels -> 16 - } - |> json.int -} - -@internal -pub fn parameter_role_to_json(parameter: ParameterRole) -> Json { - let type_ = [#("type", json.int(8))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_mentionable_to_json(parameter: ParameterMentionable) -> Json { - let type_ = [#("type", json.int(9))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_number_to_json(parameter: ParameterNumber) -> Json { - let type_ = [#("type", json.int(10))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - let choices = case parameter.choices { - Some(choices) -> [#("choices", json.array(choices, number_choice_to_json))] - None -> [] - } - - let min_value = case parameter.min_value { - Some(min_value) -> [#("min_value", json.float(min_value))] - None -> [] - } - - let max_value = case parameter.max_value { - Some(max_value) -> [#("max_value", json.float(max_value))] - None -> [] - } - - let is_autocomplete = [ - #("autocomplete", json.bool(parameter.is_autocomplete)), - ] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - choices, - min_value, - max_value, - is_autocomplete, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn number_choice_to_json(choice: NumberChoice) -> Json { - let name = [#("name", json.string(choice.name))] - - let name_localizations = case choice.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let value = [#("value", json.float(choice.value))] - - [name, name_localizations, value] - |> list.flatten - |> json.object -} - -@internal -pub fn parameter_attachment_to_json(parameter: ParameterAttachment) -> Json { - let type_ = [#("type", json.int(11))] - - let name = [#("name", json.string(parameter.name))] - - let name_localizations = case parameter.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(parameter.description))] - - let description_localizations = case parameter.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let is_required = [#("required", json.bool(parameter.is_required))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - is_required, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_global_to_json(modify: ModifyGlobal) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let name_localizations = - modification.to_json( - modify.name_localizations, - "name_localizations", - json.dict(_, function.identity, json.string), - ) - - let description = case modify.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let description_localizations = - modification.to_json( - modify.description_localizations, - "description_localizations", - json.dict(_, function.identity, json.string), - ) - - let parameters = case modify.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - let default_member_permissions = - modification.to_json( - modify.default_member_permissions, - "default_member_permissions", - permission.to_json, - ) - - let integration_types = case modify.integration_types { - Some(types) -> [ - #( - "integration_types", - json.array(types, application.installation_context_to_json), - ), - ] - None -> [] - } - - let contexts = case modify.contexts { - Some(contexts) -> [#("contexts", json.array(contexts, context_to_json))] - None -> [] - } - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - [ - name, - name_localizations, - description, - description_localizations, - parameters, - default_member_permissions, - integration_types, - contexts, - is_nsfw, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_for_guild_to_json(create: CreateForGuild) -> Json { - case create { - CreateSlashForGuild(create) -> - create_slash_command_for_guild_to_json(create) - CreateUserForGuild(create) -> create_user_command_for_guild_to_json(create) - CreateMessageForGuild(create) -> - create_message_command_for_guild_to_json(create) - } -} - -@internal -pub fn create_slash_command_for_guild_to_json( - create: CreateSlashCommandForGuild, -) -> Json { - let type_ = [#("type", json.int(1))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let description = [#("description", json.string(create.description))] - - let description_localizations = case create.description_localizations { - Some(localizations) -> [ - #( - "description_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let parameters = case create.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - [ - type_, - name, - name_localizations, - description, - description_localizations, - parameters, - default_member_permissions, - is_nsfw, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn create_user_command_for_guild_to_json( - create: CreateUserCommandForGuild, -) -> Json { - let type_ = [#("type", json.int(2))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - [type_, name, name_localizations, default_member_permissions, is_nsfw] - |> list.flatten - |> json.object -} - -pub fn create_message_command_for_guild_to_json( - create: CreateMessageCommandForGuild, -) -> Json { - let type_ = [#("type", json.int(3))] - - let name = [#("name", json.string(create.name))] - - let name_localizations = case create.name_localizations { - Some(localizations) -> [ - #( - "name_localizations", - json.dict(localizations, function.identity, json.string), - ), - ] - None -> [] - } - - let default_member_permissions = case create.default_member_permissions { - Some(permissions) -> [ - #("default_member_permissions", permission.to_json(permissions)), - ] - None -> [] - } - - let is_nsfw = [#("nsfw", json.bool(create.is_nsfw))] - - [type_, name, name_localizations, default_member_permissions, is_nsfw] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_for_guild_to_json(modify: ModifyForGuild) -> Json { - case modify { - ModifySlashForGuild(modify) -> - modify_slash_command_for_guild_to_json(modify) - ModifyUserForGuild(modify) -> modify_user_command_for_guild_to_json(modify) - ModifyMessageForGuild(modify) -> - modify_message_command_for_guild_to_json(modify) - } -} - -@internal -pub fn modify_slash_command_for_guild_to_json( - modify: ModifySlashCommandForGuild, -) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let name_localizations = - modification.to_json( - modify.name_localizations, - "name_localizations", - json.dict(_, function.identity, json.string), - ) - - let description = case modify.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let description_localizations = - modification.to_json( - modify.description_localizations, - "description_localizations", - json.dict(_, function.identity, json.string), - ) - - let parameters = case modify.parameters { - Some(parameters) -> [ - #("options", json.array(parameters, parameter_to_json)), - ] - None -> [] - } - - let default_member_permissions = - modification.to_json( - modify.default_member_permissions, - "default_member_permissions", - permission.to_json, - ) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - [ - name, - name_localizations, - description, - description_localizations, - parameters, - default_member_permissions, - is_nsfw, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_user_command_for_guild_to_json( - modify: ModifyUserCommandForGuild, -) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let name_localizations = - modification.to_json( - modify.name_localizations, - "name_localizations", - json.dict(_, function.identity, json.string), - ) - - let default_member_permissions = - modification.to_json( - modify.default_member_permissions, - "default_member_permissions", - permission.to_json, - ) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - [name, name_localizations, default_member_permissions, is_nsfw] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_message_command_for_guild_to_json( - modify: ModifyMessageCommandForGuild, -) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let name_localizations = - modification.to_json( - modify.name_localizations, - "name_localizations", - json.dict(_, function.identity, json.string), - ) - - let default_member_permissions = - modification.to_json( - modify.default_member_permissions, - "default_member_permissions", - permission.to_json, - ) - - let is_nsfw = case modify.is_nsfw { - Some(nsfw) -> [#("nsfw", json.bool(nsfw))] - None -> [] - } - - [name, name_localizations, default_member_permissions, is_nsfw] - |> list.flatten - |> json.object -} - -@internal -pub fn permission_to_json(permission: Permission) -> Json { - let id = case permission { - ChannelPermission(channel_id:, ..) -> channel_id - RolePermission(role_id:, ..) -> role_id - UserPermission(user_id:, ..) -> user_id - } - - let type_ = case permission { - RolePermission(..) -> 1 - UserPermission(..) -> 2 - ChannelPermission(..) -> 3 - } - - json.object([ - #("id", json.string(id)), - #("type", json.int(type_)), - #("permission", json.bool(permission.is_permission_granted)), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_all_global( - client: grom.Client, - for application_id: String, -) -> Result(List(Command), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/commands", - ) - |> request.set_query([#("with_localizations", "true")]) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_global( - client: grom.Client, - for application_id: String, - using create: CreateGlobal, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" <> application_id <> "/commands", - ) - |> request.set_body(create |> create_global_to_json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create_global_slash_command( - named name: String, - description description: String, -) -> CreateGlobalSlashCommand { - CreateGlobalSlashCommand( - name, - None, - description, - None, - None, - None, - None, - None, - False, - ) -} - -pub fn new_create_global_user_command( - named name: String, -) -> CreateGlobalUserCommand { - CreateGlobalUserCommand(name, None, None, None, None, False) -} - -pub fn new_create_global_message_command( - named name: String, -) -> CreateGlobalMessageCommand { - CreateGlobalMessageCommand(name, None, None, None, None, False) -} - -pub fn get_global( - client: grom.Client, - of application_id: String, - id command_id: String, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" <> application_id <> "/commands/" <> command_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_global( - client: grom.Client, - of application_id: String, - id command_id: String, - using modify: ModifyGlobal, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/applications/" <> application_id <> "/commands/" <> command_id, - ) - |> request.set_body(modify |> modify_global_to_json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_global() -> ModifyGlobal { - ModifyGlobal(None, Skip, None, Skip, None, Skip, None, None, None) -} - -pub fn delete_global( - client: grom.Client, - of application_id: String, - id command_id: String, -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/applications/" <> application_id <> "/commands/" <> command_id, - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn bulk_overwrite_global( - client: grom.Client, - of application_id: String, - new commands: List(CreateGlobal), -) -> Result(List(Command), grom.Error) { - let json = json.array(commands, create_global_to_json) - - use response <- result.try( - client - |> rest.new_request( - http.Put, - "/applications/" <> application_id <> "/commands", - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_all_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, -) -> Result(List(Command), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands", - ) - |> request.set_query([#("with_localizations", "true")]) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, - using create: CreateForGuild, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands", - ) - |> request.set_body(create |> create_for_guild_to_json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create_slash_command_for_guild( - named name: String, - description description: String, -) -> CreateSlashCommandForGuild { - CreateSlashCommandForGuild(name, None, description, None, None, None, False) -} - -pub fn new_create_user_command_for_guild( - named name: String, -) -> CreateUserCommandForGuild { - CreateUserCommandForGuild(name, None, None, False) -} - -pub fn new_create_message_command_for_guild( - named name: String, -) -> CreateMessageCommandForGuild { - CreateMessageCommandForGuild(name, None, None, False) -} - -pub fn get_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, - id command_id: String, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands/" - <> command_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, - id command_id: String, - using modify: ModifyForGuild, -) -> Result(Command, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands/" - <> command_id, - ) - |> request.set_body(modify |> modify_for_guild_to_json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, - id command_id: String, -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands/" - <> command_id, - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn bulk_overwrite_for_guild( - client: grom.Client, - application application_id: String, - guild guild_id: String, - new commands: List(CreateForGuild), -) -> Result(List(Command), grom.Error) { - let json = json.array(commands, create_for_guild_to_json) - - use response <- result.try( - client - |> rest.new_request( - http.Put, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands", - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_all_guild_permissions( - client: grom.Client, - of application_id: String, - in guild_id: String, -) -> Result(List(GuildPermissions), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands/permissions", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: guild_permissions_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_guild_permissions( - client: grom.Client, - of application_id: String, - in guild_id: String, - for command_id: String, -) -> Result(GuildPermissions, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/applications/" - <> application_id - <> "/guilds/" - <> guild_id - <> "/commands/" - <> command_id - <> "/permissions", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: guild_permissions_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_parameter_attachment( - named name: String, - description description: String, -) -> ParameterAttachment { - ParameterAttachment(name, None, description, None, False) -} - -pub fn new_parameter_boolean( - named name: String, - description description: String, -) -> ParameterBoolean { - ParameterBoolean(name, None, description, None, False) -} - -pub fn new_parameter_channel( - named name: String, - description description: String, -) -> ParameterChannel { - ParameterChannel(name, None, description, None, False, None) -} - -pub fn new_parameter_integer( - named name: String, - description description: String, -) -> ParameterInteger { - ParameterInteger( - name, - None, - description, - None, - False, - None, - None, - None, - False, - ) -} - -pub fn new_parameter_mentionable( - named name: String, - description description: String, -) -> ParameterMentionable { - ParameterMentionable(name, None, description, None, False) -} - -pub fn new_parameter_number( - named name: String, - description description: String, -) -> ParameterNumber { - ParameterNumber(name, None, description, None, False, None, None, None, False) -} - -pub fn new_parameter_role( - named name: String, - description description: String, -) -> ParameterRole { - ParameterRole(name, None, description, None, False) -} - -pub fn new_parameter_sub_command( - named name: String, - description description: String, -) -> ParameterSubCommand { - ParameterSubCommand(name, None, description, None, None) -} - -pub fn new_parameter_sub_command_group( - named name: String, - description description: String, - sub_commands parameters: List(Parameter), -) -> ParameterSubCommandGroup { - ParameterSubCommandGroup(name, None, description, None, Some(parameters)) -} - -pub fn new_parameter_text( - named name: String, - description description: String, -) -> ParameterText { - ParameterText(name, None, description, None, False, None, None, None, False) -} - -pub fn new_parameter_user( - named name: String, - description description: String, -) -> ParameterUser { - ParameterUser(name, None, description, None, False) -} - -pub fn new_text_choice(named name: String, value value: String) -> TextChoice { - TextChoice(name, None, value) -} - -pub fn new_integer_choice(named name: String, value value: Int) -> IntegerChoice { - IntegerChoice(name, None, value) -} - -pub fn new_number_choice(named name: String, value value: Float) -> NumberChoice { - NumberChoice(name, None, value) -} diff --git a/src/grom/component/action_row.gleam b/src/grom/component/action_row.gleam deleted file mode 100644 index 820a205..0000000 --- a/src/grom/component/action_row.gleam +++ /dev/null @@ -1,117 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/button.{type Button} -import grom/component/channel_select.{type ChannelSelect} -import grom/component/mentionable_select.{type MentionableSelect} -import grom/component/role_select.{type RoleSelect} -import grom/component/string_select.{type StringSelect} -import grom/component/text_input.{type TextInput} -import grom/component/user_select.{type UserSelect} - -// TYPES ----------------------------------------------------------------------- - -pub type ActionRow { - ActionRow(id: Option(Int), components: List(Component)) -} - -pub type Component { - Button(Button) - TextInput(TextInput) - StringSelect(StringSelect) - UserSelect(UserSelect) - RoleSelect(RoleSelect) - MentionableSelect(MentionableSelect) - ChannelSelect(ChannelSelect) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(ActionRow) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use components <- decode.field("components", decode.list(component_decoder())) - - decode.success(ActionRow(id:, components:)) -} - -@internal -pub fn component_decoder() -> decode.Decoder(Component) { - use type_ <- decode.field("type", decode.int) - case type_ { - 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)) - } - _ -> - decode.failure( - 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 -> [] - } - - let components = [ - #("components", json.array(action_row.components, component_to_json)), - ] - - [type_, id, components] - |> list.flatten - |> json.object -} - -@internal -pub fn component_to_json(component: Component) -> Json { - case component { - Button(button) -> button.to_json(button) - TextInput(text_input) -> text_input.to_json(text_input) - StringSelect(string_select) -> string_select.to_json(string_select) - UserSelect(user_select) -> user_select.to_json(user_select) - RoleSelect(role_select) -> role_select.to_json(role_select) - MentionableSelect(mentionable_select) -> - mentionable_select.to_json(mentionable_select) - ChannelSelect(channel_select) -> channel_select.to_json(channel_select) - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(containing components: List(Component)) -> ActionRow { - ActionRow(id: None, components:) -} diff --git a/src/grom/component/button.gleam b/src/grom/component/button.gleam deleted file mode 100644 index b5649af..0000000 --- a/src/grom/component/button.gleam +++ /dev/null @@ -1,217 +0,0 @@ -import gleam/dynamic/decode -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, - ) - Link( - id: Option(Int), - is_disabled: Bool, - label: Option(String), - emoji: Option(Emoji), - url: String, - ) - Premium(id: Option(Int), is_disabled: Bool, sku_id: String) -} - -pub type Style { - Primary - Secondary - Success - Danger -} - -pub type Emoji { - Emoji(id: Option(String), name: String, is_animated: Bool) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Button) { - use style <- decode.field("style", decode.int) - case style { - 1 | 2 | 3 | 4 -> regular_decoder() - 5 -> link_decoder() - 6 -> premium_decoder() - _ -> decode.failure(Regular(None, False, Primary, None, None, ""), "Button") - } -} - -fn regular_decoder() -> decode.Decoder(Button) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use style <- decode.field("style", style_decoder()) - use label <- decode.optional_field( - "label", - None, - decode.optional(decode.string), - ) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji_decoder()), - ) - use custom_id <- decode.field("custom_id", decode.string) - use is_disabled <- decode.optional_field("disabled", False, decode.bool) - - decode.success(Regular(id:, style:, label:, emoji:, custom_id:, is_disabled:)) -} - -fn link_decoder() -> decode.Decoder(Button) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use label <- decode.optional_field( - "label", - None, - decode.optional(decode.string), - ) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji_decoder()), - ) - use url <- decode.field("url", decode.string) - use is_disabled <- decode.optional_field("disabled", False, decode.bool) - - decode.success(Link(id:, label:, emoji:, url:, is_disabled:)) -} - -fn premium_decoder() -> decode.Decoder(Button) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use sku_id <- decode.field("sku_id", decode.string) - use is_disabled <- decode.optional_field("disabled", False, decode.bool) - - decode.success(Premium(id:, sku_id:, is_disabled:)) -} - -@internal -pub fn style_decoder() -> decode.Decoder(Style) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Primary) - 2 -> decode.success(Secondary) - 3 -> decode.success(Success) - 4 -> decode.success(Danger) - _ -> 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new_regular(custom_id custom_id: String) -> Button { - Regular(None, False, Primary, None, None, custom_id:) -} - -pub fn new_link(url url: String) -> Button { - Link(None, False, None, None, url:) -} - -pub fn new_premium(sku_id sku_id: String) -> Button { - Premium(None, False, sku_id:) -} - -pub fn new_emoji(named name: String) -> Emoji { - Emoji(None, name, False) -} diff --git a/src/grom/component/channel_select.gleam b/src/grom/component/channel_select.gleam deleted file mode 100644 index b94300f..0000000 --- a/src/grom/component/channel_select.gleam +++ /dev/null @@ -1,167 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type ChannelSelect { - ChannelSelect( - id: Option(Int), - custom_id: String, - channel_types: Option(List(ChannelType)), - placeholder: Option(String), - default_values: Option(List(DefaultValue)), - min_values: Int, - max_values: Int, - is_disabled: Bool, - ) -} - -pub type ChannelType { - Text - Dm - Category - Announcement - Forum - Media -} - -pub type DefaultValue { - DefaultValue(id: String) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(ChannelSelect) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use channel_types <- decode.optional_field( - "channel_types", - None, - decode.optional(decode.list(channel_type_decoder())), - ) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - use default_values <- decode.optional_field( - "default_values", - None, - decode.optional(decode.list(default_value_decoder())), - ) - 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("disabled", False, decode.bool) - - decode.success(ChannelSelect( - id:, - custom_id:, - channel_types:, - placeholder:, - default_values:, - min_values:, - max_values:, - is_disabled:, - )) -} - -@internal -pub fn default_value_decoder() -> decode.Decoder(DefaultValue) { - use id <- decode.field("id", decode.string) - decode.success(DefaultValue(id:)) -} - -@internal -pub fn channel_type_decoder() -> decode.Decoder(ChannelType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Text) - 1 -> decode.success(Dm) - 4 -> decode.success(Category) - 5 -> decode.success(Announcement) - 15 -> decode.success(Forum) - 16 -> decode.success(Media) - _ -> 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")), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id custom_id: String) -> ChannelSelect { - ChannelSelect(None, custom_id, None, None, None, 1, 1, False) -} diff --git a/src/grom/component/checkbox.gleam b/src/grom/component/checkbox.gleam deleted file mode 100644 index f61a8e1..0000000 --- a/src/grom/component/checkbox.gleam +++ /dev/null @@ -1,36 +0,0 @@ -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -pub type Checkbox { - Checkbox( - id: Option(Int), - custom_id: String, - /// Defaults to False. - is_default_selected: Bool, - ) -} - -@internal -pub fn to_json(checkbox: Checkbox) -> Json { - let type_ = [#("type", json.int(23))] - - let id = case checkbox.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let custom_id = [#("custom_id", json.string(checkbox.custom_id))] - - let is_default_selected = [ - #("default", json.bool(checkbox.is_default_selected)), - ] - - [type_, id, custom_id, is_default_selected] - |> list.flatten - |> json.object -} - -pub fn new(custom_id custom_id: String) -> Checkbox { - Checkbox(None, custom_id, False) -} diff --git a/src/grom/component/checkbox_group.gleam b/src/grom/component/checkbox_group.gleam deleted file mode 100644 index 3c32c29..0000000 --- a/src/grom/component/checkbox_group.gleam +++ /dev/null @@ -1,91 +0,0 @@ -import gleam/json.{type Json} -import gleam/list -import gleam/option.{None, Some} - -pub type CheckboxGroup { - CheckboxGroup( - id: option.Option(Int), - /// 1-100 characters. - custom_id: String, - /// Minimum 1, maximum 10. - options: List(Option), - /// Defaults to 1. If set to 0, `is_required` must be set to False. - min_values: option.Option(Int), - /// Defaults to the count of options. - max_values: option.Option(Int), - /// Defaults to True. - is_required: Bool, - ) -} - -pub type Option { - Option( - /// Dev facing value, max 100 characters - value: String, - /// User facing text, max 100 characters - label: String, - /// Max 100 characters - description: option.Option(String), - is_default_selected: Bool, - ) -} - -@internal -pub fn to_json(group: CheckboxGroup) -> Json { - let type_ = [#("type", json.int(22))] - - let id = case group.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let custom_id = [#("custom_id", json.string(group.custom_id))] - - let options = [#("options", json.array(group.options, option_to_json))] - - let min_values = case group.min_values { - Some(min) -> [#("min_values", json.int(min))] - None -> [] - } - - let max_values = case group.max_values { - Some(max) -> [#("max_values", json.int(max))] - None -> [] - } - - let is_required = [#("required", json.bool(group.is_required))] - - [type_, id, custom_id, options, min_values, max_values, is_required] - |> list.flatten - |> json.object -} - -fn option_to_json(option: Option) -> Json { - let value = [#("value", json.string(option.value))] - - let label = [#("label", json.string(option.label))] - - let description = case option.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let is_default_selected = [ - #("default", json.bool(option.is_default_selected)), - ] - - [value, label, description, is_default_selected] - |> list.flatten - |> json.object -} - -pub fn new( - custom_id custom_id: String, - options options: List(Option), -) -> CheckboxGroup { - CheckboxGroup(None, custom_id, options, None, None, True) -} - -pub fn new_option(named label: String, value value: String) -> Option { - Option(value, label, None, False) -} diff --git a/src/grom/component/container.gleam b/src/grom/component/container.gleam deleted file mode 100644 index 2852226..0000000 --- a/src/grom/component/container.gleam +++ /dev/null @@ -1,126 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/action_row.{type ActionRow} -import grom/component/file.{type File} -import grom/component/media_gallery.{type MediaGallery} -import grom/component/section.{type Section} -import grom/component/separator.{type Separator} -import grom/component/text_display.{type TextDisplay} - -// TYPES ----------------------------------------------------------------------- - -pub type Container { - Container( - id: Option(Int), - components: List(Component), - accent_color: Option(Int), - is_spoiler: Bool, - ) -} - -pub type Component { - ActionRow(ActionRow) - TextDisplay(TextDisplay) - Section(Section) - MediaGallery(MediaGallery) - Separator(Separator) - File(File) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Container) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use components <- decode.field("components", decode.list(component_decoder())) - use accent_color <- decode.optional_field( - "accent_color", - None, - decode.optional(decode.int), - ) - use is_spoiler <- decode.optional_field("spoiler", False, decode.bool) - decode.success(Container(id:, components:, accent_color:, is_spoiler:)) -} - -@internal -pub fn component_decoder() -> decode.Decoder(Component) { - use type_ <- decode.field("type", decode.int) - case type_ { - 1 -> { - use action_row <- decode.then(action_row.decoder()) - decode.success(ActionRow(action_row)) - } - 9 -> { - use section <- decode.then(section.decoder()) - decode.success(Section(section)) - } - 10 -> { - use text_display <- decode.then(text_display.decoder()) - decode.success(TextDisplay(text_display)) - } - 12 -> { - use media_gallery <- decode.then(media_gallery.decoder()) - decode.success(MediaGallery(media_gallery)) - } - 13 -> { - use file <- decode.then(file.decoder()) - decode.success(File(file)) - } - 14 -> { - use separator <- decode.then(separator.decoder()) - decode.success(Separator(separator)) - } - _ -> - decode.failure( - TextDisplay(text_display.TextDisplay(None, "")), - "Component", - ) - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(container: Container) -> Json { - let type_ = [#("type", json.int(17))] - - let id = case container.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let components = [ - #("components", json.array(container.components, component_to_json)), - ] - - let accent_color = case container.accent_color { - Some(color) -> [#("accent_color", json.int(color))] - None -> [] - } - - let is_spoiler = [#("spoiler", json.bool(container.is_spoiler))] - - [type_, id, components, accent_color, is_spoiler] - |> list.flatten - |> json.object -} - -@internal -pub fn component_to_json(component: Component) -> Json { - case component { - ActionRow(action_row) -> action_row.to_json(action_row) - TextDisplay(text_display) -> text_display.to_json(text_display) - Section(section) -> section.to_json(section) - MediaGallery(media_gallery) -> media_gallery.to_json(media_gallery) - Separator(separator) -> separator.to_json(separator) - File(file) -> file.to_json(file) - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(containing components: List(Component)) -> Container { - Container(None, components, None, False) -} diff --git a/src/grom/component/file.gleam b/src/grom/component/file.gleam deleted file mode 100644 index c84f079..0000000 --- a/src/grom/component/file.gleam +++ /dev/null @@ -1,56 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/unfurled_media_item.{type UnfurledMediaItem} - -// TYPES ----------------------------------------------------------------------- - -pub type File { - File( - id: Option(Int), - file: UnfurledMediaItem, - is_spoiler: Bool, - name: String, - size: Int, - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(File) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use file <- decode.field("file", unfurled_media_item.decoder()) - use is_spoiler <- decode.optional_field("spoiler", False, decode.bool) - use name <- decode.field("name", decode.string) - use size <- decode.field("size", decode.int) - - decode.success(File(id:, file:, is_spoiler:, name:, size:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(file: File) -> Json { - let type_ = [#("type", json.int(13))] - - let id = case file.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let file_ = [#("file", unfurled_media_item.to_json(file.file))] - - let is_spoiler = [#("spoiler", json.bool(file.is_spoiler))] - - [type_, id, file_, is_spoiler] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(named name: String, holding file: UnfurledMediaItem) -> File { - File(None, file, False, name, 0) -} diff --git a/src/grom/component/file_upload.gleam b/src/grom/component/file_upload.gleam deleted file mode 100644 index a20dd77..0000000 --- a/src/grom/component/file_upload.gleam +++ /dev/null @@ -1,70 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type FileUpload { - FileUpload( - id: Option(Int), - custom_id: String, - min_values: Int, - max_values: Int, - is_required: Bool, - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(FileUpload) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use min_values <- decode.optional_field("min_values", 1, decode.int) - use max_values <- decode.optional_field("max_values", 1, decode.int) - use is_required <- decode.optional_field("required", True, decode.bool) - - decode.success(FileUpload( - id:, - custom_id:, - min_values:, - max_values:, - is_required:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(file_upload: FileUpload) -> Json { - let id = case file_upload.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - [ - id, - [ - #("type", json.int(19)), - #("custom_id", json.string(file_upload.custom_id)), - #("min_values", json.int(file_upload.min_values)), - #("max_values", json.int(file_upload.max_values)), - #("required", json.bool(file_upload.is_required)), - ], - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id: String) -> FileUpload { - FileUpload( - id: None, - custom_id:, - min_values: 1, - max_values: 1, - is_required: True, - ) -} diff --git a/src/grom/component/label.gleam b/src/grom/component/label.gleam deleted file mode 100644 index 89624f3..0000000 --- a/src/grom/component/label.gleam +++ /dev/null @@ -1,85 +0,0 @@ -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/channel_select.{type ChannelSelect} -import grom/component/checkbox.{type Checkbox} -import grom/component/checkbox_group.{type CheckboxGroup} -import grom/component/file_upload.{type FileUpload} -import grom/component/mentionable_select.{type MentionableSelect} -import grom/component/radio_group.{type RadioGroup} -import grom/component/role_select.{type RoleSelect} -import grom/component/string_select.{type StringSelect} -import grom/component/text_input.{type TextInput} -import grom/component/user_select.{type UserSelect} - -// TYPES ----------------------------------------------------------------------- - -pub type Label { - Label( - id: Option(Int), - label: String, - description: Option(String), - component: Component, - ) -} - -pub type Component { - TextInput(TextInput) - StringSelect(StringSelect) - UserSelect(UserSelect) - RoleSelect(RoleSelect) - MentionableSelect(MentionableSelect) - ChannelSelect(ChannelSelect) - FileUpload(FileUpload) - RadioGroup(RadioGroup) - CheckboxGroup(CheckboxGroup) - Checkbox(Checkbox) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(label: Label) -> Json { - let type_ = [#("type", json.int(18))] - - let id = case label.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let description = case label.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let label_ = [#("label", json.string(label.label))] - - let component = [#("component", component_to_json(label.component))] - - [type_, id, description, label_, component] - |> list.flatten - |> json.object -} - -@internal -pub fn component_to_json(component: Component) -> Json { - case component { - TextInput(text_input) -> text_input.to_json(text_input) - StringSelect(string_select) -> string_select.to_json(string_select) - UserSelect(user_select) -> user_select.to_json(user_select) - RoleSelect(role_select) -> role_select.to_json(role_select) - MentionableSelect(mentionable_select) -> - mentionable_select.to_json(mentionable_select) - ChannelSelect(channel_select) -> channel_select.to_json(channel_select) - FileUpload(file_upload) -> file_upload.to_json(file_upload) - RadioGroup(radio_group) -> radio_group.to_json(radio_group) - CheckboxGroup(checkbox_group) -> checkbox_group.to_json(checkbox_group) - Checkbox(checkbox) -> checkbox.to_json(checkbox) - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(labeled label: String, containing component: Component) -> Label { - Label(None, label, None, component) -} diff --git a/src/grom/component/media_gallery.gleam b/src/grom/component/media_gallery.gleam deleted file mode 100644 index 71f2dc5..0000000 --- a/src/grom/component/media_gallery.gleam +++ /dev/null @@ -1,80 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/unfurled_media_item.{type UnfurledMediaItem} - -// TYPES ----------------------------------------------------------------------- - -pub type MediaGallery { - MediaGallery(id: Option(Int), items: List(Item)) -} - -pub type Item { - Item(media: UnfurledMediaItem, description: Option(String), is_spoiler: Bool) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(MediaGallery) { - use id <- decode.field("id", decode.optional(decode.int)) - use items <- decode.field("items", decode.list(item_decoder())) - decode.success(MediaGallery(id:, items:)) -} - -@internal -pub fn item_decoder() -> decode.Decoder(Item) { - use media <- decode.field("media", unfurled_media_item.decoder()) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - use is_spoiler <- decode.optional_field("spoiler", False, decode.bool) - decode.success(Item(media:, description:, is_spoiler:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(media_gallery: MediaGallery) -> Json { - let type_ = [#("type", json.int(12))] - - let id = case media_gallery.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let items = [#("items", json.array(media_gallery.items, item_to_json))] - - [type_, id, items] - |> list.flatten - |> json.object -} - -@internal -pub fn item_to_json(item: Item) -> Json { - let media = [#("media", unfurled_media_item.to_json(item.media))] - - let description = case item.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let is_spoiler = [#("spoiler", json.bool(item.is_spoiler))] - - [media, description, is_spoiler] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(containing items: List(Item)) -> MediaGallery { - MediaGallery(None, items) -} - -pub fn new_item(showing media: UnfurledMediaItem) -> Item { - Item(media, None, False) -} diff --git a/src/grom/component/mentionable_select.gleam b/src/grom/component/mentionable_select.gleam deleted file mode 100644 index 07ee94e..0000000 --- a/src/grom/component/mentionable_select.gleam +++ /dev/null @@ -1,129 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type MentionableSelect { - MentionableSelect( - id: Option(Int), - custom_id: String, - placeholder: Option(String), - default_values: Option(List(DefaultValue)), - min_values: Int, - max_values: Int, - is_disabled: Bool, - ) -} - -pub type DefaultValue { - UserValue(id: String) - RoleValue(id: String) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(MentionableSelect) { - use id <- decode.field("id", decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - use default_values <- decode.field( - "default_values", - decode.optional(decode.list(default_value_decoder())), - ) - 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) - decode.success(MentionableSelect( - id:, - custom_id:, - placeholder:, - default_values:, - min_values:, - max_values:, - is_disabled:, - )) -} - -@internal -pub fn default_value_decoder() -> decode.Decoder(DefaultValue) { - use type_ <- decode.field("type", decode.string) - use id <- decode.field("id", decode.string) - - case type_ { - "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" - }), - ), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id custom_id: String) -> MentionableSelect { - MentionableSelect(None, custom_id, None, None, 1, 1, False) -} diff --git a/src/grom/component/radio_group.gleam b/src/grom/component/radio_group.gleam deleted file mode 100644 index bb2a7ef..0000000 --- a/src/grom/component/radio_group.gleam +++ /dev/null @@ -1,75 +0,0 @@ -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option as GOption, None, Some} - -pub type RadioGroup { - RadioGroup( - id: GOption(Int), - custom_id: String, - options: List(Option), - /// Defaults to True. - is_required: Bool, - ) -} - -pub type Option { - Option( - /// Dev facing value, max 100 characters - value: String, - /// User facing text, max 100 characters - label: String, - /// Max 100 characters - description: GOption(String), - is_default_selected: Bool, - ) -} - -@internal -pub fn to_json(group: RadioGroup) -> Json { - let type_ = [#("type", json.int(21))] - - let id = case group.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let custom_id = [#("custom_id", json.string(group.custom_id))] - - let options = [#("options", json.array(group.options, option_to_json))] - - let is_required = [#("required", json.bool(group.is_required))] - - [type_, id, custom_id, options, is_required] - |> list.flatten - |> json.object -} - -fn option_to_json(option: Option) -> Json { - let value = [#("value", json.string(option.value))] - - let label = [#("label", json.string(option.label))] - - let description = case option.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let is_default_selected = [ - #("default", json.bool(option.is_default_selected)), - ] - - [value, label, description, is_default_selected] - |> list.flatten - |> json.object -} - -pub fn new( - custom_id custom_id: String, - options options: List(Option), -) -> RadioGroup { - RadioGroup(None, custom_id, options, True) -} - -pub fn new_option(named label: String, value value: String) -> Option { - Option(value, label, None, False) -} diff --git a/src/grom/component/role_select.gleam b/src/grom/component/role_select.gleam deleted file mode 100644 index 9dfb8b7..0000000 --- a/src/grom/component/role_select.gleam +++ /dev/null @@ -1,125 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type RoleSelect { - RoleSelect( - id: Option(Int), - custom_id: String, - placeholder: Option(String), - default_values: Option(List(DefaultValue)), - min_values: Int, - max_values: Int, - is_disabled: Bool, - ) -} - -pub type DefaultValue { - DefaultValue(id: String) -} - -pub type InteractionResponse { - InteractionResponse( - id: Int, - custom_id: String, - selected_roles_ids: List(String), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(RoleSelect) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - use default_values <- decode.optional_field( - "default_values", - None, - decode.optional(decode.list(default_value_decoder())), - ) - 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) - decode.success(RoleSelect( - id:, - custom_id:, - placeholder:, - default_values:, - min_values:, - max_values:, - is_disabled:, - )) -} - -@internal -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")), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id custom_id: String) -> RoleSelect { - RoleSelect(None, custom_id, None, None, 1, 1, False) -} diff --git a/src/grom/component/section.gleam b/src/grom/component/section.gleam deleted file mode 100644 index 7bfd96e..0000000 --- a/src/grom/component/section.gleam +++ /dev/null @@ -1,150 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/component/button.{type Button} -import grom/component/text_display.{type TextDisplay} -import grom/component/unfurled_media_item.{type UnfurledMediaItem} - -// TYPES ----------------------------------------------------------------------- - -pub type Section { - Section(id: Option(Int), components: List(Component), accessory: Accessory) -} - -pub type Component { - TextDisplay(TextDisplay) -} - -pub type Accessory { - Thumbnail( - id: Option(Int), - media: UnfurledMediaItem, - description: Option(String), - is_spoiler: Bool, - ) - Button(Button) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Section) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use components <- decode.field("components", decode.list(component_decoder())) - use accessory <- decode.field("accessory", accessory_decoder()) - decode.success(Section(id:, components:, accessory:)) -} - -@internal -pub fn component_decoder() -> decode.Decoder(Component) { - use type_ <- decode.field("type", decode.int) - case type_ { - 10 -> { - use text_display <- decode.then(text_display.decoder()) - decode.success(TextDisplay(text_display)) - } - _ -> - decode.failure( - TextDisplay(text_display.TextDisplay(None, "")), - "Component", - ) - } -} - -@internal -pub fn accessory_decoder() -> decode.Decoder(Accessory) { - use type_ <- decode.field("type", decode.int) - case type_ { - 2 -> { - use button <- decode.then(button.decoder()) - decode.success(Button(button)) - } - 11 -> { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use media <- decode.field("media", unfurled_media_item.decoder()) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - use is_spoiler <- decode.optional_field("spoiler", False, decode.bool) - decode.success(Thumbnail(id:, media:, description:, is_spoiler:)) - } - _ -> - decode.failure( - 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 = [#("media", unfurled_media_item.to_json(accessory.media))] - - 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 - } - } -} - -// PUBLIC API FUNCTION --------------------------------------------------------- - -pub fn new( - containing components: List(Component), - accompanied_by accessory: Accessory, -) -> Section { - Section(None, components:, accessory:) -} - -pub fn new_thumbnail(showing media: UnfurledMediaItem) -> Accessory { - Thumbnail(None, media, None, False) -} diff --git a/src/grom/component/separator.gleam b/src/grom/component/separator.gleam deleted file mode 100644 index 5e28a6c..0000000 --- a/src/grom/component/separator.gleam +++ /dev/null @@ -1,75 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type Separator { - Separator(id: Option(Int), show_divider: Bool, spacing: Spacing) -} - -pub type Spacing { - SmallPadding - LargePadding -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Separator) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use show_divider <- decode.optional_field("divider", True, decode.bool) - use spacing <- decode.optional_field( - "spacing", - SmallPadding, - spacing_decoder(), - ) - - decode.success(Separator(id:, show_divider:, spacing:)) -} - -@internal -pub fn spacing_decoder() -> decode.Decoder(Spacing) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(SmallPadding) - 2 -> decode.success(LargePadding) - _ -> decode.failure(SmallPadding, "Spacing") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(separator: Separator) -> Json { - let type_ = [#("type", json.int(14))] - - let id = case separator.id { - Some(id) -> [#("id", json.int(id))] - None -> [] - } - - let show_divider = [#("divider", json.bool(separator.show_divider))] - - let spacing = [#("spacing", spacing_to_json(separator.spacing))] - - [type_, id, show_divider, spacing] - |> list.flatten - |> json.object -} - -@internal -pub fn spacing_to_json(spacing: Spacing) -> Json { - case spacing { - SmallPadding -> 1 - LargePadding -> 2 - } - |> json.int -} - -// pUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new() -> Separator { - Separator(None, True, SmallPadding) -} diff --git a/src/grom/component/string_select.gleam b/src/grom/component/string_select.gleam deleted file mode 100644 index ffcc414..0000000 --- a/src/grom/component/string_select.gleam +++ /dev/null @@ -1,179 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option as GOption, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type StringSelect { - StringSelect( - id: GOption(Int), - custom_id: String, - options: List(Option), - placeholder: GOption(String), - min_values: Int, - max_values: Int, - is_disabled: Bool, - ) -} - -pub type Option { - Option( - label: String, - value: String, - description: GOption(String), - emoji: GOption(Emoji), - is_default: Bool, - ) -} - -pub type Emoji { - Emoji(id: GOption(String), name: String, is_animated: Bool) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(StringSelect) { - use id <- decode.field("id", decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use options <- decode.field("options", decode.list(option_decoder())) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - 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("disabled", False, decode.bool) - decode.success(StringSelect( - id:, - custom_id:, - options:, - placeholder:, - min_values:, - max_values:, - is_disabled:, - )) -} - -@internal -pub fn option_decoder() -> decode.Decoder(Option) { - use label <- decode.field("label", decode.string) - use value <- decode.field("value", decode.string) - 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new( - custom_id custom_id: String, - containing options: List(Option), -) -> StringSelect { - StringSelect(None, custom_id, options, None, 1, 1, False) -} - -pub fn new_option(labeled label: String, value value: String) -> Option { - Option(label, value, None, None, False) -} diff --git a/src/grom/component/text_display.gleam b/src/grom/component/text_display.gleam deleted file mode 100644 index fb209c5..0000000 --- a/src/grom/component/text_display.gleam +++ /dev/null @@ -1,43 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type TextDisplay { - TextDisplay(id: Option(Int), content: String) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(TextDisplay) { - 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(showing content: String) -> TextDisplay { - TextDisplay(None, content) -} diff --git a/src/grom/component/text_input.gleam b/src/grom/component/text_input.gleam deleted file mode 100644 index c94af9d..0000000 --- a/src/grom/component/text_input.gleam +++ /dev/null @@ -1,128 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type TextInput { - TextInput( - id: Option(Int), - custom_id: String, - style: Style, - min_length: Int, - max_length: Int, - is_required: Bool, - value: Option(String), - placeholder: Option(String), - ) -} - -pub type Style { - Short - Paragraph -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(TextInput) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use style <- decode.field("style", style_decoder()) - 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", - None, - decode.optional(decode.string), - ) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - - decode.success(TextInput( - id:, - custom_id:, - style:, - min_length:, - max_length:, - is_required:, - value:, - placeholder:, - )) -} - -@internal -pub fn style_decoder() -> decode.Decoder(Style) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Short) - 2 -> decode.success(Paragraph) - _ -> 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 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, - 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id custom_id: String, style style: Style) -> TextInput { - TextInput(None, custom_id, style, 0, 4000, True, None, None) -} diff --git a/src/grom/component/unfurled_media_item.gleam b/src/grom/component/unfurled_media_item.gleam deleted file mode 100644 index a1fa897..0000000 --- a/src/grom/component/unfurled_media_item.gleam +++ /dev/null @@ -1,58 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/option.{type Option, None} - -// TYPES ----------------------------------------------------------------------- - -pub type UnfurledMediaItem { - UnfurledMediaItem( - url: String, - proxy_url: Option(String), - height: Option(Int), - width: Option(Int), - content_type: Option(String), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(UnfurledMediaItem) { - use url <- decode.field("url", decode.string) - use proxy_url <- decode.optional_field( - "proxy_url", - None, - decode.optional(decode.string), - ) - use height <- decode.optional_field( - "height", - None, - decode.optional(decode.int), - ) - use width <- decode.optional_field("width", None, decode.optional(decode.int)) - use content_type <- decode.optional_field( - "content_type", - None, - decode.optional(decode.string), - ) - decode.success(UnfurledMediaItem( - url:, - proxy_url:, - height:, - width:, - content_type:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(unfurled_media_item: UnfurledMediaItem) -> Json { - json.object([#("url", json.string(unfurled_media_item.url))]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(url url: String) -> UnfurledMediaItem { - UnfurledMediaItem(url, None, None, None, None) -} diff --git a/src/grom/component/user_select.gleam b/src/grom/component/user_select.gleam deleted file mode 100644 index d8a095c..0000000 --- a/src/grom/component/user_select.gleam +++ /dev/null @@ -1,125 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type UserSelect { - UserSelect( - id: Option(Int), - custom_id: String, - placeholder: Option(String), - default_values: Option(List(DefaultValue)), - min_values: Int, - max_values: Int, - is_disabled: Bool, - ) -} - -pub type DefaultValue { - DefaultValue(id: String) -} - -pub type InteractionResponse { - InteractionResponse( - id: Int, - custom_id: String, - selected_users_ids: List(String), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(UserSelect) { - use id <- decode.optional_field("id", None, decode.optional(decode.int)) - use custom_id <- decode.field("custom_id", decode.string) - use placeholder <- decode.optional_field( - "placeholder", - None, - decode.optional(decode.string), - ) - use default_values <- decode.optional_field( - "default_values", - None, - decode.optional(decode.list(default_value_decoder())), - ) - 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) - decode.success(UserSelect( - id:, - custom_id:, - placeholder:, - default_values:, - min_values:, - max_values:, - is_disabled:, - )) -} - -@internal -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 { - json.object([ - #("id", json.string(default_value.id)), - #("type", json.string("user")), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new(custom_id custom_id: String) -> UserSelect { - UserSelect(None, custom_id, None, None, 1, 1, False) -} diff --git a/src/grom/emoji.gleam b/src/grom/emoji.gleam deleted file mode 100644 index d213dcb..0000000 --- a/src/grom/emoji.gleam +++ /dev/null @@ -1,110 +0,0 @@ -import gleam/dynamic/decode -import gleam/http/request.{type Request} -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import grom/internal/rest -import grom/user.{type User} - -// TYPES ---------------------------------------------------------------------- - -pub type Emoji { - Emoji( - id: Option(String), - name: Option(String), - role_ids: Option(List(String)), - user: Option(User), - requires_colons: Bool, - is_managed: Bool, - is_animated: Bool, - is_available: Bool, - ) -} - -// DECODERS ------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Emoji) { - use id <- decode.field("id", decode.optional(decode.string)) - use name <- decode.field("name", decode.optional(decode.string)) - use role_ids <- decode.optional_field( - "roles", - None, - decode.optional(decode.list(decode.string)), - ) - use user <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - use requires_colons <- decode.optional_field( - "require_colons", - False, - decode.bool, - ) - use is_managed <- decode.optional_field("managed", False, decode.bool) - use is_animated <- decode.optional_field("animated", False, decode.bool) - use is_available <- decode.optional_field("available", False, decode.bool) - decode.success(Emoji( - id:, - name:, - role_ids:, - user:, - requires_colons:, - is_managed:, - is_animated:, - is_available:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(emoji: Emoji) -> Json { - let id = [#("id", json.nullable(emoji.id, json.string))] - - let name = [#("name", json.nullable(emoji.name, json.string))] - - let role_ids = case emoji.role_ids { - Some(ids) -> [#("roles", json.array(ids, json.string))] - None -> [] - } - - let user = case emoji.user { - Some(user) -> [#("user", user.to_json(user))] - None -> [] - } - - let requires_colons = [ - #("require_colons", json.bool(emoji.requires_colons)), - ] - - let is_managed = [#("managed", json.bool(emoji.is_managed))] - - let is_animated = [#("animated", json.bool(emoji.is_animated))] - - let is_available = [#("available", json.bool(emoji.is_available))] - - [ - id, - name, - role_ids, - user, - requires_colons, - is_managed, - is_animated, - is_available, - ] - |> list.flatten - |> json.object -} - -/// Returns the HTTP GET request to the image URL in WEBP format. -/// Only available for custom emojis, not built-in (unicode) ones. -pub fn request(id id: String) -> Request(String) { - rest.new_cdn_request(to: "/emojis/" <> id <> ".webp", query: [ - // we can do that, if `animated=true`, and the emoji isn't actually animated - // it'll just give us the static version - #("animated", "true"), - ]) -} diff --git a/src/grom/entitlement.gleam b/src/grom/entitlement.gleam deleted file mode 100644 index 3d8459a..0000000 --- a/src/grom/entitlement.gleam +++ /dev/null @@ -1,210 +0,0 @@ -//// An `Entitlement` is something a user is _entitled_ to have. Crazy, right? -//// -//// An example of an entitlement is the license key for the premium version of your bot. -//// See [SKU](sku.html). - -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/option.{type Option, None} -import gleam/result -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/internal/rest -import grom/internal/time_rfc3339 - -// TYPES ----------------------------------------------------------------------- - -pub type Entitlement { - Entitlement( - id: String, - sku_id: String, - application_id: String, - user_id: Option(String), - type_: Type, - is_deleted: Bool, - starts_at: Option(Timestamp), - ends_at: Option(Timestamp), - guild_id: Option(String), - /// Only present on consumable SKUs. - is_consumed: Option(Bool), - ) -} - -pub type Type { - Purchase - PremiumSubscription - DeveloperGift - TestModePurchase - FreePurchase - UserGift - PremiumPurchase - ApplicationSubscription -} - -pub type OwnerType { - GuildOwner - UserOwner -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Entitlement) { - use id <- decode.field("id", decode.string) - use sku_id <- decode.field("sku_id", decode.string) - use application_id <- decode.field("application_id", decode.string) - use user_id <- decode.optional_field( - "user_id", - None, - decode.optional(decode.string), - ) - use type_ <- decode.field("type", type_decoder()) - use is_deleted <- decode.field("deleted", decode.bool) - use starts_at <- decode.field( - "starts_at", - decode.optional(time_rfc3339.decoder()), - ) - use ends_at <- decode.field( - "ends_at", - decode.optional(time_rfc3339.decoder()), - ) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use is_consumed <- decode.optional_field( - "consumed", - None, - decode.optional(decode.bool), - ) - decode.success(Entitlement( - id:, - sku_id:, - application_id:, - user_id:, - type_:, - is_deleted:, - starts_at:, - ends_at:, - guild_id:, - is_consumed:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Purchase) - 2 -> decode.success(PremiumSubscription) - 3 -> decode.success(DeveloperGift) - 4 -> decode.success(TestModePurchase) - 5 -> decode.success(FreePurchase) - 6 -> decode.success(UserGift) - 7 -> decode.success(PremiumPurchase) - 8 -> decode.success(ApplicationSubscription) - _ -> decode.failure(Purchase, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn owner_type_to_json(owner_type: OwnerType) -> Json { - case owner_type { - GuildOwner -> 1 - UserOwner -> 2 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for application_id: String, - id entitlement_id: String, -) -> Result(Entitlement, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" <> application_id <> "/entitlements/" <> entitlement_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn consume( - client: grom.Client, - for application_id: String, - id entitlement_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" - <> application_id - <> "/entitlements/" - <> entitlement_id - <> "/consume", - ) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn create_test( - client: grom.Client, - for application_id: String, - sku sku_id: String, - to owner_id: String, - owner_is owner_type: OwnerType, -) -> Result(Entitlement, grom.Error) { - let json = - json.object([ - #("sku_id", json.string(sku_id)), - #("owner_id", json.string(owner_id)), - #("owner_type", owner_type_to_json(owner_type)), - ]) - - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/applications/" <> application_id <> "/entitlements", - ) - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_test( - client: grom.Client, - for application_id: String, - id entitlement_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/applications/" <> application_id <> "/entitlements/" <> entitlement_id, - ) - |> rest.execute, - ) - - Ok(Nil) -} diff --git a/src/grom/file.gleam b/src/grom/file.gleam deleted file mode 100644 index ddb357c..0000000 --- a/src/grom/file.gleam +++ /dev/null @@ -1,3 +0,0 @@ -pub type File { - File(name: String, content_type: String, content: BitArray) -} diff --git a/src/grom/gateway.gleam b/src/grom/gateway.gleam deleted file mode 100644 index e10fa0e..0000000 --- a/src/grom/gateway.gleam +++ /dev/null @@ -1,3636 +0,0 @@ -import gleam/dynamic/decode -import gleam/erlang/process.{type Subject} -import gleam/float -import gleam/http -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/otp/actor -import gleam/otp/static_supervisor -import gleam/otp/supervision -import gleam/result -import gleam/string -import gleam/time/duration.{type Duration} -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/activity.{type Activity} -import grom/application -import grom/channel.{type Channel} -import grom/channel/thread.{type Thread} -import grom/command -import grom/emoji.{type Emoji} -import grom/entitlement.{type Entitlement} -import grom/gateway/intent.{type Intent} -import grom/guild.{type Guild} -import grom/guild/audit_log -import grom/guild/auto_moderation -import grom/guild/integration.{type Integration} -import grom/guild/role.{type Role} -import grom/guild/scheduled_event.{type ScheduledEvent} -import grom/guild_member.{type GuildMember} -import grom/interaction.{type Interaction} -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/internal/time_timestamp -import grom/invite -import grom/message -import grom/message/reaction -import grom/modification.{type Modification, Skip} -import grom/soundboard -import grom/stage_instance.{type StageInstance} -import grom/sticker.{type Sticker} -import grom/subscription.{type Subscription} -import grom/user.{type User, User} -import grom/voice -import operating_system -import stratus - -// TYPES ----------------------------------------------------------------------- - -pub type Data { - Data( - url: String, - recommended_shards: Int, - session_start_limits: SessionStartLimits, - ) -} - -pub type ReadyApplication { - ReadyApplication(id: String, flags: List(application.Flag)) -} - -pub type Shard { - Shard(id: Int, num_shards: Int) -} - -pub type Event { - ReadyEvent(ReadyMessage) - AllShardsReadyEvent(AllShardsReadyMessage) - ErrorEvent(grom.Error) - ResumedEvent - RateLimitedEvent(RateLimitedMessage) - ApplicationCommandPermissionsUpdatedEvent(command.GuildPermissions) - AutoModerationRuleCreatedEvent(auto_moderation.Rule) - AutoModerationRuleUpdatedEvent(auto_moderation.Rule) - AutoModerationRuleDeletedEvent(auto_moderation.Rule) - AutoModerationActionExecutedEvent(AutoModerationActionExecutedMessage) - ChannelCreatedEvent(Channel) - ChannelUpdatedEvent(Channel) - ChannelDeletedEvent(Channel) - ThreadCreatedEvent(ThreadCreatedMessage) - ThreadUpdatedEvent(Thread) - ThreadDeletedEvent(ThreadDeletedMessage) - ThreadListSyncedEvent(ThreadListSyncedMessage) - /// Could not test decoding. Please report any attempts in issues. - ThreadMemberUpdatedEvent(ThreadMemberUpdatedMessage) - PresenceUpdatedEvent(PresenceUpdatedMessage) - ThreadMembersUpdatedEvent(ThreadMembersUpdatedMessage) - ChannelPinsUpdatedEvent(ChannelPinsUpdatedMessage) - /// Could not test decoding. Please report any attempts in issues. - EntitlementCreatedEvent(Entitlement) - /// Could not test decoding. Please report any attempts in issues. - EntitlementUpdatedEvent(Entitlement) - /// Could not test decoding. Please report any attempts in issues. - EntitlementDeletedEvent(Entitlement) - GuildCreatedEvent(GuildCreatedMessage) - GuildUpdatedEvent(Guild) - /// If `!guild.unavailable`, then the user was removed from the guild. - GuildDeletedEvent(guild.UnavailableGuild) - AuditLogEntryCreatedEvent(AuditLogEntryCreatedMessage) - GuildBanCreatedEvent(GuildBanMessage) - GuildBanDeletedEvent(GuildBanMessage) - GuildEmojisUpdatedEvent(GuildEmojisUpdatedMessage) - GuildStickersUpdatedEvent(GuildStickersUpdatedMessage) - GuildIntegrationsUpdatedEvent(GuildIntegrationsUpdatedMessage) - GuildMemberCreatedEvent(GuildMemberCreatedMessage) - GuildMemberDeletedEvent(GuildMemberDeletedMessage) - GuildMemberUpdatedEvent(GuildMemberUpdatedMessage) - GuildMembersChunkEvent(GuildMembersChunkMessage) - RoleCreatedEvent(RoleCreatedMessage) - RoleUpdatedEvent(RoleUpdatedMessage) - RoleDeletedEvent(RoleDeletedMessage) - ScheduledEventCreatedEvent(ScheduledEvent) - ScheduledEventUpdatedEvent(ScheduledEvent) - ScheduledEventDeletedEvent(ScheduledEvent) - ScheduledEventUserCreatedEvent(ScheduledEventUserMessage) - ScheduledEventUserDeletedEvent(ScheduledEventUserMessage) - GuildSoundboardSoundCreatedEvent(soundboard.Sound) - GuildSoundboardSoundUpdatedEvent(soundboard.Sound) - GuildSoundboardSoundDeletedEvent(GuildSoundboardSoundDeletedMessage) - GuildSoundboardSoundsUpdatedEvent(GuildSoundboardSoundsUpdatedMessage) - /// Sent in response to [`request_soundboard_sounds`](#request_soundboard_sounds). - SoundboardSoundsEvent(SoundboardSoundsMessage) - IntegrationCreatedEvent(IntegrationCreatedMessage) - IntegrationUpdatedEvent(IntegrationUpdatedMessage) - IntegrationDeletedEvent(IntegrationDeletedMessage) - InviteCreatedEvent(InviteCreatedMessage) - InviteDeletedEvent(InviteDeletedMessage) - MessageCreatedEvent(MessageCreatedMessage) - MessageUpdatedEvent(MessageUpdatedMessage) - MessageDeletedEvent(MessageDeletedMessage) - MessagesBulkDeletedEvent(MessagesBulkDeletedMessage) - MessageReactionCreatedEvent(MessageReactionCreatedMessage) - MessageReactionDeletedEvent(MessageReactionDeletedMessage) - MessageAllReactionsDeletedEvent(MessageAllReactionsDeletedMessage) - MessageEmojiReactionsDeletedEvent(MessageEmojiReactionsDeletedMessage) - TypingStartedEvent(TypingStartedMessage) - CurrentUserUpdatedEvent(User) - VoiceChannelEffectSentEvent(VoiceChannelEffectSentMessage) - VoiceStateUpdatedEvent(voice.State) - VoiceServerUpdatedEvent(VoiceServerUpdatedMessage) - InteractionCreatedEvent(Interaction) - StageInstanceCreatedEvent(StageInstance) - StageInstanceUpdatedEvent(StageInstance) - StageInstanceDeletedEvent(StageInstance) - /// Could not test decoding. Please report any attempts in issues. - SubscriptionCreatedEvent(Subscription) - /// Could not test decoding. Please report any attempts in issues. - SubscriptionUpdatedEvent(Subscription) - /// Could not test decoding. Please report any attempts in issues. - SubscriptionDeletedEvent(Subscription) - PollVoteCreatedEvent(PollVoteCreatedMessage) - PollVoteDeletedEvent(PollVoteDeletedMessage) - UnknownEvent -} - -pub type SessionStartLimits { - SessionStartLimits( - maximum_starts: Int, - remaining_starts: Int, - resets_after: Duration, - max_identify_requests_per_5_seconds: Int, - ) -} - -pub type ClientStatus { - ClientStatus( - desktop: Option(String), - mobile: Option(String), - web: Option(String), - ) -} - -// RECEIVE EVENTS -------------------------------------------------------------- - -type ReceivedMessage { - Hello(HelloMessage) - Dispatch(sequence: Int, message: DispatchedMessage) - HeartbeatAcknowledged - HeartbeatRequest - ReconnectRequest - InvalidSession(can_reconnect: Bool) -} - -type HelloMessage { - HelloMessage(heartbeat_interval: Duration) -} - -// RECEIVED DISPATCH EVENTS ---------------------------------------------------- - -type DispatchedMessage { - Ready(ReadyMessage) - Resumed - RateLimited(RateLimitedMessage) - ApplicationCommandPermissionsUpdated(command.GuildPermissions) - AutoModerationRuleCreated(auto_moderation.Rule) - AutoModerationRuleUpdated(auto_moderation.Rule) - AutoModerationRuleDeleted(auto_moderation.Rule) - AutoModerationActionExecuted(AutoModerationActionExecutedMessage) - ChannelCreated(Channel) - ChannelUpdated(Channel) - ChannelDeleted(Channel) - ThreadCreated(ThreadCreatedMessage) - ThreadUpdated(Thread) - ThreadDeleted(ThreadDeletedMessage) - ThreadListSynced(ThreadListSyncedMessage) - /// Fired if the current user's thread member gets updated. - ThreadMemberUpdated(ThreadMemberUpdatedMessage) - PresenceUpdated(PresenceUpdatedMessage) - ThreadMembersUpdated(ThreadMembersUpdatedMessage) - ChannelPinsUpdated(ChannelPinsUpdatedMessage) - EntitlementCreated(Entitlement) - EntitlementUpdated(Entitlement) - EntitlementDeleted(Entitlement) - GuildCreated(GuildCreatedMessage) - GuildUpdated(Guild) - GuildDeleted(guild.UnavailableGuild) - AuditLogEntryCreated(AuditLogEntryCreatedMessage) - GuildBanCreated(GuildBanMessage) - GuildBanDeleted(GuildBanMessage) - GuildEmojisUpdated(GuildEmojisUpdatedMessage) - GuildStickersUpdated(GuildStickersUpdatedMessage) - GuildIntegrationsUpdated(GuildIntegrationsUpdatedMessage) - GuildMemberCreated(GuildMemberCreatedMessage) - GuildMemberDeleted(GuildMemberDeletedMessage) - GuildMemberUpdated(GuildMemberUpdatedMessage) - GuildMembersChunk(GuildMembersChunkMessage) - RoleCreated(RoleCreatedMessage) - RoleUpdated(RoleUpdatedMessage) - RoleDeleted(RoleDeletedMessage) - ScheduledEventCreated(ScheduledEvent) - ScheduledEventUpdated(ScheduledEvent) - ScheduledEventDeleted(ScheduledEvent) - ScheduledEventUserCreated(ScheduledEventUserMessage) - ScheduledEventUserDeleted(ScheduledEventUserMessage) - GuildSoundboardSoundCreated(soundboard.Sound) - GuildSoundboardSoundUpdated(soundboard.Sound) - GuildSoundboardSoundDeleted(GuildSoundboardSoundDeletedMessage) - GuildSoundboardSoundsUpdated(GuildSoundboardSoundsUpdatedMessage) - SoundboardSounds(SoundboardSoundsMessage) - IntegrationCreated(IntegrationCreatedMessage) - IntegrationUpdated(IntegrationUpdatedMessage) - IntegrationDeleted(IntegrationDeletedMessage) - InviteCreated(InviteCreatedMessage) - InviteDeleted(InviteDeletedMessage) - MessageCreated(MessageCreatedMessage) - MessageUpdated(MessageUpdatedMessage) - MessageDeleted(MessageDeletedMessage) - MessagesBulkDeleted(MessagesBulkDeletedMessage) - MessageReactionCreated(MessageReactionCreatedMessage) - MessageReactionDeleted(MessageReactionDeletedMessage) - MessageAllReactionsDeleted(MessageAllReactionsDeletedMessage) - MessageEmojiReactionsDeleted(MessageEmojiReactionsDeletedMessage) - TypingStarted(TypingStartedMessage) - CurrentUserUpdated(User) - VoiceChannelEffectSent(VoiceChannelEffectSentMessage) - VoiceStateUpdated(voice.State) - VoiceServerUpdated(VoiceServerUpdatedMessage) - InteractionCreated(Interaction) - StageInstanceCreated(StageInstance) - StageInstanceUpdated(StageInstance) - StageInstanceDeleted(StageInstance) - SubscriptionCreated(Subscription) - SubscriptionUpdated(Subscription) - SubscriptionDeleted(Subscription) - PollVoteCreated(PollVoteCreatedMessage) - PollVoteDeleted(PollVoteDeletedMessage) - UnknownDispatchedMessage -} - -pub type ReadyMessage { - ReadyMessage( - api_version: Int, - user: User, - guilds: List(guild.UnavailableGuild), - session_id: String, - resume_gateway_url: String, - shard: Option(Shard), - application: ReadyApplication, - ) -} - -pub type AllShardsReadyMessage { - AllShardsReadyMessage( - api_version: Int, - user: User, - guilds: List(guild.UnavailableGuild), - application: ReadyApplication, - shard_count: Int, - ) -} - -pub type RateLimitedMessage { - RateLimitedMessage( - limited_opcode: Int, - retry_after: Duration, - metadata: RateLimitedMetadata, - ) -} - -pub type RateLimitedMetadata { - RequestGuildMembersRateLimited(guild_id: String, nonce: Option(String)) -} - -pub type AutoModerationActionExecutedMessage { - AutoModerationActionExecutedMessage( - guild_id: String, - action: auto_moderation.Action, - rule_id: String, - rule_trigger_type: auto_moderation.TriggerType, - user_id: String, - channel_id: Option(String), - message_id: Option(String), - alert_system_message_id: Option(String), - content: Option(String), - matched_keyword: Option(String), - matched_content: Option(String), - ) -} - -pub type ThreadCreatedMessage { - ThreadCreatedMessage(thread: Thread, is_newly_created: Bool) -} - -pub type ThreadDeletedMessage { - ThreadDeletedMessage( - id: String, - guild_id: String, - parent_id: String, - type_: thread.Type, - ) -} - -pub type ThreadListSyncedMessage { - ThreadListSyncedMessage( - guild_id: String, - /// If `None`, then threads are synced for the entire guild. - channel_ids: Option(List(String)), - threads: List(Thread), - /// A list of thread members for the current user. - members: List(thread.Member), - ) -} - -pub type ThreadMemberUpdatedMessage { - ThreadMemberUpdatedMessage(thread_member: thread.Member, guild_id: String) -} - -pub type PresenceUpdatedMessage { - PresenceUpdatedMessage( - user_id: Option(String), - guild_id: Option(String), - status: Option(String), - activities: Option(List(Activity)), - client_status: Option(ClientStatus), - ) -} - -pub type ThreadMembersUpdatedMessage { - ThreadMembersUpdatedMessage( - id: String, - guild_id: String, - member_count: Int, - added_members: Option( - List(#(thread.Member, Option(PresenceUpdatedMessage))), - ), - removed_member_ids: Option(List(String)), - ) -} - -pub type ChannelPinsUpdatedMessage { - ChannelPinsUpdatedMessage( - guild_id: Option(String), - channel_id: String, - last_pin_timestamp: Option(Timestamp), - ) -} - -pub type GuildCreatedMessage { - GuildCreatedMessage( - guild: Guild, - joined_at: Timestamp, - is_large: Bool, - member_count: Int, - voice_states: List(voice.State), - /// If the guild has over 75k members, this will be only your bot and users in voice channels. - members: List(GuildMember), - channels: List(Channel), - threads: List(Thread), - /// If you don't have the `GuildPresences` intent enabled, or if the guild has over 75k members, this will only have presences for your bot and users in voice channels. - presences: List(PresenceUpdatedMessage), - stage_instances: List(StageInstance), - scheduled_events: List(ScheduledEvent), - soundboard_sounds: List(soundboard.Sound), - ) - UnavailableGuildCreatedMessage(guild.UnavailableGuild) -} - -pub type AuditLogEntryCreatedMessage { - AuditLogEntryCreatedMessage(entry: audit_log.Entry, guild_id: String) -} - -pub type GuildBanMessage { - GuildBanMessage(guild_id: String, user: User) -} - -pub type GuildEmojisUpdatedMessage { - GuildEmojisUpdatedMessage(guild_id: String, emojis: List(Emoji)) -} - -pub type GuildStickersUpdatedMessage { - GuildStickersUpdatedMessage(guild_id: String, stickers: List(Sticker)) -} - -pub type GuildIntegrationsUpdatedMessage { - GuildIntegrationsUpdatedMessage(guild_id: String) -} - -pub type GuildMemberCreatedMessage { - GuildMemberCreatedMessage(guild_id: String, guild_member: GuildMember) -} - -pub type GuildMemberDeletedMessage { - GuildMemberDeletedMessage(guild_id: String, user: User) -} - -pub type GuildMemberUpdatedMessage { - GuildMemberUpdatedMessage( - guild_id: String, - role_ids: List(String), - user: User, - nick: Modification(String), - avatar_hash: Option(String), - banner_hash: Option(String), - joined_at: Option(Timestamp), - premium_since: Option(Timestamp), - is_deaf: Option(Bool), - is_mute: Option(Bool), - is_pending: Option(Bool), - communication_disabled_until: Modification(Timestamp), - flags: Option(List(guild_member.Flag)), - avatar_decoration_data: Modification(user.AvatarDecorationData), - ) -} - -pub type GuildMembersChunkMessage { - GuildMembersChunkMessage( - guild_id: String, - members: List(GuildMember), - chunk_index: Int, - chunk_count: Int, - not_found_ids: Option(List(String)), - presences: Option(List(PresenceUpdatedMessage)), - nonce: Option(String), - ) -} - -pub type RoleCreatedMessage { - RoleCreatedMessage(guild_id: String, role: Role) -} - -pub type RoleUpdatedMessage { - RoleUpdatedMessage(guild_id: String, role: Role) -} - -pub type RoleDeletedMessage { - RoleDeletedMessage(guild_id: String, role_id: String) -} - -pub type ScheduledEventUserMessage { - ScheduledEventUserMessage( - scheduled_event_id: String, - user_id: String, - guild_id: String, - ) -} - -pub type GuildSoundboardSoundDeletedMessage { - GuildSoundboardSoundDeletedMessage(sound_id: String, guild_id: String) -} - -pub type GuildSoundboardSoundsUpdatedMessage { - GuildSoundboardSoundsUpdatedMessage( - soundboard_sounds: List(soundboard.Sound), - guild_id: String, - ) -} - -pub type SoundboardSoundsMessage { - SoundboardSoundsMessage( - soundboard_sounds: List(soundboard.Sound), - guild_id: String, - ) -} - -pub type IntegrationCreatedMessage { - IntegrationCreatedMessage(integration: Integration, guild_id: String) -} - -pub type IntegrationUpdatedMessage { - IntegrationUpdatedMessage(integration: Integration, guild_id: String) -} - -pub type IntegrationDeletedMessage { - IntegrationDeletedMessage( - id: String, - guild_id: String, - application_id: Option(String), - ) -} - -pub type InviteCreatedMessage { - InviteCreatedMessage( - channel_id: String, - code: String, - created_at: Timestamp, - guild_id: Option(String), - inviter: Option(User), - max_age: Duration, - max_uses: Int, - target_type: Option(invite.TargetType), - is_temporary: Bool, - uses: Int, - expires_at: Option(Timestamp), - ) -} - -pub type InviteDeletedMessage { - InviteDeletedMessage( - channel_id: String, - guild_id: Option(String), - code: String, - ) -} - -pub type MessageCreatedMessage { - MessageCreatedMessage( - message: message.Message, - guild_id: Option(String), - member: Option(GuildMember), - mentions: List(User), - ) -} - -pub type MessageUpdatedMessage { - MessageUpdatedMessage( - message: message.Message, - guild_id: Option(String), - member: Option(GuildMember), - mentions: List(User), - ) -} - -pub type MessageDeletedMessage { - MessageDeletedMessage( - id: String, - channel_id: String, - guild_id: Option(String), - ) -} - -pub type MessagesBulkDeletedMessage { - MessagesBulkDeletedMessage( - ids: List(String), - channel_id: String, - guild_id: Option(String), - ) -} - -pub type MessageReactionCreatedMessage { - MessageReactionCreatedMessage( - user_id: String, - channel_id: String, - message_id: String, - guild_id: Option(String), - member: Option(GuildMember), - emoji: Emoji, - message_author_id: Option(String), - is_burst: Bool, - burst_colors: Option(List(String)), - type_: reaction.Type, - ) -} - -pub type MessageReactionDeletedMessage { - MessageReactionDeletedMessage( - user_id: String, - channel_id: String, - message_id: String, - guild_id: Option(String), - emoji: Emoji, - is_burst: Bool, - type_: reaction.Type, - ) -} - -pub type MessageAllReactionsDeletedMessage { - MessageAllReactionsDeletedMessage( - channel_id: String, - message_id: String, - guild_id: Option(String), - ) -} - -pub type MessageEmojiReactionsDeletedMessage { - MessageEmojiReactionsDeletedMessage( - channel_id: String, - message_id: String, - guild_id: Option(String), - emoji: Emoji, - ) -} - -pub type TypingStartedMessage { - TypingStartedMessage( - channel_id: String, - guild_id: Option(String), - user_id: String, - timestamp: Timestamp, - member: Option(GuildMember), - ) -} - -pub type VoiceChannelEffectSentMessage { - VoiceChannelEffectSentMessage( - channel_id: String, - guild_id: String, - user_id: String, - emoji: Option(Emoji), - animation_type: Option(voice.AnimationType), - animation_id: Option(Int), - sound_id: Option(soundboard.SoundId), - sound_volume: Option(Float), - ) -} - -pub type VoiceServerUpdatedMessage { - VoiceServerUpdatedMessage( - token: String, - guild_id: String, - endpoint: Option(String), - ) -} - -pub type PollVoteCreatedMessage { - PollVoteCreatedMessage( - user_id: String, - channel_id: String, - message_id: String, - guild_id: Option(String), - answer_id: Int, - ) -} - -pub type PollVoteDeletedMessage { - PollVoteDeletedMessage( - user_id: String, - channel_id: String, - message_id: String, - guild_id: Option(String), - answer_id: Int, - ) -} - -// USER MESSAGES --------------------------------------------------------------- - -pub type UpdatePresenceMessage { - UpdatePresenceMessage( - /// Only for Idle. - since: Option(Timestamp), - activities: List(Activity), - status: PresenceStatus, - is_afk: Bool, - ) -} - -pub type UpdateVoiceStateMessage { - UpdateVoiceStateMessage( - guild_id: String, - /// Set to `None` if disconnecting. - channel_id: Option(String), - is_self_muted: Bool, - is_self_deafened: Bool, - ) -} - -pub type RequestGuildMembersMessage { - RequestAllGuildMembersMessage( - guild_id: String, - with_presences: Bool, - nonce: Option(String), - ) - RequestGuildMembersByIdsMessage( - guild_id: String, - with_presences: Bool, - nonce: Option(String), - user_ids: List(String), - ) - RequestGuildMembersByQueryMessage( - guild_id: String, - with_presences: Bool, - nonce: Option(String), - query: String, - limit: Int, - ) -} - -pub type PresenceStatus { - Online - DoNotDisturb - Idle - Invisible - Offline -} - -// SEND EVENTS ----------------------------------------------------------------- - -type HeartbeatMessage { - HeartbeatMessage(last_sequence: Option(Int)) -} - -pub type BaseIdentifyMessage { - BaseIdentifyMessage( - token: String, - properties: IdentifyProperties, - supports_compression: Bool, - max_offline_members: Option(Int), - presence: Option(UpdatePresenceMessage), - intents: List(Intent), - ) -} - -type IdentifyMessage { - IdentifyMessage( - shard: Shard, - token: String, - properties: IdentifyProperties, - supports_compression: Bool, - max_offline_members: Option(Int), - presence: Option(UpdatePresenceMessage), - intents: List(Intent), - ) -} - -type ResumeMessage { - ResumeMessage(token: String, session_id: String, last_sequence: Int) -} - -pub type IdentifyProperties { - IdentifyProperties(os: String, browser: String, device: String) -} - -pub opaque type Next(state) { - Continue(state: state) - Stop - StopAbnormal(reason: String) -} - -pub opaque type Builder(state) { - Builder( - identify: BaseIdentifyMessage, - data: Data, - init: fn(Subject(Message)) -> Result(state, String), - handler: fn(state, Event) -> Next(state), - close: fn(state) -> Nil, - shard_count: Option(Int), - ) -} - -type ConnectionManagerMessage { - UpdateWebsocket(to: Subject(stratus.InternalMessage(StratusUserMessage))) - SendUserMessage(message: StratusUserMessage) -} - -type ConnectionManagerState { - ConnectionManagerState( - websocket: Option(Subject(stratus.InternalMessage(StratusUserMessage))), - queued_messages: List(StratusUserMessage), - ) -} - -type Connection { - GettingReady( - gateway_request: Request(String), - manager: Subject(ConnectionManagerMessage), - subject: Subject(Message), - identify: IdentifyMessage, - ) - Welcomed( - gateway_request: Request(String), - manager: Subject(ConnectionManagerMessage), - subject: Subject(Message), - identify: IdentifyMessage, - heartbeat_manager: Subject(HeartbeatManagerMessage), - sequence: Option(Int), - ) - Identified( - gateway_request: Request(String), - manager: Subject(ConnectionManagerMessage), - subject: Subject(Message), - identify: IdentifyMessage, - heartbeat_manager: Subject(HeartbeatManagerMessage), - sequence: Option(Int), - session_id: String, - resume_gateway_url: String, - ) -} - -type StratusUserMessage { - StartSendResume(ResumingInfo) - StartSendHeartbeat - StartHeartbeatInequalityDisconnect - StartIdentify(Connection) - UserMessage(UserMessage) -} - -type UserMessage { - StartPresenceUpdate(UpdatePresenceMessage) - StartVoiceStateUpdate(UpdateVoiceStateMessage) - StartGuildMembersRequest(RequestGuildMembersMessage) - StartSoundboardSoundsRequest(guild_ids: List(String)) -} - -type BucketedShard { - BucketedShard(start_after: Duration, shard: Shard) -} - -type ShardSpawnerMessage { - StartConnection -} - -type Gateway(user_state) { - Gateway( - user_state: user_state, - identify: BaseIdentifyMessage, - data: Data, - shard_count: Int, - event_handler: fn(user_state, Event) -> Next(user_state), - shards: List(#(Shard, Subject(ConnectionManagerMessage))), - all_ready: Option(AllShardsReadyMessage), - ) -} - -// Shards do not need the user's state. -// They do however need to know whether to continue or stop -type NextForShards { - ShardContinue - ShardStop - ShardStopAbnormal(reason: String) -} - -pub opaque type Message { - MessageFromUser(UserMessage) - MessageFromDiscord(event: Event, reply_to: Subject(NextForShards)) - RegisterShard(#(Shard, Subject(ConnectionManagerMessage))) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn data_decoder() -> decode.Decoder(Data) { - use url <- decode.field("url", decode.string) - use recommended_shards <- decode.field("shards", decode.int) - use session_start_limits <- decode.field( - "session_start_limit", - session_start_limits_decoder(), - ) - - decode.success(Data(url:, recommended_shards:, session_start_limits:)) -} - -@internal -pub fn session_start_limits_decoder() -> decode.Decoder(SessionStartLimits) { - use maximum_starts <- decode.field("total", decode.int) - use remaining_starts <- decode.field("remaining", decode.int) - use resets_after <- decode.field( - "reset_after", - time_duration.from_milliseconds_decoder(), - ) - - use max_identify_requests_per_5_seconds <- decode.field( - "max_concurrency", - decode.int, - ) - - decode.success(SessionStartLimits( - maximum_starts:, - remaining_starts:, - resets_after:, - max_identify_requests_per_5_seconds:, - )) -} - -fn message_decoder() -> decode.Decoder(ReceivedMessage) { - use opcode <- decode.field("op", decode.int) - case opcode { - 0 -> { - use sequence <- decode.field("s", decode.int) - use type_ <- decode.field("t", decode.string) - use message <- decode.field("d", dispatched_message_decoder(type_)) - decode.success(Dispatch(sequence:, message:)) - } - 1 -> decode.success(HeartbeatRequest) - 7 -> decode.success(ReconnectRequest) - 9 -> { - use can_reconnect <- decode.field("d", decode.bool) - decode.success(InvalidSession(can_reconnect:)) - } - 10 -> { - use msg <- decode.field("d", hello_event_decoder()) - decode.success(Hello(msg)) - } - 11 -> decode.success(HeartbeatAcknowledged) - _ -> - decode.failure(Hello(HelloMessage(duration.seconds(0))), "ReceivedEvent") - } -} - -fn dispatched_message_decoder( - type_: String, -) -> decode.Decoder(DispatchedMessage) { - case type_ { - "READY" -> decode.map(ready_message_decoder(), Ready) - "RESUMED" -> decode.success(Resumed) - "RATE_LIMITED" -> decode.map(rate_limited_message_decoder(), RateLimited) - "APPLICATION_COMMAND_PERMISSIONS_UPDATE" -> - decode.map( - command.guild_permissions_decoder(), - ApplicationCommandPermissionsUpdated, - ) - "AUTO_MODERATION_RULE_CREATE" -> - decode.map(auto_moderation.rule_decoder(), AutoModerationRuleCreated) - "AUTO_MODERATION_RULE_UPDATE" -> - decode.map(auto_moderation.rule_decoder(), AutoModerationRuleUpdated) - "AUTO_MODERATION_RULE_DELETE" -> - decode.map(auto_moderation.rule_decoder(), AutoModerationRuleDeleted) - "AUTO_MODERATION_ACTION_EXECUTION" -> - decode.map( - auto_moderation_action_executed_message_decoder(), - AutoModerationActionExecuted, - ) - "CHANNEL_CREATE" -> decode.map(channel.decoder(), ChannelCreated) - "CHANNEL_UPDATE" -> decode.map(channel.decoder(), ChannelUpdated) - "CHANNEL_DELETE" -> decode.map(channel.decoder(), ChannelDeleted) - "THREAD_CREATE" -> - decode.map(thread_created_message_decoder(), ThreadCreated) - "THREAD_UPDATE" -> decode.map(thread.decoder(), ThreadUpdated) - "THREAD_DELETE" -> - decode.map(thread_deleted_message_decoder(), ThreadDeleted) - "THREAD_LIST_SYNC" -> - decode.map(thread_list_synced_message_decoder(), ThreadListSynced) - "THREAD_MEMBER_UPDATE" -> - decode.map(thread_member_updated_message_decoder(), ThreadMemberUpdated) - "PRESENCE_UPDATE" -> - decode.map(presence_updated_message_decoder(), PresenceUpdated) - "THREAD_MEMBERS_UPDATE" -> - decode.map(thread_members_updated_message_decoder(), ThreadMembersUpdated) - "CHANNEL_PINS_UPDATE" -> - decode.map(channel_pins_updated_message_decoder(), ChannelPinsUpdated) - "ENTITLEMENT_CREATE" -> - decode.map(entitlement.decoder(), EntitlementCreated) - "ENTITLEMENT_UPDATE" -> - decode.map(entitlement.decoder(), EntitlementUpdated) - "ENTITLEMENT_DELETE" -> - decode.map(entitlement.decoder(), EntitlementDeleted) - "GUILD_CREATE" -> decode.map(guild_created_message_decoder(), GuildCreated) - "GUILD_UPDATE" -> decode.map(guild.decoder(), GuildUpdated) - "GUILD_DELETE" -> - decode.map(guild.unavailable_guild_decoder(), GuildDeleted) - "GUILD_AUDIT_LOG_ENTRY_CREATE" -> - decode.map( - audit_log_entry_created_message_decoder(), - AuditLogEntryCreated, - ) - "GUILD_BAN_ADD" -> decode.map(guild_ban_message_decoder(), GuildBanCreated) - "GUILD_BAN_REMOVE" -> - decode.map(guild_ban_message_decoder(), GuildBanDeleted) - "GUILD_EMOJIS_UPDATE" -> - decode.map(guild_emojis_updated_message_decoder(), GuildEmojisUpdated) - "GUILD_STICKERS_UPDATE" -> - decode.map(guild_stickers_updated_message_decoder(), GuildStickersUpdated) - "GUILD_INTEGRATIONS_UPDATE" -> - decode.map( - guild_integrations_updated_message_decoder(), - GuildIntegrationsUpdated, - ) - "GUILD_MEMBER_ADD" -> - decode.map(guild_member_created_message_decoder(), GuildMemberCreated) - "GUILD_MEMBER_REMOVE" -> - decode.map(guild_member_deleted_message_decoder(), GuildMemberDeleted) - "GUILD_MEMBER_UPDATE" -> - decode.map(guild_member_updated_message_decoder(), GuildMemberUpdated) - "GUILD_MEMBERS_CHUNK" -> - decode.map(guild_members_chunk_message_decoder(), GuildMembersChunk) - "GUILD_ROLE_CREATE" -> - decode.map(role_created_message_decoder(), RoleCreated) - "GUILD_ROLE_UPDATE" -> - decode.map(role_updated_message_decoder(), RoleUpdated) - "GUILD_ROLE_DELETE" -> - decode.map(role_deleted_message_decoder(), RoleDeleted) - "GUILD_SCHEDULED_EVENT_CREATE" -> - decode.map(scheduled_event.decoder(), ScheduledEventCreated) - "GUILD_SCHEDULED_EVENT_UPDATE" -> - decode.map(scheduled_event.decoder(), ScheduledEventUpdated) - "GUILD_SCHEDULED_EVENT_DELETE" -> - decode.map(scheduled_event.decoder(), ScheduledEventDeleted) - "GUILD_SCHEDULED_EVENT_USER_ADD" -> - decode.map( - scheduled_event_user_message_decoder(), - ScheduledEventUserCreated, - ) - "GUILD_SCHEDULED_EVENT_USER_REMOVE" -> - decode.map( - scheduled_event_user_message_decoder(), - ScheduledEventUserDeleted, - ) - "GUILD_SOUNDBOARD_SOUND_CREATE" -> - decode.map(soundboard.sound_decoder(), GuildSoundboardSoundCreated) - "GUILD_SOUNDBOARD_SOUND_UPDATE" -> - decode.map(soundboard.sound_decoder(), GuildSoundboardSoundUpdated) - "GUILD_SOUNDBOARD_SOUND_DELETE" -> - decode.map( - guild_soundboard_sound_deleted_message_decoder(), - GuildSoundboardSoundDeleted, - ) - "GUILD_SOUNDBOARD_SOUNDS_UPDATE" -> - decode.map( - guild_soundboard_sounds_updated_message_decoder(), - GuildSoundboardSoundsUpdated, - ) - "SOUNDBOARD_SOUNDS" -> - decode.map(soundboard_sounds_message_decoder(), SoundboardSounds) - "INTEGRATION_CREATE" -> - decode.map(integration_created_message_decoder(), IntegrationCreated) - "INTEGRATION_UPDATE" -> - decode.map(integration_updated_message_decoder(), IntegrationUpdated) - "INTEGRATION_DELETE" -> - decode.map(integration_deleted_message_decoder(), IntegrationDeleted) - "INVITE_CREATE" -> - decode.map(invite_created_message_decoder(), InviteCreated) - "INVITE_DELETE" -> - decode.map(invite_deleted_message_decoder(), InviteDeleted) - "MESSAGE_CREATE" -> - decode.map(message_created_message_decoder(), MessageCreated) - "MESSAGE_UPDATE" -> - decode.map(message_updated_message_decoder(), MessageUpdated) - "MESSAGE_DELETE" -> - decode.map(message_deleted_message_decoder(), MessageDeleted) - "MESSAGE_DELETE_BULK" -> - decode.map(messages_bulk_deleted_message_decoder(), MessagesBulkDeleted) - "MESSAGE_REACTION_ADD" -> - decode.map( - message_reaction_created_message_decoder(), - MessageReactionCreated, - ) - "MESSAGE_REACTION_REMOVE" -> - decode.map( - message_reaction_deleted_message_decoder(), - MessageReactionDeleted, - ) - "MESSAGE_REACTION_REMOVE_ALL" -> - decode.map( - message_all_reactions_deleted_message_decoder(), - MessageAllReactionsDeleted, - ) - "MESSAGE_REACTION_REMOVE_EMOJI" -> - decode.map( - message_emoji_reactions_deleted_message_decoder(), - MessageEmojiReactionsDeleted, - ) - "TYPING_START" -> - decode.map(typing_started_message_decoder(), TypingStarted) - "USER_UPDATE" -> decode.map(user.decoder(), CurrentUserUpdated) - "VOICE_CHANNEL_EFFECT_SEND" -> - decode.map( - voice_channel_effect_sent_message_decoder(), - VoiceChannelEffectSent, - ) - "VOICE_STATE_UPDATE" -> decode.map(voice.state_decoder(), VoiceStateUpdated) - "VOICE_SERVER_UPDATE" -> - decode.map(voice_server_updated_message_decoder(), VoiceServerUpdated) - "INTERACTION_CREATE" -> - decode.map(interaction.decoder(), InteractionCreated) - "STAGE_INSTANCE_CREATE" -> - decode.map(stage_instance.decoder(), StageInstanceCreated) - "STAGE_INSTANCE_UPDATE" -> - decode.map(stage_instance.decoder(), StageInstanceUpdated) - "STAGE_INSTANCE_DELETE" -> - decode.map(stage_instance.decoder(), StageInstanceDeleted) - "SUBSCRIPTION_CREATE" -> - decode.map(subscription.decoder(), SubscriptionCreated) - "SUBSCRIPTION_UPDATE" -> - decode.map(subscription.decoder(), SubscriptionUpdated) - "SUBSCRIPTION_DELETE" -> - decode.map(subscription.decoder(), SubscriptionDeleted) - "MESSAGE_POLL_VOTE_ADD" -> - decode.map(poll_vote_created_message_decoder(), PollVoteCreated) - "MESSAGE_POLL_VOTE_REMOVE" -> - decode.map(poll_vote_deleted_message_decoder(), PollVoteDeleted) - _ -> decode.success(UnknownDispatchedMessage) - } -} - -@internal -pub fn auto_moderation_action_executed_message_decoder() -> decode.Decoder( - AutoModerationActionExecutedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use action <- decode.field("action", auto_moderation.action_decoder()) - use rule_id <- decode.field("rule_id", decode.string) - use rule_trigger_type <- decode.field( - "rule_trigger_type", - auto_moderation.trigger_type_decoder(), - ) - use user_id <- decode.field("user_id", decode.string) - use channel_id <- decode.optional_field( - "channel_id", - None, - decode.optional(decode.string), - ) - use message_id <- decode.optional_field( - "message_id", - None, - decode.optional(decode.string), - ) - use alert_system_message_id <- decode.optional_field( - "alert_system_message_id", - None, - decode.optional(decode.string), - ) - use content <- decode.optional_field( - "content", - None, - decode.optional(decode.string), - ) - use matched_keyword <- decode.field( - "matched_keyword", - decode.optional(decode.string), - ) - use matched_content <- decode.optional_field( - "matched_content", - None, - decode.optional(decode.string), - ) - - decode.success(AutoModerationActionExecutedMessage( - guild_id:, - action:, - rule_id:, - rule_trigger_type:, - user_id:, - channel_id:, - message_id:, - alert_system_message_id:, - content:, - matched_keyword:, - matched_content:, - )) -} - -@internal -pub fn thread_created_message_decoder() -> decode.Decoder(ThreadCreatedMessage) { - use thread <- decode.then(thread.decoder()) - use is_newly_created <- decode.optional_field( - "newly_created", - False, - decode.bool, - ) - - decode.success(ThreadCreatedMessage(thread:, is_newly_created:)) -} - -@internal -pub fn thread_deleted_message_decoder() -> decode.Decoder(ThreadDeletedMessage) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use parent_id <- decode.field("parent_id", decode.string) - use type_ <- decode.field("type", thread.type_decoder()) - - decode.success(ThreadDeletedMessage(id:, guild_id:, parent_id:, type_:)) -} - -@internal -pub fn thread_list_synced_message_decoder() -> decode.Decoder( - ThreadListSyncedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use channel_ids <- decode.optional_field( - "channel_ids", - None, - decode.optional(decode.list(decode.string)), - ) - use threads <- decode.field("threads", decode.list(thread.decoder())) - use members <- decode.field("members", decode.list(thread.member_decoder())) - - decode.success(ThreadListSyncedMessage( - guild_id:, - channel_ids:, - threads:, - members:, - )) -} - -@internal -pub fn thread_member_updated_message_decoder() -> decode.Decoder( - ThreadMemberUpdatedMessage, -) { - use thread_member <- decode.then(thread.member_decoder()) - use guild_id <- decode.field("guild_id", decode.string) - - decode.success(ThreadMemberUpdatedMessage(thread_member:, guild_id:)) -} - -@internal -pub fn rate_limited_message_decoder() -> decode.Decoder(RateLimitedMessage) { - use limited_opcode <- decode.field("opcode", decode.int) - use retry_after <- decode.field( - "retry_after", - time_duration.from_float_seconds_decoder(), - ) - use metadata <- decode.field("meta", case limited_opcode { - 8 -> request_guild_members_rate_limited_decoder() - _ -> - decode.failure( - RequestGuildMembersRateLimited("", None), - "RateLimitedMetadata", - ) - }) - - decode.success(RateLimitedMessage(limited_opcode:, retry_after:, metadata:)) -} - -@internal -pub fn request_guild_members_rate_limited_decoder() -> decode.Decoder( - RateLimitedMetadata, -) { - use guild_id <- decode.field("guild_id", decode.string) - use nonce <- decode.optional_field( - "nonce", - None, - decode.optional(decode.string), - ) - - decode.success(RequestGuildMembersRateLimited(guild_id:, nonce:)) -} - -fn hello_event_decoder() -> decode.Decoder(HelloMessage) { - use heartbeat_interval <- decode.field( - "heartbeat_interval", - time_duration.from_milliseconds_decoder(), - ) - - decode.success(HelloMessage(heartbeat_interval:)) -} - -@internal -pub fn ready_message_decoder() -> decode.Decoder(ReadyMessage) { - use api_version <- decode.field("v", decode.int) - use user <- decode.field("user", user.decoder()) - use guilds <- decode.field( - "guilds", - decode.list(of: guild.unavailable_guild_decoder()), - ) - use session_id <- decode.field("session_id", decode.string) - use resume_gateway_url <- decode.field("resume_gateway_url", decode.string) - use shard <- decode.optional_field( - "shard", - None, - decode.optional(shard_decoder()), - ) - use application <- decode.field("application", ready_application_decoder()) - - decode.success(ReadyMessage( - api_version:, - user:, - guilds:, - session_id:, - resume_gateway_url:, - shard:, - application:, - )) -} - -@internal -pub fn shard_decoder() -> decode.Decoder(Shard) { - use id <- decode.field(0, decode.int) - use num_shards <- decode.field(1, decode.int) - decode.success(Shard(id:, num_shards:)) -} - -@internal -pub fn ready_application_decoder() -> decode.Decoder(ReadyApplication) { - use id <- decode.field("id", decode.string) - use flags <- decode.field("flags", flags.decoder(application.bits_flags())) - decode.success(ReadyApplication(id:, flags:)) -} - -@internal -pub fn presence_updated_message_decoder() -> decode.Decoder( - PresenceUpdatedMessage, -) { - use user_id <- decode.then(decode.optionally_at( - ["user", "id"], - None, - decode.optional(decode.string), - )) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use status <- decode.optional_field( - "status", - None, - decode.optional(decode.string), - ) - use activities <- decode.optional_field( - "activities", - None, - decode.optional(decode.list(activity.decoder())), - ) - use client_status <- decode.optional_field( - "client_status", - None, - decode.optional(client_status_decoder()), - ) - decode.success(PresenceUpdatedMessage( - user_id:, - guild_id:, - status:, - activities:, - client_status:, - )) -} - -@internal -pub fn client_status_decoder() -> decode.Decoder(ClientStatus) { - use desktop <- decode.optional_field( - "desktop", - None, - decode.optional(decode.string), - ) - use mobile <- decode.optional_field( - "mobile", - None, - decode.optional(decode.string), - ) - use web <- decode.optional_field("web", None, decode.optional(decode.string)) - - decode.success(ClientStatus(desktop:, mobile:, web:)) -} - -@internal -pub fn thread_members_updated_message_decoder() -> decode.Decoder( - ThreadMembersUpdatedMessage, -) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use member_count <- decode.field("member_count", decode.int) - use added_members <- decode.optional_field( - "added_members", - None, - decode.optional( - decode.list({ - use thread_member <- decode.then(thread.member_decoder()) - use presence <- decode.field( - "presence", - decode.optional(presence_updated_message_decoder()), - ) - - decode.success(#(thread_member, presence)) - }), - ), - ) - use removed_member_ids <- decode.optional_field( - "removed_member_ids", - None, - decode.optional(decode.list(decode.string)), - ) - - decode.success(ThreadMembersUpdatedMessage( - id:, - guild_id:, - member_count:, - added_members:, - removed_member_ids:, - )) -} - -@internal -pub fn channel_pins_updated_message_decoder() -> decode.Decoder( - ChannelPinsUpdatedMessage, -) { - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use channel_id <- decode.field("channel_id", decode.string) - use last_pin_timestamp <- decode.optional_field( - "last_pin_timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - - decode.success(ChannelPinsUpdatedMessage( - guild_id:, - channel_id:, - last_pin_timestamp:, - )) -} - -@internal -pub fn guild_created_message_decoder() -> decode.Decoder(GuildCreatedMessage) { - let unavailable_guild_decoder = { - use unavailable_guild <- decode.then(guild.unavailable_guild_decoder()) - decode.success(UnavailableGuildCreatedMessage(unavailable_guild)) - } - - let available_guild_decoder = { - use guild <- decode.then(guild.decoder()) - use joined_at <- decode.field("joined_at", time_rfc3339.decoder()) - use is_large <- decode.field("large", decode.bool) - use member_count <- decode.field("member_count", decode.int) - use voice_states <- decode.field( - "voice_states", - decode.list(voice.state_decoder()), - ) - use members <- decode.field("members", decode.list(guild_member.decoder())) - use channels <- decode.field("channels", decode.list(channel.decoder())) - use threads <- decode.field("threads", decode.list(thread.decoder())) - use presences <- decode.field( - "presences", - decode.list(presence_updated_message_decoder()), - ) - use stage_instances <- decode.field( - "stage_instances", - decode.list(stage_instance.decoder()), - ) - use scheduled_events <- decode.field( - "guild_scheduled_events", - decode.list(scheduled_event.decoder()), - ) - use soundboard_sounds <- decode.field( - "soundboard_sounds", - decode.list(soundboard.sound_decoder()), - ) - - decode.success(GuildCreatedMessage( - guild:, - joined_at:, - is_large:, - member_count:, - voice_states:, - members:, - channels:, - threads:, - presences:, - stage_instances:, - scheduled_events:, - soundboard_sounds:, - )) - } - - decode.one_of(available_guild_decoder, or: [unavailable_guild_decoder]) -} - -@internal -pub fn audit_log_entry_created_message_decoder() -> decode.Decoder( - AuditLogEntryCreatedMessage, -) { - use entry <- decode.then(audit_log.entry_decoder()) - use guild_id <- decode.field("guild_id", decode.string) - - decode.success(AuditLogEntryCreatedMessage(entry:, guild_id:)) -} - -@internal -pub fn guild_ban_message_decoder() -> decode.Decoder(GuildBanMessage) { - use guild_id <- decode.field("guild_id", decode.string) - use user <- decode.field("user", user.decoder()) - - decode.success(GuildBanMessage(guild_id:, user:)) -} - -@internal -pub fn guild_emojis_updated_message_decoder() -> decode.Decoder( - GuildEmojisUpdatedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use emojis <- decode.field("emojis", decode.list(emoji.decoder())) - decode.success(GuildEmojisUpdatedMessage(guild_id:, emojis:)) -} - -@internal -pub fn guild_stickers_updated_message_decoder() -> decode.Decoder( - GuildStickersUpdatedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use stickers <- decode.field("stickers", decode.list(sticker.decoder())) - decode.success(GuildStickersUpdatedMessage(guild_id:, stickers:)) -} - -@internal -pub fn guild_integrations_updated_message_decoder() -> decode.Decoder( - GuildIntegrationsUpdatedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - decode.success(GuildIntegrationsUpdatedMessage(guild_id:)) -} - -@internal -pub fn guild_member_created_message_decoder() -> decode.Decoder( - GuildMemberCreatedMessage, -) { - use guild_member <- decode.then(guild_member.decoder()) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(GuildMemberCreatedMessage(guild_id:, guild_member:)) -} - -@internal -pub fn guild_member_deleted_message_decoder() -> decode.Decoder( - GuildMemberDeletedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use user <- decode.field("user", user.decoder()) - decode.success(GuildMemberDeletedMessage(guild_id:, user:)) -} - -@internal -pub fn guild_member_updated_message_decoder() -> decode.Decoder( - GuildMemberUpdatedMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use role_ids <- decode.field("roles", decode.list(decode.string)) - use user <- decode.field("user", user.decoder()) - use nick <- decode.optional_field( - "nick", - Skip, - modification.decoder(decode.string), - ) - use avatar_hash <- decode.field("avatar", decode.optional(decode.string)) - use banner_hash <- decode.field("banner", decode.optional(decode.string)) - use joined_at <- decode.field( - "joined_at", - decode.optional(time_rfc3339.decoder()), - ) - use premium_since <- decode.optional_field( - "premium_since", - None, - decode.optional(time_rfc3339.decoder()), - ) - use is_deaf <- decode.optional_field( - "deaf", - None, - decode.optional(decode.bool), - ) - use is_mute <- decode.optional_field( - "mute", - None, - decode.optional(decode.bool), - ) - use is_pending <- decode.optional_field( - "pending", - None, - decode.optional(decode.bool), - ) - use communication_disabled_until <- decode.optional_field( - "communication_disabled_until", - Skip, - modification.decoder(time_rfc3339.decoder()), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(guild_member.bits_member_flags())), - ) - use avatar_decoration_data <- decode.optional_field( - "avatar_decoration_data", - Skip, - modification.decoder(user.avatar_decoration_data_decoder()), - ) - decode.success(GuildMemberUpdatedMessage( - guild_id:, - role_ids:, - user:, - nick:, - avatar_hash:, - banner_hash:, - joined_at:, - premium_since:, - is_deaf:, - is_mute:, - is_pending:, - communication_disabled_until:, - flags:, - avatar_decoration_data:, - )) -} - -@internal -pub fn guild_members_chunk_message_decoder() -> decode.Decoder( - GuildMembersChunkMessage, -) { - use guild_id <- decode.field("guild_id", decode.string) - use members <- decode.field("members", decode.list(guild_member.decoder())) - use chunk_index <- decode.field("chunk_index", decode.int) - use chunk_count <- decode.field("chunk_count", decode.int) - use not_found_ids <- decode.optional_field( - "not_found", - None, - decode.optional(decode.list(decode.string)), - ) - use presences <- decode.optional_field( - "presences", - None, - decode.optional(decode.list(presence_updated_message_decoder())), - ) - use nonce <- decode.field("nonce", decode.optional(decode.string)) - decode.success(GuildMembersChunkMessage( - guild_id:, - members:, - chunk_index:, - chunk_count:, - not_found_ids:, - presences:, - nonce:, - )) -} - -@internal -pub fn role_created_message_decoder() -> decode.Decoder(RoleCreatedMessage) { - use guild_id <- decode.field("guild_id", decode.string) - use role <- decode.field("role", role.decoder()) - decode.success(RoleCreatedMessage(guild_id:, role:)) -} - -@internal -pub fn role_updated_message_decoder() -> decode.Decoder(RoleUpdatedMessage) { - use guild_id <- decode.field("guild_id", decode.string) - use role <- decode.field("role", role.decoder()) - decode.success(RoleUpdatedMessage(guild_id:, role:)) -} - -@internal -pub fn role_deleted_message_decoder() -> decode.Decoder(RoleDeletedMessage) { - use guild_id <- decode.field("guild_id", decode.string) - use role_id <- decode.field("role_id", decode.string) - decode.success(RoleDeletedMessage(guild_id:, role_id:)) -} - -@internal -pub fn scheduled_event_user_message_decoder() -> decode.Decoder( - ScheduledEventUserMessage, -) { - use scheduled_event_id <- decode.field( - "guild_scheduled_event_id", - decode.string, - ) - use user_id <- decode.field("user_id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(ScheduledEventUserMessage( - scheduled_event_id:, - user_id:, - guild_id:, - )) -} - -@internal -pub fn guild_soundboard_sound_deleted_message_decoder() -> decode.Decoder( - GuildSoundboardSoundDeletedMessage, -) { - use sound_id <- decode.field("sound_id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(GuildSoundboardSoundDeletedMessage(sound_id:, guild_id:)) -} - -@internal -pub fn guild_soundboard_sounds_updated_message_decoder() -> decode.Decoder( - GuildSoundboardSoundsUpdatedMessage, -) { - use soundboard_sounds <- decode.field( - "soundboard_sounds", - decode.list(soundboard.sound_decoder()), - ) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(GuildSoundboardSoundsUpdatedMessage( - soundboard_sounds:, - guild_id:, - )) -} - -@internal -pub fn soundboard_sounds_message_decoder() -> decode.Decoder( - SoundboardSoundsMessage, -) { - use soundboard_sounds <- decode.field( - "soundboard_sounds", - decode.list(soundboard.sound_decoder()), - ) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(SoundboardSoundsMessage(soundboard_sounds:, guild_id:)) -} - -@internal -pub fn integration_created_message_decoder() -> decode.Decoder( - IntegrationCreatedMessage, -) { - use integration <- decode.then(integration.decoder()) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(IntegrationCreatedMessage(integration:, guild_id:)) -} - -@internal -pub fn integration_updated_message_decoder() -> decode.Decoder( - IntegrationUpdatedMessage, -) { - use integration <- decode.then(integration.decoder()) - use guild_id <- decode.field("guild_id", decode.string) - decode.success(IntegrationUpdatedMessage(integration:, guild_id:)) -} - -@internal -pub fn integration_deleted_message_decoder() -> decode.Decoder( - IntegrationDeletedMessage, -) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use application_id <- decode.optional_field( - "application_id", - None, - decode.optional(decode.string), - ) - - decode.success(IntegrationDeletedMessage(id:, guild_id:, application_id:)) -} - -@internal -pub fn invite_created_message_decoder() -> decode.Decoder(InviteCreatedMessage) { - use channel_id <- decode.field("channel_id", decode.string) - use code <- decode.field("code", decode.string) - use created_at <- decode.field("created_at", time_rfc3339.decoder()) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use inviter <- decode.optional_field( - "inviter", - None, - decode.optional(user.decoder()), - ) - use max_age <- decode.field( - "max_age", - time_duration.from_int_seconds_decoder(), - ) - use max_uses <- decode.field("max_uses", decode.int) - use target_type <- decode.optional_field( - "target_type", - None, - decode.optional(decode.int), - ) - - use target_type <- decode.then(case target_type { - Some(1) -> { - use streaming_user <- decode.field("target_user", user.decoder()) - decode.success(Some(invite.ForStream(streaming_user:))) - } - Some(2) -> { - use application <- decode.field( - "target_application", - application.decoder(), - ) - decode.success(Some(invite.ForEmbeddedApplication(application:))) - } - Some(_) -> - decode.failure( - Some( - invite.ForStream(User( - "", - "", - "", - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - )), - ), - "TargetType", - ) - None -> decode.success(None) - }) - use is_temporary <- decode.field("temporary", decode.bool) - use uses <- decode.field("uses", decode.int) - use expires_at <- decode.field( - "expires_at", - decode.optional(time_rfc3339.decoder()), - ) - decode.success(InviteCreatedMessage( - channel_id:, - code:, - created_at:, - guild_id:, - inviter:, - max_age:, - max_uses:, - target_type:, - is_temporary:, - uses:, - expires_at:, - )) -} - -@internal -pub fn invite_deleted_message_decoder() -> decode.Decoder(InviteDeletedMessage) { - use channel_id <- decode.field("channel_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use code <- decode.field("code", decode.string) - - decode.success(InviteDeletedMessage(channel_id:, guild_id:, code:)) -} - -@internal -pub fn message_created_message_decoder() -> decode.Decoder( - MessageCreatedMessage, -) { - use message <- decode.then(message.decoder()) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - use mentions <- decode.field("mentions", decode.list(user.decoder())) - - decode.success(MessageCreatedMessage(message:, guild_id:, member:, mentions:)) -} - -@internal -pub fn message_updated_message_decoder() -> decode.Decoder( - MessageUpdatedMessage, -) { - use message <- decode.then(message.decoder()) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - use mentions <- decode.field("mentions", decode.list(user.decoder())) - - decode.success(MessageUpdatedMessage(message:, guild_id:, member:, mentions:)) -} - -@internal -pub fn message_deleted_message_decoder() -> decode.Decoder( - MessageDeletedMessage, -) { - use id <- decode.field("id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - - decode.success(MessageDeletedMessage(id:, channel_id:, guild_id:)) -} - -@internal -pub fn messages_bulk_deleted_message_decoder() -> decode.Decoder( - MessagesBulkDeletedMessage, -) { - use ids <- decode.field("ids", decode.list(decode.string)) - use channel_id <- decode.field("channel_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - - decode.success(MessagesBulkDeletedMessage(ids:, channel_id:, guild_id:)) -} - -@internal -pub fn message_reaction_created_message_decoder() -> decode.Decoder( - MessageReactionCreatedMessage, -) { - use user_id <- decode.field("user_id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - use emoji <- decode.field("emoji", emoji.decoder()) - use message_author_id <- decode.optional_field( - "message_author_id", - None, - decode.optional(decode.string), - ) - use is_burst <- decode.field("burst", decode.bool) - use burst_colors <- decode.optional_field( - "burst_colors", - None, - decode.optional(decode.list(decode.string)), - ) - use type_ <- decode.field("type", reaction.type_decoder()) - decode.success(MessageReactionCreatedMessage( - user_id:, - channel_id:, - message_id:, - guild_id:, - member:, - emoji:, - message_author_id:, - is_burst:, - burst_colors:, - type_:, - )) -} - -@internal -pub fn message_reaction_deleted_message_decoder() -> decode.Decoder( - MessageReactionDeletedMessage, -) { - use user_id <- decode.field("user_id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use emoji <- decode.field("emoji", emoji.decoder()) - use is_burst <- decode.field("burst", decode.bool) - use type_ <- decode.field("type", reaction.type_decoder()) - decode.success(MessageReactionDeletedMessage( - user_id:, - channel_id:, - message_id:, - guild_id:, - emoji:, - is_burst:, - type_:, - )) -} - -@internal -pub fn message_all_reactions_deleted_message_decoder() -> decode.Decoder( - MessageAllReactionsDeletedMessage, -) { - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - - decode.success(MessageAllReactionsDeletedMessage( - channel_id:, - message_id:, - guild_id:, - )) -} - -@internal -pub fn message_emoji_reactions_deleted_message_decoder() -> decode.Decoder( - MessageEmojiReactionsDeletedMessage, -) { - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use emoji <- decode.field("emoji", emoji.decoder()) - - decode.success(MessageEmojiReactionsDeletedMessage( - channel_id:, - message_id:, - guild_id:, - emoji:, - )) -} - -@internal -pub fn typing_started_message_decoder() -> decode.Decoder(TypingStartedMessage) { - use channel_id <- decode.field("channel_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use user_id <- decode.field("user_id", decode.string) - use timestamp <- decode.field( - "timestamp", - time_timestamp.from_unix_seconds_decoder(), - ) - use member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - - decode.success(TypingStartedMessage( - channel_id:, - guild_id:, - user_id:, - timestamp:, - member:, - )) -} - -@internal -pub fn voice_channel_effect_sent_message_decoder() -> decode.Decoder( - VoiceChannelEffectSentMessage, -) { - use channel_id <- decode.field("channel_id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use user_id <- decode.field("user_id", decode.string) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji.decoder()), - ) - use animation_type <- decode.optional_field( - "animation_type", - None, - decode.optional(voice.animation_type_decoder()), - ) - use animation_id <- decode.optional_field( - "animation_id", - None, - decode.optional(decode.int), - ) - use sound_id <- decode.optional_field( - "sound_id", - None, - decode.optional(soundboard.sound_id_decoder()), - ) - use sound_volume <- decode.optional_field( - "sound_volume", - None, - decode.optional(decode.float), - ) - decode.success(VoiceChannelEffectSentMessage( - channel_id:, - guild_id:, - user_id:, - emoji:, - animation_type:, - animation_id:, - sound_id:, - sound_volume:, - )) -} - -@internal -pub fn voice_server_updated_message_decoder() -> decode.Decoder( - VoiceServerUpdatedMessage, -) { - use token <- decode.field("token", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use endpoint <- decode.optional_field( - "endpoint", - None, - decode.optional(decode.string), - ) - decode.success(VoiceServerUpdatedMessage(token:, guild_id:, endpoint:)) -} - -@internal -pub fn poll_vote_created_message_decoder() -> decode.Decoder( - PollVoteCreatedMessage, -) { - use user_id <- decode.field("user_id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use answer_id <- decode.field("answer_id", decode.int) - - decode.success(PollVoteCreatedMessage( - user_id:, - channel_id:, - message_id:, - guild_id:, - answer_id:, - )) -} - -@internal -pub fn poll_vote_deleted_message_decoder() -> decode.Decoder( - PollVoteDeletedMessage, -) { - use user_id <- decode.field("user_id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use answer_id <- decode.field("answer_id", decode.int) - - decode.success(PollVoteDeletedMessage( - user_id:, - channel_id:, - message_id:, - guild_id:, - answer_id:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -fn resume_to_json(message: ResumeMessage) -> Json { - json.object([ - #("op", json.int(6)), - #( - "d", - json.object([ - #("token", json.string(message.token)), - #("session_id", json.string(message.session_id)), - #("seq", json.int(message.last_sequence)), - ]), - ), - ]) -} - -fn identify_to_json(msg: IdentifyMessage) -> Json { - let data = { - let token = [#("token", json.string(msg.token))] - - let properties = [ - #("properties", identify_properties_to_json(msg.properties)), - ] - - let supports_compression = [ - #("compress", json.bool(msg.supports_compression)), - ] - - let max_offline_members = case msg.max_offline_members { - Some(threshold) -> [#("large_threshold", json.int(threshold))] - None -> [] - } - - let shard = [ - #("shard", json.array([msg.shard.id, msg.shard.num_shards], json.int)), - ] - - let presence = case msg.presence { - Some(presence) -> [ - #("presence", update_presence_to_json(presence, False)), - ] - None -> [] - } - - let intents = [ - #("intents", flags.to_json(msg.intents, intent.bits_intents())), - ] - - [ - token, - properties, - supports_compression, - max_offline_members, - shard, - presence, - intents, - ] - |> list.flatten - |> json.object - } - - json.object([#("op", json.int(2)), #("d", data)]) -} - -fn identify_properties_to_json(properties: IdentifyProperties) -> Json { - [ - #("os", json.string(properties.os)), - #("browser", json.string(properties.browser)), - #("device", json.string(properties.device)), - ] - |> json.object -} - -fn heartbeat_to_json(heartbeat: HeartbeatMessage) -> Json { - json.object([ - #("op", json.int(1)), - #("d", json.nullable(heartbeat.last_sequence, json.int)), - ]) -} - -fn update_presence_to_json( - msg: UpdatePresenceMessage, - with_opcode: Bool, -) -> Json { - let data = { - let since = case msg.since, msg.status { - Some(timestamp), Idle -> [ - #("since", json.int(time_timestamp.to_unix_milliseconds(timestamp))), - ] - _, _ -> [#("since", json.null())] - } - - let activities = [ - #("activities", json.array(msg.activities, activity.to_json)), - ] - - let status = [#("status", presence_status_to_json(msg.status))] - - let is_afk = [#("afk", json.bool(msg.is_afk))] - - [since, activities, status, is_afk] - |> list.flatten - |> json.object - } - - case with_opcode { - True -> json.object([#("op", json.int(3)), #("d", data)]) - False -> data - } -} - -fn presence_status_to_json(status: PresenceStatus) -> Json { - case status { - Online -> "online" - DoNotDisturb -> "dnd" - Idle -> "idle" - Invisible -> "invisible" - Offline -> "offline" - } - |> json.string -} - -fn request_guild_members_message_to_json( - message: RequestGuildMembersMessage, -) -> Json { - let data = { - let guild_id = [#("guild_id", json.string(message.guild_id))] - - let with_presences = [#("presences", json.bool(message.with_presences))] - - let nonce = case message.nonce { - Some(nonce) -> [#("nonce", json.string(nonce))] - None -> [] - } - - let #(query, limit, user_ids) = case message { - RequestAllGuildMembersMessage(..) -> #( - [#("query", json.string(""))], - [#("limit", json.int(0))], - [], - ) - RequestGuildMembersByIdsMessage(..) -> #([], [], [ - #("user_ids", json.array(message.user_ids, json.string)), - ]) - RequestGuildMembersByQueryMessage(..) -> #( - [#("query", json.string(message.query))], - [#("limit", json.int(message.limit))], - [], - ) - } - - [guild_id, with_presences, nonce, query, limit, user_ids] - |> list.flatten - |> json.object - } - - json.object([#("op", json.int(8)), #("d", data)]) -} - -fn update_voice_state_message_to_json( - message: UpdateVoiceStateMessage, -) -> Json { - json.object([ - #("op", json.int(4)), - #( - "d", - json.object([ - #("guild_id", json.string(message.guild_id)), - #("channel_id", json.nullable(message.channel_id, json.string)), - #("self_mute", json.bool(message.is_self_muted)), - #("self_deaf", json.bool(message.is_self_deafened)), - ]), - ), - ]) -} - -// FUNCTIONS ------------------------------------------------------------------- - -pub fn get_data(client: grom.Client) -> Result(Data, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/gateway/bot") - |> rest.execute, - ) - - response.body - |> json.parse(using: data_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn continue(state: state) -> Next(state) { - Continue(state) -} - -pub fn stop() -> Next(state) { - Stop -} - -pub fn stop_abnormal(reason: String) -> Next(state) { - StopAbnormal(reason:) -} - -/// Only use this function if you have no intention of sending user messages, such as: -/// * `UpdatePresence` -/// * `UpdateVoiceState` -/// * `RequestGuildMembers` -/// * `RequestSoundboardSounds` -@deprecated("The gateway API is broken, won't work and shouldn't be used. -If the only event you need is the InteractionCreated event, look into the HTTP Interactions pattern. -If you're in need of listening to other events, at this point you're out of luck.") -pub fn new( - state: state, - identify: BaseIdentifyMessage, - data: Data, -) -> Builder(state) { - Builder( - identify:, - data:, - init: fn(_) { Ok(state) }, - handler: fn(state, _event) { continue(state) }, - close: fn(_state) { Nil }, - shard_count: None, - ) -} - -/// You should hold the gateway subject in your state. -/// You'll send user messages to that subject. -@deprecated("The gateway API is broken, won't work and shouldn't be used. -If the only event you need is the InteractionCreated event, look into the HTTP Interactions pattern. -If you're in need of listening to other events, at this point you're out of luck.") -pub fn new_with_initializer( - init: fn(Subject(Message)) -> Result(user_state, String), - identify: BaseIdentifyMessage, - data: Data, -) -> Builder(user_state) { - Builder( - identify:, - data:, - init:, - handler: fn(state, _event) { continue(state) }, - close: fn(_state) { Nil }, - shard_count: None, - ) -} - -pub fn on_event( - builder: Builder(state), - do handler: fn(state, Event) -> Next(state), -) -> Builder(state) { - Builder(..builder, handler:) -} - -pub fn on_close( - builder: Builder(state), - do handler: fn(state) -> Nil, -) -> Builder(state) { - Builder(..builder, close: handler) -} - -/// Use this to force the amount of shards to a set number. -/// By default, grom will resort to the recommended shard count given to us from Discord. -/// Unless you have more than 150,000 guilds using your bot, you likely shouldn't use this function. -pub fn with_shards( - builder: Builder(user_state), - count count: Int, -) -> Builder(user_state) { - Builder(..builder, shard_count: Some(count)) -} - -@deprecated("The gateway API is broken, won't work and shouldn't be used. -If the only event you need is the InteractionCreated event, look into the HTTP Interactions pattern. -If you're in need of listening to other events, at this point you're out of luck.") -pub fn start( - builder: Builder(state), -) -> Result(actor.Started(Subject(Message)), actor.StartError) { - let shard_count = case builder.shard_count { - Some(count) -> count - None -> builder.data.recommended_shards - } - - let max_concurrency = - builder.data.session_start_limits.max_identify_requests_per_5_seconds - let supervisor = static_supervisor.new(static_supervisor.OneForOne) - - let shard_ids = - int.range(from: 0, to: shard_count, with: [], run: list.prepend) - |> list.reverse - - let shards = - shard_ids - |> list.map(fn(id) { Shard(id, shard_count) }) - - let bucketed_shards = - shards - |> list.sized_chunk(max_concurrency) - |> list.index_map(fn(shards, bucket) { - shards - |> list.map(fn(shard) { - BucketedShard(duration.seconds(5 * bucket), shard) - }) - }) - - actor.new_with_initialiser(1000, fn(subject) { - use user_state <- result.try(builder.init(subject)) - - let state = - Gateway( - user_state:, - identify: builder.identify, - data: builder.data, - event_handler: builder.handler, - shard_count:, - shards: [], - all_ready: None, - ) - - // i have no idea whether this is actually required - let selector = - process.new_selector() - |> process.select(subject) - - let supervisor = - bucketed_shards - |> list.flatten - |> list.fold(supervisor, fn(supervisor, shard) { - supervisor - |> static_supervisor.add(supervised_shard_spawner( - builder, - shard, - subject, - )) - }) - - use _supervisor <- result.try( - supervisor - |> static_supervisor.start - |> result.map_error(string.inspect), - ) - - actor.initialised(state) - |> actor.selecting(selector) - |> actor.returning(subject) - |> Ok - }) - |> actor.on_message(on_gateway_message) - |> actor.start -} - -/// Returns the ID of the shard that will handle an event from a specified guild. -/// Some things to mind: -/// * You can get the shard count from `gateway.Data` (for the recommended amount of shards), or it could be a number you specified when you created the gateway. -/// * Events which do not have any guild (DMs, presence updates, etc.) will always be handled by shard 0. -/// * The process of distributing events to shards is automated by grom. You can, however, use this function for a health check command. -pub fn get_shard_for_guild( - guild_id id: Int, - shard_count shard_count: Int, -) -> Int { - { id |> int.bitwise_shift_right(22) } % shard_count -} - -fn get_guild_ids_from_user_message( - message: UserMessage, - failure: a, - next: fn(List(Int)) -> a, -) -> a { - case message { - StartPresenceUpdate(_) -> next([0]) - StartVoiceStateUpdate(msg) -> - case int.parse(msg.guild_id) { - Ok(id) -> next([id]) - Error(_) -> failure - } - StartGuildMembersRequest(msg) -> - case int.parse(msg.guild_id) { - Ok(id) -> next([id]) - Error(_) -> failure - } - StartSoundboardSoundsRequest(guild_ids) -> - guild_ids - |> list.filter_map(int.parse) - |> next - } -} - -fn on_gateway_message( - gateway: Gateway(user_state), - message: Message, -) -> actor.Next(Gateway(user_state), a) { - case message { - MessageFromUser(msg) -> { - use guild_ids <- get_guild_ids_from_user_message( - msg, - actor.continue(gateway), - ) - let shard_ids = - list.map(guild_ids, get_shard_for_guild(_, gateway.shard_count)) - - let connections = - gateway.shards - |> list.filter_map(fn(shard) { - let #(shard, subject) = shard - case list.contains(shard_ids, shard.id) { - True -> Ok(subject) - False -> Error(Nil) - } - }) - - connections - |> list.each(fn(connection) { - connection - |> process.send(SendUserMessage(UserMessage(msg))) - }) - - actor.continue(gateway) - } - MessageFromDiscord(ReadyEvent(ready), reply_to) -> { - let all_ready = case gateway.all_ready { - Some(old) -> - AllShardsReadyMessage( - ..old, - guilds: list.append(old.guilds, ready.guilds), - shard_count: old.shard_count + 1, - ) - None -> - AllShardsReadyMessage( - api_version: ready.api_version, - user: ready.user, - guilds: ready.guilds, - application: ready.application, - shard_count: 1, - ) - } - - let next = case all_ready.shard_count == gateway.shard_count { - True -> - gateway.event_handler( - gateway.user_state, - AllShardsReadyEvent(all_ready), - ) - False -> gateway.event_handler(gateway.user_state, ReadyEvent(ready)) - } - - case next { - Continue(state) -> { - process.send(reply_to, ShardContinue) - actor.continue( - Gateway(..gateway, user_state: state, all_ready: Some(all_ready)), - ) - } - Stop -> { - process.send(reply_to, ShardStop) - actor.stop() - } - StopAbnormal(reason) -> { - process.send(reply_to, ShardStopAbnormal(reason)) - actor.stop_abnormal(reason) - } - } - } - MessageFromDiscord(msg, reply_to) -> { - let next = gateway.event_handler(gateway.user_state, msg) - case next { - Continue(state) -> { - process.send(reply_to, ShardContinue) - actor.continue(Gateway(..gateway, user_state: state)) - } - Stop -> { - process.send(reply_to, ShardStop) - actor.stop() - } - StopAbnormal(reason) -> { - process.send(reply_to, ShardStopAbnormal(reason)) - actor.stop_abnormal(reason) - } - } - } - RegisterShard(shard_subject) -> - actor.continue( - Gateway(..gateway, shards: [shard_subject, ..gateway.shards]), - ) - } -} - -fn supervised_shard_spawner( - builder: Builder(user_state), - shard: BucketedShard, - subject: Subject(Message), -) -> supervision.ChildSpecification(Subject(ShardSpawnerMessage)) { - supervision.supervisor(fn() { new_shard_spawner(builder, shard, subject) }) -} - -fn new_shard_spawner( - builder: Builder(user_state), - shard: BucketedShard, - subject: Subject(Message), -) -> Result(actor.Started(Subject(ShardSpawnerMessage)), actor.StartError) { - actor.new_with_initialiser(50, fn(subject) { - let selector = - process.new_selector() - |> process.select(subject) - - let seconds_to_wait = - shard.start_after - |> duration.to_seconds - |> float.multiply(1000.0) - |> float.round - - subject - |> process.send_after(seconds_to_wait, StartConnection) - - actor.initialised(Nil) - |> actor.selecting(selector) - |> actor.returning(subject) - |> Ok - }) - |> actor.on_message(on_shard_spawner_message(builder, shard, subject)) - |> actor.start -} - -fn on_shard_spawner_message( - builder: Builder(user_state), - shard: BucketedShard, - subject: Subject(Message), -) -> fn(a, ShardSpawnerMessage) -> actor.Next(Nil, b) { - fn(_state, message: ShardSpawnerMessage) { - case message { - StartConnection -> { - let result = start_connection(builder, shard, subject) - case result { - Ok(_) -> actor.continue(Nil) - Error(err) -> actor.stop_abnormal(string.inspect(err)) - } - } - } - } -} - -fn start_connection( - builder: Builder(user_state), - shard: BucketedShard, - subject: Subject(Message), -) -> Result(Nil, grom.Error) { - let request_url = - builder.data.url - |> string.replace(each: "wss://", with: "https://") - |> string.append(suffix: "?v=10&encoding=json") - - use request <- result.try( - request.to(request_url) - |> result.replace_error(grom.InvalidGatewayUrl(request_url)), - ) - - use connection_manager <- result.try( - actor.new(ConnectionManagerState(None, [])) - |> actor.on_message(on_connection_manager_message) - |> actor.start - |> result.map_error(grom.CouldNotStartActor), - ) - - let identify = - IdentifyMessage( - shard: shard.shard, - token: builder.identify.token, - properties: builder.identify.properties, - supports_compression: builder.identify.supports_compression, - max_offline_members: builder.identify.max_offline_members, - presence: builder.identify.presence, - intents: builder.identify.intents, - ) - - let initial_connection_state = - GettingReady( - gateway_request: request, - manager: connection_manager.data, - subject:, - identify:, - ) - - use websocket <- result.try( - stratus.new(request:, state: initial_connection_state) - |> stratus.on_message(on_message) - |> stratus.on_close(try_reconnect) - |> stratus.start - |> result.map_error(grom.CouldNotInitializeWebsocketConnection), - ) - - process.send(connection_manager.data, UpdateWebsocket(to: websocket.data)) - process.send(subject, RegisterShard(#(shard.shard, connection_manager.data))) - - Ok(Nil) -} - -fn try_reconnect( - connection_state: Connection, - reason: stratus.CloseReason, -) -> Nil { - let can_reconnect = case reason { - stratus.NotProvided -> True - stratus.Custom(custom_reason) -> { - let code = stratus.get_custom_code(custom_reason) - let allowed_codes = [4000, 4001, 4002, 4003, 4005, 4007, 4008, 4009] - - allowed_codes - |> list.contains(code) - } - _ -> False - } - - case can_reconnect, connection_state { - True, _ -> try_resume(connection_state) - False, Welcomed(heartbeat_manager:, ..) - | False, Identified(heartbeat_manager:, ..) - -> process.send(heartbeat_manager, StopHeartbeats) - False, _ -> Nil - } -} - -type ResumingInfo { - ResumingInfo( - heartbeat_manager: Subject(HeartbeatManagerMessage), - sequence: Option(Int), - resume_gateway_url: String, - session_id: String, - ) -} - -fn try_resume(connection_state: Connection) -> Nil { - case connection_state { - Identified(..) -> - resume( - connection_state, - ResumingInfo( - heartbeat_manager: connection_state.heartbeat_manager, - sequence: connection_state.sequence, - resume_gateway_url: connection_state.resume_gateway_url, - session_id: connection_state.session_id, - ), - ) - _ -> reconnect(connection_state) - } -} - -fn resume(connection_state: Connection, resuming_info: ResumingInfo) -> Nil { - let request_url = - resuming_info.resume_gateway_url - |> string.replace(each: "wss://", with: "https://") - |> string.append(suffix: "?v=10&encoding=json") - - let request_result = - request.to(request_url) - |> result.replace_error(grom.InvalidGatewayUrl(request_url)) - - use request <- - fn(next) { - case request_result { - Ok(request) -> next(request) - Error(_) -> reconnect(connection_state) - } - } - - let websocket_result = - stratus.new(request:, state: connection_state) - |> stratus.on_message(on_message) - |> stratus.on_close(try_reconnect) - |> stratus.start - - // AHHHHHHHHH THIS IS SO FUCKING UGLY - // BUT NO TYPE ANNOTATIONS KILLS MY LSP - // HERE'S THE VERSION WITHOUT THEM - // - // use websocket <- - // fn(next) { - // case websocket_result { - // Ok(websocket) -> next(websocket) - // Error(_) -> reconnect(connection_state) - // } - // } - use - websocket: actor.Started( - Subject(stratus.InternalMessage(StratusUserMessage)), - ) - <- - fn( - next: fn( - actor.Started(Subject(stratus.InternalMessage(StratusUserMessage))), - ) -> Nil, - ) -> Nil { - case websocket_result { - Ok(websocket) -> next(websocket) - Error(_) -> reconnect(connection_state) - } - } - - process.send(connection_state.manager, UpdateWebsocket(to: websocket.data)) - process.send( - connection_state.manager, - SendUserMessage(StartSendResume(resuming_info)), - ) - process.send(resuming_info.heartbeat_manager, StopHeartbeats) -} - -fn reconnect(connection_state: Connection) -> Nil { - case connection_state { - Welcomed(heartbeat_manager:, ..) | Identified(heartbeat_manager:, ..) -> - process.send(heartbeat_manager, StopHeartbeats) - _ -> Nil - } - - let start_result = - stratus.new( - connection_state.gateway_request, - GettingReady( - gateway_request: connection_state.gateway_request, - manager: connection_state.manager, - subject: connection_state.subject, - identify: connection_state.identify, - ), - ) - |> stratus.on_message(on_message) - |> stratus.on_close(try_reconnect) - |> stratus.start - |> result.map_error(grom.CouldNotInitializeWebsocketConnection) - - use - websocket: actor.Started( - Subject(stratus.InternalMessage(StratusUserMessage)), - ) - <- - fn(next) { - case start_result { - Ok(websocket) -> next(websocket) - Error(_) -> Nil - } - } - - process.send(connection_state.manager, UpdateWebsocket(to: websocket.data)) -} - -fn on_connection_manager_message( - current: ConnectionManagerState, - message: ConnectionManagerMessage, -) { - case message { - UpdateWebsocket(to: new) -> { - list.each(current.queued_messages, fn(msg) { - msg - |> stratus.to_user_message - |> process.send(new, _) - }) - - actor.continue( - ConnectionManagerState(websocket: Some(new), queued_messages: []), - ) - } - SendUserMessage(msg) -> - case current.websocket { - Some(ws) -> { - msg - |> stratus.to_user_message - |> process.send(ws, _) - actor.continue(current) - } - None -> - actor.continue( - ConnectionManagerState(..current, queued_messages: [ - msg, - ..current.queued_messages - ]), - ) - } - } -} - -pub fn identify( - client: grom.Client, - intents intents: List(Intent), -) -> BaseIdentifyMessage { - BaseIdentifyMessage( - token: client.token, - properties: IdentifyProperties( - os: operating_system.name(), - browser: "grom", - device: "grom", - ), - supports_compression: False, - max_offline_members: None, - presence: None, - intents:, - ) -} - -pub fn identify_with_presence( - identify: BaseIdentifyMessage, - presence: UpdatePresenceMessage, -) -> BaseIdentifyMessage { - BaseIdentifyMessage(..identify, presence: Some(presence)) -} - -pub fn update_presence( - gateway: Subject(Message), - using message: UpdatePresenceMessage, -) -> Nil { - process.send(gateway, MessageFromUser(StartPresenceUpdate(message))) -} - -pub fn update_voice_state( - gateway: Subject(Message), - using message: UpdateVoiceStateMessage, -) -> Nil { - process.send(gateway, MessageFromUser(StartVoiceStateUpdate(message))) -} - -pub fn request_guild_members( - gateway: Subject(Message), - using message: RequestGuildMembersMessage, -) -> Nil { - process.send(gateway, MessageFromUser(StartGuildMembersRequest(message))) -} - -/// It's possible that this function is buggy, and I cannot test it right now. -/// If you use multiple shards, you might: -/// a) receive multiple SoundboardSounds events with the same data -/// b) receive multiple SoundboardSounds events with different data based on the guild-shard relationship -/// Please report any usage of this function with multiple shards in issues. -pub fn request_soundboard_sounds( - gateway: Subject(Message), - for guild_ids: List(String), -) -> Nil { - process.send( - gateway, - MessageFromUser(StartSoundboardSoundsRequest(guild_ids)), - ) -} - -fn on_message( - connection_state: Connection, - message: stratus.Message(StratusUserMessage), - connection: stratus.Connection, -) -> stratus.Next(Connection, StratusUserMessage) { - case message { - stratus.Text(text_message) -> - on_text_message(connection_state, connection, text_message) - stratus.Binary(_) -> stratus.continue(connection_state) - stratus.User(StartIdentify(true_state)) -> - on_start_identify(true_state, connection) - stratus.User(StartSendHeartbeat) -> - on_start_send_heartbeat(connection_state, connection) - stratus.User(UserMessage(StartGuildMembersRequest(message))) -> - on_start_guild_members_request(connection_state, connection, message) - stratus.User(UserMessage(StartVoiceStateUpdate(message))) -> - on_start_voice_state_update(connection_state, connection, message) - stratus.User(UserMessage(StartPresenceUpdate(message))) -> - on_start_presence_update(connection_state, connection, message) - stratus.User(UserMessage(StartSoundboardSoundsRequest(ids))) -> - on_start_soundboard_sounds_request(connection_state, connection, ids) - stratus.User(StartSendResume(resuming_info)) -> - on_start_resume(connection_state, connection, resuming_info) - stratus.User(StartHeartbeatInequalityDisconnect) -> - on_start_heartbeat_inequality_disconnect(connection_state, connection) - } -} - -fn on_start_heartbeat_inequality_disconnect( - connection_state: Connection, - connection: stratus.Connection, -) -> stratus.Next(Connection, StratusUserMessage) { - let _ = - connection - |> stratus.close(because: stratus.GoingAway(body: <<>>)) - - let _ = reconnect(connection_state) - - stratus.stop() -} - -fn on_start_resume( - connection_state: Connection, - connection: stratus.Connection, - resuming_info: ResumingInfo, -) -> stratus.Next(Connection, a) { - let message = - ResumeMessage( - token: connection_state.identify.token, - session_id: resuming_info.session_id, - // this seems to be a crime - last_sequence: resuming_info.sequence |> option.unwrap(0), - ) - - let send_result = - message - |> resume_to_json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn on_start_guild_members_request( - connection_state: Connection, - connection: stratus.Connection, - msg: RequestGuildMembersMessage, -) -> stratus.Next(Connection, a) { - let send_result = - msg - |> request_guild_members_message_to_json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn on_start_voice_state_update( - connection_state: Connection, - connection: stratus.Connection, - msg: UpdateVoiceStateMessage, -) -> stratus.Next(Connection, a) { - let send_result = - msg - |> update_voice_state_message_to_json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn on_start_presence_update( - connection_state: Connection, - connection: stratus.Connection, - msg: UpdatePresenceMessage, -) -> stratus.Next(Connection, a) { - let send_result = - msg - |> update_presence_to_json(True) - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn on_start_soundboard_sounds_request( - connection_state: Connection, - connection: stratus.Connection, - guild_ids: List(String), -) -> stratus.Next(Connection, a) { - let json = - json.object([ - #("op", json.int(31)), - #("d", json.object([#("guild_ids", json.array(guild_ids, json.string))])), - ]) - - let send_result = - json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn send_error(err: grom.Error, connection_state: Connection) { - let next = - process.call_forever(connection_state.subject, MessageFromDiscord( - ErrorEvent(err), - _, - )) - - case next { - ShardContinue -> stratus.continue(connection_state) - ShardStop -> stratus.stop() - ShardStopAbnormal(reason) -> stratus.stop_abnormal(reason) - } -} - -fn on_text_message( - connection_state: Connection, - connection: stratus.Connection, - text_message: String, -) -> stratus.Next(Connection, StratusUserMessage) { - use message <- - fn(next) { - case parse_message(text_message) { - Ok(msg) -> next(msg) - Error(err) -> send_error(err, connection_state) - } - } - - case message { - Hello(event) -> on_hello_event(connection_state, event) - Dispatch(sequence, message) -> - on_dispatch(connection_state, sequence, message) - HeartbeatAcknowledged -> on_heartbeat_acknowledged(connection_state) - HeartbeatRequest -> on_heartbeat_request(connection_state) - ReconnectRequest -> on_reconnect_request(connection_state, connection) - InvalidSession(can_reconnect) -> - on_invalid_session(connection_state, can_reconnect) - } -} - -fn on_invalid_session( - connection_state: Connection, - can_resume: Bool, -) -> stratus.Next(Connection, a) { - case can_resume, connection_state { - True, Identified(..) -> - resume( - connection_state, - ResumingInfo( - heartbeat_manager: connection_state.heartbeat_manager, - sequence: connection_state.sequence, - resume_gateway_url: connection_state.resume_gateway_url, - session_id: connection_state.session_id, - ), - ) - _, _ -> reconnect(connection_state) - } - stratus.stop() -} - -fn on_reconnect_request( - connection_state: Connection, - connection: stratus.Connection, -) -> stratus.Next(a, b) { - let _ = stratus.close(connection, because: stratus.NotProvided) - - let _ = try_resume(connection_state) - stratus.stop() -} - -fn on_dispatch( - connection_state: Connection, - sequence: Int, - message: DispatchedMessage, -) -> stratus.Next(Connection, a) { - case connection_state { - GettingReady(..) -> stratus.continue(connection_state) - Welcomed(..) -> - case message { - Ready(msg) -> - on_ready( - connection_state, - connection_state.heartbeat_manager, - sequence, - msg, - ) - _ -> stratus.continue(connection_state) - } - Identified(..) -> { - let event = case message { - // technically impossible - Ready(msg) -> ReadyEvent(msg) - - Resumed -> ResumedEvent - RateLimited(msg) -> RateLimitedEvent(msg) - ApplicationCommandPermissionsUpdated(perms) -> - ApplicationCommandPermissionsUpdatedEvent(perms) - AutoModerationRuleCreated(rule) -> AutoModerationRuleCreatedEvent(rule) - AutoModerationRuleUpdated(rule) -> AutoModerationRuleUpdatedEvent(rule) - AutoModerationRuleDeleted(rule) -> AutoModerationRuleDeletedEvent(rule) - AutoModerationActionExecuted(msg) -> - AutoModerationActionExecutedEvent(msg) - ChannelCreated(channel) -> ChannelCreatedEvent(channel) - ChannelUpdated(channel) -> ChannelUpdatedEvent(channel) - ChannelDeleted(channel) -> ChannelDeletedEvent(channel) - ThreadCreated(msg) -> ThreadCreatedEvent(msg) - ThreadUpdated(thread) -> ThreadUpdatedEvent(thread) - ThreadDeleted(msg) -> ThreadDeletedEvent(msg) - ThreadListSynced(msg) -> ThreadListSyncedEvent(msg) - ThreadMemberUpdated(msg) -> ThreadMemberUpdatedEvent(msg) - PresenceUpdated(msg) -> PresenceUpdatedEvent(msg) - ThreadMembersUpdated(msg) -> ThreadMembersUpdatedEvent(msg) - ChannelPinsUpdated(msg) -> ChannelPinsUpdatedEvent(msg) - EntitlementCreated(entitlement) -> EntitlementCreatedEvent(entitlement) - EntitlementUpdated(entitlement) -> EntitlementUpdatedEvent(entitlement) - EntitlementDeleted(entitlement) -> EntitlementDeletedEvent(entitlement) - GuildCreated(msg) -> GuildCreatedEvent(msg) - GuildUpdated(guild) -> GuildUpdatedEvent(guild) - GuildDeleted(guild) -> GuildDeletedEvent(guild) - AuditLogEntryCreated(msg) -> AuditLogEntryCreatedEvent(msg) - GuildBanCreated(msg) -> GuildBanCreatedEvent(msg) - GuildBanDeleted(msg) -> GuildBanDeletedEvent(msg) - GuildEmojisUpdated(msg) -> GuildEmojisUpdatedEvent(msg) - GuildStickersUpdated(msg) -> GuildStickersUpdatedEvent(msg) - GuildIntegrationsUpdated(msg) -> GuildIntegrationsUpdatedEvent(msg) - GuildMemberCreated(msg) -> GuildMemberCreatedEvent(msg) - GuildMemberDeleted(msg) -> GuildMemberDeletedEvent(msg) - GuildMemberUpdated(msg) -> GuildMemberUpdatedEvent(msg) - GuildMembersChunk(msg) -> GuildMembersChunkEvent(msg) - RoleCreated(msg) -> RoleCreatedEvent(msg) - RoleUpdated(msg) -> RoleUpdatedEvent(msg) - RoleDeleted(msg) -> RoleDeletedEvent(msg) - ScheduledEventCreated(event) -> ScheduledEventCreatedEvent(event) - ScheduledEventUpdated(event) -> ScheduledEventUpdatedEvent(event) - ScheduledEventDeleted(event) -> ScheduledEventDeletedEvent(event) - ScheduledEventUserCreated(msg) -> ScheduledEventUserCreatedEvent(msg) - ScheduledEventUserDeleted(msg) -> ScheduledEventUserDeletedEvent(msg) - GuildSoundboardSoundCreated(sound) -> - GuildSoundboardSoundCreatedEvent(sound) - GuildSoundboardSoundUpdated(sound) -> - GuildSoundboardSoundUpdatedEvent(sound) - GuildSoundboardSoundDeleted(msg) -> - GuildSoundboardSoundDeletedEvent(msg) - GuildSoundboardSoundsUpdated(msg) -> - GuildSoundboardSoundsUpdatedEvent(msg) - SoundboardSounds(msg) -> SoundboardSoundsEvent(msg) - IntegrationCreated(msg) -> IntegrationCreatedEvent(msg) - IntegrationUpdated(msg) -> IntegrationUpdatedEvent(msg) - IntegrationDeleted(msg) -> IntegrationDeletedEvent(msg) - InviteCreated(msg) -> InviteCreatedEvent(msg) - InviteDeleted(msg) -> InviteDeletedEvent(msg) - MessageCreated(msg) -> MessageCreatedEvent(msg) - MessageUpdated(msg) -> MessageUpdatedEvent(msg) - MessageDeleted(msg) -> MessageDeletedEvent(msg) - MessagesBulkDeleted(msg) -> MessagesBulkDeletedEvent(msg) - MessageReactionCreated(msg) -> MessageReactionCreatedEvent(msg) - MessageReactionDeleted(msg) -> MessageReactionDeletedEvent(msg) - MessageAllReactionsDeleted(msg) -> MessageAllReactionsDeletedEvent(msg) - MessageEmojiReactionsDeleted(msg) -> - MessageEmojiReactionsDeletedEvent(msg) - TypingStarted(msg) -> TypingStartedEvent(msg) - CurrentUserUpdated(user) -> CurrentUserUpdatedEvent(user) - VoiceChannelEffectSent(msg) -> VoiceChannelEffectSentEvent(msg) - VoiceStateUpdated(voice_state) -> VoiceStateUpdatedEvent(voice_state) - VoiceServerUpdated(msg) -> VoiceServerUpdatedEvent(msg) - InteractionCreated(interaction) -> InteractionCreatedEvent(interaction) - StageInstanceCreated(stage_instance) -> - StageInstanceCreatedEvent(stage_instance) - StageInstanceUpdated(stage_instance) -> - StageInstanceUpdatedEvent(stage_instance) - StageInstanceDeleted(stage_instance) -> - StageInstanceDeletedEvent(stage_instance) - SubscriptionCreated(subscription) -> - SubscriptionCreatedEvent(subscription) - SubscriptionUpdated(subscription) -> - SubscriptionUpdatedEvent(subscription) - SubscriptionDeleted(subscription) -> - SubscriptionDeletedEvent(subscription) - PollVoteCreated(msg) -> PollVoteCreatedEvent(msg) - PollVoteDeleted(msg) -> PollVoteDeletedEvent(msg) - UnknownDispatchedMessage -> UnknownEvent - } - - let next = - process.call_forever(connection_state.subject, MessageFromDiscord( - event, - _, - )) - - case next { - ShardContinue -> - stratus.continue( - Identified(..connection_state, sequence: Some(sequence)), - ) - ShardStop -> stratus.stop() - ShardStopAbnormal(reason) -> stratus.stop_abnormal(reason) - } - } - } -} - -fn on_ready( - connection_state: Connection, - heartbeat_manager: Subject(HeartbeatManagerMessage), - sequence: Int, - message: ReadyMessage, -) -> stratus.Next(Connection, a) { - let next = - process.call_forever(connection_state.subject, MessageFromDiscord( - ReadyEvent(message), - _, - )) - - let new_state = - Identified( - gateway_request: connection_state.gateway_request, - manager: connection_state.manager, - subject: connection_state.subject, - identify: connection_state.identify, - heartbeat_manager:, - sequence: Some(sequence), - session_id: message.session_id, - resume_gateway_url: message.resume_gateway_url, - ) - - case next { - ShardContinue -> stratus.continue(new_state) - ShardStop -> stratus.stop() - ShardStopAbnormal(reason) -> stratus.stop_abnormal(reason) - } -} - -fn on_heartbeat_request( - connection_state: Connection, -) -> stratus.Next(Connection, a) { - start_send_heartbeat(connection_state) - stratus.continue(connection_state) -} - -fn on_heartbeat_acknowledged( - connection_state: Connection, -) -> stratus.Next(Connection, a) { - case connection_state { - GettingReady(..) -> stratus.continue(connection_state) - Welcomed(heartbeat_manager:, ..) | Identified(heartbeat_manager:, ..) -> { - process.send(heartbeat_manager, HeartbeatAck) - stratus.continue(connection_state) - } - } -} - -fn on_hello_event( - connection_state: Connection, - event: HelloMessage, -) -> stratus.Next(Connection, a) { - // Stop any existing heartbeat manager before creating a new one. - // This is necessary when reconnecting or resuming, where a heartbeat manager - // from the previous connection may still be running. - case connection_state { - Welcomed(heartbeat_manager:, ..) | Identified(heartbeat_manager:, ..) -> - process.send(heartbeat_manager, StopHeartbeats) - GettingReady(..) -> Nil - } - - let heartbeat_start_result = - start_heartbeats( - every: event.heartbeat_interval, - send_to: connection_state.manager, - ) - - case heartbeat_start_result { - Ok(heartbeat_manager) -> - case connection_state { - GettingReady(..) | Welcomed(..) -> { - // Fresh connection: transition to Welcomed and send IDENTIFY. - let state = - Welcomed( - gateway_request: connection_state.gateway_request, - identify: connection_state.identify, - subject: connection_state.subject, - manager: connection_state.manager, - heartbeat_manager: heartbeat_manager.data, - sequence: None, - ) - send_identify(state) - stratus.continue(state) - } - Identified(..) -> { - // Resume: keep the Identified state (preserving session_id, - // resume_gateway_url, and sequence) and only replace the - // heartbeat manager. The RESUME payload was already queued - // in resume(), so we must not send IDENTIFY here. - stratus.continue( - Identified( - ..connection_state, - heartbeat_manager: heartbeat_manager.data, - ), - ) - } - } - Error(err) -> send_error(err, connection_state) - } -} - -fn send_identify(connection_state: Connection) -> Nil { - process.send( - connection_state.manager, - SendUserMessage(StartIdentify(connection_state)), - ) -} - -fn on_start_identify( - connection_state: Connection, - connection: stratus.Connection, -) -> stratus.Next(Connection, a) { - let send_result = - connection_state.identify - |> identify_to_json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> stratus.continue(connection_state) - Error(err) -> send_error(err, connection_state) - } -} - -fn start_send_heartbeat(connection_state: Connection) -> Nil { - process.send(connection_state.manager, SendUserMessage(StartSendHeartbeat)) -} - -fn on_start_send_heartbeat( - connection_state: Connection, - connection: stratus.Connection, -) -> stratus.Next(Connection, a) { - use heartbeat_manager, sequence <- - fn(next) { - case connection_state { - GettingReady(..) -> stratus.continue(connection_state) - Welcomed(sequence:, heartbeat_manager:, ..) - | Identified(sequence:, heartbeat_manager:, ..) -> - next(heartbeat_manager, sequence) - } - } - - let send_result = - sequence - |> HeartbeatMessage - |> heartbeat_to_json - |> json.to_string - |> stratus.send_text_message(connection, _) - |> result.map_error(grom.CouldNotSendEvent) - - case send_result { - Ok(_) -> { - process.send(heartbeat_manager, HeartbeatSent) - stratus.continue(connection_state) - } - Error(err) -> send_error(err, connection_state) - } -} - -fn parse_message(text_message: String) -> Result(ReceivedMessage, grom.Error) { - text_message - |> json.parse(using: message_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -// HEARTBEATS ------------------------------------------------------------------ - -type HeartbeatManagerMessage { - HeartbeatSent - HeartbeatAck - ResetHeartbeatCount - GetHeartbeatCount(reply_to: Subject(HeartbeatCount)) - SendHeartbeat - StopHeartbeats -} - -type HeartbeatState { - HeartbeatState( - interval: Duration, - subject: Subject(HeartbeatManagerMessage), - count: HeartbeatCount, - connection_manager: Subject(ConnectionManagerMessage), - ) -} - -type HeartbeatCount { - HeartbeatCount(heartbeat: Int, heartbeat_ack: Int) -} - -fn start_heartbeats( - every interval: Duration, - send_to connection_manager: Subject(ConnectionManagerMessage), -) -> Result(actor.Started(Subject(HeartbeatManagerMessage)), grom.Error) { - let initial_wait = - interval - |> duration.to_seconds - |> float.multiply(heartbeat_jitter()) - |> float.round - |> duration.seconds - - actor.new_with_initialiser(20, fn(subject) { - let selector = - process.new_selector() - |> process.select(subject) - - subject - |> process.send_after( - initial_wait - |> duration.to_seconds - |> float.multiply(1000.0) - |> float.round, - SendHeartbeat, - ) - - actor.initialised(HeartbeatState( - interval:, - subject:, - connection_manager:, - count: HeartbeatCount(0, 0), - )) - |> actor.selecting(selector) - |> actor.returning(subject) - |> Ok - }) - |> actor.on_message(on_heartbeat_manager_message) - |> actor.start - |> result.map_error(grom.CouldNotStartActor) -} - -fn on_heartbeat_manager_message( - current: HeartbeatState, - message: HeartbeatManagerMessage, -) -> actor.Next(HeartbeatState, a) { - case message { - HeartbeatSent -> - actor.continue( - HeartbeatState( - ..current, - count: HeartbeatCount( - ..current.count, - heartbeat: current.count.heartbeat + 1, - ), - ), - ) - HeartbeatAck -> - actor.continue( - HeartbeatState( - ..current, - count: HeartbeatCount( - ..current.count, - heartbeat_ack: current.count.heartbeat_ack + 1, - ), - ), - ) - SendHeartbeat -> { - use <- check_heartbeat_equality(current) - - // tell stratus to actually send it via the websocket - process.send( - current.connection_manager, - SendUserMessage(StartSendHeartbeat), - ) - - // schedule sending the next heartbeat - process.send_after( - current.subject, - current.interval - |> duration.to_seconds - |> float.multiply(1000.0) - |> float.round, - SendHeartbeat, - ) - - // count will be increased when it actually sends, this just tells stratus to send it, it can fail - actor.continue(current) - } - GetHeartbeatCount(reply_to: requester) -> { - actor.send(requester, current.count) - actor.continue(current) - } - ResetHeartbeatCount -> - actor.continue(HeartbeatState(..current, count: HeartbeatCount(0, 0))) - StopHeartbeats -> actor.stop() - } -} - -fn check_heartbeat_equality( - current: HeartbeatState, - next: fn() -> actor.Next(a, b), -) -> actor.Next(a, b) { - case current.count.heartbeat == current.count.heartbeat_ack { - True -> next() - False -> { - process.send( - current.connection_manager, - SendUserMessage(StartHeartbeatInequalityDisconnect), - ) - actor.stop() - } - } -} - -// sounds better -const heartbeat_jitter: fn() -> Float = float.random diff --git a/src/grom/gateway/intent.gleam b/src/grom/gateway/intent.gleam deleted file mode 100644 index cc807ab..0000000 --- a/src/grom/gateway/intent.gleam +++ /dev/null @@ -1,103 +0,0 @@ -import gleam/int - -// TYPES ----------------------------------------------------------------------- - -pub type Intent { - Guilds - GuildMembers - GuildModeration - GuildExpressions - GuildIntegrations - GuildWebhooks - GuildInvites - GuildVoiceStates - GuildPresences - GuildMessages - GuildMessageReactions - GuildMessageTyping - DirectMessages - DirectMessageReactions - DirectMessageTyping - MessageContent - GuildScheduledEvents - AutoModerationConfiguration - AutoModerationExecution - GuildMessagePolls - DirectMessagePolls -} - -// CONSTANTS ------------------------------------------------------------------- - -pub const all = [ - Guilds, - GuildMembers, - GuildModeration, - GuildExpressions, - GuildIntegrations, - GuildWebhooks, - GuildInvites, - GuildVoiceStates, - GuildPresences, - GuildMessages, - GuildMessageReactions, - GuildMessageTyping, - DirectMessages, - DirectMessageReactions, - DirectMessageTyping, - MessageContent, - GuildScheduledEvents, - AutoModerationConfiguration, - AutoModerationExecution, - GuildMessagePolls, - DirectMessagePolls, -] - -pub const all_unprivileged = [ - Guilds, - GuildModeration, - GuildExpressions, - GuildIntegrations, - GuildWebhooks, - GuildInvites, - GuildVoiceStates, - GuildMessages, - GuildMessageReactions, - GuildMessageTyping, - DirectMessages, - DirectMessageReactions, - DirectMessageTyping, - GuildScheduledEvents, - AutoModerationConfiguration, - AutoModerationExecution, - GuildMessagePolls, - DirectMessagePolls, -] - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_intents() -> List(#(Int, Intent)) { - [ - #(int.bitwise_shift_left(1, 0), Guilds), - #(int.bitwise_shift_left(1, 1), GuildMembers), - #(int.bitwise_shift_left(1, 2), GuildModeration), - #(int.bitwise_shift_left(1, 3), GuildExpressions), - #(int.bitwise_shift_left(1, 4), GuildIntegrations), - #(int.bitwise_shift_left(1, 5), GuildWebhooks), - #(int.bitwise_shift_left(1, 6), GuildInvites), - #(int.bitwise_shift_left(1, 7), GuildVoiceStates), - #(int.bitwise_shift_left(1, 8), GuildPresences), - #(int.bitwise_shift_left(1, 9), GuildMessages), - #(int.bitwise_shift_left(1, 10), GuildMessageReactions), - #(int.bitwise_shift_left(1, 11), GuildMessageTyping), - #(int.bitwise_shift_left(1, 12), DirectMessages), - #(int.bitwise_shift_left(1, 13), DirectMessageReactions), - #(int.bitwise_shift_left(1, 14), DirectMessageTyping), - #(int.bitwise_shift_left(1, 15), MessageContent), - #(int.bitwise_shift_left(1, 16), GuildScheduledEvents), - #(int.bitwise_shift_left(1, 20), AutoModerationConfiguration), - #(int.bitwise_shift_left(1, 21), AutoModerationExecution), - #(int.bitwise_shift_left(1, 24), GuildMessagePolls), - #(int.bitwise_shift_left(1, 25), DirectMessagePolls), - ] -} diff --git a/src/grom/guild.gleam b/src/grom/guild.gleam deleted file mode 100644 index 059c8cd..0000000 --- a/src/grom/guild.gleam +++ /dev/null @@ -1,2206 +0,0 @@ -import gleam/bool -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string -import gleam/time/duration.{type Duration} -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/channel/thread.{type Thread} -import grom/emoji.{type Emoji} -import grom/guild/auto_moderation -import grom/guild/integration.{type Integration} -import grom/guild/role.{type Role} -import grom/guild/scheduled_event.{type ScheduledEvent} -import grom/guild_member.{type GuildMember} -import grom/image -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/modification.{type Modification, Skip} -import grom/soundboard -import grom/sticker.{type Sticker} -import grom/user.{type User} -import grom/voice -import multipart_form -import multipart_form/field - -// TYPES ---------------------------------------------------------------------- - -pub type Guild { - Guild( - id: String, - name: String, - icon: Option(String), - icon_hash: Option(String), - splash: Option(String), - discovery_splash: Option(String), - is_current_user_owner: Option(Bool), - owner_id: String, - current_user_permissions: Option(String), - afk_channel_id: Option(String), - // TODO: Maybe make this a Duration - afk_timeout: Int, - is_widget_enabled: Option(Bool), - widget_channel_id: Option(String), - verification_level: VerificationLevel, - default_message_notification_setting: DefaultMessageNotificationSetting, - explicit_content_filter_setting: ExplicitContentFilterSetting, - roles: List(Role), - emojis: List(Emoji), - features: List(Feature), - mfa_level: MfaLevel, - application_id: Option(String), - system_channel_id: Option(String), - system_channel_flags: List(SystemChannelFlag), - rules_channel_id: Option(String), - max_presences: Option(Int), - max_members: Option(Int), - vanity_url_code: Option(String), - description: Option(String), - banner_hash: Option(String), - premium_tier: PremiumTier, - premium_subscription_count: Option(Int), - preferred_locale: String, - public_updates_channel_id: Option(String), - max_video_channel_users: Option(Int), - max_stage_video_channel_users: Option(Int), - approximate_member_count: Option(Int), - approximate_presence_count: Option(Int), - welcome_screen: Option(WelcomeScreen), - nsfw_level: NsfwLevel, - stickers: Option(List(Sticker)), - is_premium_progress_bar_enabled: Bool, - safety_alerts_channel_id: Option(String), - incidents_data: Option(IncidentsData), - ) -} - -pub type UnavailableGuild { - UnavailableGuild(id: String, unavailable: Bool) -} - -pub type IncidentsData { - IncidentsData( - invites_disabled_until: Option(Timestamp), - dms_disabled_until: Option(Timestamp), - dms_spam_disabled_at: Option(Timestamp), - raid_detected_at: Option(Timestamp), - ) -} - -pub type DefaultMessageNotificationSetting { - NotifyForAllMessages - NotifyOnlyForMentions -} - -pub type VerificationLevel { - NoVerification - LowVerification - MediumVerification - HighVerification - VeryHighVerification -} - -pub type ExplicitContentFilterSetting { - ExplicitContentFilterDisabled - ExplicitContentFilterForMembersWithoutRoles - ExplicitContentFilterForAllMembers -} - -pub type MfaLevel { - NoMfaRequired - MfaRequired -} - -pub type Feature { - HasAnimatedBanner - HasAnimatedIcon - UsesOldPermissionConfigurationBehavior - UsesAutoModeration - HasBanner - IsCommunity - EnabledMonetization - HasRoleSubscriptionPromotionPage - IsDeveloperSupportServer - IsDiscoverable - IsFeaturable - HasInvitesDisabled - CanSetInviteSplash - HasMemberVerificationGateEnabled - HasMoreSoundboardSounds - HasMoreStickers - CanCreateAnnouncementChannels - IsPartnered - CanBePreviewed - HasRaidAlertsDisabled - CanSetRoleIcons - HasRoleSubscriptionsAvailableForPurchase - HasRoleSubscriptionsEnabled - CreatedSoundboardSounds - HasTicketedEventsEnabled - CanSetVanityUrl - IsVerified - CanSet384KbpsBitrate - HasWelcomeScreenEnabled - CanHaveGuestInvites - CanSetEnhancedRoleColors - /// Workaround for undocumented guild features. - UnknownFeature -} - -pub type SystemChannelFlag { - SuppressJoinNotifications - SuppressPremiumSubscriptionNotifications - SuppressGuildReminderNotifications - SuppressJoinNotificationReplies - SuppressRoleSubscriptionPurchaseNotifications - SuppressRoleSubscriptionPurchaseNotificationReplies -} - -pub type PremiumTier { - NoPremiumTier - PremiumTier1 - PremiumTier2 - PremiumTier3 -} - -pub type NsfwLevel { - NsfwLevelDefault - NsfwExplicit - NsfwSafe - NsfwAgeRestricted -} - -pub type Preview { - Preview( - id: String, - name: String, - icon_hash: Option(String), - splash_hash: Option(String), - discovery_splash_hash: Option(String), - emojis: List(Emoji), - features: List(Feature), - approximate_member_count: Int, - approximate_presence_count: Int, - description: Option(String), - stickers: List(Sticker), - ) -} - -pub type Ban { - Ban(reason: Option(String), user: User) -} - -pub type Template { - Template( - code: String, - name: String, - description: Option(String), - usage_count: Int, - creator_id: String, - creator: User, - created_at: Timestamp, - updated_at: Timestamp, - source_guild_id: String, - is_dirty: Option(Bool), - ) -} - -pub type WelcomeScreen { - WelcomeScreen( - description: Option(String), - welcome_channels: List(WelcomeChannel), - ) -} - -pub type WelcomeChannel { - WelcomeChannel( - channel_id: String, - description: String, - emoji_id: Option(String), - emoji_name: Option(String), - ) -} - -pub type Modify { - Modify( - name: Option(String), - verification_level: Option(VerificationLevel), - default_message_notification_setting: Option( - DefaultMessageNotificationSetting, - ), - explicit_content_filter_setting: Option(ExplicitContentFilterSetting), - afk_channel_id: Modification(String), - afk_timeout: Option(Int), - icon: Modification(image.Data), - owner_id: Option(String), - splash: Modification(image.Data), - discovery_splash: Modification(image.Data), - banner: Modification(image.Data), - system_channel_id: Modification(String), - system_channel_flags: Option(List(SystemChannelFlag)), - rules_channel_id: Modification(String), - public_updates_channel_id: Modification(String), - preferred_locale: Modification(String), - features: Option(List(Feature)), - description: Modification(String), - is_premium_progress_bar_enabled: Option(Bool), - safety_alerts_channel_id: Modification(String), - ) -} - -pub type ReceivedThreads { - ReceivedThreads(threads: List(Thread), current_members: List(thread.Member)) -} - -pub type BulkBanResponse { - BulkBanResponse( - banned_users_ids: List(String), - failed_users_ids: List(String), - ) -} - -pub type RoleToMove { - RoleToMove(id: String, position: Option(Int)) -} - -pub type Prune { - Prune(days: Int, compute_pruned_count: Bool, include_role_ids: List(String)) -} - -pub type PruneResponse { - PruneResponse(pruned_count: Option(Int)) -} - -pub type VanityInvite { - VanityInvite(code: Option(String), uses: Int) -} - -pub type ModifyWelcomeScreen { - ModifyWelcomeScreen( - is_enabled: Option(Bool), - welcome_channels: Modification(List(WelcomeChannel)), - description: Modification(String), - ) -} - -pub type ModifyIncidentActions { - ModifyIncidentActions( - invites_disabled_until: Modification(Timestamp), - dms_disabled_until: Modification(Timestamp), - ) -} - -pub type CreateSoundboardSound { - CreateSoundboardSound( - name: String, - sound: soundboard.Data, - volume: Option(Float), - emoji_id: Option(String), - emoji_name: Option(String), - ) -} - -pub type ModifySoundboardSound { - ModifySoundboardSound( - name: Option(String), - volume: Modification(Float), - emoji_id: Modification(String), - emoji_name: Modification(String), - ) -} - -pub type CreateSticker { - CreateSticker( - name: String, - description: String, - tags: String, - file: sticker.File, - ) -} - -pub type ModifySticker { - ModifySticker( - name: Option(String), - description: Modification(String), - tags: Option(String), - ) -} - -pub type IconFormat { - PngIcon - JpegIcon - WebpIcon - GifIcon -} - -pub type SplashFormat { - PngSplash - JpegSplash - WebpSplash -} - -pub type BannerFormat { - PngBanner - JpegBanner - WebpBanner - GifBanner -} - -pub type TagBadgeFormat { - PngTagBadge - JpegTagBadge - WebpTagBadge -} - -// FLAGS ------------------------------------------------------------------ - -@internal -pub fn bits_system_channel_flags() -> List(#(Int, SystemChannelFlag)) { - [ - #(int.bitwise_shift_left(1, 0), SuppressJoinNotifications), - #(int.bitwise_shift_left(1, 1), SuppressPremiumSubscriptionNotifications), - #(int.bitwise_shift_left(1, 2), SuppressGuildReminderNotifications), - #(int.bitwise_shift_left(1, 3), SuppressJoinNotificationReplies), - #( - int.bitwise_shift_left(1, 4), - SuppressRoleSubscriptionPurchaseNotifications, - ), - #( - int.bitwise_shift_left(1, 5), - SuppressRoleSubscriptionPurchaseNotificationReplies, - ), - ] -} - -// DECODERS ------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Guild) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use icon <- decode.field("icon", decode.optional(decode.string)) - use icon_hash <- decode.optional_field( - "icon_hash", - None, - decode.optional(decode.string), - ) - use splash <- decode.field("splash", decode.optional(decode.string)) - use discovery_splash <- decode.field( - "discovery_splash", - decode.optional(decode.string), - ) - use is_current_user_owner <- decode.optional_field( - "owner", - None, - decode.optional(decode.bool), - ) - use owner_id <- decode.field("owner_id", decode.string) - use current_user_permissions <- decode.optional_field( - "permissions", - None, - decode.optional(decode.string), - ) - use afk_channel_id <- decode.field( - "afk_channel_id", - decode.optional(decode.string), - ) - use afk_timeout <- decode.field("afk_timeout", decode.int) - use is_widget_enabled <- decode.optional_field( - "widget_enabled", - None, - decode.optional(decode.bool), - ) - use widget_channel_id <- decode.optional_field( - "widget_channel_id", - None, - decode.optional(decode.string), - ) - use verification_level <- decode.field( - "verification_level", - verification_level_decoder(), - ) - use default_message_notification_setting <- decode.field( - "default_message_notifications", - default_message_notification_setting_decoder(), - ) - use explicit_content_filter_setting <- decode.field( - "explicit_content_filter", - explicit_content_filter_setting_decoder(), - ) - use roles <- decode.field("roles", decode.list(role.decoder())) - use emojis <- decode.field("emojis", decode.list(emoji.decoder())) - use features <- decode.field("features", decode.list(feature_decoder())) - use mfa_level <- decode.field("mfa_level", mfa_level_decoder()) - use application_id <- decode.field( - "application_id", - decode.optional(decode.string), - ) - use system_channel_id <- decode.field( - "system_channel_id", - decode.optional(decode.string), - ) - use system_channel_flags <- decode.field( - "system_channel_flags", - flags.decoder(bits_system_channel_flags()), - ) - use rules_channel_id <- decode.field( - "rules_channel_id", - decode.optional(decode.string), - ) - use max_presences <- decode.optional_field( - "max_presences", - None, - decode.optional(decode.int), - ) - use max_members <- decode.optional_field( - "max_members", - None, - decode.optional(decode.int), - ) - use vanity_url_code <- decode.field( - "vanity_url_code", - decode.optional(decode.string), - ) - use description <- decode.field("description", decode.optional(decode.string)) - use banner_hash <- decode.field("banner", decode.optional(decode.string)) - use premium_tier <- decode.field("premium_tier", premium_tier_decoder()) - use premium_subscription_count <- decode.optional_field( - "premium_subscription_count", - None, - decode.optional(decode.int), - ) - use preferred_locale <- decode.field("preferred_locale", decode.string) - use public_updates_channel_id <- decode.field( - "public_updates_channel_id", - decode.optional(decode.string), - ) - use max_video_channel_users <- decode.optional_field( - "max_video_channel_users", - None, - decode.optional(decode.int), - ) - use max_stage_video_channel_users <- decode.optional_field( - "max_stage_video_channel_users", - None, - decode.optional(decode.int), - ) - use approximate_member_count <- decode.optional_field( - "approximate_member_count", - None, - decode.optional(decode.int), - ) - use approximate_presence_count <- decode.optional_field( - "approximate_presence_count", - None, - decode.optional(decode.int), - ) - use welcome_screen <- decode.optional_field( - "welcome_screen", - None, - decode.optional(welcome_screen_decoder()), - ) - use nsfw_level <- decode.field("nsfw_level", nsfw_level_decoder()) - use stickers <- decode.optional_field( - "stickers", - None, - decode.optional(decode.list(sticker.decoder())), - ) - use is_premium_progress_bar_enabled <- decode.field( - "premium_progress_bar_enabled", - decode.bool, - ) - use safety_alerts_channel_id <- decode.field( - "safety_alerts_channel_id", - decode.optional(decode.string), - ) - use incidents_data <- decode.field( - "incidents_data", - decode.optional(incidents_data_decoder()), - ) - decode.success(Guild( - id:, - name:, - icon:, - icon_hash:, - splash:, - discovery_splash:, - is_current_user_owner:, - owner_id:, - current_user_permissions:, - afk_channel_id:, - afk_timeout:, - is_widget_enabled:, - widget_channel_id:, - verification_level:, - default_message_notification_setting:, - explicit_content_filter_setting:, - roles:, - emojis:, - features:, - mfa_level:, - application_id:, - system_channel_id:, - system_channel_flags:, - rules_channel_id:, - max_presences:, - max_members:, - vanity_url_code:, - description:, - banner_hash:, - premium_tier:, - premium_subscription_count:, - preferred_locale:, - public_updates_channel_id:, - max_video_channel_users:, - max_stage_video_channel_users:, - approximate_member_count:, - approximate_presence_count:, - welcome_screen:, - nsfw_level:, - stickers:, - is_premium_progress_bar_enabled:, - safety_alerts_channel_id:, - incidents_data:, - )) -} - -@internal -pub fn unavailable_guild_decoder() -> decode.Decoder(UnavailableGuild) { - use id <- decode.field("id", decode.string) - use unavailable <- decode.optional_field("unavailable", False, decode.bool) - decode.success(UnavailableGuild(id:, unavailable:)) -} - -@internal -pub fn feature_decoder() -> decode.Decoder(Feature) { - use variant <- decode.then(decode.string) - case variant { - "ANIMATED_BANNER" -> decode.success(HasAnimatedBanner) - "ANIMATED_ICON" -> decode.success(HasAnimatedIcon) - "APPLICATION_COMMAND_PERMISSIONS_V2" -> - decode.success(UsesOldPermissionConfigurationBehavior) - "AUTO_MODERATION" -> decode.success(UsesAutoModeration) - "BANNER" -> decode.success(HasBanner) - "COMMUNITY" -> decode.success(IsCommunity) - "CREATOR_MONETIZABLE_PROVISIONAL" -> decode.success(EnabledMonetization) - "CREATOR_STORE_PAGE" -> decode.success(HasRoleSubscriptionPromotionPage) - "DEVELOPER_SUPPORT_SERVER" -> decode.success(IsDeveloperSupportServer) - "DISCOVERABLE" -> decode.success(IsDiscoverable) - "FEATURABLE" -> decode.success(IsFeaturable) - "INVITES_DISABLED" -> decode.success(HasInvitesDisabled) - "INVITE_SPLASH" -> decode.success(CanSetInviteSplash) - "MEMBER_VERIFICATION_GATE_ENABLED" -> - decode.success(HasMemberVerificationGateEnabled) - "MORE_SOUNDBOARD" -> decode.success(HasMoreSoundboardSounds) - "MORE_STICKERS" -> decode.success(HasMoreStickers) - "NEWS" -> decode.success(CanCreateAnnouncementChannels) - "PARTNERED" -> decode.success(IsPartnered) - "PREVIEW_ENABLED" -> decode.success(CanBePreviewed) - "RAID_ALERTS_DISABLED" -> decode.success(HasRaidAlertsDisabled) - "ROLE_ICONS" -> decode.success(CanSetRoleIcons) - "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" -> - decode.success(HasRoleSubscriptionsAvailableForPurchase) - "ROLE_SUBSCRIPTIONS_ENABLED" -> decode.success(HasRoleSubscriptionsEnabled) - "SOUNDBOARD" -> decode.success(CreatedSoundboardSounds) - "TICKETED_EVENTS_ENABLED" -> decode.success(HasTicketedEventsEnabled) - "VANITY_URL" -> decode.success(CanSetVanityUrl) - "VERIFIED" -> decode.success(IsVerified) - "VIP_REGIONS" -> decode.success(CanSet384KbpsBitrate) - "WELCOME_SCREEN_ENABLED" -> decode.success(HasWelcomeScreenEnabled) - "GUESTS_ENABLED" -> decode.success(CanHaveGuestInvites) - "ENHANCED_ROLE_COLORS" -> decode.success(CanSetEnhancedRoleColors) - _ -> decode.success(UnknownFeature) - } -} - -@internal -pub fn default_message_notification_setting_decoder() -> decode.Decoder( - DefaultMessageNotificationSetting, -) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NotifyForAllMessages) - 1 -> decode.success(NotifyOnlyForMentions) - _ -> - decode.failure(NotifyForAllMessages, "DefaultMessageNotificationSetting") - } -} - -@internal -pub fn explicit_content_filter_setting_decoder() -> decode.Decoder( - ExplicitContentFilterSetting, -) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(ExplicitContentFilterDisabled) - 1 -> decode.success(ExplicitContentFilterForMembersWithoutRoles) - 2 -> decode.success(ExplicitContentFilterForAllMembers) - _ -> - decode.failure( - ExplicitContentFilterDisabled, - "ExplicitContentFilterSetting", - ) - } -} - -@internal -pub fn mfa_level_decoder() -> decode.Decoder(MfaLevel) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NoMfaRequired) - 1 -> decode.success(MfaRequired) - _ -> decode.failure(NoMfaRequired, "MfaLevel") - } -} - -@internal -pub fn verification_level_decoder() -> decode.Decoder(VerificationLevel) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NoVerification) - 1 -> decode.success(LowVerification) - 2 -> decode.success(MediumVerification) - 3 -> decode.success(HighVerification) - 4 -> decode.success(VeryHighVerification) - _ -> decode.failure(NoVerification, "VerificationLevel") - } -} - -@internal -pub fn nsfw_level_decoder() -> decode.Decoder(NsfwLevel) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NsfwLevelDefault) - 1 -> decode.success(NsfwExplicit) - 2 -> decode.success(NsfwSafe) - 3 -> decode.success(NsfwAgeRestricted) - _ -> decode.failure(NsfwLevelDefault, "NsfwLevel") - } -} - -@internal -pub fn premium_tier_decoder() -> decode.Decoder(PremiumTier) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NoPremiumTier) - 1 -> decode.success(PremiumTier1) - 2 -> decode.success(PremiumTier2) - 3 -> decode.success(PremiumTier3) - _ -> decode.failure(NoPremiumTier, "PremiumTier") - } -} - -@internal -pub fn incidents_data_decoder() -> decode.Decoder(IncidentsData) { - use invites_disabled_until <- decode.field( - "invites_disabled_until", - decode.optional(time_rfc3339.decoder()), - ) - use dms_disabled_until <- decode.field( - "dms_disabled_until", - decode.optional(time_rfc3339.decoder()), - ) - use dms_spam_disabled_at <- decode.optional_field( - "dms_spam_disabled_at", - None, - decode.optional(time_rfc3339.decoder()), - ) - use raid_detected_at <- decode.optional_field( - "raid_detected_at", - None, - decode.optional(time_rfc3339.decoder()), - ) - decode.success(IncidentsData( - invites_disabled_until:, - dms_disabled_until:, - dms_spam_disabled_at:, - raid_detected_at:, - )) -} - -@internal -pub fn welcome_screen_decoder() -> decode.Decoder(WelcomeScreen) { - use description <- decode.field("description", decode.optional(decode.string)) - use welcome_channels <- decode.field( - "welcome_channels", - decode.list(welcome_channel_decoder()), - ) - decode.success(WelcomeScreen(description:, welcome_channels:)) -} - -@internal -pub fn welcome_channel_decoder() -> decode.Decoder(WelcomeChannel) { - use channel_id <- decode.field("channel_id", decode.string) - use description <- decode.field("description", decode.string) - use emoji_id <- decode.field("emoji_id", decode.optional(decode.string)) - use emoji_name <- decode.field("emoji_name", decode.optional(decode.string)) - decode.success(WelcomeChannel( - channel_id:, - description:, - emoji_id:, - emoji_name:, - )) -} - -@internal -pub fn ban_decoder() -> decode.Decoder(Ban) { - use reason <- decode.field("reason", decode.optional(decode.string)) - use user <- decode.field("user", user.decoder()) - decode.success(Ban(reason:, user:)) -} - -@internal -pub fn preview_decoder() -> decode.Decoder(Preview) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use icon_hash <- decode.field("icon", decode.optional(decode.string)) - use splash_hash <- decode.field("splash", decode.optional(decode.string)) - use discovery_splash_hash <- decode.field( - "discovery_splash", - decode.optional(decode.string), - ) - use emojis <- decode.field("emojis", decode.list(emoji.decoder())) - use features <- decode.field("features", decode.list(feature_decoder())) - use approximate_member_count <- decode.field( - "approximate_member_count", - decode.int, - ) - use approximate_presence_count <- decode.field( - "approximate_presence_count", - decode.int, - ) - use description <- decode.field("description", decode.optional(decode.string)) - use stickers <- decode.field("stickers", decode.list(sticker.decoder())) - decode.success(Preview( - id:, - name:, - icon_hash:, - splash_hash:, - discovery_splash_hash:, - emojis:, - features:, - approximate_member_count:, - approximate_presence_count:, - description:, - stickers:, - )) -} - -@internal -pub fn template_decoder() -> decode.Decoder(Template) { - use code <- decode.field("code", decode.string) - use name <- decode.field("name", decode.string) - use description <- decode.field("description", decode.optional(decode.string)) - use usage_count <- decode.field("usage_count", decode.int) - use creator_id <- decode.field("creator_id", decode.string) - use creator <- decode.field("creator", user.decoder()) - use created_at <- decode.field("created_at", time_rfc3339.decoder()) - use updated_at <- decode.field("updated_at", time_rfc3339.decoder()) - use source_guild_id <- decode.field("source_guild_id", decode.string) - use is_dirty <- decode.field("is_dirty", decode.optional(decode.bool)) - decode.success(Template( - code:, - name:, - description:, - usage_count:, - creator_id:, - creator:, - created_at:, - updated_at:, - source_guild_id:, - is_dirty:, - )) -} - -@internal -pub fn received_threads_decoder() -> decode.Decoder(ReceivedThreads) { - use threads <- decode.field("threads", decode.list(thread.decoder())) - use current_members <- decode.field( - "members", - decode.list(thread.member_decoder()), - ) - decode.success(ReceivedThreads(threads:, current_members:)) -} - -@internal -pub fn bulk_ban_response_decoder() -> decode.Decoder(BulkBanResponse) { - use banned_users_ids <- decode.field( - "banned_users", - decode.list(decode.string), - ) - use failed_users_ids <- decode.field( - "failed_users", - decode.list(decode.string), - ) - - decode.success(BulkBanResponse(banned_users_ids:, failed_users_ids:)) -} - -@internal -pub fn prune_response_decoder() -> decode.Decoder(PruneResponse) { - use pruned_count <- decode.field("pruned", decode.optional(decode.int)) - decode.success(PruneResponse(pruned_count:)) -} - -@internal -pub fn vanity_invite_decoder() -> decode.Decoder(VanityInvite) { - use code <- decode.field("code", decode.optional(decode.string)) - use uses <- decode.field("uses", decode.int) - decode.success(VanityInvite(code:, uses:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let verification_level = case modify.verification_level { - Some(level) -> [#("verification_level", verification_level_to_json(level))] - None -> [] - } - - let default_message_notification_setting = case - modify.default_message_notification_setting - { - Some(setting) -> [ - #( - "default_message_notifications", - default_message_notification_setting_to_json(setting), - ), - ] - None -> [] - } - - let explicit_content_filter_setting = case - modify.explicit_content_filter_setting - { - Some(setting) -> [ - #( - "explicit_content_filter", - explicit_content_filter_setting_to_json(setting), - ), - ] - None -> [] - } - - let afk_channel_id = - modify.afk_channel_id - |> modification.to_json("afk_channel_id", json.string) - - let afk_timeout = case modify.afk_timeout { - Some(timeout) -> [#("afk_timeout", json.int(timeout))] - None -> [] - } - - let icon = - modify.icon - |> modification.to_json("icon", image.to_json) - - let owner_id = case modify.owner_id { - Some(id) -> [#("owner_id", json.string(id))] - None -> [] - } - - let splash = - modify.splash - |> modification.to_json("splash", image.to_json) - - let discovery_splash = - modify.discovery_splash - |> modification.to_json("discovery_splash", image.to_json) - - let banner = - modify.banner - |> modification.to_json("banner", image.to_json) - - let system_channnel_id = - modify.system_channel_id - |> modification.to_json("system_channel_id", json.string) - - let system_channel_flags = case modify.system_channel_flags { - Some(flags) -> [ - #( - "system_channel_flags", - flags.to_json(flags, bits_system_channel_flags()), - ), - ] - None -> [] - } - - let rules_channel_id = - modify.rules_channel_id - |> modification.to_json("rules_channel_id", json.string) - - let public_updates_channel_id = - modify.public_updates_channel_id - |> modification.to_json("public_updates_channel_id", json.string) - - let preferred_locale = - modify.preferred_locale - |> modification.to_json("preferred_locale", json.string) - - let features = case modify.features { - Some(features) -> [#("features", json.array(features, feature_to_json))] - None -> [] - } - - let description = - modify.description - |> modification.to_json("description", json.string) - - let is_premium_progress_bar_enabled = case - modify.is_premium_progress_bar_enabled - { - Some(premium_progress_bar_enabled) -> [ - #("premium_progress_bar_enabled", json.bool(premium_progress_bar_enabled)), - ] - None -> [] - } - - let safety_alerts_channel_id = - modify.safety_alerts_channel_id - |> modification.to_json("safety_alerts_channel_id", json.string) - - [ - name, - verification_level, - default_message_notification_setting, - explicit_content_filter_setting, - afk_channel_id, - afk_timeout, - icon, - owner_id, - splash, - discovery_splash, - banner, - system_channnel_id, - system_channel_flags, - rules_channel_id, - public_updates_channel_id, - preferred_locale, - features, - description, - is_premium_progress_bar_enabled, - safety_alerts_channel_id, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn verification_level_to_json(verification_level: VerificationLevel) -> Json { - case verification_level { - NoVerification -> 0 - LowVerification -> 1 - MediumVerification -> 2 - HighVerification -> 3 - VeryHighVerification -> 4 - } - |> json.int -} - -@internal -pub fn default_message_notification_setting_to_json( - setting: DefaultMessageNotificationSetting, -) -> Json { - case setting { - NotifyForAllMessages -> 0 - NotifyOnlyForMentions -> 1 - } - |> json.int -} - -@internal -pub fn explicit_content_filter_setting_to_json( - setting: ExplicitContentFilterSetting, -) -> Json { - case setting { - ExplicitContentFilterDisabled -> 0 - ExplicitContentFilterForMembersWithoutRoles -> 1 - ExplicitContentFilterForAllMembers -> 2 - } - |> json.int -} - -@internal -pub fn feature_to_json(feature: Feature) -> Json { - case feature { - HasAnimatedBanner -> "ANIMATED_BANNER" - HasAnimatedIcon -> "ANIMATED_ICON" - UsesOldPermissionConfigurationBehavior -> - "APPLICATION_COMMAND_PERMISSIONS_V2" - UsesAutoModeration -> "AUTO_MODERATION" - HasBanner -> "BANNER" - IsCommunity -> "COMMUNITY" - EnabledMonetization -> "CREATOR_MONETIZABLE_PROVISIONAL" - HasRoleSubscriptionPromotionPage -> "CREATOR_STORE_PAGE" - IsDeveloperSupportServer -> "DEVELOPER_SUPPORT_SERVER" - IsDiscoverable -> "DISCOVERABLE" - IsFeaturable -> "FEATURABLE" - HasInvitesDisabled -> "INVITES_DISABLED" - CanSetInviteSplash -> "INVITE_SPLASH" - HasMemberVerificationGateEnabled -> "MEMBER_VERIFICATION_GATE_ENABLED" - HasMoreSoundboardSounds -> "MORE_SOUNDBOARD" - HasMoreStickers -> "MORE_STICKERS" - CanCreateAnnouncementChannels -> "NEWS" - IsPartnered -> "PARTNERED" - CanBePreviewed -> "PREVIEW_ENABLED" - HasRaidAlertsDisabled -> "RAID_ALERTS_DISABLED" - CanSetRoleIcons -> "ROLE_ICONS" - HasRoleSubscriptionsAvailableForPurchase -> - "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" - HasRoleSubscriptionsEnabled -> "ROLE_SUBSCRIPTIONS_ENABLED" - CreatedSoundboardSounds -> "SOUNDBOARD" - HasTicketedEventsEnabled -> "TICKETED_EVENTS_ENABLED" - CanSetVanityUrl -> "VANITY_URL" - IsVerified -> "VERIFIED" - CanSet384KbpsBitrate -> "VIP_REGIONS" - HasWelcomeScreenEnabled -> "WELCOME_SCREEN_ENABLED" - CanHaveGuestInvites -> "GUESTS_ENABLED" - CanSetEnhancedRoleColors -> "ENHANCED_ROLE_COLORS" - UnknownFeature -> "UNKNOWN" - } - |> json.string -} - -@internal -pub fn role_to_move_to_json(role_to_move: RoleToMove) -> Json { - let id = [#("id", json.string(role_to_move.id))] - let position = case role_to_move.position { - Some(position) -> [#("position", json.int(position))] - None -> [] - } - - [id, position] - |> list.flatten - |> json.object -} - -@internal -pub fn prune_to_json(prune: Prune) -> Json { - json.object([ - #("days", json.int(prune.days)), - #("compute_prune_count", json.bool(prune.compute_pruned_count)), - #("include_roles", json.array(prune.include_role_ids, json.string)), - ]) -} - -@internal -pub fn modify_welcome_screen_to_json(modify: ModifyWelcomeScreen) -> Json { - let is_enabled = case modify.is_enabled { - Some(enabled) -> [#("enabled", json.bool(enabled))] - None -> [] - } - - let welcome_channels = - modify.welcome_channels - |> modification.to_json("welcome_channels", json.array( - _, - welcome_channel_to_json, - )) - - let description = - modify.description - |> modification.to_json("description", json.string) - - [is_enabled, welcome_channels, description] - |> list.flatten - |> json.object -} - -@internal -pub fn welcome_channel_to_json(welcome_channel: WelcomeChannel) -> Json { - json.object([ - #("channel_id", json.string(welcome_channel.channel_id)), - #("description", json.string(welcome_channel.description)), - #("emoji_id", json.nullable(welcome_channel.emoji_id, json.string)), - #("emoji_name", json.nullable(welcome_channel.emoji_name, json.string)), - ]) -} - -@internal -pub fn modify_incident_actions_to_json(modify: ModifyIncidentActions) -> Json { - json.object( - list.flatten([ - modification.to_json( - modify.invites_disabled_until, - "invites_disabled_until", - time_rfc3339.to_json, - ), - modification.to_json( - modify.dms_disabled_until, - "dms_disabled_until", - time_rfc3339.to_json, - ), - ]), - ) -} - -@internal -pub fn create_soundboard_sound_to_json(create: CreateSoundboardSound) -> Json { - let name = [#("name", json.string(create.name))] - - let sound = [#("sound", soundboard.data_to_json(create.sound))] - - let volume = case create.volume { - Some(volume) -> [#("volume", json.float(volume))] - None -> [] - } - - let emoji_id = case create.emoji_id { - Some(id) -> [#("emoji_id", json.string(id))] - None -> [] - } - - let emoji_name = case create.emoji_name { - Some(name) -> [#("emoji_name", json.string(name))] - None -> [] - } - - [name, sound, volume, emoji_id, emoji_name] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_soundboard_sound_to_json(modify: ModifySoundboardSound) -> Json { - json.object( - [ - case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - }, - modification.to_json(modify.volume, "volume", json.float), - modification.to_json(modify.emoji_id, "emoji_id", json.string), - modification.to_json(modify.emoji_name, "emoji_name", json.string), - ] - |> list.flatten, - ) -} - -@internal -pub fn modify_sticker_to_json(modify: ModifySticker) -> Json { - let name = case modify.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let description = - modification.to_json(modify.description, "description", json.string) - - let tags = case modify.tags { - Some(tags) -> [#("tags", json.string(tags))] - None -> [] - } - - [name, description, tags] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -/// `get_counts`: Whether to get the `approximate_member_count` and `approximate_presence_count` -pub fn get( - client: grom.Client, - id guild_id: String, - get_counts with_counts: Bool, -) -> Result(Guild, grom.Error) { - let query = [#("with_counts", bool.to_string(with_counts))] - - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id) - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_preview( - client: grom.Client, - for guild_id: String, -) -> Result(Preview, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/preview") - |> rest.execute, - ) - - response.body - |> json.parse(using: preview_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - id guild_id: String, - with modify: Modify, - because reason: Option(String), -) -> Result(Guild, grom.Error) { - let json = modify |> modify_to_json - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/guilds/" <> guild_id) - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify( - None, - None, - None, - None, - Skip, - None, - Skip, - None, - Skip, - Skip, - Skip, - Skip, - None, - Skip, - Skip, - Skip, - None, - Skip, - None, - Skip, - ) -} - -pub fn leave( - client: grom.Client, - id guild_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request(http.Delete, "/users/@me/guilds/" <> guild_id) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn get_auto_moderation_rules( - client: grom.Client, - for guild_id: String, -) -> Result(List(auto_moderation.Rule), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/auto-moderation/rules", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(auto_moderation.rule_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_emojis( - client: grom.Client, - for guild_id: String, -) -> Result(List(Emoji), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/emojis") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(emoji.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_emoji( - client: grom.Client, - in guild_id: String, - id emoji_id: String, -) -> Result(Emoji, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/emojis/" <> emoji_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_emoji( - client: grom.Client, - in guild_id: String, - named name: String, - bytes image: image.Data, - allowed_roles roles: List(String), - because reason: Option(String), -) -> Result(Emoji, grom.Error) { - let json = - json.object([ - #("name", json.string(name)), - #("image", json.string(image |> image.to_base64)), - #("roles", json.array(roles, json.string)), - ]) - - use response <- result.try( - client - |> rest.new_request(http.Post, "/guilds/" <> guild_id <> "/emojis") - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_emoji( - client: grom.Client, - in guild_id: String, - id emoji_id: String, - rename name: Option(String), - allowed_roles roles: Modification(List(String)), - because reason: Option(String), -) -> Result(Emoji, grom.Error) { - let json = - [ - case name { - Some(name) -> [#("name", json.string(name))] - None -> [] - }, - modification.to_json(roles, "roles", json.array(_, json.string)), - ] - |> list.flatten - |> json.object - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/emojis/" <> emoji_id, - ) - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: emoji.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_emoji( - client: grom.Client, - from guild_id: String, - id emoji_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/emojis/" <> emoji_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn delete( - client: grom.Client, - id guild_id: String, -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request(http.Delete, "/guilds/" <> guild_id) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn get_active_threads( - client: grom.Client, - in guild_id: String, -) -> Result(ReceivedThreads, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/threads/active") - |> rest.execute, - ) - - response.body - |> json.parse(using: received_threads_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -/// If `maximum` is not provided, a default of `1` will be used. -/// See: https://discord.com/developers/docs/resources/guild#list-guild-members -pub fn get_members( - client: grom.Client, - for guild_id: String, - maximum limit: Option(Int), - later_than_id after: Option(String), -) -> Result(List(GuildMember), grom.Error) { - let query = - [ - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - case after { - Some(id) -> [#("after", id)] - None -> [] - }, - ] - |> list.flatten - - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/members") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(guild_member.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn search_for_members( - client: grom.Client, - in guild_id: String, - named query_param: String, - maximum limit: Option(Int), -) -> Result(List(GuildMember), grom.Error) { - let query = - [ - [#("query", query_param)], - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - ] - |> list.flatten - - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/members/search") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(guild_member.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -/// See: https://discord.com/developers/docs/resources/guild#get-guild-bans -pub fn get_bans( - client: grom.Client, - for guild_id: String, - maximum limit: Option(Int), - earlier_than_id before: Option(String), - later_than_id after: Option(String), -) -> Result(List(Ban), grom.Error) { - let query = - [ - case limit { - Some(limit) -> [#("limit", int.to_string(limit))] - None -> [] - }, - case before, after { - Some(before), _ -> [#("before", before)] - None, Some(after) -> [#("after", after)] - None, None -> [] - }, - ] - |> list.flatten - - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/bans") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(ban_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_ban( - client: grom.Client, - from guild_id: String, - for user_id: String, -) -> Result(Ban, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/bans/" <> user_id) - |> rest.execute, - ) - - response.body - |> json.parse(using: ban_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_ban( - client: grom.Client, - in guild_id: String, - for user_id: String, - delete_messages_since delete_message_duration: Option(Duration), - because reason: Option(String), -) -> Result(Nil, grom.Error) { - let json = - case delete_message_duration { - Some(duration) -> [ - #("delete_message_seconds", time_duration.to_int_seconds_json(duration)), - ] - None -> [] - } - |> json.object - |> json.to_string - - use _response <- result.try( - client - |> rest.new_request(http.Put, "/guilds/" <> guild_id <> "/bans/" <> user_id) - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn remove_ban( - client: grom.Client, - in guild_id: String, - from user_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/bans/" <> user_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn bulk_ban( - client: grom.Client, - in guild_id: String, - users user_ids: List(String), - delete_messages_since delete_message_duration: Option(Duration), - because reason: Option(String), -) -> Result(BulkBanResponse, grom.Error) { - let json = - [ - [#("user_ids", json.array(user_ids, json.string))], - case delete_message_duration { - Some(duration) -> [ - #( - "delete_message_seconds", - time_duration.to_int_seconds_json(duration), - ), - ] - None -> [] - }, - ] - |> list.flatten - |> json.object - |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Post, "/guilds/" <> guild_id <> "/bulk-ban") - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: bulk_ban_response_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_roles( - client: grom.Client, - for guild_id: String, -) -> Result(List(Role), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/roles") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(role.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -/// Returns a list of all of the guild's roles. -pub fn move_roles( - client: grom.Client, - in guild_id: String, - roles roles: List(RoleToMove), - because reason: Option(String), -) -> Result(List(Role), grom.Error) { - let json = - roles - |> json.array(role_to_move_to_json) - |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/guilds/" <> guild_id <> "/roles") - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(role.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn begin_prune( - client: grom.Client, - in guild_id: String, - using prune: Prune, - because reason: Option(String), -) -> Result(PruneResponse, grom.Error) { - let json = prune |> prune_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Post, "/guilds/" <> guild_id <> "/prune") - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: prune_response_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_voice_regions( - client: grom.Client, - for guild_id: String, -) -> Result(List(voice.Region), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/regions") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: voice.region_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_integrations( - client: grom.Client, - for guild_id: String, -) -> Result(List(Integration), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/integrations") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: integration.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_vanity_invite( - client: grom.Client, - for guild_id: String, -) -> Result(VanityInvite, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/vanity-url") - |> rest.execute, - ) - - response.body - |> json.parse(using: vanity_invite_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_welcome_screen( - client: grom.Client, - for guild_id: String, -) -> Result(WelcomeScreen, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/welcome-screen") - |> rest.execute, - ) - - response.body - |> json.parse(using: welcome_screen_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_welcome_screen( - client: grom.Client, - in guild_id: String, - using modify: ModifyWelcomeScreen, - because reason: Option(String), -) -> Result(WelcomeScreen, grom.Error) { - let json = modify |> modify_welcome_screen_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/guilds/" <> guild_id <> "/welcome-screen") - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: welcome_screen_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_welcome_screen() -> ModifyWelcomeScreen { - ModifyWelcomeScreen(None, Skip, Skip) -} - -pub fn modify_incident_actions( - client: grom.Client, - in guild_id: String, - using modify: ModifyIncidentActions, -) -> Result(IncidentsData, grom.Error) { - let json = modify |> modify_incident_actions_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Put, "/guilds/" <> guild_id <> "/incident-actions") - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: incidents_data_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_soundboard_sounds( - client: grom.Client, - id guild_id: String, -) -> Result(List(soundboard.Sound), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/soundboard-sounds", - ) - |> rest.execute, - ) - - let response_decoder = { - use sounds <- decode.field( - "items", - decode.list(of: soundboard.sound_decoder()), - ) - decode.success(sounds) - } - - response.body - |> json.parse(using: response_decoder) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_soundboard_sound( - client: grom.Client, - in guild_id: String, - id sound_id: String, -) -> Result(soundboard.Sound, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/soundboard-sounds/" <> sound_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: soundboard.sound_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_soundboard_sound( - client: grom.Client, - in guild_id: String, - using create: CreateSoundboardSound, - because reason: Option(String), -) -> Result(soundboard.Sound, grom.Error) { - let json = create |> create_soundboard_sound_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/guilds/" <> guild_id <> "/soundboard-sounds", - ) - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: soundboard.sound_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create_soundboard_sound( - name: String, - sound: soundboard.Data, -) -> CreateSoundboardSound { - CreateSoundboardSound(name, sound, None, None, None) -} - -pub fn modify_soundboard_sound( - client: grom.Client, - in guild_id: String, - id sound_id: String, - using modify: ModifySoundboardSound, - because reason: Option(String), -) -> Result(soundboard.Sound, grom.Error) { - let json = modify |> modify_soundboard_sound_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/soundboard-sounds/" <> sound_id, - ) - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: soundboard.sound_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_soundboard_sound( - client: grom.Client, - in guild_id: String, - id sound_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/soundboard-sounds/" <> sound_id, - ) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} - -pub fn get_stickers( - client: grom.Client, - for guild_id: String, -) -> Result(List(Sticker), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/stickers") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: sticker.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_sticker( - client: grom.Client, - from guild_id: String, - id sticker_id: String, -) -> Result(Sticker, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/stickers/" <> sticker_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: sticker.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_sticker( - client: grom.Client, - in guild_id: String, - using create: CreateSticker, - because reason: Option(String), -) -> Result(Sticker, grom.Error) { - let request = - client - |> rest.new_request(http.Post, "/guilds/" <> guild_id <> "/stickers") - |> rest.with_reason(reason) - |> multipart_form.to_request(form: [ - #("name", field.String(create.name)), - #("description", field.String(create.description)), - #("tags", field.String(create.tags)), - #( - "file", - field.File( - create.file.name, - sticker.content_type_to_string(create.file.content_type), - create.file.content, - ), - ), - ]) - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: sticker.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_sticker( - client: grom.Client, - in guild_id: String, - id sticker_id: String, - using modify: ModifySticker, - because reason: Option(String), -) -> Result(Sticker, grom.Error) { - let json = modify |> modify_sticker_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/stickers/" <> sticker_id, - ) - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: sticker.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_sticker( - client: grom.Client, - from guild_id: String, - id sticker_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/stickers/" <> sticker_id, - ) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} - -pub fn get_scheduled_events( - client: grom.Client, - for guild_id: String, -) -> Result(List(ScheduledEvent), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/scheduled-events") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: scheduled_event.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_sticker() -> ModifySticker { - ModifySticker(None, Skip, None) -} - -pub fn new_modify_incident_actions() -> ModifyIncidentActions { - ModifyIncidentActions(Skip, Skip) -} - -/// Returns a Dict of Role IDs to member counts. -pub fn get_role_member_counts( - client: grom.Client, - for guild_id: String, -) -> Result(Dict(String, Int), grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/roles/member-counts", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.dict(decode.string, decode.int)) - |> result.map_error(grom.CouldNotDecode) -} - -/// You probably want to use `guild.icon`, not `guild.icon_hash`. -/// `guild.icon_hash` is `None` in 90% of cases. -/// -/// If `format == GifIcon` and the icon isn't animated, will fallback to WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn icon_request( - id id: String, - icon icon: String, - format format: IconFormat, -) -> Request(String) { - let is_animated = - icon - |> string.starts_with("a_") - - let extension = case format, is_animated { - PngIcon, _ -> ".png" - JpegIcon, _ -> ".jpg" - WebpIcon, _ -> ".webp" - GifIcon, True -> ".gif" - GifIcon, False -> ".webp" - } - - let query = case format, is_animated { - WebpIcon, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request(to: "/icons/" <> id <> "/" <> icon <> extension, query:) -} - -pub fn splash_request( - id id: String, - splash splash: String, - format format: SplashFormat, -) -> Request(String) { - let extension = case format { - PngSplash -> ".png" - JpegSplash -> ".jpg" - WebpSplash -> ".webp" - } - - rest.new_cdn_request( - to: "/splashes/" <> id <> "/" <> splash <> extension, - query: [], - ) -} - -pub fn discovery_splash_request( - id id: String, - splash discovery_splash: String, - format format: SplashFormat, -) -> Request(String) { - let extension = case format { - PngSplash -> ".png" - JpegSplash -> ".jpg" - WebpSplash -> ".webp" - } - - rest.new_cdn_request( - to: "/discovery-splashes/" <> id <> "/" <> discovery_splash <> extension, - query: [], - ) -} - -/// If `format == GifBanner` and the banner isn't animated, will fallback to static WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn banner_request( - id id: String, - banner banner: String, - format format: BannerFormat, -) -> Request(String) { - let is_animated = - banner - |> string.starts_with("a_") - - let extension = case format, is_animated { - PngBanner, _ -> ".png" - JpegBanner, _ -> ".jpg" - WebpBanner, _ -> ".webp" - GifBanner, True -> ".gif" - GifBanner, False -> ".webp" - } - - let query = case format, is_animated { - WebpBanner, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request( - to: "/banners/" <> id <> "/" <> banner <> extension, - query:, - ) -} - -pub fn tag_badge_request( - id id: String, - hash badge: String, - format format: TagBadgeFormat, -) -> Request(String) { - let extension = case format { - PngTagBadge -> ".png" - JpegTagBadge -> ".jpeg" - WebpTagBadge -> ".webp" - } - - rest.new_cdn_request( - to: "/guild-tag-badges/" <> id <> "/" <> badge <> extension, - query: [], - ) -} diff --git a/src/grom/guild/audit_log.gleam b/src/grom/guild/audit_log.gleam deleted file mode 100644 index cb10b58..0000000 --- a/src/grom/guild/audit_log.gleam +++ /dev/null @@ -1,568 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/int -import gleam/json -import gleam/option.{type Option, None} -import gleam/result -import gleam/set.{type Set} -import grom -import grom/channel.{type Channel} -import grom/channel/permission_overwrite -import grom/command.{type Command} -import grom/guild/auto_moderation -import grom/guild/integration -import grom/guild/scheduled_event.{type ScheduledEvent} -import grom/internal/rest -import grom/user.{type User} -import grom/webhook.{type Webhook} - -// TYPES ----------------------------------------------------------------------- - -pub type AuditLog { - AuditLog( - application_commands: List(Command), - entries: List(Entry), - auto_moderation_rules: List(auto_moderation.Rule), - scheduled_events: List(ScheduledEvent), - integrations: List(PartialIntegration), - threads: List(Channel), - users: List(User), - webhooks: List(Webhook), - ) -} - -pub type PartialIntegration { - PartialIntegration( - id: String, - name: String, - type_: String, - account: PartialUser, - application_id: String, - ) -} - -pub type PartialUser { - PartialUser(name: String, id: String) -} - -pub type GetQuery { - UserId(String) - EntryType(EntryType) - BeforeId(String) - AfterId(String) - Limit(Int) -} - -/// The change object describes what was changed, its old value and new value. -/// -/// You will need to decode the old and new values based on what was changed -/// using [gleam/dynamic/decode](https://hexdocs.pm/gleam_stdlib/gleam/dynamic/decode.html) -pub type Change { - Change( - /// What was changed. Generally a name of a field in an object's constructor. - /// - /// Some fields are _undocumented_. - /// - /// See [exceptions](https://discord.com/developers/docs/resources/audit-log#audit-log-change-object-audit-log-change-exceptions). - key: String, - old_value: Option(decode.Dynamic), - new_value: Option(decode.Dynamic), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn change_decoder() -> decode.Decoder(Change) { - use key <- decode.field("key", decode.string) - use old_value <- decode.optional_field( - "old_value", - None, - decode.optional(decode.dynamic), - ) - use new_value <- decode.optional_field( - "new_value", - None, - decode.optional(decode.dynamic), - ) - decode.success(Change(key:, old_value:, new_value:)) -} - -// TYPES ----------------------------------------------------------------------- - -pub type Entry { - Entry( - target_id: Option(String), - changes: Option(List(Change)), - user_id: Option(String), - id: String, - type_: EntryType, - info: Option(EntryInfo), - reason: Option(String), - ) -} - -pub type EntryType { - GuildUpdate - ChannelCreate - ChannelUpdate - ChannelDelete - ChannelOverwriteCreate - ChannelOverwriteUpdate - ChannelOverwriteDelete - MemberKick - MemberPrune - MemberBanAdd - MemberBanRemove - MemberUpdate - MemberRoleUpdate - MemberMove - MemberDisconnect - BotAdd - RoleCreate - RoleUpdate - RoleDelete - InviteCreate - InviteUpdate - InviteDelete - WebhookCreate - WebhookUpdate - WebhookDelete - EmojiCreate - EmojiUpdate - EmojiDelete - MessageDelete - MessageBulkDelete - MessagePin - MessageUnpin - IntegrationCreate - IntegrationUpdate - IntegrationDelete - StageInstanceCreate - StageInstanceUpdate - StageInstanceDelete - StickerCreate - StickerUpdate - StickerDelete - ScheduledEventCreate - ScheduledEventUpdate - ScheduledEventDelete - ThreadCreate - ThreadUpdate - ThreadDelete - ApplicationCommandPermissionUpdate - SoundboardSoundCreate - SoundboardSoundUpdate - SoundboardSoundDelete - AutoModerationRuleCreate - AutoModerationRuleUpdate - AutoModerationRuleDelete - AutoModerationBlockMessage - AutoModerationFlagToChannel - AutoModerationUserCommunicationDisabled - CreatorMonetizationRequestCreated - CreatorMonetizationTermsAccepted - OnboardingPromptCreate - OnboardingPromptUpdate - OnboardingPromptDelete - OnboardingCreate - OnboardingUpdate - HomeSettingsCreate - HomeSettingsUpdate -} - -pub type EntryInfo { - ChannelOverwriteEntry( - id: String, - type_: permission_overwrite.Type, - /// Only present if `type_ == permission_overwrite.Role`. - role_name: Option(String), - ) - MemberKicked( - /// Only present if an integration performed the action. - integration_type: Option(integration.Type), - ) - MemberRoleUpdated( - /// Only present if an integration performed the action. - integration_type: Option(integration.Type), - ) - MembersPruned(delete_member_days: Int, count: Int) - MembersMoved(channel_id: String, count: Int) - MembersDisconnected(count: Int) - MessagesDeleted(channel_id: String, count: Int) - MessagesBulkDeleted(count: Int) - MessagePinned(channel_id: String, message_id: String) - MessageUnpinned(channel_id: String, message_id: String) - StageInstanceEntry(channel_id: String) - ApplicationCommandPermissionUpdated(application_id: String) - AutoModerationTriggered(rule_name: String, channel_id: String) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn entry_decoder() -> decode.Decoder(Entry) { - use target_id <- decode.field("target_id", decode.optional(decode.string)) - use changes <- decode.optional_field( - "changes", - None, - decode.optional(decode.list(change_decoder())), - ) - use user_id <- decode.field("user_id", decode.optional(decode.string)) - use id <- decode.field("id", decode.string) - use type_ <- decode.field("action_type", entry_type_decoder()) - use info <- decode.optional_field( - "options", - None, - decode.optional(entry_info_decoder(type_)), - ) - use reason <- decode.optional_field( - "reason", - None, - decode.optional(decode.string), - ) - decode.success(Entry( - target_id:, - changes:, - user_id:, - id:, - type_:, - info:, - reason:, - )) -} - -@internal -pub fn entry_type_decoder() -> decode.Decoder(EntryType) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(GuildUpdate) - 10 -> decode.success(ChannelCreate) - 11 -> decode.success(ChannelUpdate) - 12 -> decode.success(ChannelDelete) - 13 -> decode.success(ChannelOverwriteCreate) - 14 -> decode.success(ChannelOverwriteUpdate) - 15 -> decode.success(ChannelOverwriteDelete) - 20 -> decode.success(MemberKick) - 21 -> decode.success(MemberPrune) - 22 -> decode.success(MemberBanAdd) - 23 -> decode.success(MemberBanRemove) - 24 -> decode.success(MemberUpdate) - 25 -> decode.success(MemberRoleUpdate) - 26 -> decode.success(MemberMove) - 27 -> decode.success(MemberDisconnect) - 28 -> decode.success(BotAdd) - 30 -> decode.success(RoleCreate) - 31 -> decode.success(RoleUpdate) - 32 -> decode.success(RoleDelete) - 40 -> decode.success(InviteCreate) - 41 -> decode.success(InviteUpdate) - 42 -> decode.success(InviteDelete) - 50 -> decode.success(WebhookCreate) - 51 -> decode.success(WebhookUpdate) - 52 -> decode.success(WebhookDelete) - 60 -> decode.success(EmojiCreate) - 61 -> decode.success(EmojiUpdate) - 62 -> decode.success(EmojiDelete) - 72 -> decode.success(MessageDelete) - 73 -> decode.success(MessageBulkDelete) - 74 -> decode.success(MessagePin) - 75 -> decode.success(MessageUnpin) - 80 -> decode.success(IntegrationCreate) - 81 -> decode.success(IntegrationUpdate) - 82 -> decode.success(IntegrationDelete) - 83 -> decode.success(StageInstanceCreate) - 84 -> decode.success(StageInstanceUpdate) - 85 -> decode.success(StageInstanceDelete) - 90 -> decode.success(StickerCreate) - 91 -> decode.success(StickerUpdate) - 92 -> decode.success(StickerDelete) - 100 -> decode.success(ScheduledEventCreate) - 101 -> decode.success(ScheduledEventUpdate) - 102 -> decode.success(ScheduledEventDelete) - 110 -> decode.success(ThreadCreate) - 111 -> decode.success(ThreadUpdate) - 112 -> decode.success(ThreadDelete) - 121 -> decode.success(ApplicationCommandPermissionUpdate) - 130 -> decode.success(SoundboardSoundCreate) - 131 -> decode.success(SoundboardSoundUpdate) - 132 -> decode.success(SoundboardSoundDelete) - 140 -> decode.success(AutoModerationRuleCreate) - 141 -> decode.success(AutoModerationRuleUpdate) - 142 -> decode.success(AutoModerationRuleDelete) - 143 -> decode.success(AutoModerationBlockMessage) - 144 -> decode.success(AutoModerationFlagToChannel) - 145 -> decode.success(AutoModerationUserCommunicationDisabled) - 150 -> decode.success(CreatorMonetizationRequestCreated) - 151 -> decode.success(CreatorMonetizationTermsAccepted) - 163 -> decode.success(OnboardingPromptCreate) - 164 -> decode.success(OnboardingPromptUpdate) - 165 -> decode.success(OnboardingPromptDelete) - 166 -> decode.success(OnboardingCreate) - 167 -> decode.success(OnboardingUpdate) - 190 -> decode.success(HomeSettingsCreate) - 191 -> decode.success(HomeSettingsUpdate) - _ -> decode.failure(GuildUpdate, "Type") - } -} - -@internal -pub fn entry_info_decoder(entry_type: EntryType) -> decode.Decoder(EntryInfo) { - let string_int_decoder = { - use string <- decode.then(decode.string) - case int.parse(string) { - Ok(int) -> decode.success(int) - Error(_) -> decode.failure(0, "Int") - } - } - case entry_type { - ApplicationCommandPermissionUpdate -> { - use application_id <- decode.field("application_id", decode.string) - decode.success(ApplicationCommandPermissionUpdated(application_id:)) - } - AutoModerationBlockMessage - | AutoModerationFlagToChannel - | AutoModerationUserCommunicationDisabled -> { - use rule_name <- decode.field("auto_moderation_rule_name", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - decode.success(AutoModerationTriggered(rule_name:, channel_id:)) - } - MemberMove -> { - use channel_id <- decode.field("channel_id", decode.string) - use count <- decode.field("count", string_int_decoder) - decode.success(MembersMoved(channel_id:, count:)) - } - MessagePin -> { - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - decode.success(MessagePinned(channel_id:, message_id:)) - } - MessageUnpin -> { - use channel_id <- decode.field("channel_id", decode.string) - use message_id <- decode.field("message_id", decode.string) - decode.success(MessageUnpinned(channel_id:, message_id:)) - } - MessageDelete -> { - use channel_id <- decode.field("channel_id", decode.string) - use count <- decode.field("count", string_int_decoder) - decode.success(MessagesDeleted(channel_id:, count:)) - } - StageInstanceCreate | StageInstanceUpdate | StageInstanceDelete -> { - use channel_id <- decode.field("channel_id", decode.string) - decode.success(StageInstanceEntry(channel_id:)) - } - MessageBulkDelete -> { - use count <- decode.field("count", string_int_decoder) - decode.success(MessagesBulkDeleted(count:)) - } - MemberDisconnect -> { - use count <- decode.field("count", string_int_decoder) - decode.success(MembersDisconnected(count:)) - } - MemberPrune -> { - use delete_member_days <- decode.field( - "delete_member_days", - string_int_decoder, - ) - use count <- decode.field("members_removed", string_int_decoder) - decode.success(MembersPruned(delete_member_days:, count:)) - } - ChannelOverwriteCreate | ChannelOverwriteUpdate | ChannelOverwriteDelete -> { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", permission_overwrite.type_decoder()) - use role_name <- decode.optional_field( - "role_name", - None, - decode.optional(decode.string), - ) - decode.success(ChannelOverwriteEntry(id:, type_:, role_name:)) - } - MemberKick -> { - use integration_type <- decode.optional_field( - "integration_type", - None, - decode.optional(integration.type_decoder()), - ) - decode.success(MemberKicked(integration_type:)) - } - MemberRoleUpdate -> { - use integration_type <- decode.optional_field( - "integration_type", - None, - decode.optional(integration.type_decoder()), - ) - decode.success(MemberRoleUpdated(integration_type:)) - } - _ -> decode.failure(ApplicationCommandPermissionUpdated(""), "Info") - } -} - -// INTERNAL FUNCTIONS ---------------------------------------------------------- - -@internal -pub fn entry_type_to_int(entry_type: EntryType) -> Int { - case entry_type { - GuildUpdate -> 1 - ChannelCreate -> 10 - ChannelUpdate -> 11 - ChannelDelete -> 12 - ChannelOverwriteCreate -> 13 - ChannelOverwriteUpdate -> 14 - ChannelOverwriteDelete -> 15 - MemberKick -> 20 - MemberPrune -> 21 - MemberBanAdd -> 22 - MemberBanRemove -> 23 - MemberUpdate -> 24 - MemberRoleUpdate -> 25 - MemberMove -> 26 - MemberDisconnect -> 27 - BotAdd -> 28 - RoleCreate -> 30 - RoleUpdate -> 31 - RoleDelete -> 32 - InviteCreate -> 40 - InviteUpdate -> 41 - InviteDelete -> 42 - WebhookCreate -> 50 - WebhookUpdate -> 51 - WebhookDelete -> 52 - EmojiCreate -> 60 - EmojiUpdate -> 61 - EmojiDelete -> 62 - MessageDelete -> 72 - MessageBulkDelete -> 73 - MessagePin -> 74 - MessageUnpin -> 75 - IntegrationCreate -> 80 - IntegrationUpdate -> 81 - IntegrationDelete -> 82 - StageInstanceCreate -> 83 - StageInstanceUpdate -> 84 - StageInstanceDelete -> 85 - StickerCreate -> 90 - StickerUpdate -> 91 - StickerDelete -> 92 - ScheduledEventCreate -> 100 - ScheduledEventUpdate -> 101 - ScheduledEventDelete -> 102 - ThreadCreate -> 110 - ThreadUpdate -> 111 - ThreadDelete -> 112 - ApplicationCommandPermissionUpdate -> 121 - SoundboardSoundCreate -> 130 - SoundboardSoundUpdate -> 131 - SoundboardSoundDelete -> 132 - AutoModerationRuleCreate -> 140 - AutoModerationRuleUpdate -> 141 - AutoModerationRuleDelete -> 142 - AutoModerationBlockMessage -> 143 - AutoModerationFlagToChannel -> 145 - AutoModerationUserCommunicationDisabled -> 146 - CreatorMonetizationRequestCreated -> 150 - CreatorMonetizationTermsAccepted -> 151 - OnboardingPromptCreate -> 163 - OnboardingPromptUpdate -> 164 - OnboardingPromptDelete -> 165 - OnboardingCreate -> 166 - OnboardingUpdate -> 167 - HomeSettingsCreate -> 190 - HomeSettingsUpdate -> 191 - } -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(AuditLog) { - use application_commands <- decode.field( - "application_commands", - decode.list(command.decoder()), - ) - use entries <- decode.field("entries", decode.list(entry_decoder())) - use auto_moderation_rules <- decode.field( - "auto_moderation_rules", - decode.list(auto_moderation.rule_decoder()), - ) - use scheduled_events <- decode.field( - "scheduled_events", - decode.list(scheduled_event.decoder()), - ) - use integrations <- decode.field( - "integrations", - decode.list(partial_integration_decoder()), - ) - use threads <- decode.field("threads", decode.list(channel.decoder())) - use users <- decode.field("users", decode.list(user.decoder())) - use webhooks <- decode.field("webhooks", decode.list(webhook.decoder())) - decode.success(AuditLog( - application_commands:, - entries:, - auto_moderation_rules:, - scheduled_events:, - integrations:, - threads:, - users:, - webhooks:, - )) -} - -@internal -pub fn partial_integration_decoder() -> decode.Decoder(PartialIntegration) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", decode.string) - use account <- decode.field("account", partial_user_decoder()) - use application_id <- decode.field("application_id", decode.string) - decode.success(PartialIntegration( - id:, - name:, - type_:, - account:, - application_id:, - )) -} - -@internal -pub fn partial_user_decoder() -> decode.Decoder(PartialUser) { - use name <- decode.field("name", decode.string) - use id <- decode.field("id", decode.string) - decode.success(PartialUser(name:, id:)) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for guild_id: String, - with query: Set(GetQuery), -) -> Result(AuditLog, grom.Error) { - let query = - query - |> set.map(fn(parameter) { - case parameter { - UserId(id) -> #("user_id", id) - EntryType(type_) -> #( - "action_type", - type_ - |> entry_type_to_int - |> int.to_string, - ) - BeforeId(id) -> #("before", id) - AfterId(id) -> #("after", id) - Limit(limit) -> #("limit", limit |> int.to_string) - } - }) - - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/audit-logs") - |> request.set_query(query |> set.to_list) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} diff --git a/src/grom/guild/auto_moderation.gleam b/src/grom/guild/auto_moderation.gleam deleted file mode 100644 index 4f9435e..0000000 --- a/src/grom/guild/auto_moderation.gleam +++ /dev/null @@ -1,520 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -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 -import grom/internal/rest -import grom/internal/time_duration - -// TYPES ----------------------------------------------------------------------- - -pub type Rule { - Rule( - id: String, - guild_id: String, - name: String, - creator_id: String, - trigger: Trigger, - actions: List(Action), - is_enabled: Bool, - exempt_role_ids: List(String), - exempt_channel_ids: List(String), - ) -} - -pub type TriggerType { - Keyword - Spam - KeywordPreset - MentionSpam - MemberProfile -} - -pub type Trigger { - KeywordTrigger( - keyword_filter: List(String), - regex_patterns: List(String), - allow_list: List(String), - ) - MemberProfileTrigger( - keyword_filter: List(String), - regex_patterns: List(String), - allow_list: List(String), - ) - KeywordPresetTrigger(presets: List(KeywordPreset), allow_list: List(String)) - MentionSpamTrigger( - total_mention_limit: Int, - is_mention_raid_protection_enabled: Bool, - ) - SpamTrigger -} - -pub type KeywordPreset { - ProfanityPreset - SexualContentPreset - SlursPreset -} - -pub type Action { - BlockMessage(custom_message: Option(String)) - SendAlertMessage(channel_id: String) - TimeoutMember(duration: Duration) - BlockMemberInteraction -} - -pub type CreateRule { - CreateRule( - name: String, - trigger: Trigger, - actions: List(Action), - is_enabled: Bool, - exempt_role_ids: Option(List(String)), - exempt_channel_ids: Option(List(String)), - ) -} - -pub type ModifyRule { - ModifyRule( - name: Option(String), - /// You can only modify the inner data, you _can't_ change the trigger. - trigger: Option(Trigger), - actions: Option(List(Action)), - is_enabled: Option(Bool), - exempt_role_ids: Option(List(String)), - exempt_channel_ids: Option(List(String)), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn rule_decoder() -> decode.Decoder(Rule) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use name <- decode.field("name", decode.string) - use creator_id <- decode.field("creator_id", decode.string) - use trigger_type <- decode.field("trigger_type", decode.int) - use trigger <- decode.field("trigger_metadata", trigger_decoder(trigger_type)) - use actions <- decode.field("actions", decode.list(action_decoder())) - use is_enabled <- decode.field("enabled", decode.bool) - use exempt_role_ids <- decode.field( - "exempt_roles", - decode.list(decode.string), - ) - use exempt_channel_ids <- decode.field( - "exempt_channels", - decode.list(decode.string), - ) - - decode.success(Rule( - id:, - guild_id:, - name:, - creator_id:, - trigger:, - actions:, - is_enabled:, - exempt_role_ids:, - exempt_channel_ids:, - )) -} - -@internal -pub fn trigger_decoder(trigger_type: Int) -> decode.Decoder(Trigger) { - case trigger_type { - 1 | 6 -> { - use keyword_filter <- decode.field( - "keyword_filter", - decode.list(decode.string), - ) - use regex_patterns <- decode.field( - "regex_patterns", - decode.list(decode.string), - ) - use allow_list <- decode.field("allow_list", decode.list(decode.string)) - - case trigger_type { - 1 -> - decode.success(KeywordTrigger( - keyword_filter:, - regex_patterns:, - allow_list:, - )) - 6 -> - decode.success(MemberProfileTrigger( - keyword_filter:, - regex_patterns:, - allow_list:, - )) - _ -> decode.failure(KeywordTrigger([], [], []), "Trigger") - } - } - 3 -> decode.success(SpamTrigger) - 4 -> { - use presets <- decode.field( - "presets", - decode.list(keyword_preset_decoder()), - ) - use allow_list <- decode.field("allow_list", decode.list(decode.string)) - - decode.success(KeywordPresetTrigger(presets:, allow_list:)) - } - 5 -> { - use total_mention_limit <- decode.field("mention_total_limit", decode.int) - use is_mention_raid_protection_enabled <- decode.field( - "mention_raid_protection_enabled", - decode.bool, - ) - - decode.success(MentionSpamTrigger( - total_mention_limit:, - is_mention_raid_protection_enabled:, - )) - } - _ -> decode.failure(SpamTrigger, "Trigger") - } -} - -@internal -pub fn trigger_type_decoder() -> decode.Decoder(TriggerType) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Keyword) - 3 -> decode.success(Spam) - 4 -> decode.success(KeywordPreset) - 5 -> decode.success(MentionSpam) - 6 -> decode.success(MemberProfile) - _ -> decode.failure(Keyword, "TriggerType") - } -} - -@internal -pub fn keyword_preset_decoder() -> decode.Decoder(KeywordPreset) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(ProfanityPreset) - 2 -> decode.success(SexualContentPreset) - 3 -> decode.success(SlursPreset) - _ -> decode.failure(ProfanityPreset, "KeywordPreset") - } -} - -@internal -pub fn action_decoder() -> decode.Decoder(Action) { - use type_ <- decode.field("type", decode.int) - use action <- decode.field("metadata", case type_ { - 1 -> { - use custom_message <- decode.optional_field( - "custom_message", - None, - decode.optional(decode.string), - ) - decode.success(BlockMessage(custom_message:)) - } - 2 -> { - use channel_id <- decode.field("channel_id", decode.string) - decode.success(SendAlertMessage(channel_id:)) - } - 3 -> { - use duration <- decode.field( - "duration_seconds", - time_duration.from_int_seconds_decoder(), - ) - decode.success(TimeoutMember(duration:)) - } - 4 -> decode.success(BlockMemberInteraction) - _ -> decode.failure(BlockMessage(None), "Action") - }) - - decode.success(action) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_rule_to_json(create_rule: CreateRule) -> Json { - let name = [#("name", json.string(create_rule.name))] - - let event_type = [ - #("event_type", case create_rule.trigger { - MemberProfileTrigger(..) -> json.int(2) - _ -> json.int(1) - }), - ] - - let trigger_type = [ - #("trigger_type", case create_rule.trigger { - KeywordTrigger(..) -> json.int(1) - SpamTrigger -> json.int(3) - KeywordPresetTrigger(..) -> json.int(4) - MentionSpamTrigger(..) -> json.int(5) - MemberProfileTrigger(..) -> json.int(6) - }), - ] - - let trigger = case create_rule.trigger { - SpamTrigger -> [] - _ -> [#("trigger_metadata", trigger_to_json(create_rule.trigger))] - } - - let is_enabled = [#("enabled", json.bool(create_rule.is_enabled))] - - let exempt_role_ids = case create_rule.exempt_role_ids { - Some(ids) -> [#("exempt_roles", json.array(ids, json.string))] - None -> [] - } - - let exempt_channel_ids = case create_rule.exempt_channel_ids { - Some(ids) -> [#("exempt_channels", json.array(ids, json.string))] - None -> [] - } - - [ - name, - event_type, - trigger_type, - trigger, - is_enabled, - exempt_role_ids, - exempt_channel_ids, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn trigger_to_json(trigger: Trigger) -> Json { - case trigger { - KeywordTrigger(..) -> [ - #("keyword_filter", json.array(trigger.keyword_filter, json.string)), - #("regex_patterns", json.array(trigger.regex_patterns, json.string)), - #("allow_list", json.array(trigger.allow_list, json.string)), - ] - SpamTrigger -> [] - KeywordPresetTrigger(..) -> [ - #("presets", json.array(trigger.presets, keyword_preset_to_json)), - #("allow_list", json.array(trigger.allow_list, json.string)), - ] - MentionSpamTrigger(..) -> [ - #("mention_total_limit", json.int(trigger.total_mention_limit)), - #( - "mention_raid_protection_enabled", - json.bool(trigger.is_mention_raid_protection_enabled), - ), - ] - MemberProfileTrigger(..) -> [ - #("keyword_filter", json.array(trigger.keyword_filter, json.string)), - #("regex_patterns", json.array(trigger.regex_patterns, json.string)), - #("allow_list", json.array(trigger.allow_list, json.string)), - ] - } - |> json.object -} - -@internal -pub fn keyword_preset_to_json(keyword_preset: KeywordPreset) -> Json { - case keyword_preset { - ProfanityPreset -> 1 - SexualContentPreset -> 2 - SlursPreset -> 3 - } - |> json.int -} - -@internal -pub fn action_to_json(action: Action) -> Json { - [ - [ - #("type", case action { - BlockMessage(..) -> json.int(1) - SendAlertMessage(..) -> json.int(2) - TimeoutMember(..) -> json.int(3) - BlockMemberInteraction -> json.int(4) - }), - ], - case action { - BlockMemberInteraction -> [] - BlockMessage(..) -> [ - #("metadata", { - let custom_message = case action.custom_message { - Some(message) -> [#("custom_message", json.string(message))] - None -> [] - } - - [custom_message] - |> list.flatten - |> json.object - }), - ] - TimeoutMember(..) -> [ - #( - "metadata", - json.object([ - #( - "duration_seconds", - time_duration.to_int_seconds_json(action.duration), - ), - ]), - ), - ] - SendAlertMessage(..) -> [ - #( - "metadata", - json.object([#("channel_id", json.string(action.channel_id))]), - ), - ] - }, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_rule_to_json(modify_rule: ModifyRule) -> Json { - let name = case modify_rule.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let trigger = case modify_rule.trigger { - Some(SpamTrigger) | None -> [] - Some(trigger) -> [#("trigger_metadata", trigger_to_json(trigger))] - } - - let actions = case modify_rule.actions { - Some(actions) -> [#("actions", json.array(actions, action_to_json))] - None -> [] - } - - let is_enabled = case modify_rule.is_enabled { - Some(enabled) -> [#("enabled", json.bool(enabled))] - None -> [] - } - - let exempt_role_ids = case modify_rule.exempt_role_ids { - Some(ids) -> [#("exempt_roles", json.array(ids, json.string))] - None -> [] - } - - let exempt_channel_ids = case modify_rule.exempt_channel_ids { - Some(ids) -> [#("exempt_channels", json.array(ids, json.string))] - None -> [] - } - - [name, trigger, actions, is_enabled, exempt_role_ids, exempt_channel_ids] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_rule( - client: grom.Client, - from guild_id: String, - id rule_id: String, -) -> Result(Rule, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/auto-moderation/rules/" <> rule_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: rule_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create_rule( - client: grom.Client, - in guild_id: String, - with create_rule: CreateRule, - because reason: Option(String), -) -> Result(Rule, grom.Error) { - let json = create_rule |> create_rule_to_json - - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/guilds/" <> guild_id <> "/auto-moderation/rules", - ) - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: rule_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create_rule( - named name: String, - on trigger: Trigger, - do actions: List(Action), - enable is_enabled: Bool, -) -> CreateRule { - CreateRule( - name:, - trigger:, - actions:, - is_enabled:, - exempt_role_ids: None, - exempt_channel_ids: None, - ) -} - -pub fn modify_rule( - client: grom.Client, - in guild_id: String, - id rule_id: String, - with modify_rule: ModifyRule, - because reason: Option(String), -) -> Result(Rule, grom.Error) { - let json = modify_rule |> modify_rule_to_json - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/auto-moderation/rules/" <> rule_id, - ) - |> request.set_body(json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: rule_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_rule() -> ModifyRule { - ModifyRule(None, None, None, None, None, None) -} - -pub fn delete_rule( - client: grom.Client, - from guild_id: String, - id rule_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/auto-moderation/rules/" <> rule_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} diff --git a/src/grom/guild/integration.gleam b/src/grom/guild/integration.gleam deleted file mode 100644 index 542c8b6..0000000 --- a/src/grom/guild/integration.gleam +++ /dev/null @@ -1,199 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/option.{type Option, None} -import gleam/result -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/internal/rest -import grom/internal/time_rfc3339 -import grom/user.{type User} - -// TYPES ---------------------------------------------------------------------- - -pub type Integration { - Integration( - id: String, - name: String, - type_: Type, - is_enabled: Bool, - is_syncing: Option(Bool), - role_id: Option(String), - are_emoticons_enabled: Option(Bool), - expire_behavior: Option(ExpireBehavior), - expire_grace_period: Option(Int), - user: Option(User), - account: Account, - synced_at: Option(Timestamp), - subscriber_count: Option(Int), - is_revoked: Option(Bool), - application: Option(Application), - scopes: Option(List(String)), - ) -} - -pub type Type { - Twitch - YouTube - Discord - GuildSubscription -} - -pub type Account { - Account(id: String, name: String) -} - -pub type Application { - Application( - id: String, - name: String, - icon_hash: Option(String), - description: String, - bot: Option(User), - ) -} - -pub type ExpireBehavior { - RemoveRole - Kick -} - -// DECODERS ------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Integration) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", type_decoder()) - use is_enabled <- decode.field("enabled", decode.bool) - use is_syncing <- decode.optional_field( - "syncing", - None, - decode.optional(decode.bool), - ) - use role_id <- decode.optional_field( - "role_id", - None, - decode.optional(decode.string), - ) - use are_emoticons_enabled <- decode.optional_field( - "enable_emoticons", - None, - decode.optional(decode.bool), - ) - use expire_behavior <- decode.optional_field( - "expire_behavior", - None, - decode.optional(expire_behavior_decoder()), - ) - use expire_grace_period <- decode.optional_field( - "expire_grace_period", - None, - decode.optional(decode.int), - ) - use user <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - use account <- decode.field("account", account_decoder()) - use synced_at <- decode.optional_field( - "synced_at", - None, - decode.optional(time_rfc3339.decoder()), - ) - use subscriber_count <- decode.optional_field( - "subscriber_count", - None, - decode.optional(decode.int), - ) - use is_revoked <- decode.optional_field( - "revoked", - None, - decode.optional(decode.bool), - ) - use application <- decode.optional_field( - "application", - None, - decode.optional(application_decoder()), - ) - use scopes <- decode.optional_field( - "scopes", - None, - decode.optional(decode.list(decode.string)), - ) - decode.success(Integration( - id:, - name:, - type_:, - is_enabled:, - is_syncing:, - role_id:, - are_emoticons_enabled:, - expire_behavior:, - expire_grace_period:, - user:, - account:, - synced_at:, - subscriber_count:, - is_revoked:, - application:, - scopes:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.string) - case variant { - "twitch" -> decode.success(Twitch) - "youtube" -> decode.success(YouTube) - "discord" -> decode.success(Discord) - "guild_subscription" -> decode.success(GuildSubscription) - _ -> decode.failure(Twitch, "Type") - } -} - -@internal -pub fn account_decoder() -> decode.Decoder(Account) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - decode.success(Account(id:, name:)) -} - -@internal -pub fn application_decoder() -> decode.Decoder(Application) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use icon_hash <- decode.field("icon_hash", decode.optional(decode.string)) - use description <- decode.field("description", decode.string) - use bot <- decode.optional_field("bot", None, decode.optional(user.decoder())) - decode.success(Application(id:, name:, icon_hash:, description:, bot:)) -} - -@internal -pub fn expire_behavior_decoder() -> decode.Decoder(ExpireBehavior) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(RemoveRole) - 1 -> decode.success(Kick) - _ -> decode.failure(RemoveRole, "ExpireBehavior") - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn delete( - client: grom.Client, - from guild_id: String, - id integration_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/integrations/" <> integration_id, - ) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} diff --git a/src/grom/guild/onboarding.gleam b/src/grom/guild/onboarding.gleam deleted file mode 100644 index 16ed875..0000000 --- a/src/grom/guild/onboarding.gleam +++ /dev/null @@ -1,320 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/emoji.{type Emoji} -import grom/internal/rest -import grom/modification.{type Modification, Skip} - -// TYPES ----------------------------------------------------------------------- - -pub type Onboarding { - Onboarding( - guild_id: String, - prompts: List(Prompt), - default_channel_ids: List(String), - is_enabled: Bool, - mode: Mode, - ) -} - -pub type Prompt { - Prompt( - id: String, - type_: PromptType, - options: List(PromptOption), - title: String, - is_single_select: Bool, - is_required: Bool, - is_in_onboarding: Bool, - ) -} - -pub type PromptOption { - PromptOption( - id: String, - channel_ids: List(String), - role_ids: List(String), - emoji: Option(Emoji), - emoji_id: Option(String), - emoji_name: Option(String), - is_emoji_animated: Option(Bool), - title: String, - description: Option(String), - ) -} - -pub type Mode { - Default - Advanced -} - -pub type PromptType { - MultipleChoice - Dropdown -} - -pub type Modify { - Modify( - prompts: Modification(List(Prompt)), - default_channel_ids: Modification(List(String)), - is_enabled: Option(Bool), - mode: Modification(Mode), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Onboarding) { - use guild_id <- decode.field("guild_id", decode.string) - use prompts <- decode.field("prompts", decode.list(prompt_decoder())) - use default_channel_ids <- decode.field( - "default_channel_ids", - decode.list(decode.string), - ) - use is_enabled <- decode.field("is_enabled", decode.bool) - use mode <- decode.field("mode", mode_decoder()) - decode.success(Onboarding( - guild_id:, - prompts:, - default_channel_ids:, - is_enabled:, - mode:, - )) -} - -@internal -pub fn prompt_decoder() -> decode.Decoder(Prompt) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", prompt_type_decoder()) - use options <- decode.field("options", decode.list(prompt_option_decoder())) - use title <- decode.field("title", decode.string) - use is_single_select <- decode.field("single_select", decode.bool) - use is_required <- decode.field("required", decode.bool) - use is_in_onboarding <- decode.field("in_onboarding", decode.bool) - decode.success(Prompt( - id:, - type_:, - options:, - title:, - is_single_select:, - is_required:, - is_in_onboarding:, - )) -} - -@internal -pub fn prompt_option_decoder() -> decode.Decoder(PromptOption) { - use id <- decode.field("id", decode.string) - use channel_ids <- decode.field("channel_ids", decode.list(decode.string)) - use role_ids <- decode.field("role_ids", decode.list(decode.string)) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji.decoder()), - ) - use emoji_id <- decode.optional_field( - "emoji_id", - None, - decode.optional(decode.string), - ) - use emoji_name <- decode.optional_field( - "emoji_name", - None, - decode.optional(decode.string), - ) - use is_emoji_animated <- decode.optional_field( - "emoji_animated", - None, - decode.optional(decode.bool), - ) - use title <- decode.field("title", decode.string) - use description <- decode.field("description", decode.optional(decode.string)) - decode.success(PromptOption( - id:, - channel_ids:, - role_ids:, - emoji:, - emoji_id:, - emoji_name:, - is_emoji_animated:, - title:, - description:, - )) -} - -@internal -pub fn mode_decoder() -> decode.Decoder(Mode) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Default) - 1 -> decode.success(Advanced) - _ -> decode.failure(Default, "Mode") - } -} - -@internal -pub fn prompt_type_decoder() -> decode.Decoder(PromptType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(MultipleChoice) - 1 -> decode.success(Dropdown) - _ -> decode.failure(MultipleChoice, "PromptType") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - json.object( - [ - modification.to_json(modify.prompts, "prompts", json.array( - _, - prompt_to_json, - )), - modification.to_json( - modify.default_channel_ids, - "default_channel_ids", - json.array(_, json.string), - ), - case modify.is_enabled { - Some(enabled) -> [#("enabled", json.bool(enabled))] - None -> [] - }, - modification.to_json(modify.mode, "mode", mode_to_json), - ] - |> list.flatten, - ) -} - -@internal -pub fn prompt_to_json(prompt: Prompt) -> Json { - json.object([ - #("id", json.string(prompt.id)), - #("type", prompt_type_to_json(prompt.type_)), - #("options", json.array(prompt.options, of: prompt_option_to_json)), - #("title", json.string(prompt.title)), - #("single_select", json.bool(prompt.is_single_select)), - #("required", json.bool(prompt.is_required)), - #("in_onboarding", json.bool(prompt.is_in_onboarding)), - ]) -} - -@internal -pub fn prompt_type_to_json(prompt_type: PromptType) -> Json { - case prompt_type { - MultipleChoice -> 0 - Dropdown -> 1 - } - |> json.int -} - -@internal -pub fn prompt_option_to_json(prompt_option: PromptOption) -> Json { - let id = [#("id", json.string(prompt_option.id))] - - let channel_ids = [ - #("channel_ids", json.array(prompt_option.channel_ids, json.string)), - ] - - let role_ids = [ - #("role_ids", json.array(prompt_option.role_ids, json.string)), - ] - - let emoji = case prompt_option.emoji { - Some(emoji) -> [#("emoji", emoji.to_json(emoji))] - None -> [] - } - - let emoji_id = case prompt_option.emoji_id { - Some(id) -> [#("emoji_id", json.string(id))] - None -> [] - } - - let emoji_name = case prompt_option.emoji_name { - Some(name) -> [#("emoji_name", json.string(name))] - None -> [] - } - - let is_emoji_animated = case prompt_option.is_emoji_animated { - Some(emoji_animated) -> [#("emoji_animated", json.bool(emoji_animated))] - None -> [] - } - - let title = [#("title", json.string(prompt_option.title))] - - let description = [ - #("description", json.nullable(prompt_option.description, json.string)), - ] - - [ - id, - channel_ids, - role_ids, - emoji, - emoji_id, - emoji_name, - is_emoji_animated, - title, - description, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn mode_to_json(mode: Mode) -> Json { - case mode { - Default -> 0 - Advanced -> 1 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for guild_id: String, -) -> Result(Onboarding, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/onboarding") - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - in guild_id: String, - using modify: Modify, - because reason: Option(String), -) -> Result(Onboarding, grom.Error) { - let json = modify |> modify_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Put, "/guilds/" <> guild_id <> "/onboarding") - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify(Skip, Skip, None, Skip) -} diff --git a/src/grom/guild/role.gleam b/src/grom/guild/role.gleam deleted file mode 100644 index 55fcee8..0000000 --- a/src/grom/guild/role.gleam +++ /dev/null @@ -1,418 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/image -import grom/internal/flags -import grom/internal/rest -import grom/modification.{type Modification, Skip} -import grom/permission.{type Permission} - -// TYPES ----------------------------------------------------------------------- - -pub type Role { - Role( - id: String, - name: String, - colors: Colors, - is_hoisted: Bool, - icon_hash: Option(String), - unicode_emoji: Option(String), - position: Int, - permissions: List(Permission), - is_managed: Bool, - is_mentionable: Bool, - tags: Option(Tags), - flags: List(Flag), - ) -} - -pub type Tags { - Tags( - bot_id: Option(String), - integration_id: Option(String), - is_premium_subscriber: Bool, - subscription_listing_id: Option(String), - is_available_for_purchase: Bool, - is_guild_connections: Bool, - ) -} - -pub type Flag { - InPrompt -} - -pub type Colors { - Colors(primary: Int, secondary: Option(Int), tertiary: Option(Int)) -} - -pub type Create { - Create( - /// If None -> "new role" - name: Option(String), - /// If None -> same as @everyone permissions - permissions: Option(List(Permission)), - colors: Option(Colors), - is_hoisted: Bool, - icon: Option(image.Data), - unicode_emoji: Option(String), - is_mentionable: Bool, - ) -} - -pub type Modify { - Modify( - name: Modification(String), - permissions: Modification(List(Permission)), - colors: Modification(Colors), - is_hoisted: Option(Bool), - icon: Modification(image.Data), - unicode_emoji: Modification(String), - is_mentionable: Option(Bool), - ) -} - -pub type IconFormat { - PngIcon - JpegIcon - WebpIcon -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [#(int.bitwise_shift_left(1, 0), InPrompt)] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Role) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use colors <- decode.field("colors", colors_decoder()) - use is_hoisted <- decode.field("hoist", decode.bool) - use icon_hash <- decode.optional_field( - "icon", - None, - decode.optional(decode.string), - ) - use unicode_emoji <- decode.optional_field( - "unicode_emoji", - None, - decode.optional(decode.string), - ) - use position <- decode.field("position", decode.int) - use permissions <- decode.field("permissions", permission.decoder()) - use is_managed <- decode.field("managed", decode.bool) - use is_mentionable <- decode.field("mentionable", decode.bool) - use tags <- decode.optional_field( - "tags", - None, - decode.optional(tags_decoder()), - ) - use flags <- decode.field("flags", flags.decoder(bits_flags())) - decode.success(Role( - id:, - name:, - colors:, - is_hoisted:, - icon_hash:, - unicode_emoji:, - position:, - permissions:, - is_managed:, - is_mentionable:, - tags:, - flags:, - )) -} - -@internal -pub fn tags_decoder() -> decode.Decoder(Tags) { - use bot_id <- decode.optional_field( - "bot_id", - None, - decode.optional(decode.string), - ) - use integration_id <- decode.optional_field( - "integration_id", - None, - decode.optional(decode.string), - ) - use is_premium_subscriber <- decode.optional_field( - "premium_subscriber", - False, - decode.success(True), - ) - use subscription_listing_id <- decode.optional_field( - "subscription_listing_id", - None, - decode.optional(decode.string), - ) - use is_available_for_purchase <- decode.optional_field( - "available_for_purchase", - False, - decode.success(True), - ) - use is_guild_connections <- decode.optional_field( - "guild_connections", - False, - decode.success(True), - ) - decode.success(Tags( - bot_id:, - integration_id:, - is_premium_subscriber:, - subscription_listing_id:, - is_available_for_purchase:, - is_guild_connections:, - )) -} - -@internal -pub fn colors_decoder() -> decode.Decoder(Colors) { - use primary <- decode.field("primary_color", decode.int) - use secondary <- decode.field("secondary_color", decode.optional(decode.int)) - use tertiary <- decode.field("tertiary_color", decode.optional(decode.int)) - - decode.success(Colors(primary:, secondary:, tertiary:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let name = case create.name { - Some(name) -> [#("name", json.string(name))] - None -> [] - } - - let permissions = case create.permissions { - Some(permissions) -> [#("permissions", permission.to_json(permissions))] - None -> [] - } - - let colors = case create.colors { - Some(colors) -> [#("colors", colors_to_json(colors))] - None -> [] - } - - let is_hoisted = [#("hoist", json.bool(create.is_hoisted))] - - let icon = case create.icon { - Some(icon) -> [#("icon", image.to_json(icon))] - None -> [] - } - - let unicode_emoji = case create.unicode_emoji { - Some(emoji) -> [#("unicode_emoji", json.string(emoji))] - None -> [] - } - - let is_mentionable = [#("mentionable", json.bool(create.is_mentionable))] - - [name, permissions, colors, is_hoisted, icon, unicode_emoji, is_mentionable] - |> list.flatten - |> json.object -} - -@internal -pub fn colors_to_json(colors: Colors) -> Json { - json.object([ - #("primary_color", json.int(colors.primary)), - #("secondary_color", json.nullable(colors.secondary, json.int)), - #("tertiary_color", json.nullable(colors.tertiary, json.int)), - ]) -} - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let name = - modify.name - |> modification.to_json("name", json.string) - - let permissions = - modify.permissions - |> modification.to_json("permissions", permission.to_json) - - let colors = - modify.colors - |> modification.to_json("colors", colors_to_json) - - let is_hoisted = case modify.is_hoisted { - Some(hoisted) -> [#("hoisted", json.bool(hoisted))] - None -> [] - } - - let icon = - modify.icon - |> modification.to_json("icon", image.to_json) - - let unicode_emoji = - modify.unicode_emoji - |> modification.to_json("unicode_emoji", json.string) - - let is_mentionable = case modify.is_mentionable { - Some(mentionable) -> [#("mentionable", json.bool(mentionable))] - None -> [] - } - - [name, permissions, colors, is_hoisted, icon, unicode_emoji, is_mentionable] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for guild_id: String, - id role_id: String, -) -> Result(Role, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/roles/" <> role_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create( - client: grom.Client, - in guild_id: String, - using create: Create, - because reason: Option(String), -) -> Result(Role, grom.Error) { - let json = - create - |> create_to_json - |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Post, "/guilds/" <> guild_id <> "/roles") - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -/// Usage example: -/// ```gleam -/// let create_role_data = role.Create(..role.new_create(), name: Some("name")) -/// -/// use role <- result.try( -/// client -/// |> role.create( -/// in: "guild_id", -/// using: create_role_data, -/// because: Some("reason") -/// ) -/// ) -/// ``` -pub fn new_create() -> Create { - Create(None, None, None, False, None, None, False) -} - -pub fn modify( - client: grom.Client, - in guild_id: String, - id role_id: String, - using modify: Modify, - because reason: Option(String), -) -> Result(Role, grom.Error) { - let json = - modify - |> modify_to_json - |> json.to_string - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/roles/" <> role_id, - ) - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -/// Usage example: -/// ```gleam -/// let modify_role_data = role.Modify( -/// ..role.new_modify(), -/// name: New("name"), -/// icon: Delete, -/// ) -/// -/// use role <- result.try( -/// client -/// |> role.modify( -/// in: "guild_id", -/// id: "role_id", -/// using: modify_role_data, -/// because: Some("reason"), -/// ), -/// ) -/// ``` -pub fn new_modify() -> Modify { - Modify(Skip, Skip, Skip, None, Skip, Skip, None) -} - -pub fn delete( - client: grom.Client, - from guild_id: String, - id role_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/roles/" <> role_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn icon_request( - id id: String, - hash icon: String, - format format: IconFormat, -) -> Request(String) { - let extension = case format { - PngIcon -> ".png" - JpegIcon -> ".jpg" - WebpIcon -> ".webp" - } - - rest.new_cdn_request( - to: "/role-icons/" <> id <> "/" <> icon <> extension, - query: [], - ) -} diff --git a/src/grom/guild/scheduled_event.gleam b/src/grom/guild/scheduled_event.gleam deleted file mode 100644 index 8b68246..0000000 --- a/src/grom/guild/scheduled_event.gleam +++ /dev/null @@ -1,366 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/guild/scheduled_event/recurrence_rule.{type RecurrenceRule} -import grom/image -import grom/internal/rest -import grom/internal/time_rfc3339 -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type ScheduledEvent { - ScheduledEvent( - id: String, - guild_id: String, - channel_id: Option(String), - creator_id: Option(String), - name: String, - description: Option(String), - scheduled_start_time: Timestamp, - scheduled_end_time: Option(Timestamp), - privacy_level: PrivacyLevel, - status: Status, - entity_type: EntityType, - entity_id: Option(String), - entity_metadata: Option(EntityMetadata), - creator: Option(User), - image_hash: Option(String), - recurrence_rule: Option(RecurrenceRule), - ) -} - -pub type PrivacyLevel { - GuildOnly -} - -pub type Status { - Scheduled - Active - Completed - Canceled -} - -pub type EntityType { - InStageInstance - InVoiceChannel - ExternallyHosted -} - -pub type EntityMetadata { - EntityMetadata(location: Option(String)) -} - -pub type Create { - CreateExternal( - entity_metadata: EntityMetadata, - name: String, - privacy_level: PrivacyLevel, - scheduled_start_time: Timestamp, - scheduled_end_time: Timestamp, - description: Option(String), - image: Option(image.Data), - recurrence_rule: Option(recurrence_rule.Create), - ) - CreateInStageInstance( - channel_id: String, - name: String, - privacy_level: PrivacyLevel, - scheduled_start_time: Timestamp, - scheduled_end_time: Option(Timestamp), - description: Option(String), - image: Option(image.Data), - recurrence_rule: Option(recurrence_rule.Create), - ) - CreateInVoiceChannel( - channel_id: String, - name: String, - privacy_level: PrivacyLevel, - scheduled_start_time: Timestamp, - scheduled_end_time: Option(Timestamp), - description: Option(String), - image: Option(image.Data), - recurrence_rule: Option(recurrence_rule.Create), - ) -} - -pub type CoverImageFormat { - PngCoverImage - JpegCoverImage - WebpCoverImage -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(ScheduledEvent) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use channel_id <- decode.field("channel_id", decode.optional(decode.string)) - use creator_id <- decode.optional_field( - "creator_id", - None, - decode.optional(decode.string), - ) - use name <- decode.field("name", decode.string) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - use scheduled_start_time <- decode.field( - "scheduled_start_time", - time_rfc3339.decoder(), - ) - use scheduled_end_time <- decode.field( - "scheduled_end_time", - decode.optional(time_rfc3339.decoder()), - ) - use privacy_level <- decode.field("privacy_level", privacy_level_decoder()) - use status <- decode.field("status", status_decoder()) - use entity_type <- decode.field("entity_type", entity_type_decoder()) - use entity_id <- decode.field("entity_id", decode.optional(decode.string)) - use entity_metadata <- decode.field( - "entity_metadata", - decode.optional(entity_metadata_decoder()), - ) - use creator <- decode.optional_field( - "creator", - None, - decode.optional(user.decoder()), - ) - use image_hash <- decode.optional_field( - "image_hash", - None, - decode.optional(decode.string), - ) - use recurrence_rule <- decode.field( - "recurrence_rule", - decode.optional(recurrence_rule.decoder()), - ) - decode.success(ScheduledEvent( - id:, - guild_id:, - channel_id:, - creator_id:, - name:, - description:, - scheduled_start_time:, - scheduled_end_time:, - privacy_level:, - status:, - entity_type:, - entity_id:, - entity_metadata:, - creator:, - image_hash:, - recurrence_rule:, - )) -} - -@internal -pub fn privacy_level_decoder() -> decode.Decoder(PrivacyLevel) { - use variant <- decode.then(decode.int) - case variant { - 2 -> decode.success(GuildOnly) - _ -> decode.failure(GuildOnly, "PrivacyLevel") - } -} - -@internal -pub fn status_decoder() -> decode.Decoder(Status) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Scheduled) - 2 -> decode.success(Active) - 3 -> decode.success(Completed) - 4 -> decode.success(Canceled) - _ -> decode.failure(Scheduled, "Status") - } -} - -@internal -pub fn entity_type_decoder() -> decode.Decoder(EntityType) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(InStageInstance) - 2 -> decode.success(InVoiceChannel) - 3 -> decode.success(ExternallyHosted) - _ -> decode.failure(InStageInstance, "EntityType") - } -} - -@internal -pub fn entity_metadata_decoder() -> decode.Decoder(EntityMetadata) { - use location <- decode.optional_field( - "location", - None, - decode.optional(decode.string), - ) - decode.success(EntityMetadata(location:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let channel_id = case create { - CreateInStageInstance(channel_id:, ..) -> [ - #("channel_id", json.string(channel_id)), - ] - CreateInVoiceChannel(channel_id:, ..) -> [ - #("channel_id", json.string(channel_id)), - ] - _ -> [] - } - - let entity_metadata = case create { - CreateExternal(entity_metadata:, ..) -> [ - #("entity_metadata", entity_metadata_to_json(entity_metadata)), - ] - _ -> [] - } - - let name = [#("name", json.string(create.name))] - - let privacy_level = [ - #("privacy_level", privacy_level_to_json(create.privacy_level)), - ] - - let scheduled_start_time = [ - #("scheduled_start_time", time_rfc3339.to_json(create.scheduled_start_time)), - ] - - let scheduled_end_time = case create { - CreateExternal(scheduled_end_time:, ..) -> [ - #("scheduled_end_time", time_rfc3339.to_json(scheduled_end_time)), - ] - CreateInStageInstance(scheduled_end_time:, ..) - | CreateInVoiceChannel(scheduled_end_time:, ..) -> - case scheduled_end_time { - Some(time) -> [#("scheduled_end_time", time_rfc3339.to_json(time))] - None -> [] - } - } - - let description = case create.description { - Some(description) -> [#("description", json.string(description))] - None -> [] - } - - let entity_type = [ - #("entity_type", case create { - CreateInStageInstance(..) -> json.int(1) - CreateInVoiceChannel(..) -> json.int(2) - CreateExternal(..) -> json.int(3) - }), - ] - - let image = case create.image { - Some(image) -> [#("image", image.to_json(image))] - None -> [] - } - - let recurrence_rule = case create.recurrence_rule { - Some(rule) -> [#("recurrence_rule", recurrence_rule.create_to_json(rule))] - None -> [] - } - - [ - channel_id, - entity_metadata, - name, - privacy_level, - scheduled_start_time, - scheduled_end_time, - description, - entity_type, - image, - recurrence_rule, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn entity_metadata_to_json(entity_metadata: EntityMetadata) -> Json { - json.object(case entity_metadata.location { - Some(location) -> [#("location", json.string(location))] - None -> [] - }) -} - -@internal -pub fn privacy_level_to_json(privacy_level: PrivacyLevel) -> Json { - case privacy_level { - GuildOnly -> 2 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn create( - client: grom.Client, - in guild_id: String, - using create: Create, - because reason: Option(String), -) -> Result(ScheduledEvent, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/guilds/" <> guild_id <> "/scheduled-events", - ) - |> request.set_body(create |> create_to_json |> json.to_string) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get( - client: grom.Client, - for guild_id: String, - id event_id: String, -) -> Result(ScheduledEvent, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/scheduled-events/" <> event_id, - ) - |> request.set_query([#("with_user_count", "true")]) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn cover_image_request( - id id: String, - hash cover_image: String, - format format: CoverImageFormat, -) -> Request(String) { - let extension = case format { - PngCoverImage -> ".png" - JpegCoverImage -> ".jpg" - WebpCoverImage -> ".webp" - } - - rest.new_cdn_request( - to: "/guild-events/" <> id <> "/" <> cover_image <> extension, - query: [], - ) -} diff --git a/src/grom/guild/scheduled_event/recurrence_rule.gleam b/src/grom/guild/scheduled_event/recurrence_rule.gleam deleted file mode 100644 index fbae4a3..0000000 --- a/src/grom/guild/scheduled_event/recurrence_rule.gleam +++ /dev/null @@ -1,259 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/time/timestamp.{type Timestamp} -import grom/internal/time_rfc3339 - -// TYPES ----------------------------------------------------------------------- - -pub type RecurrenceRule { - RecurrenceRule( - start: Timestamp, - end: Option(Timestamp), - frequency: Frequency, - interval: Int, - by_weekday: Option(List(Weekday)), - by_n_weekday: Option(List(NWeekday)), - by_month: Option(List(Month)), - by_month_day: Option(List(Int)), - by_year_day: Option(List(Int)), - count: Option(Int), - ) -} - -pub type Frequency { - Yearly - Monthly - Weekly - Daily -} - -pub type Weekday { - Monday - Tuesday - Wednesday - Thursday - Friday - Saturday - Sunday -} - -pub type NWeekday { - NWeekday(n: Int, day: Weekday) -} - -pub type Month { - January - February - March - April - May - June - July - August - September - October - November - December -} - -pub type Create { - Create( - start: Timestamp, - frequency: Frequency, - interval: Int, - by_weekday: Option(List(Weekday)), - by_n_weekday: Option(List(NWeekday)), - by_month: Option(List(Month)), - by_month_day: Option(List(Int)), - ) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(RecurrenceRule) { - use start <- decode.field("start", time_rfc3339.decoder()) - use end <- decode.field("end", decode.optional(time_rfc3339.decoder())) - use frequency <- decode.field("frequency", frequency_decoder()) - use interval <- decode.field("interval", decode.int) - use by_weekday <- decode.field( - "by_weekday", - decode.optional(decode.list(weekday_decoder())), - ) - use by_n_weekday <- decode.field( - "by_n_weekday", - decode.optional(decode.list(n_weekday_decoder())), - ) - use by_month <- decode.field( - "by_month", - decode.optional(decode.list(month_decoder())), - ) - use by_month_day <- decode.field( - "by_month_day", - decode.optional(decode.list(decode.int)), - ) - use by_year_day <- decode.field( - "by_year_day", - decode.optional(decode.list(decode.int)), - ) - use count <- decode.field("count", decode.optional(decode.int)) - decode.success(RecurrenceRule( - start:, - end:, - frequency:, - interval:, - by_weekday:, - by_n_weekday:, - by_month:, - by_month_day:, - by_year_day:, - count:, - )) -} - -@internal -pub fn frequency_decoder() -> decode.Decoder(Frequency) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Yearly) - 1 -> decode.success(Monthly) - 2 -> decode.success(Weekly) - 3 -> decode.success(Daily) - _ -> decode.failure(Yearly, "Frequency") - } -} - -@internal -pub fn weekday_decoder() -> decode.Decoder(Weekday) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Monday) - 1 -> decode.success(Tuesday) - 2 -> decode.success(Wednesday) - 3 -> decode.success(Thursday) - 4 -> decode.success(Friday) - 5 -> decode.success(Saturday) - 6 -> decode.success(Sunday) - _ -> decode.failure(Monday, "Weekday") - } -} - -@internal -pub fn n_weekday_decoder() -> decode.Decoder(NWeekday) { - use n <- decode.field("n", decode.int) - use day <- decode.field("day", weekday_decoder()) - decode.success(NWeekday(n:, day:)) -} - -@internal -pub fn month_decoder() -> decode.Decoder(Month) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(January) - 2 -> decode.success(February) - 3 -> decode.success(March) - 4 -> decode.success(April) - 5 -> decode.success(May) - 6 -> decode.success(June) - 7 -> decode.success(July) - 8 -> decode.success(August) - 9 -> decode.success(September) - 10 -> decode.success(October) - 11 -> decode.success(November) - 12 -> decode.success(December) - _ -> decode.failure(January, "Month") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let start = [#("start", time_rfc3339.to_json(create.start))] - - let frequency = [#("frequency", frequency_to_json(create.frequency))] - - let interval = [#("interval", json.int(create.interval))] - - let by_weekday = case create.by_weekday { - Some(days) -> [#("by_weekday", json.array(days, weekday_to_json))] - None -> [] - } - - let by_n_weekday = case create.by_n_weekday { - Some(n_weekdays) -> [ - #("by_n_weekday", json.array(n_weekdays, n_weekday_to_json)), - ] - None -> [] - } - - let by_month = case create.by_month { - Some(months) -> [#("by_month", json.array(months, month_to_json))] - None -> [] - } - - let by_month_day = case create.by_month_day { - Some(days_of_the_month) -> [ - #("by_month_days", json.array(days_of_the_month, json.int)), - ] - None -> [] - } - - [start, frequency, interval, by_weekday, by_n_weekday, by_month, by_month_day] - |> list.flatten - |> json.object -} - -@internal -pub fn frequency_to_json(frequency: Frequency) -> Json { - case frequency { - Yearly -> 0 - Monthly -> 1 - Weekly -> 2 - Daily -> 3 - } - |> json.int -} - -@internal -pub fn weekday_to_json(weekday: Weekday) -> Json { - case weekday { - Monday -> 0 - Tuesday -> 1 - Wednesday -> 2 - Thursday -> 3 - Friday -> 4 - Saturday -> 5 - Sunday -> 6 - } - |> json.int -} - -@internal -pub fn n_weekday_to_json(n_weekday: NWeekday) -> Json { - json.object([ - #("n", json.int(n_weekday.n)), - #("day", weekday_to_json(n_weekday.day)), - ]) -} - -@internal -pub fn month_to_json(month: Month) -> Json { - case month { - January -> 1 - February -> 2 - March -> 3 - April -> 4 - May -> 5 - June -> 6 - July -> 7 - August -> 8 - September -> 9 - October -> 10 - November -> 11 - December -> 12 - } - |> json.int -} diff --git a/src/grom/guild_member.gleam b/src/grom/guild_member.gleam deleted file mode 100644 index d605503..0000000 --- a/src/grom/guild_member.gleam +++ /dev/null @@ -1,442 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_rfc3339 -import grom/modification.{type Modification, Skip} -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type GuildMember { - Member( - user: Option(User), - nick: Option(String), - /// This is the per-guild avatar hash. - /// If you want the regular avatar hash, use member.user.avatar_hash. - avatar_hash: Option(String), - /// This is the per-guild banner hash. - /// If you want the regular banner hash, use member.user.banner_hash. - banner_hash: Option(String), - roles: List(String), - joined_at: Timestamp, - premium_since: Option(Timestamp), - is_deaf: Option(Bool), - is_mute: Option(Bool), - flags: List(Flag), - is_pending: Option(Bool), - permissions: Option(String), - communication_disabled_until: Option(Timestamp), - avatar_decoration_data: Option(user.AvatarDecorationData), - ) -} - -pub type Flag { - DidRejoin - CompletedOnboarding - BypassesVerification - StartedOnboarding - IsGuest - StartedHomeActions - CompletedHomeActions - QuarantinedBecauseOfUsername - AcknowledgedDmSettingsUpsell -} - -pub type Modify { - Modify( - nick: Modification(String), - role_ids: Modification(List(String)), - is_mute: Option(Bool), - is_deaf: Option(Bool), - voice_channel_id: Modification(String), - communication_disabled_until: Modification(Timestamp), - flags: Modification(List(Flag)), - ) -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_member_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 0), DidRejoin), - #(int.bitwise_shift_left(1, 1), CompletedOnboarding), - #(int.bitwise_shift_left(1, 2), BypassesVerification), - #(int.bitwise_shift_left(1, 3), StartedOnboarding), - #(int.bitwise_shift_left(1, 4), IsGuest), - #(int.bitwise_shift_left(1, 5), StartedHomeActions), - #(int.bitwise_shift_left(1, 6), CompletedHomeActions), - #(int.bitwise_shift_left(1, 7), QuarantinedBecauseOfUsername), - #(int.bitwise_shift_left(1, 9), AcknowledgedDmSettingsUpsell), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(GuildMember) { - use user <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - use nick <- decode.optional_field( - "nick", - None, - decode.optional(decode.string), - ) - use avatar_hash <- decode.optional_field( - "avatar", - None, - decode.optional(decode.string), - ) - use banner_hash <- decode.optional_field( - "banner", - None, - decode.optional(decode.string), - ) - use roles <- decode.field("roles", decode.list(decode.string)) - use joined_at <- decode.field("joined_at", time_rfc3339.decoder()) - use premium_since <- decode.optional_field( - "premium_since", - None, - decode.optional(time_rfc3339.decoder()), - ) - use is_deaf <- decode.optional_field( - "deaf", - None, - decode.optional(decode.bool), - ) - use is_mute <- decode.optional_field( - "mute", - None, - decode.optional(decode.bool), - ) - use flags <- decode.field("flags", flags.decoder(bits_member_flags())) - use is_pending <- decode.optional_field( - "pending", - None, - decode.optional(decode.bool), - ) - use permissions <- decode.optional_field( - "permissions", - None, - decode.optional(decode.string), - ) - use communication_disabled_until <- decode.optional_field( - "communication_disabled_until", - None, - decode.optional(time_rfc3339.decoder()), - ) - use avatar_decoration_data <- decode.optional_field( - "avatar_decoration_data", - None, - decode.optional(user.avatar_decoration_data_decoder()), - ) - decode.success(Member( - user:, - nick:, - avatar_hash:, - banner_hash:, - roles:, - joined_at:, - premium_since:, - is_deaf:, - is_mute:, - flags:, - is_pending:, - permissions:, - communication_disabled_until:, - avatar_decoration_data:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let nick = - modify.nick - |> modification.to_json("nick", json.string) - - let role_ids = - modify.role_ids - |> modification.to_json("roles", json.array(_, json.string)) - - let is_mute = case modify.is_mute { - Some(mute) -> [#("mute", json.bool(mute))] - None -> [] - } - - let is_deaf = case modify.is_deaf { - Some(deaf) -> [#("deaf", json.bool(deaf))] - None -> [] - } - - let voice_channel_id = - modify.voice_channel_id - |> modification.to_json("channel_id", json.string) - - let communication_disabled_until = - modify.communication_disabled_until - |> modification.to_json( - "communication_disabled_until", - time_rfc3339.to_json, - ) - - let flags = - modify.flags - |> modification.to_json("flags", flags.to_json(_, bits_member_flags())) - - [ - nick, - role_ids, - is_mute, - is_deaf, - voice_channel_id, - communication_disabled_until, - flags, - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - for guild_id: String, - id user_id: String, -) -> Result(GuildMember, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/guilds/" <> guild_id <> "/members/" <> user_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - in guild_id: String, - id user_id: String, - with modify: Modify, - because reason: Option(String), -) -> Result(GuildMember, grom.Error) { - let json = - modify - |> modify_to_json - |> json.to_string - - use response <- result.try( - client - |> rest.new_request( - http.Patch, - "/guilds/" <> guild_id <> "/members/" <> user_id, - ) - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn set_current_nick( - client: grom.Client, - in guild_id: String, - to nick: Modification(String), - because reason: Option(String), -) -> Result(GuildMember, grom.Error) { - let json = - nick - |> modification.to_json("nick", json.string) - |> json.object - |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/guilds/" <> guild_id <> "/members/@me") - |> request.set_body(json) - |> rest.with_reason(reason) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn add_role( - client: grom.Client, - in guild_id: String, - to user_id: String, - id role_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Put, - "/guilds/" <> guild_id <> "/members/" <> user_id <> "/roles/" <> role_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn remove_role( - client: grom.Client, - in guild_id: String, - from user_id: String, - id role_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/members/" <> user_id <> "/roles/" <> role_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn remove( - client: grom.Client, - from guild_id: String, - id user_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/guilds/" <> guild_id <> "/members/" <> user_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn kick( - client: grom.Client, - from guild_id: String, - id user_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - remove(client, from: guild_id, id: user_id, because: reason) -} - -pub fn new_modify() -> Modify { - Modify(Skip, Skip, None, None, Skip, Skip, Skip) -} - -/// Note that this will give you the per-guild avatar of the member. -/// If you want the overall avatar - use `user.avatar_request(member.user)`. -/// -/// If `format == user.GifAvatar` and the avatar isn't animated, will fallback to static WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn avatar_request( - in guild_id: String, - id member_id: String, - hash avatar: String, - format format: user.AvatarFormat, -) -> Request(String) { - { - let is_animated = - avatar - |> string.starts_with("a_") - - let extension = case format, is_animated { - user.PngAvatar, _ -> ".png" - user.JpegAvatar, _ -> ".jpg" - user.WebpAvatar, _ -> ".webp" - user.GifAvatar, True -> ".gif" - user.GifAvatar, False -> ".webp" - } - - let query = case format, is_animated { - user.WebpAvatar, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request( - to: "/guilds/" - <> guild_id - <> "/users/" - <> member_id - <> "/avatars/" - <> avatar - <> extension, - query:, - ) - } -} - -/// Note that this will give you the per-guild banner of the member. -/// If you want the overall banner - use `user.banner_request(member.user)`. -/// -/// If `format == user.GifBanner` and the banner isn't animated, will fallback to static WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn banner_request( - in guild_id: String, - id member_id: String, - hash banner: String, - format format: user.BannerFormat, -) -> Request(String) { - { - let is_animated = - banner - |> string.starts_with("a_") - - let extension = case format, is_animated { - user.PngBanner, _ -> ".png" - user.JpegBanner, _ -> ".jpg" - user.WebpBanner, _ -> ".webp" - user.GifBanner, True -> ".gif" - user.GifBanner, False -> ".webp" - } - - let query = case format, is_animated { - user.WebpBanner, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request( - to: "/guilds/" - <> guild_id - <> "/users/" - <> member_id - <> "/banners/" - <> banner - <> extension, - query:, - ) - } -} diff --git a/src/grom/image.gleam b/src/grom/image.gleam deleted file mode 100644 index b85453e..0000000 --- a/src/grom/image.gleam +++ /dev/null @@ -1,43 +0,0 @@ -import gleam/bit_array -import gleam/json.{type Json} - -// TYPES ----------------------------------------------------------------------- - -pub opaque type Data { - Data(String) -} - -pub type ContentType { - Jpeg - Png - Gif -} - -// FUNCTIONS ------------------------------------------------------------------- - -pub fn from_bit_array( - image data: BitArray, - file_type content_type: ContentType, -) -> Data { - let mime = case content_type { - Jpeg -> "image/jpeg" - Png -> "image/png" - Gif -> "image/gif" - } - let base64 = bit_array.base64_encode(data, False) - - Data("data:" <> mime <> ";base64," <> base64) -} - -@internal -pub fn to_base64(image: Data) -> String { - let Data(base64) = image - base64 -} - -@internal -pub fn to_json(image: Data) -> Json { - image - |> to_base64 - |> json.string -} diff --git a/src/grom/interaction.gleam b/src/grom/interaction.gleam deleted file mode 100644 index 7bcb56a..0000000 --- a/src/grom/interaction.gleam +++ /dev/null @@ -1,1883 +0,0 @@ -import gleam/bit_array -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/http/response -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/application -import grom/channel/thread -import grom/entitlement.{type Entitlement} -import grom/file.{type File} -import grom/guild/role.{type Role} -import grom/guild_member.{type GuildMember} -import grom/internal/flags -import grom/internal/rest -import grom/message.{type Message} -import grom/message/allowed_mentions.{type AllowedMentions} -import grom/message/attachment.{type Attachment} -import grom/message/embed.{type Embed} -import grom/message/poll -import grom/modal -import grom/modification.{type Modification, Skip} -import grom/permission.{type Permission} -import grom/user.{type User} -import kryptos/eddsa -import status_code - -// TYPES ----------------------------------------------------------------------- - -pub type Interaction { - Interaction( - id: String, - application_id: String, - data: Data, - invokement_info: InvokementInfo, - channel_id: String, - token: String, - /// For modals/component interactions, the message it was triggered from. - message: Option(Message), - application_permissions: List(Permission), - locale: String, - entitlements: Option(List(Entitlement)), - authorizing_integration_owners_ids: Dict( - application.InstallationContext, - String, - ), - context: Option(Context), - attachment_size_limit_bytes: Int, - ) -} - -pub type Data { - CommandExecuted(CommandExecution) - MessageComponentExecuted(MessageComponentExecution) - ModalSubmitted(ModalSubmission) -} - -pub type CommandExecution { - SlashCommandExecuted(SlashCommandExecution) - MessageCommandExecuted(MessageCommandExecution) - UserCommandExecuted(UserCommandExecution) -} - -pub type MessageComponentExecution { - ButtonExecuted(ButtonExecution) - StringSelectExecuted(StringSelectExecution) - UserSelectExecuted(UserSelectExecution) - RoleSelectExecuted(RoleSelectExecution) - MentionableSelectExecuted(MentionableSelectExecution) - ChannelSelectExecuted(ChannelSelectExecution) -} - -pub type ModalSubmission { - ModalSubmission( - custom_id: String, - components: List(SubmittedModalComponent), - resolved: Option(Resolved), - ) -} - -pub type SubmittedModalComponent { - TextDisplaySubmitted(TextDisplaySubmission) - LabelSubmitted(LabelSubmission) -} - -pub type RadioGroupSubmission { - RadioGroupSubmission(id: Int, custom_id: String, value: Option(String)) -} - -pub type ButtonExecution { - ButtonExecution(custom_id: String, resolved: Option(Resolved)) -} - -pub type StringSelectExecution { - StringSelectExecution( - custom_id: String, - selected_values: List(String), - resolved: Option(Resolved), - ) -} - -pub type TextInputSubmission { - TextInputSubmission(id: Int, custom_id: String, value: String) -} - -pub type LabelSubmission { - LabelSubmission(id: Int, component: SubmittedLabelComponent) -} - -pub type SubmittedLabelComponent { - LabelTextInputSubmitted(TextInputSubmission) - LabelStringSelectSubmitted(StringSelectExecution) - LabelUserSelectSubmitted(UserSelectExecution) - LabelRoleSelectSubmitted(RoleSelectExecution) - LabelMentionableSelectSubmitted(MentionableSelectExecution) - LabelChannelSelectSubmitted(ChannelSelectExecution) - LabelFileUploadSubmitted(FileUploadSubmission) - LabelRadioGroupSubmitted(RadioGroupSubmission) - LabelCheckboxGroupSubmitted(CheckboxGroupSubmission) - LabelCheckboxSubmitted(CheckboxSubmission) -} - -pub type CheckboxSubmission { - CheckboxSubmission(id: Int, custom_id: String, is_selected: Bool) -} - -pub type CheckboxGroupSubmission { - CheckboxGroupSubmission( - id: Int, - custom_id: String, - selected_values: List(String), - ) -} - -pub type TextDisplaySubmission { - TextDisplaySubmission(id: Int) -} - -pub type FileUploadSubmission { - FileUploadSubmission( - id: Int, - custom_id: String, - uploaded_files_ids: List(String), - ) -} - -pub type UserSelectExecution { - UserSelectExecution( - custom_id: String, - selected_users_ids: List(String), - resolved: Option(Resolved), - ) -} - -pub type RoleSelectExecution { - RoleSelectExecution( - custom_id: String, - selected_roles_ids: List(String), - resolved: Option(Resolved), - ) -} - -pub type MentionableSelectExecution { - MentionableSelectExecution( - custom_id: String, - selected_mentionables_ids: List(String), - resolved: Option(Resolved), - ) -} - -pub type ChannelSelectExecution { - ChannelSelectExecution( - custom_id: String, - selected_channels_ids: List(String), - resolved: Option(Resolved), - ) -} - -pub type SlashCommandOption { - StringSlashCommandOption( - name: String, - value: String, - /// For autocomplete. - is_focused: Bool, - ) - IntegerSlashCommandOption( - name: String, - value: Int, - /// For autocomplete. - is_focused: Bool, - ) - NumberSlashCommandOption( - name: String, - value: Float, - /// For autocomplete. - is_focused: Bool, - ) - BoolSlashCommandOption( - name: String, - value: Bool, - /// For autocomplete. - is_focused: Bool, - ) - UserSlashCommandOption( - /// Name of the option, NOT of the user. - name: String, - user_id: String, - /// For autocomplete. - is_focused: Bool, - ) - ChannelSlashCommandOption( - /// Name of the option, NOT of the channel. - name: String, - channel_id: String, - /// For autocomplete. - is_focused: Bool, - ) - RoleSlashCommandOption( - /// Name of the option, NOT of the role. - name: String, - role_id: String, - /// For autocomplete. - is_focused: Bool, - ) - MentionableSlashCommandOption( - /// Name of the option, NOT of the mentionable. - name: String, - mentionable_id: String, - /// For autocomplete. - is_focused: Bool, - ) - AttachmentSlashCommandOption( - /// Name of the option, NOT of the attachment. - name: String, - /// You likely want to search for this in `resolved`. - attachment_id: String, - /// For autocomplete. - is_focused: Bool, - ) - SubCommandSlashCommandOption(name: String, options: List(SlashCommandOption)) - SubCommandGroupSlashCommandOption( - name: String, - options: List(SlashCommandOption), - ) -} - -pub type SlashCommandExecution { - SlashCommandExecution( - id: String, - name: String, - resolved: Option(Resolved), - options: List(SlashCommandOption), - registered_to_guild_id: Option(String), - ) -} - -pub type MessageCommandExecution { - MessageCommandExecution( - id: String, - name: String, - resolved: Option(Resolved), - registered_to_guild_id: Option(String), - message_id: String, - ) -} - -pub type UserCommandExecution { - UserCommandExecution( - id: String, - name: String, - resolved: Option(Resolved), - registered_to_guild_id: Option(String), - user_id: String, - ) -} - -pub type Context { - TriggeredInGuild - TriggeredInBotDms - TriggeredInPrivateChannel -} - -pub type InvokementInfo { - InvokedInGuild(guild_id: String, member: GuildMember, guild_locale: String) - InvokedInDm(user: User) -} - -pub type Resolved { - Resolved( - users: Option(Dict(String, User)), - members: Option(Dict(String, GuildMember)), - roles: Option(Dict(String, Role)), - channels: Option(Dict(String, ResolvedChannel)), - messages: Option(Dict(String, Message)), - attachments: Option(Dict(String, Attachment)), - ) -} - -// TODO: update this, also make channels a tad bit better (kinda suck rn) -// (maybe after release?!) (seems like a bad idea) (i also don't wanna push this off) -pub type ResolvedChannel { - ResolvedTextChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedVoiceChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedCategoryChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedAnnouncementChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedAnnouncementThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedPublicThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedPrivateThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedStageChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedForumChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedMediaChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) -} - -pub type Response { - RespondWithChannelMessageWithSource(ResponseMessage) - /// You should only set the `flags` property in this response. - RespondWithDeferredChannelMessageWithSource(ResponseMessage) - RespondWithDeferredUpdateMessage - RespondWithUpdateMessage(ResponseMessage) - RespondWithModal(ModalResponse) -} - -pub type ResponseMessage { - ResponseMessage( - is_tts: Option(Bool), - content: Option(String), - embeds: Option(List(Embed)), - allowed_mentions: Option(AllowedMentions), - /// For `RespondWithDeferredChannelMessageWithSource`s, you can only set the `EphemeralResponseMessage` flag. - /// If you need any other flags - use them with the edited response message. - flags: Option(List(ResponseMessageFlag)), - components: Option(List(message.Component)), - attachments: Option(List(attachment.Create)), - files: Option(List(File)), - poll: Option(poll.Create), - ) -} - -pub type ResponseMessageFlag { - ResponseMessageWithSuppressedEmbeds - EphemeralResponseMessage - ResponseMessageWithComponentsV2 - VoiceResponseMessage - ResponseMessageWithSuppressedNotifications -} - -pub type ModalResponse { - ModalResponse( - custom_id: String, - title: String, - components: List(modal.Component), - ) -} - -pub type ModifyOriginalResponse { - ModifyOriginalResponse( - content: Modification(String), - embeds: Modification(List(Embed)), - flags: Modification(List(ModifyOriginalResponseFlag)), - allowed_mentions: Modification(AllowedMentions), - components: Modification(List(message.Component)), - files: Modification(List(File)), - attachments: Modification(List(attachment.Create)), - poll: Modification(poll.Create), - ) -} - -pub type ModifyOriginalResponseFlag { - ModifyResponseWithSuppressedEmbeds - ModifyResponseWithComponentsV2 -} - -pub type Followup { - Followup( - content: Option(String), - is_tts: Bool, - embeds: Option(List(Embed)), - allowed_mentions: Option(AllowedMentions), - components: Option(List(message.Component)), - files: Option(List(File)), - attachments: Option(List(attachment.Create)), - flags: Option(List(FollowupFlag)), - poll: Option(poll.Create), - ) -} - -pub type FollowupFlag { - EphemeralFollowup - FollowupWithSuppressedEmbeds - FollowupWithSuppressedNotifications - FollowupWithComponentsV2 -} - -pub type ModifyFollowup { - ModifyFollowup( - content: Modification(String), - embeds: Modification(List(Embed)), - flags: Modification(List(ModifyFollowupFlag)), - allowed_mentions: Modification(AllowedMentions), - components: Modification(List(message.Component)), - files: Modification(List(File)), - attachments: Modification(List(attachment.Create)), - poll: Modification(poll.Create), - ) -} - -pub type ModifyFollowupFlag { - ModifyFollowupWithSuppressedEmbeds -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_response_message_flags() -> List(#(Int, ResponseMessageFlag)) { - [ - #(int.bitwise_shift_left(1, 2), ResponseMessageWithSuppressedEmbeds), - #(int.bitwise_shift_left(1, 6), EphemeralResponseMessage), - #(int.bitwise_shift_left(1, 15), ResponseMessageWithComponentsV2), - #(int.bitwise_shift_left(1, 13), VoiceResponseMessage), - #(int.bitwise_shift_left(1, 12), ResponseMessageWithSuppressedNotifications), - ] -} - -@internal -pub fn bits_modify_original_response_flags() -> List( - #(Int, ModifyOriginalResponseFlag), -) { - [ - #(int.bitwise_shift_left(1, 2), ModifyResponseWithSuppressedEmbeds), - #(int.bitwise_shift_left(1, 15), ModifyResponseWithComponentsV2), - ] -} - -@internal -pub fn bits_followup_flags() -> List(#(Int, FollowupFlag)) { - [ - #(int.bitwise_shift_left(1, 6), EphemeralFollowup), - #(int.bitwise_shift_left(1, 2), FollowupWithSuppressedEmbeds), - #(int.bitwise_shift_left(1, 12), FollowupWithSuppressedNotifications), - #(int.bitwise_shift_left(1, 15), FollowupWithComponentsV2), - ] -} - -@internal -pub fn bits_modify_followup_flags() -> List(#(Int, ModifyFollowupFlag)) { - [#(int.bitwise_shift_left(1, 2), ModifyFollowupWithSuppressedEmbeds)] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Interaction) { - use id <- decode.field("id", decode.string) - use application_id <- decode.field("application_id", decode.string) - use type_ <- decode.field("type", decode.int) - use invokement_info <- decode.then(invokement_info_decoder()) - use channel_id <- decode.field("channel_id", decode.string) - use token <- decode.field("token", decode.string) - use data <- decode.field("data", data_decoder(type_)) - use message <- decode.optional_field( - "message", - None, - decode.optional(message.decoder()), - ) - use application_permissions <- decode.field( - "app_permissions", - permission.decoder(), - ) - use locale <- decode.field("locale", decode.string) - use entitlements <- decode.optional_field( - "entitlements", - None, - decode.optional(decode.list(of: entitlement.decoder())), - ) - use authorizing_integration_owners_ids <- decode.field( - "authorizing_integration_owners", - decode.dict( - application.installation_context_string_decoder(), - decode.string, - ), - ) - use context <- decode.optional_field( - "context", - None, - decode.optional(context_decoder()), - ) - use attachment_size_limit_bytes <- decode.field( - "attachment_size_limit", - decode.int, - ) - - decode.success(Interaction( - id:, - application_id:, - data:, - invokement_info:, - channel_id:, - token:, - message:, - application_permissions:, - locale:, - entitlements:, - authorizing_integration_owners_ids:, - context:, - attachment_size_limit_bytes:, - )) -} - -@internal -pub fn data_decoder(type_: Int) -> decode.Decoder(Data) { - case type_ { - 2 | 4 -> decode.map(command_execution_decoder(), CommandExecuted) - 3 -> - decode.map( - message_component_execution_decoder(), - MessageComponentExecuted, - ) - 5 -> decode.map(modal_submission_decoder(), ModalSubmitted) - _ -> - decode.failure( - CommandExecuted( - SlashCommandExecuted(SlashCommandExecution("", "", None, [], None)), - ), - "Data", - ) - } -} - -@internal -pub fn modal_submission_decoder() -> decode.Decoder(ModalSubmission) { - use custom_id <- decode.field("custom_id", decode.string) - use components <- decode.field( - "components", - decode.list(of: submitted_modal_component_decoder()), - ) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - - decode.success(ModalSubmission(custom_id:, components:, resolved:)) -} - -@internal -pub fn submitted_modal_component_decoder() -> decode.Decoder( - SubmittedModalComponent, -) { - use type_ <- decode.field("type", decode.int) - case type_ { - 10 -> decode.map(text_display_submission_decoder(), TextDisplaySubmitted) - 18 -> decode.map(label_submission_decoder(), LabelSubmitted) - _ -> - decode.failure( - TextDisplaySubmitted(TextDisplaySubmission(0)), - "SubmittedModalComponent", - ) - } -} - -fn radio_group_submission_decoder() -> decode.Decoder(RadioGroupSubmission) { - use id <- decode.field("id", decode.int) - use custom_id <- decode.field("custom_id", decode.string) - use value <- decode.field("value", decode.optional(decode.string)) - - decode.success(RadioGroupSubmission(id:, custom_id:, value:)) -} - -@internal -pub fn text_input_submission_decoder() -> decode.Decoder(TextInputSubmission) { - use id <- decode.field("id", decode.int) - use custom_id <- decode.field("custom_id", decode.string) - use value <- decode.field("value", decode.string) - - decode.success(TextInputSubmission(id:, custom_id:, value:)) -} - -@internal -pub fn text_display_submission_decoder() -> decode.Decoder( - TextDisplaySubmission, -) { - use id <- decode.field("id", decode.int) - decode.success(TextDisplaySubmission(id:)) -} - -@internal -pub fn file_upload_submission_decoder() -> decode.Decoder(FileUploadSubmission) { - use id <- decode.field("id", decode.int) - use custom_id <- decode.field("custom_id", decode.string) - use uploaded_files_ids <- decode.field( - "values", - decode.list(of: decode.string), - ) - - decode.success(FileUploadSubmission(id:, custom_id:, uploaded_files_ids:)) -} - -@internal -pub fn label_submission_decoder() -> decode.Decoder(LabelSubmission) { - use id <- decode.field("id", decode.int) - use component <- decode.field( - "component", - submitted_label_component_decoder(), - ) - decode.success(LabelSubmission(id:, component:)) -} - -fn checkbox_submission_decoder() -> decode.Decoder(CheckboxSubmission) { - use id <- decode.field("id", decode.int) - use custom_id <- decode.field("custom_id", decode.string) - use is_selected <- decode.field("value", decode.bool) - - decode.success(CheckboxSubmission(id, custom_id, is_selected)) -} - -@internal -pub fn submitted_label_component_decoder() -> decode.Decoder( - SubmittedLabelComponent, -) { - use type_ <- decode.field("type", decode.int) - case type_ { - 3 -> - decode.map(string_select_execution_decoder(), LabelStringSelectSubmitted) - 4 -> decode.map(text_input_submission_decoder(), LabelTextInputSubmitted) - 5 -> decode.map(user_select_execution_decoder(), LabelUserSelectSubmitted) - 6 -> decode.map(role_select_execution_decoder(), LabelRoleSelectSubmitted) - 7 -> - decode.map( - mentionable_select_execution_decoder(), - LabelMentionableSelectSubmitted, - ) - 8 -> - decode.map( - channel_select_execution_decoder(), - LabelChannelSelectSubmitted, - ) - 19 -> decode.map(file_upload_submission_decoder(), LabelFileUploadSubmitted) - 21 -> decode.map(radio_group_submission_decoder(), LabelRadioGroupSubmitted) - 22 -> - decode.map( - checkbox_group_submission_decoder(), - LabelCheckboxGroupSubmitted, - ) - 23 -> decode.map(checkbox_submission_decoder(), LabelCheckboxSubmitted) - _ -> - decode.failure( - LabelStringSelectSubmitted(StringSelectExecution("", [], None)), - "SubmittedLabelComponent", - ) - } -} - -fn checkbox_group_submission_decoder() -> decode.Decoder( - CheckboxGroupSubmission, -) { - use id <- decode.field("id", decode.int) - use custom_id <- decode.field("custom_id", decode.string) - use selected_values <- decode.field("values", decode.list(of: decode.string)) - - decode.success(CheckboxGroupSubmission(id, custom_id, selected_values)) -} - -@internal -pub fn command_execution_decoder() -> decode.Decoder(CommandExecution) { - use command_type <- decode.field("type", decode.int) - case command_type { - 1 -> decode.map(slash_command_execution_decoder(), SlashCommandExecuted) - 2 -> decode.map(user_command_execution_decoder(), UserCommandExecuted) - 3 -> decode.map(message_command_execution_decoder(), MessageCommandExecuted) - _ -> - decode.failure( - SlashCommandExecuted(SlashCommandExecution("", "", None, [], None)), - "CommandExecution", - ) - } -} - -@internal -pub fn button_execution_decoder() -> decode.Decoder(ButtonExecution) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - - decode.success(ButtonExecution(custom_id:, resolved:)) -} - -@internal -pub fn string_select_execution_decoder() -> decode.Decoder( - StringSelectExecution, -) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use selected_values <- decode.field("values", decode.list(decode.string)) - decode.success(StringSelectExecution(custom_id:, selected_values:, resolved:)) -} - -@internal -pub fn user_select_execution_decoder() -> decode.Decoder(UserSelectExecution) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use selected_values <- decode.field("values", decode.list(decode.string)) - decode.success(UserSelectExecution( - custom_id:, - selected_users_ids: selected_values, - resolved:, - )) -} - -@internal -pub fn role_select_execution_decoder() -> decode.Decoder(RoleSelectExecution) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use selected_values <- decode.field("values", decode.list(decode.string)) - decode.success(RoleSelectExecution( - custom_id:, - selected_roles_ids: selected_values, - resolved:, - )) -} - -@internal -pub fn mentionable_select_execution_decoder() -> decode.Decoder( - MentionableSelectExecution, -) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use selected_values <- decode.field("values", decode.list(decode.string)) - decode.success(MentionableSelectExecution( - custom_id:, - selected_mentionables_ids: selected_values, - resolved:, - )) -} - -@internal -pub fn channel_select_execution_decoder() -> decode.Decoder( - ChannelSelectExecution, -) { - use custom_id <- decode.field("custom_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use selected_values <- decode.field("values", decode.list(decode.string)) - decode.success(ChannelSelectExecution( - custom_id:, - selected_channels_ids: selected_values, - resolved:, - )) -} - -@internal -pub fn message_component_execution_decoder() -> decode.Decoder( - MessageComponentExecution, -) { - use component_type <- decode.field("component_type", decode.int) - - case component_type { - 2 -> decode.map(button_execution_decoder(), ButtonExecuted) - 3 -> decode.map(string_select_execution_decoder(), StringSelectExecuted) - 5 -> decode.map(user_select_execution_decoder(), UserSelectExecuted) - 6 -> decode.map(role_select_execution_decoder(), RoleSelectExecuted) - 7 -> - decode.map( - mentionable_select_execution_decoder(), - MentionableSelectExecuted, - ) - 8 -> decode.map(channel_select_execution_decoder(), ChannelSelectExecuted) - _ -> - decode.failure( - ButtonExecuted(ButtonExecution("", None)), - "MessageComponentExecution", - ) - } -} - -@internal -pub fn slash_command_execution_decoder() -> decode.Decoder( - SlashCommandExecution, -) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use options <- decode.optional_field( - "options", - [], - decode.list(of: slash_command_option_decoder()), - ) - use registered_to_guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - decode.success(SlashCommandExecution( - id:, - name:, - resolved:, - options:, - registered_to_guild_id:, - )) -} - -@internal -pub fn context_decoder() -> decode.Decoder(Context) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(TriggeredInGuild) - 1 -> decode.success(TriggeredInBotDms) - 2 -> decode.success(TriggeredInPrivateChannel) - _ -> decode.failure(TriggeredInGuild, "Context") - } -} - -@internal -pub fn invokement_info_decoder() -> decode.Decoder(InvokementInfo) { - let in_guild_decoder = { - use guild_id <- decode.field("guild_id", decode.string) - use member <- decode.field("member", guild_member.decoder()) - use guild_locale <- decode.field("guild_locale", decode.string) - - decode.success(InvokedInGuild(guild_id:, member:, guild_locale:)) - } - let in_dm_decoder = { - use user <- decode.field("user", user.decoder()) - decode.success(InvokedInDm(user:)) - } - - decode.one_of(in_guild_decoder, or: [in_dm_decoder]) -} - -@internal -pub fn slash_command_option_decoder() -> decode.Decoder(SlashCommandOption) { - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", decode.int) - use is_focused <- decode.optional_field("focused", False, decode.bool) - - case type_ { - 1 | 2 -> { - use options <- decode.field( - "options", - decode.list(of: slash_command_option_decoder()), - ) - case type_ { - 1 -> decode.success(SubCommandSlashCommandOption(name:, options:)) - 2 -> decode.success(SubCommandGroupSlashCommandOption(name:, options:)) - _ -> - decode.failure( - SubCommandSlashCommandOption("", []), - "SlashCommandOption", - ) - } - } - 3 -> { - use value <- decode.field("value", decode.string) - decode.success(StringSlashCommandOption(name:, value:, is_focused:)) - } - 4 -> { - use value <- decode.field("value", decode.int) - decode.success(IntegerSlashCommandOption(name:, value:, is_focused:)) - } - 5 -> { - use value <- decode.field("value", decode.bool) - decode.success(BoolSlashCommandOption(name:, value:, is_focused:)) - } - 6 | 7 | 8 | 9 | 11 -> { - use id <- decode.field("value", decode.string) - case type_ { - 6 -> - decode.success(UserSlashCommandOption(name:, user_id: id, is_focused:)) - 7 -> - decode.success(ChannelSlashCommandOption( - name:, - channel_id: id, - is_focused:, - )) - 8 -> - decode.success(RoleSlashCommandOption(name:, role_id: id, is_focused:)) - 9 -> - decode.success(MentionableSlashCommandOption( - name:, - mentionable_id: id, - is_focused:, - )) - 11 -> - decode.success(AttachmentSlashCommandOption( - name:, - attachment_id: id, - is_focused:, - )) - _ -> - decode.failure( - SubCommandSlashCommandOption("", []), - "SlashCommandOption", - ) - } - } - 10 -> { - use value <- decode.field("value", decode.float) - decode.success(NumberSlashCommandOption(name:, value:, is_focused:)) - } - _ -> - decode.failure(SubCommandSlashCommandOption("", []), "SlashCommandOption") - } -} - -@internal -pub fn resolved_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, guild_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, resolved_channel_decoder())), - ) - use messages <- decode.optional_field( - "messages", - None, - decode.optional(decode.dict(decode.string, message.decoder())), - ) - use attachments <- decode.optional_field( - "attachments", - None, - decode.optional(decode.dict(decode.string, attachment.decoder())), - ) - - decode.success(Resolved( - users:, - members:, - roles:, - channels:, - messages:, - attachments:, - )) -} - -@internal -pub fn resolved_channel_decoder() -> decode.Decoder(ResolvedChannel) { - use type_ <- decode.field("type", decode.int) - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use current_user_permissions <- decode.field( - "permissions", - permission.decoder(), - ) - - case type_ { - 0 -> - decode.success(ResolvedTextChannel(id:, name:, current_user_permissions:)) - 2 -> - decode.success(ResolvedVoiceChannel(id:, name:, current_user_permissions:)) - 4 -> - decode.success(ResolvedCategoryChannel( - id:, - name:, - current_user_permissions:, - )) - 5 -> - decode.success(ResolvedAnnouncementChannel( - id:, - name:, - current_user_permissions:, - )) - 10 | 11 | 12 -> { - use metadata <- decode.field("thread_metadata", thread.metadata_decoder()) - use parent_id <- decode.field("parent_id", decode.string) - case type_ { - 10 -> - decode.success(ResolvedAnnouncementThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - 11 -> - decode.success(ResolvedPublicThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - 12 -> - decode.success(ResolvedPrivateThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - _ -> decode.failure(ResolvedTextChannel("", "", []), "ResolvedChannel") - } - } - 13 -> - decode.success(ResolvedStageChannel(id:, name:, current_user_permissions:)) - 15 -> - decode.success(ResolvedForumChannel(id:, name:, current_user_permissions:)) - 16 -> - decode.success(ResolvedMediaChannel(id:, name:, current_user_permissions:)) - _ -> decode.failure(ResolvedTextChannel("", "", []), "ResolvedChannel") - } -} - -@internal -pub fn user_command_execution_decoder() -> decode.Decoder(UserCommandExecution) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use registered_to_guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use user_id <- decode.field("target_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - - decode.success(UserCommandExecution( - id:, - name:, - resolved:, - registered_to_guild_id:, - user_id:, - )) -} - -@internal -pub fn message_command_execution_decoder() -> decode.Decoder( - MessageCommandExecution, -) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use registered_to_guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use message_id <- decode.field("target_id", decode.string) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - decode.success(MessageCommandExecution( - id:, - name:, - resolved:, - registered_to_guild_id:, - message_id:, - )) -} - -@internal -pub fn response_decoder() -> decode.Decoder(Response) { - use type_ <- decode.field("type", decode.int) - use data <- decode.optional_field( - "data", - None, - decode.optional(case type_ { - 4 -> - decode.map( - response_message_decoder(), - RespondWithChannelMessageWithSource, - ) - 5 -> - decode.map( - response_message_decoder(), - RespondWithDeferredChannelMessageWithSource, - ) - 7 -> decode.map(response_message_decoder(), RespondWithUpdateMessage) - _ -> decode.failure(RespondWithDeferredUpdateMessage, "Response") - }), - ) - - case data { - Some(data) -> decode.success(data) - None -> - case type_ { - 6 -> decode.success(RespondWithDeferredUpdateMessage) - _ -> decode.failure(RespondWithDeferredUpdateMessage, "Response") - } - } -} - -@internal -pub fn response_message_decoder() -> decode.Decoder(ResponseMessage) { - use is_tts <- decode.optional_field("tts", None, decode.optional(decode.bool)) - use content <- decode.optional_field( - "content", - None, - decode.optional(decode.string), - ) - use embeds <- decode.optional_field( - "embed", - None, - decode.optional(decode.list(of: embed.decoder())), - ) - use allowed_mentions <- decode.optional_field( - "allowed_mentions", - None, - decode.optional(allowed_mentions.decoder()), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_response_message_flags())), - ) - use components <- decode.optional_field( - "components", - None, - decode.optional(decode.list(of: message.component_decoder())), - ) - use attachments <- decode.optional_field( - "attachments", - None, - decode.optional(decode.list(of: attachment.create_decoder())), - ) - use poll <- decode.optional_field( - "poll", - None, - decode.optional(poll.create_decoder()), - ) - - decode.success(ResponseMessage( - is_tts:, - content:, - embeds:, - allowed_mentions:, - flags:, - components:, - attachments:, - files: None, - poll:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn response_to_json(response: Response) -> Json { - let type_ = [ - #("type", case response { - RespondWithChannelMessageWithSource(..) -> json.int(4) - RespondWithDeferredChannelMessageWithSource(..) -> json.int(5) - RespondWithDeferredUpdateMessage -> json.int(6) - RespondWithUpdateMessage(..) -> json.int(7) - RespondWithModal(..) -> json.int(9) - }), - ] - - let data = case response { - RespondWithChannelMessageWithSource(message) -> [ - #("data", response_message_to_json(message)), - ] - RespondWithDeferredChannelMessageWithSource(message) -> [ - #("data", response_message_to_json(message)), - ] - RespondWithDeferredUpdateMessage -> [] - RespondWithUpdateMessage(message) -> [ - #("data", response_message_to_json(message)), - ] - RespondWithModal(modal) -> [#("data", modal_response_to_json(modal))] - } - - [type_, data] - |> list.flatten - |> json.object -} - -@internal -pub fn response_message_to_json(message: ResponseMessage) -> Json { - let is_tts = case message.is_tts { - Some(tts) -> [#("tts", json.bool(tts))] - None -> [] - } - - 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 -> [] - } - - let flags = case message.flags { - Some(flags) -> [ - #("flags", flags.to_json(flags, bits_response_message_flags())), - ] - None -> [] - } - - let components = case message.components { - Some(components) -> [ - #("components", json.array(components, message.component_to_json)), - ] - None -> [] - } - - let attachment = case message.attachments { - Some(attachments) -> [ - #("attachments", json.array(attachments, attachment.create_to_json)), - ] - None -> [] - } - - let poll = case message.poll { - Some(poll) -> [#("poll", poll.create_to_json(poll))] - None -> [] - } - - [ - is_tts, - content, - embeds, - allowed_mentions, - flags, - components, - attachment, - poll, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn modal_response_to_json(modal: ModalResponse) -> Json { - json.object([ - #("custom_id", json.string(modal.custom_id)), - #("title", json.string(modal.title)), - #("components", json.array(modal.components, modal.component_to_json)), - ]) -} - -@internal -pub fn modify_original_response_to_json( - modify: ModifyOriginalResponse, -) -> Json { - [ - modification.to_json(modify.content, "content", json.string), - modification.to_json(modify.embeds, "embeds", json.array(_, embed.to_json)), - modification.to_json(modify.flags, "flags", flags.to_json( - _, - bits_modify_original_response_flags(), - )), - modification.to_json(modify.components, "components", json.array( - _, - message.component_to_json, - )), - modification.to_json(modify.attachments, "attachments", json.array( - _, - attachment.create_to_json, - )), - modification.to_json(modify.poll, "poll", poll.create_to_json), - ] - |> list.flatten - |> json.object -} - -@internal -pub fn followup_to_json(followup: Followup) -> Json { - let content = case followup.content { - Some(content) -> [#("content", json.string(content))] - None -> [] - } - - let is_tts = [#("tts", json.bool(followup.is_tts))] - - let embeds = case followup.embeds { - Some(embeds) -> [#("embeds", json.array(embeds, embed.to_json))] - None -> [] - } - - let allowed_mentions = case followup.allowed_mentions { - Some(allowed_mentions) -> [ - #("allowed_mentions", allowed_mentions.to_json(allowed_mentions)), - ] - None -> [] - } - - let components = case followup.components { - Some(components) -> [ - #("components", json.array(components, message.component_to_json)), - ] - None -> [] - } - - let attachments = case followup.attachments { - Some(attachments) -> [ - #("attachments", json.array(attachments, attachment.create_to_json)), - ] - None -> [] - } - - let flags = case followup.flags { - Some(flags) -> [#("flags", flags.to_json(flags, bits_followup_flags()))] - None -> [] - } - - let poll = case followup.poll { - Some(poll) -> [#("poll", poll.create_to_json(poll))] - None -> [] - } - - [ - content, - is_tts, - embeds, - allowed_mentions, - components, - attachments, - flags, - poll, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn modify_followup_to_json(modify: ModifyFollowup) -> Json { - [ - modification.to_json(modify.content, "content", json.string), - modification.to_json(modify.embeds, "embeds", json.array(_, embed.to_json)), - modification.to_json(modify.flags, "flags", flags.to_json( - _, - bits_modify_followup_flags(), - )), - modification.to_json(modify.components, "components", json.array( - _, - message.component_to_json, - )), - modification.to_json(modify.attachments, "attachments", json.array( - _, - attachment.create_to_json, - )), - modification.to_json(modify.poll, "poll", poll.create_to_json), - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn respond( - client: grom.Client, - to interaction: Interaction, - using response: Response, -) -> Result(Nil, grom.Error) { - let json = response |> response_to_json - - let path = - "/interactions/" - <> interaction.id - <> "/" - <> interaction.token - <> "/callback" - - let multipart_request = rest.new_multipart_request( - client, - http.Post, - path, - json, - _, - ) - - let non_multipart_request = - client - |> rest.new_request(http.Post, path) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - - let request = case response { - RespondWithChannelMessageWithSource(message) -> { - case message.files { - Some(files) -> multipart_request(files) - None -> non_multipart_request - } - } - RespondWithUpdateMessage(message) -> { - case message.files { - Some(files) -> multipart_request(files) - None -> non_multipart_request - } - } - _ -> non_multipart_request - } - - rest.execute_bytes(request) - |> result.replace(Nil) -} - -pub fn get_original_response( - client: grom.Client, - for interaction: Interaction, -) -> Result(Response, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/@original", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: response_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_original_response( - client: grom.Client, - of interaction: Interaction, - using modify: ModifyOriginalResponse, -) -> Result(Message, grom.Error) { - let json = modify |> modify_original_response_to_json - - let path = - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/@original" - - let multipart_request = rest.new_multipart_request( - client, - http.Patch, - path, - json, - _, - ) - - let non_multipart_request = - rest.new_request(client, http.Patch, path) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - - let request = case modify.files { - modification.New(files) -> multipart_request(files) - _ -> non_multipart_request - } - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: message.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_original_response() -> ModifyOriginalResponse { - ModifyOriginalResponse(Skip, Skip, Skip, Skip, Skip, Skip, Skip, Skip) -} - -pub fn delete_original_response( - client: grom.Client, - of interaction: Interaction, -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/@original", - ) - |> rest.execute - |> result.replace(Nil) -} - -/// Do not use this function to send a message after deferring it. -/// Use [`modify_original_response`](#modify_original_response) instead. -pub fn followup( - client: grom.Client, - to interaction: Interaction, - using followup: Followup, -) -> Result(Message, grom.Error) { - let json = followup |> followup_to_json - - let path = - "/webhooks/" <> interaction.application_id <> "/" <> interaction.token - - let multipart_request = rest.new_multipart_request( - client, - http.Post, - path, - json, - _, - ) - - let non_multipart_request = - rest.new_request(client, http.Post, path) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - - let request = case followup.files { - Some(files) -> multipart_request(files) - None -> non_multipart_request - } - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: message.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_followup() -> Followup { - Followup(None, False, None, None, None, None, None, None, None) -} - -pub fn get_followup( - client: grom.Client, - for interaction: Interaction, - id message_id: String, -) -> Result(Message, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/" - <> message_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: message.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify_followup( - client: grom.Client, - for interaction: Interaction, - id message_id: String, - using modify: ModifyFollowup, -) -> Result(Message, grom.Error) { - let json = modify |> modify_followup_to_json - - let path = - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/" - <> message_id - - let multipart_request = rest.new_multipart_request( - client, - http.Patch, - path, - json, - _, - ) - - let non_multipart_request = - rest.new_request(client, http.Patch, path) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - - let request = case modify.files { - modification.New(files) -> multipart_request(files) - _ -> non_multipart_request - } - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: message.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify_followup() -> ModifyFollowup { - ModifyFollowup(Skip, Skip, Skip, Skip, Skip, Skip, Skip, Skip) -} - -pub fn delete_followup( - client: grom.Client, - of interaction: Interaction, - id message_id: String, -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/webhooks/" - <> interaction.application_id - <> "/" - <> interaction.token - <> "/messages/" - <> message_id, - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn new_response_message() -> ResponseMessage { - ResponseMessage(None, None, None, None, None, None, None, None, None) -} - -type PingInteraction { - PingInteraction -} - -fn ping_interaction_decoder() -> decode.Decoder(PingInteraction) { - use type_ <- decode.field("type", decode.int) - case type_ { - 1 -> decode.success(PingInteraction) - _ -> decode.failure(PingInteraction, "PingInteraction") - } -} - -pub type HttpSecurityError { - /// The public key you provided is not a valid hexadecimal number. - /// - /// This is not to be expected and results in a HTTP response with the status 500 - internal server error. - PublicKeyNotValidHexadecimal - /// Your public key's length as a number is not 32 bytes. - /// - /// This is not to be expected and results in a HTTP response with the status 500 - internal server error. - PublicKeyHasImproperLength - /// A request was made without a `x-signature-ed25519` header. - /// - /// This is to be expected - discord often does "tests", verifying that your webhook validates security headers properly. - /// - /// Returns a 401 unauthorized response. - HttpRequestSignatureNotProvided - /// A request was made without a `x-signature-timestamp` header. - /// - /// This is to be expected - discord often does "tests", verifying that your webhook validates security headers properly. - /// - /// Returns a 401 unauthorized response. - HttpRequestTimestampNotProvided - /// A request was made where the `x-signature-ed25519` was not a valid hexadecimal number. - /// - /// This is to be expected - discord often does "tests", verifying that your webhook validates security headers properly. - /// - /// Returns a 401 unauthorized response. - HttpRequestSignatureNotValidHexadecimal - /// A request was made with the correct headers, however verifying that the message: - /// - was intended for your app - /// - was sent from Discord - /// has failed. - /// - /// This could happen for several reasons: - /// - an automated test was performed by Discord, verifying whether your app validates security headers (most common) - /// - you provided an inappropriate public key (for example, another app's) - /// - there was a forgery attempt - /// - /// Returns a 401 unauthorized response. - HttpRequestVerificationFailed -} - -pub type HttpError { - /// A properly validated request was made, but it couldn't be parsed as an interaction. - /// - /// This isn't to be expected too often. - /// - /// Returns a 422 unprocessable content response. - CouldNotParseInteraction(json.DecodeError) - /// An error has happened while trying to verify the request's security headers. - /// - /// While some of these errors are to be expected, some are genuine bugs within your code. - /// - /// Check the inner field to check what error exactly happened. - CouldNotValidateSecurityHeaders(HttpSecurityError) -} - -/// A HTTP request handler middleware that: -/// - handles PING requests (which verify that your interaction endpoint is still up and secure) -/// - gives you access to the interaction object, allowing you to handle your interactions -/// - sends an HTTP response whenever you're done with handling your interactions -/// -/// Warning: Do not use long-running code to handle your interactions, as it could trigger an HTTP timeout. -/// -/// See the `http_interactions` example to see how to set-up HTTP interactions and how to properly handle them. -pub fn handle_http_interaction_request( - request: Request(String), - public_key public_key: String, - next next: fn(Result(Interaction, HttpError)) -> Nil, -) -> response.Response(String) { - case validate_security_headers(request, public_key) { - Ok(_) -> handle_validated_http_interaction_request(request, next) - Error(PublicKeyNotValidHexadecimal) as err - | Error(PublicKeyHasImproperLength) as err -> { - let Error(internal_server_error) = err - next(Error(CouldNotValidateSecurityHeaders(internal_server_error))) - - response.new(status_code.internal_server_error) - } - Error(unauthorized) -> { - next(Error(CouldNotValidateSecurityHeaders(unauthorized))) - - response.new(status_code.unauthorized) - } - } -} - -fn handle_validated_http_interaction_request( - request: Request(String), - next: fn(Result(Interaction, HttpError)) -> Nil, -) -> response.Response(String) { - let ping = json.parse(request.body, ping_interaction_decoder()) - - case ping { - Ok(_) -> handle_ping_interaction() - Error(_) -> handle_http_interaction(request, next) - } -} - -fn handle_http_interaction( - request: Request(String), - next: fn(Result(Interaction, HttpError)) -> Nil, -) -> response.Response(String) { - let interaction = json.parse(request.body, decoder()) - - case interaction { - Ok(interaction) -> { - next(Ok(interaction)) - response.new(status_code.accepted) - } - Error(err) -> { - next(Error(CouldNotParseInteraction(err))) - response.new(status_code.unprocessable_content) - } - } -} - -fn validate_security_headers( - request: Request(String), - public_key: String, -) -> Result(Nil, HttpSecurityError) { - use public_key <- result.try( - public_key - |> int.base_parse(16) - |> result.replace_error(PublicKeyNotValidHexadecimal), - ) - - use public_key <- result.try( - eddsa.public_key_from_bytes(eddsa.Ed25519, <>) - |> result.replace_error(PublicKeyHasImproperLength), - ) - - use signature <- result.try( - request - |> request.get_header("x-signature-ed25519") - |> result.replace_error(HttpRequestSignatureNotProvided), - ) - - use signature <- result.try( - signature - |> int.base_parse(16) - |> result.replace_error(HttpRequestSignatureNotValidHexadecimal), - ) - - use timestamp <- result.try( - request - |> request.get_header("x-signature-timestamp") - |> result.replace_error(HttpRequestTimestampNotProvided), - ) - - // Ed25519 signatures are 64 bytes - let signature = <> - let message = <<{ timestamp <> request.body }:utf8>> - - let is_verified = eddsa.verify(public_key, message, signature) - - case is_verified { - True -> Ok(Nil) - False -> Error(HttpRequestVerificationFailed) - } -} - -fn handle_ping_interaction() -> response.Response(String) { - let body = - [#("type", json.int(1))] - |> json.object - |> json.to_string - - response.new(status_code.ok) - |> response.set_header("content-type", "application/json") - |> response.set_body(body) -} diff --git a/src/grom/internal/base64.gleam b/src/grom/internal/base64.gleam deleted file mode 100644 index a48fda3..0000000 --- a/src/grom/internal/base64.gleam +++ /dev/null @@ -1,13 +0,0 @@ -import gleam/bit_array -import gleam/dynamic/decode - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(BitArray) { - use string <- decode.then(decode.string) - case bit_array.base64_decode(string) { - Ok(bits) -> decode.success(bits) - Error(_) -> decode.failure(<<>>, "BitArray") - } -} diff --git a/src/grom/internal/flags.gleam b/src/grom/internal/flags.gleam deleted file mode 100644 index 6a671ff..0000000 --- a/src/grom/internal/flags.gleam +++ /dev/null @@ -1,44 +0,0 @@ -import gleam/dynamic/decode -import gleam/int -import gleam/json.{type Json} -import gleam/list - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder(bits_flags: List(#(Int, flag))) -> decode.Decoder(List(flag)) { - use bits <- decode.then(decode.int) - - bits_flags - |> list.filter_map(fn(item) { - let #(bit, flag) = item - case int.bitwise_and(bits, bit) != 0 { - True -> Ok(flag) - False -> Error(Nil) - } - }) - |> decode.success -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(flags: List(flag), bits_flags: List(#(Int, flag))) -> Json { - json.int(flags |> to_int(bits_flags)) -} - -// INTERNAL FUNCTIONS ---------------------------------------------------------- - -@internal -pub fn to_int(flags: List(flag), bits_flags: List(#(Int, flag))) -> Int { - bits_flags - |> list.filter_map(fn(item) { - let #(bit, flag) = item - let is_in_flags = list.any(flags, fn(curr) { curr == flag }) - case is_in_flags { - True -> Ok(bit) - False -> Error(Nil) - } - }) - |> int.sum -} diff --git a/src/grom/internal/rest.gleam b/src/grom/internal/rest.gleam deleted file mode 100644 index 56176f5..0000000 --- a/src/grom/internal/rest.gleam +++ /dev/null @@ -1,147 +0,0 @@ -import gleam/bit_array -import gleam/http -import gleam/http/request.{type Request} -import gleam/http/response.{type Response, Response} -import gleam/httpc -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/file.{type File} -import multipart_form -import multipart_form/field -import status_code - -// CONSTANTS ------------------------------------------------------------------- - -const discord_url: String = "discord.com" - -const discord_api_path: String = "api/v10" - -const cdn_url: String = "cdn.discordapp.com" - -// INTERNAL FUNCTIONS ---------------------------------------------------------- - -@internal -pub fn execute(request: Request(String)) -> Result(Response(String), grom.Error) { - use response <- result.try( - request - |> httpc.send - |> result.map_error(grom.HttpError), - ) - - response - |> ensure_status_code_success -} - -@internal -pub fn execute_bytes( - request: Request(BitArray), -) -> Result(Response(String), grom.Error) { - use response <- result.try( - request - |> httpc.send_bits - |> result.map_error(grom.HttpError), - ) - - use body <- result.try( - response.body - |> bit_array.to_string - |> result.replace_error(grom.ResponseNotValidUtf8(response.body)), - ) - - let response = Response(..response, body:) - - response - |> ensure_status_code_success -} - -@internal -pub fn new_request( - client: grom.Client, - method: http.Method, - path: String, -) -> Request(String) { - request.new() - |> request.set_scheme(http.Https) - |> request.set_host(discord_url) - |> request.set_path(discord_api_path <> path) - |> request.set_method(method) - |> request.prepend_header("authorization", "Bot " <> client.token) - |> request.prepend_header( - "user-agent", - "DiscordBot (https://github.com/folospior/grom, " <> grom.version() <> ")", - ) - |> request.prepend_header("content-type", "application/json") -} - -@internal -pub fn new_multipart_request( - client: grom.Client, - method: http.Method, - path: String, - payload_json: Json, - files: List(File), -) -> Request(BitArray) { - let #(_, file_parts) = - files - |> list.map_fold(0, fn(acc, file) { - #(acc + 1, #( - "files[" <> acc |> int.to_string <> "]", - field.File(file.name, file.content_type, file.content), - )) - }) - - new_request(client, method, path) - |> multipart_form.to_request( - [ - [ - #( - "payload_json", - field.StringWithType( - payload_json |> json.to_string, - "application/json", - ), - ), - ], - file_parts, - ] - |> list.flatten, - ) -} - -@internal -pub fn with_reason(request: Request(a), reason: Option(String)) -> Request(a) { - case reason { - Some(reason) -> - request - |> request.prepend_header("x-audit-log-reason", reason) - None -> request - } -} - -@internal -pub fn new_cdn_request( - to path: String, - query query: List(#(String, String)), -) -> Request(String) { - request.new() - |> request.set_scheme(http.Https) - |> request.set_host(cdn_url) - |> request.set_path(path) - |> request.set_query(query) - |> request.set_method(http.Get) -} - -// PRIVATE FUNCTIONS ----------------------------------------------------------- - -fn ensure_status_code_success( - response: Response(String), -) -> Result(Response(String), grom.Error) { - case status_code.is_successful(response.status) { - True -> Ok(response) - False -> Error(grom.StatusCodeUnsuccessful(response)) - } -} diff --git a/src/grom/internal/time_duration.gleam b/src/grom/internal/time_duration.gleam deleted file mode 100644 index 1a20cb3..0000000 --- a/src/grom/internal/time_duration.gleam +++ /dev/null @@ -1,76 +0,0 @@ -import gleam/dynamic/decode -import gleam/float -import gleam/json.{type Json} -import gleam/time/duration.{type Duration} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn from_minutes_decoder() -> decode.Decoder(Duration) { - use minutes <- decode.then(decode.int) - decode.success(duration.seconds(minutes * 60)) -} - -@internal -pub fn from_milliseconds_decoder() -> decode.Decoder(Duration) { - use milliseconds <- decode.then(decode.int) - decode.success(duration.milliseconds(milliseconds)) -} - -@internal -pub fn from_float_seconds_decoder() -> decode.Decoder(Duration) { - use seconds <- decode.then(decode.float) - - seconds - |> float.round - |> duration.seconds - |> decode.success -} - -@internal -pub fn from_int_seconds_decoder() -> decode.Decoder(Duration) { - use seconds <- decode.then(decode.int) - - seconds - |> duration.seconds - |> decode.success -} - -@internal -pub fn from_int_hours_decoder() -> decode.Decoder(Duration) { - use hours <- decode.then(decode.int) - - hours - |> duration.hours - |> decode.success -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_int_seconds_json(duration: Duration) -> Json { - duration - |> to_int_seconds - |> json.int -} - -@internal -pub fn to_int_hours_json(duration: Duration) -> Json { - duration - |> to_int_hours - |> json.int -} - -// INTERNAL FUNCTIONS ---------------------------------------------------------- - -@internal -pub fn to_int_seconds(duration: Duration) -> Int { - duration - |> duration.to_seconds - |> float.round -} - -@internal -pub fn to_int_hours(duration: Duration) -> Int { - to_int_seconds(duration) / 3600 -} diff --git a/src/grom/internal/time_rfc3339.gleam b/src/grom/internal/time_rfc3339.gleam deleted file mode 100644 index 8dd7aa7..0000000 --- a/src/grom/internal/time_rfc3339.gleam +++ /dev/null @@ -1,28 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/time/duration -import gleam/time/timestamp.{type Timestamp} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Timestamp) { - use rfc3339 <- decode.then(decode.string) - rfc3339 - |> timestamp.parse_rfc3339 - |> fn(timestamp) { - case timestamp { - Ok(time) -> decode.success(time) - Error(_) -> decode.failure(timestamp.from_unix_seconds(0), "Timestamp") - } - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(timestamp: Timestamp) -> Json { - timestamp - |> timestamp.to_rfc3339(duration.seconds(0)) - |> json.string -} diff --git a/src/grom/internal/time_timestamp.gleam b/src/grom/internal/time_timestamp.gleam deleted file mode 100644 index 873bee9..0000000 --- a/src/grom/internal/time_timestamp.gleam +++ /dev/null @@ -1,22 +0,0 @@ -import gleam/dynamic/decode -import gleam/float -import gleam/time/timestamp.{type Timestamp} - -@internal -pub fn from_unix_milliseconds_decoder() -> decode.Decoder(Timestamp) { - use milliseconds <- decode.then(decode.int) - decode.success(timestamp.from_unix_seconds(milliseconds / 1000)) -} - -@internal -pub fn from_unix_seconds_decoder() -> decode.Decoder(Timestamp) { - decode.map(decode.int, timestamp.from_unix_seconds) -} - -@internal -pub fn to_unix_milliseconds(timestamp: Timestamp) -> Int { - timestamp - |> timestamp.to_unix_seconds - |> float.multiply(1000.0) - |> float.round -} diff --git a/src/grom/invite.gleam b/src/grom/invite.gleam deleted file mode 100644 index a5db035..0000000 --- a/src/grom/invite.gleam +++ /dev/null @@ -1,594 +0,0 @@ -import gleam/bit_array -import gleam/bool -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/string -import gleam/time/duration.{type Duration} -import gleam/time/timestamp.{type Timestamp} -import grom -import grom/application.{type Application} -import grom/channel.{type Channel} -import grom/guild.{type Guild} -import grom/guild/role -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/user.{type User, User} -import multipart_form -import multipart_form/field -import splitter.{type Splitter} - -// TYPES ----------------------------------------------------------------------- - -pub type Invite { - InviteWithoutMetadata(WithoutMetadata) - InviteWithMetadata(WithMetadata) -} - -pub type WithoutMetadata { - WithoutMetadata( - type_: Type, - code: String, - guild: Option(Guild), - channel: Option(Channel), - inviter: Option(User), - target_type: Option(TargetType), - approximate_presence_count: Option(Int), - approximate_member_count: Option(Int), - expires_at: Option(Timestamp), - flags: Option(List(Flag)), - roles: Option(List(PartialRole)), - ) -} - -pub type PartialRole { - PartialRole( - id: String, - name: String, - position: Int, - colors: role.Colors, - icon_hash: Option(String), - unicode_emoji: Option(String), - ) -} - -fn partial_role_decoder() -> decode.Decoder(PartialRole) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use position <- decode.field("position", decode.int) - use colors <- decode.field("colors", role.colors_decoder()) - use icon_hash <- decode.optional_field( - "icon", - None, - decode.optional(decode.string), - ) - use unicode_emoji <- decode.optional_field( - "unicode_emoji", - None, - decode.optional(decode.string), - ) - decode.success(PartialRole( - id:, - name:, - position:, - colors:, - icon_hash:, - unicode_emoji:, - )) -} - -pub type WithMetadata { - WithMetadata( - type_: Type, - code: String, - guild: Option(Guild), - channel: Option(Channel), - inviter: Option(User), - target_type: Option(TargetType), - approximate_presence_count: Option(Int), - approximate_member_count: Option(Int), - expires_at: Option(Timestamp), - uses: Int, - max_uses: Int, - max_age: Duration, - is_temporary: Bool, - created_at: Timestamp, - flags: Option(List(Flag)), - roles: Option(List(PartialRole)), - ) -} - -pub type Flag { - IsGuestInvite -} - -fn bits_flags() -> List(#(Int, Flag)) { - [#(int.bitwise_shift_left(1, 0), IsGuestInvite)] -} - -pub type Create { - Create( - max_age: Option(Duration), - max_uses: Option(Int), - is_temporary: Bool, - is_unique: Bool, - target_type: Option(CreateTargetType), - target_users_ids: Option(List(String)), - /// Automatically given roles to users accepting this invite. - role_ids: Option(List(String)), - ) -} - -pub type Type { - ToGuild - ToGroupDm - ToFriend -} - -pub type CreateTargetType { - CreateForStream(streaming_user_id: String) - CreateForEmbeddedApplication(application_id: String) -} - -pub type TargetType { - ForStream(streaming_user: User) - ForEmbeddedApplication(application: Application) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Invite) { - let without_metadata = { - use invite <- decode.then(without_metadata_decoder()) - decode.success(InviteWithoutMetadata(invite)) - } - - let with_metadata = { - use invite <- decode.then(with_metadata_decoder()) - decode.success(InviteWithMetadata(invite)) - } - - decode.one_of(with_metadata, or: [without_metadata]) -} - -@internal -pub fn without_metadata_decoder() -> decode.Decoder(WithoutMetadata) { - use type_ <- decode.field("type", type_decoder()) - use code <- decode.field("code", decode.string) - use guild <- decode.optional_field( - "guild", - None, - decode.optional(guild.decoder()), - ) - use channel <- decode.field("channel", decode.optional(channel.decoder())) - use inviter <- decode.optional_field( - "inviter", - None, - decode.optional(user.decoder()), - ) - use target_type <- decode.optional_field( - "target_type", - None, - decode.optional(decode.int), - ) - - use target_type <- decode.then(case target_type { - Some(1) -> { - use streaming_user <- decode.field("target_user", user.decoder()) - decode.success(Some(ForStream(streaming_user:))) - } - Some(2) -> { - use application <- decode.field( - "target_application", - application.decoder(), - ) - decode.success(Some(ForEmbeddedApplication(application:))) - } - Some(_) -> - decode.failure( - Some( - ForStream(User( - "", - "", - "", - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - )), - ), - "TargetType", - ) - None -> decode.success(None) - }) - use approximate_presence_count <- decode.optional_field( - "approximate_presence_count", - None, - decode.optional(decode.int), - ) - use approximate_member_count <- decode.optional_field( - "approximate_member_count", - None, - decode.optional(decode.int), - ) - use expires_at <- decode.optional_field( - "expires_at", - None, - decode.optional(time_rfc3339.decoder()), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use roles <- decode.optional_field( - "roles", - None, - decode.optional(decode.list(of: partial_role_decoder())), - ) - decode.success(WithoutMetadata( - type_:, - code:, - guild:, - channel:, - inviter:, - target_type:, - approximate_presence_count:, - approximate_member_count:, - expires_at:, - flags:, - roles:, - )) -} - -@internal -pub fn with_metadata_decoder() -> decode.Decoder(WithMetadata) { - use type_ <- decode.field("type", type_decoder()) - use code <- decode.field("code", decode.string) - use guild <- decode.optional_field( - "guild", - None, - decode.optional(guild.decoder()), - ) - use channel <- decode.field("channel", decode.optional(channel.decoder())) - use inviter <- decode.optional_field( - "inviter", - None, - decode.optional(user.decoder()), - ) - - use target_type <- decode.optional_field( - "target_type", - None, - decode.optional(decode.int), - ) - - use target_type <- decode.then(case target_type { - Some(1) -> { - use streaming_user <- decode.field("target_user", user.decoder()) - decode.success(Some(ForStream(streaming_user:))) - } - Some(2) -> { - use application <- decode.field( - "target_application", - application.decoder(), - ) - decode.success(Some(ForEmbeddedApplication(application:))) - } - Some(_) -> - decode.failure( - Some( - ForStream(User( - "", - "", - "", - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - )), - ), - "TargetType", - ) - None -> decode.success(None) - }) - - use approximate_presence_count <- decode.optional_field( - "approximate_presence_count", - None, - decode.optional(decode.int), - ) - use approximate_member_count <- decode.optional_field( - "approximate_member_count", - None, - decode.optional(decode.int), - ) - use expires_at <- decode.optional_field( - "expires_at", - None, - decode.optional(time_rfc3339.decoder()), - ) - use uses <- decode.field("uses", decode.int) - use max_uses <- decode.field("max_uses", decode.int) - use max_age <- decode.field( - "max_age", - time_duration.from_int_seconds_decoder(), - ) - use is_temporary <- decode.field("temporary", decode.bool) - use created_at <- decode.field("created_at", time_rfc3339.decoder()) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use roles <- decode.optional_field( - "roles", - None, - decode.optional(decode.list(of: partial_role_decoder())), - ) - - decode.success(WithMetadata( - type_:, - code:, - guild:, - channel:, - inviter:, - target_type:, - approximate_presence_count:, - approximate_member_count:, - expires_at:, - uses:, - max_uses:, - max_age:, - is_temporary:, - created_at:, - flags:, - roles:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(ToGuild) - 1 -> decode.success(ToGroupDm) - 2 -> decode.success(ToFriend) - _ -> decode.failure(ToGuild, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let max_age = case create.max_age { - Some(age) -> [#("max_age", time_duration.to_int_seconds_json(age))] - None -> [] - } - - let max_uses = case create.max_uses { - Some(uses) -> [#("max_uses", json.int(uses))] - None -> [] - } - - let is_temporary = [#("temporary", json.bool(create.is_temporary))] - - let is_unique = [#("unique", json.bool(create.is_unique))] - - let target_type = case create.target_type { - Some(CreateForStream(streaming_user_id)) -> [ - #("target_type", json.int(1)), - #("target_user_id", json.string(streaming_user_id)), - ] - Some(CreateForEmbeddedApplication(application_id)) -> [ - #("target_type", json.int(2)), - #("target_application_id", json.string(application_id)), - ] - None -> [] - } - - let role_ids = case create.role_ids { - Some(ids) -> [#("role_ids", json.array(ids, json.string))] - None -> [] - } - - [max_age, max_uses, is_temporary, is_unique, target_type, role_ids] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_all_for_channel( - client: grom.Client, - with_id channel_id: String, -) -> Result(List(WithMetadata), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/channels/" <> channel_id <> "/invites") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(with_metadata_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_all_for_guild( - client: grom.Client, - with_id guild_id: String, -) -> Result(List(Invite), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/guilds/" <> guild_id <> "/invites") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create( - client: grom.Client, - for channel_id: String, - with create: Create, - reason reason: Option(String), -) -> Result(WithoutMetadata, grom.Error) { - let json = create |> create_to_json - - let path = "/channels/" <> channel_id <> "/invites" - - let request = case create.target_users_ids { - Some(ids) -> { - let ids_csv = ids |> string.join("\n") |> bit_array.from_string - - client - |> rest.new_request(http.Post, path) - |> multipart_form.to_request([ - #( - "target_users_file", - field.File("target_users.csv", "text/csv", ids_csv), - ), - ]) - } - None -> { - client - |> rest.new_request(http.Post, path) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - } - } - - use response <- result.try( - request - |> rest.with_reason(reason) - |> rest.execute_bytes, - ) - - response.body - |> json.parse(using: without_metadata_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get( - client: grom.Client, - code invite_code: String, - include_counts with_counts: Bool, - scheduled_event_id guild_scheduled_event_id: Option(String), -) -> Result(Invite, grom.Error) { - let query = - [ - [#("with_counts", bool.to_string(with_counts))], - case guild_scheduled_event_id { - Some(id) -> [#("guild_scheduled_event_id", id)] - None -> [] - }, - ] - |> list.flatten - - use response <- result.try( - client - |> rest.new_request(http.Get, "/invites/" <> invite_code) - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete( - client: grom.Client, - code invite_code: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request(http.Delete, "/invites/" <> invite_code) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} - -pub fn new_create() -> Create { - Create(None, None, False, False, None, None, None) -} - -pub fn get_target_users_ids( - client: grom.Client, - for_code code: String, -) -> Result(List(String), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/invites/" <> code <> "/target-users") - |> rest.execute, - ) - - Ok(split_endlines(response.body)) -} - -// Cursed CSV handling, because this is a cursed CSV. -fn split_endlines(string: String) -> List(String) { - let splitter = splitter.new(["\r\n", "\n"]) - split_endlines_loop(string, splitter, []) -} - -fn split_endlines_loop( - string: String, - splitter: Splitter, - acc: List(String), -) -> List(String) { - case splitter.split(splitter, string) { - #(last, "", "") -> - [last, ..acc] - |> list.reverse - |> list.drop(1) - #(id, _, rest) -> split_endlines_loop(rest, splitter, [id, ..acc]) - } -} - -/// Supply the full list of users that can use the invite, not just the new users. -pub fn update_target_users( - client: grom.Client, - for_code code: String, - users_ids ids: List(String), -) -> Result(Nil, grom.Error) { - let ids_csv = ids |> string.join("\n") |> bit_array.from_string - - client - |> rest.new_request(http.Put, "/invites/" <> code <> "/target-users") - |> multipart_form.to_request([ - #("target_users_file", field.File("target_users.csv", "text/csv", ids_csv)), - ]) - |> rest.execute_bytes - |> result.replace(Nil) -} diff --git a/src/grom/message.gleam b/src/grom/message.gleam deleted file mode 100644 index 9a0789f..0000000 --- a/src/grom/message.gleam +++ /dev/null @@ -1,1499 +0,0 @@ -import gleam/bit_array -import gleam/dict.{type Dict} -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 gleam/time/timestamp.{type Timestamp} -import gleam/uri -import grom -import grom/application.{type Application} -import grom/channel/thread.{type Thread} -import grom/component/action_row.{type ActionRow} -import grom/component/container.{type Container} -import grom/component/file as component_file -import grom/component/media_gallery.{type MediaGallery} -import grom/component/section.{type Section} -import grom/component/separator.{type Separator} -import grom/component/text_display.{type TextDisplay} -import grom/file.{type File} -import grom/guild/role.{type Role} -import grom/guild_member.{type GuildMember} -import grom/internal/flags -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/message/activity.{type Activity} -import grom/message/allowed_mentions.{type AllowedMentions} -import grom/message/attachment.{type Attachment} -import grom/message/call.{type Call} -import grom/message/embed.{type Embed} -import grom/message/interaction_metadata.{type InteractionMetadata} -import grom/message/message_reference.{type MessageReference} -import grom/message/poll.{type Poll} -import grom/message/reaction.{type Reaction} -import grom/modification.{type Modification, New, Skip} -import grom/permission.{type Permission} -import grom/sticker -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Message { - Message( - id: String, - channel_id: String, - author: User, - content: String, - sent_at: Timestamp, - last_edited_at: Option(Timestamp), - is_tts: Bool, - mentions_everyone: Bool, - mentions_users: List(User), - mentions_roles: List(String), - mentions_channels: Option(List(ChannelMention)), - attachments: List(Attachment), - embeds: List(Embed), - reactions: Option(List(Reaction)), - nonce: Option(Nonce), - is_pinned: Bool, - webhook_id: Option(String), - type_: Type, - activity: Option(Activity), - application: Option(Application), - application_id: Option(String), - flags: Option(List(Flag)), - reference: Option(MessageReference), - snapshots: Option(List(Snapshot)), - refrenced_message: Option(Message), - interaction_metadata: Option(InteractionMetadata), - thread: Option(Thread), - components: Option(List(Component)), - sticker_items: Option(List(sticker.Item)), - position: Option(Int), - role_subscription_data: Option(RoleSubscriptionData), - resolved: Option(Resolved), - poll: Option(Poll), - call: Option(Call), - ) -} - -pub type Type { - Default - RecipientAdd - RecipientRemove - Call - ChannelNameChange - ChannelIconChange - ChannelPinnedMessage - UserJoin - GuildBoost - GuildBoostTier1 - GuildBoostTier2 - GuildBoostTier3 - ChannelFollowAdd - GuildDiscoveryDisqualified - GuildDiscoveryRequalified - GuildDiscoveryGracePeriodInitialWarning - GuildDiscoveryGracePeriodFinalWarning - ThreadCreated - Reply - ChatInputCommand - ThreadStarterMessage - GuildInviteReminder - ContextMenuCommand - AutoModerationAction - RoleSubscriptionPurchase - InteractionPremiumUpsell - StageStart - StageEnd - StageSpeaker - StageTopic - GuildApplicationPremiumSubscription - GuildIncidentAlertModeEnabled - GuildIncidentAlertModeDisabled - GuildIncidentReportRaid - GuildIncidentReportFalseAlarm - PurchaseNotification - PollResult - /// Used as a workaround for undocumented message types. - Unknown -} - -pub type Nonce { - IntNonce(Int) - StringNonce(String) -} - -pub type Flag { - Crossposted - IsCrosspost - SuppressEmbeds - SourceMessageDeleted - Urgent - HasThread - Ephemeral - Loading - FailedToMentionSomeRolesInThread - SuppressNotifications - IsVoiceMessage - HasSnapshot - IsComponentsV2 -} - -pub type RoleSubscriptionData { - RoleSubscriptionData( - role_subscription_listing_id: String, - tier_name: String, - total_months_subscribed: Int, - is_renewal: Bool, - ) -} - -pub type Snapshot { - Snapshot( - type_: Type, - content: String, - embeds: List(Embed), - attachments: List(Attachment), - sent_at: Timestamp, - last_edited_at: Option(Timestamp), - flags: Option(List(Flag)), - mentions_users: List(User), - mentions_roles: List(String), - sticker_items: Option(List(sticker.Item)), - components: Option(List(Component)), - ) -} - -pub type Resolved { - Resolved( - users: Option(Dict(String, User)), - members: Option(Dict(String, GuildMember)), - roles: Option(Dict(String, Role)), - channels: Option(Dict(String, ResolvedChannel)), - messages: Option(Dict(String, Message)), - attachments: Option(Dict(String, Attachment)), - ) -} - -pub type ResolvedChannel { - ResolvedTextChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedVoiceChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedCategoryChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedAnnouncementChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedAnnouncementThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedPublicThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedPrivateThread( - id: String, - name: String, - current_user_permissions: List(Permission), - metadata: thread.Metadata, - parent_id: String, - ) - ResolvedStageChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedForumChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) - ResolvedMediaChannel( - id: String, - name: String, - current_user_permissions: List(Permission), - ) -} - -pub type ChannelMention { - TextChannelMention( - // don't mind me, i'm just here for formatting ;) - channel_id: String, - guild_id: String, - channel_name: String, - ) - VoiceChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - CategoryChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - AnnouncementChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - AnnouncementThreadMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - PublicThreadMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - PrivateThreadMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - StageChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - ForumChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) - MediaChannelMention( - channel_id: String, - guild_id: String, - channel_name: String, - ) -} - -pub type Create { - Create( - content: Option(String), - nonce: Option(Nonce), - is_tts: Option(Bool), - embeds: Option(List(Embed)), - allowed_mentions: Option(AllowedMentions), - message_reference: Option(MessageReference), - components: Option(List(Component)), - sticker_ids: Option(List(String)), - files: Option(List(File)), - attachments: Option(List(attachment.Create)), - flags: Option(List(CreateFlag)), - enforce_nonce: Option(Bool), - poll: Option(poll.Create), - ) -} - -pub type CreateFlag { - CreateWithSuppressedEmbeds - CreateWithSuppressedNotifications - CreateAsVoiceMessage - CreateWithComponentsV2 -} - -pub type GetReactionsQuery { - Type(reaction.Type) - AfterUserId(String) - Limit(Int) -} - -pub type Modify { - Modify( - content: Modification(String), - embeds: Modification(List(Embed)), - flags: Modification(List(ModifyFlag)), - allowed_mentions: Modification(AllowedMentions), - components: Modification(List(Component)), - files: Modification(List(File)), - attachments: Modification(List(ModifyAttachment)), - ) -} - -pub type ModifyFlag { - ModifyEmbedSuppression - ModifyUsingComponentsV2 -} - -pub type ModifyAttachment { - ExistingAttachment(id: String) - NewAttachment(attachment.Create) -} - -pub type Component { - ActionRow(ActionRow) - Section(Section) - TextDisplay(TextDisplay) - MediaGallery(MediaGallery) - File(component_file.File) - Separator(Separator) - Container(Container) -} - -pub type StartThreadInForumOrMedia { - StartThreadInForumOrMedia( - name: String, - auto_archive_duration: Option(Duration), - rate_limit_per_user: Option(Duration), - message: StartThreadInForumOrMediaMessage, - applied_tags_ids: Option(List(String)), - files: Option(List(File)), - ) -} - -pub type StartThreadInForumOrMediaMessage { - StartThreadInForumOrMediaMessage( - content: Option(String), - embeds: Option(List(Embed)), - allowed_mentions: Option(AllowedMentions), - components: Option(List(Component)), - sticker_ids: Option(List(String)), - attachments: Option(List(attachment.Create)), - flags: Option(List(StartThreadInForumOrMediaMessageFlag)), - ) -} - -pub type StartThreadInForumOrMediaMessageFlag { - StartThreadInForumOrMediaMessageWithSuppressedEmbeds - StartThreadInForumOrMediaMessageWithSuppressedNotifications -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 0), Crossposted), - #(int.bitwise_shift_left(1, 1), IsCrosspost), - #(int.bitwise_shift_left(1, 2), SuppressEmbeds), - #(int.bitwise_shift_left(1, 3), SourceMessageDeleted), - #(int.bitwise_shift_left(1, 4), Urgent), - #(int.bitwise_shift_left(1, 5), HasThread), - #(int.bitwise_shift_left(1, 6), Ephemeral), - #(int.bitwise_shift_left(1, 7), Loading), - #(int.bitwise_shift_left(1, 8), FailedToMentionSomeRolesInThread), - #(int.bitwise_shift_left(1, 12), SuppressNotifications), - #(int.bitwise_shift_left(1, 13), IsVoiceMessage), - #(int.bitwise_shift_left(1, 14), HasSnapshot), - #(int.bitwise_shift_left(1, 15), IsComponentsV2), - ] -} - -@internal -pub fn bits_create_flags() -> List(#(Int, CreateFlag)) { - [ - #(int.bitwise_shift_left(1, 2), CreateWithSuppressedEmbeds), - #(int.bitwise_shift_left(1, 12), CreateWithSuppressedNotifications), - #(int.bitwise_shift_left(1, 13), CreateAsVoiceMessage), - #(int.bitwise_shift_left(1, 15), CreateWithComponentsV2), - ] -} - -@internal -pub fn bits_modify_flags() -> List(#(Int, ModifyFlag)) { - [ - #(int.bitwise_shift_left(1, 2), ModifyEmbedSuppression), - #(int.bitwise_shift_left(1, 15), ModifyUsingComponentsV2), - ] -} - -@internal -pub fn bits_start_thread_in_forum_or_media_message_flags() -> List( - #(Int, StartThreadInForumOrMediaMessageFlag), -) { - [ - #( - int.bitwise_shift_left(1, 2), - StartThreadInForumOrMediaMessageWithSuppressedEmbeds, - ), - #( - int.bitwise_shift_left(1, 12), - StartThreadInForumOrMediaMessageWithSuppressedNotifications, - ), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Message) { - use id <- decode.field("id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use author <- decode.field("author", user.decoder()) - use content <- decode.field("content", decode.string) - use sent_at <- decode.field("timestamp", time_rfc3339.decoder()) - use last_edited_at <- decode.field( - "edited_timestamp", - decode.optional(time_rfc3339.decoder()), - ) - use is_tts <- decode.field("tts", decode.bool) - use mentions_everyone <- decode.field("mention_everyone", decode.bool) - use mentions_users <- decode.field("mentions", decode.list(user.decoder())) - use mentions_roles <- decode.field( - "mention_roles", - decode.list(decode.string), - ) - use mentions_channels <- decode.optional_field( - "mention_channels", - None, - decode.optional(decode.list(channel_mention_decoder())), - ) - use attachments <- decode.field( - "attachments", - decode.list(attachment.decoder()), - ) - use embeds <- decode.field("embeds", decode.list(embed.decoder())) - use reactions <- decode.optional_field( - "reactions", - None, - decode.optional(decode.list(reaction.decoder())), - ) - use nonce <- decode.optional_field( - "nonce", - None, - decode.optional(nonce_decoder()), - ) - use is_pinned <- decode.field("pinned", decode.bool) - use webhook_id <- decode.optional_field( - "webhook_id", - None, - decode.optional(decode.string), - ) - use type_ <- decode.field("type", type_decoder()) - use activity <- decode.optional_field( - "activity", - None, - decode.optional(activity.decoder()), - ) - use application <- decode.optional_field( - "application", - None, - decode.optional(application.decoder()), - ) - use application_id <- decode.optional_field( - "application_id", - None, - decode.optional(decode.string), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use reference <- decode.optional_field( - "message_reference", - None, - decode.optional(message_reference.decoder()), - ) - use snapshots <- decode.optional_field( - "message_snapshots", - None, - decode.optional(decode.list(snapshot_decoder())), - ) - use refrenced_message <- decode.optional_field( - "refrenced_message", - None, - decode.optional(decoder()), - ) - use interaction_metadata <- decode.optional_field( - "interaction_metadata", - None, - decode.optional(interaction_metadata.decoder()), - ) - use thread <- decode.optional_field( - "thread", - None, - decode.optional(thread.decoder()), - ) - use components <- decode.optional_field( - "components", - None, - decode.optional(decode.list(component_decoder())), - ) - use sticker_items <- decode.optional_field( - "sticker_items", - None, - decode.optional(decode.list(sticker.item_decoder())), - ) - use position <- decode.optional_field( - "position", - None, - decode.optional(decode.int), - ) - use role_subscription_data <- decode.optional_field( - "role_subscription_data", - None, - decode.optional(role_subscription_data_decoder()), - ) - use resolved <- decode.optional_field( - "resolved", - None, - decode.optional(resolved_decoder()), - ) - use poll <- decode.optional_field( - "poll", - None, - decode.optional(poll.decoder()), - ) - use call <- decode.optional_field( - "call", - None, - decode.optional(call.decoder()), - ) - decode.success(Message( - id:, - channel_id:, - author:, - content:, - sent_at:, - last_edited_at:, - is_tts:, - mentions_everyone:, - mentions_users:, - mentions_roles:, - mentions_channels:, - attachments:, - embeds:, - reactions:, - nonce:, - is_pinned:, - webhook_id:, - type_:, - activity:, - application:, - application_id:, - flags:, - reference:, - snapshots:, - refrenced_message:, - interaction_metadata:, - thread:, - components:, - sticker_items:, - position:, - role_subscription_data:, - resolved:, - poll:, - call:, - )) -} - -@internal -pub fn nonce_decoder() -> decode.Decoder(Nonce) { - let int_decoder = { - use nonce <- decode.then(decode.int) - decode.success(IntNonce(nonce)) - } - let string_decoder = { - use nonce <- decode.then(decode.string) - decode.success(StringNonce(nonce)) - } - - decode.one_of(int_decoder, or: [string_decoder]) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Default) - 1 -> decode.success(RecipientAdd) - 2 -> decode.success(RecipientRemove) - 3 -> decode.success(Call) - 4 -> decode.success(ChannelNameChange) - 5 -> decode.success(ChannelIconChange) - 6 -> decode.success(ChannelPinnedMessage) - 7 -> decode.success(UserJoin) - 8 -> decode.success(GuildBoost) - 9 -> decode.success(GuildBoostTier1) - 10 -> decode.success(GuildBoostTier2) - 11 -> decode.success(GuildBoostTier3) - 12 -> decode.success(ChannelFollowAdd) - 14 -> decode.success(GuildDiscoveryDisqualified) - 15 -> decode.success(GuildDiscoveryRequalified) - 16 -> decode.success(GuildDiscoveryGracePeriodInitialWarning) - 17 -> decode.success(GuildDiscoveryGracePeriodFinalWarning) - 18 -> decode.success(ThreadCreated) - 19 -> decode.success(Reply) - 20 -> decode.success(ChatInputCommand) - 21 -> decode.success(ThreadStarterMessage) - 22 -> decode.success(GuildInviteReminder) - 23 -> decode.success(ContextMenuCommand) - 24 -> decode.success(AutoModerationAction) - 25 -> decode.success(RoleSubscriptionPurchase) - 26 -> decode.success(InteractionPremiumUpsell) - 27 -> decode.success(StageStart) - 28 -> decode.success(StageEnd) - 29 -> decode.success(StageSpeaker) - 31 -> decode.success(StageTopic) - 32 -> decode.success(GuildApplicationPremiumSubscription) - 36 -> decode.success(GuildIncidentAlertModeEnabled) - 37 -> decode.success(GuildIncidentAlertModeDisabled) - 38 -> decode.success(GuildIncidentReportRaid) - 39 -> decode.success(GuildIncidentReportFalseAlarm) - 44 -> decode.success(PurchaseNotification) - 46 -> decode.success(PollResult) - _ -> decode.success(Unknown) - } -} - -@internal -pub fn snapshot_decoder() -> decode.Decoder(Snapshot) { - decode.at(["message"], { - use type_ <- decode.field("type", type_decoder()) - use content <- decode.field("content", decode.string) - use embeds <- decode.field("embeds", decode.list(embed.decoder())) - use attachments <- decode.field( - "attachments", - decode.list(attachment.decoder()), - ) - use sent_at <- decode.field("timestamp", time_rfc3339.decoder()) - use last_edited_at <- decode.field( - "edited_timestamp", - decode.optional(time_rfc3339.decoder()), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use mentions_users <- decode.field("mentions", decode.list(user.decoder())) - use mentions_roles <- decode.field( - "mention_roles", - decode.list(decode.string), - ) - use sticker_items <- decode.optional_field( - "sticker_items", - None, - decode.optional(decode.list(sticker.item_decoder())), - ) - use components <- decode.optional_field( - "components", - None, - decode.optional(decode.list(component_decoder())), - ) - decode.success(Snapshot( - type_:, - content:, - embeds:, - attachments:, - sent_at:, - last_edited_at:, - flags:, - mentions_users:, - mentions_roles:, - sticker_items:, - components:, - )) - }) -} - -@internal -pub fn role_subscription_data_decoder() -> decode.Decoder(RoleSubscriptionData) { - use role_subscription_listing_id <- decode.field( - "role_subscription_listing_id", - decode.string, - ) - use tier_name <- decode.field("tier_name", decode.string) - use total_months_subscribed <- decode.field( - "total_months_subscribed", - decode.int, - ) - use is_renewal <- decode.field("is_renewal", decode.bool) - decode.success(RoleSubscriptionData( - role_subscription_listing_id:, - tier_name:, - total_months_subscribed:, - is_renewal:, - )) -} - -@internal -pub fn channel_mention_decoder() -> decode.Decoder(ChannelMention) { - use type_ <- decode.field("type", decode.int) - use channel_id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use channel_name <- decode.field("name", decode.string) - - case type_ { - 0 -> - decode.success(TextChannelMention(channel_id:, guild_id:, channel_name:)) - 2 -> - decode.success(VoiceChannelMention(channel_id:, guild_id:, channel_name:)) - 4 -> - decode.success(CategoryChannelMention( - channel_id:, - guild_id:, - channel_name:, - )) - 5 -> - decode.success(AnnouncementChannelMention( - channel_id:, - guild_id:, - channel_name:, - )) - 10 -> - decode.success(AnnouncementThreadMention( - channel_id:, - guild_id:, - channel_name:, - )) - 11 -> - decode.success(PublicThreadMention(channel_id:, guild_id:, channel_name:)) - 12 -> - decode.success(PrivateThreadMention(channel_id:, guild_id:, channel_name:)) - 13 -> - decode.success(StageChannelMention(channel_id:, guild_id:, channel_name:)) - 15 -> - decode.success(ForumChannelMention(channel_id:, guild_id:, channel_name:)) - 16 -> - decode.success(MediaChannelMention(channel_id:, guild_id:, channel_name:)) - _ -> decode.failure(TextChannelMention("", "", ""), "ChannelMention") - } -} - -@internal -pub fn resolved_channel_decoder() -> decode.Decoder(ResolvedChannel) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use type_ <- decode.field("type", decode.int) - use current_user_permissions <- decode.field( - "permissions", - permission.decoder(), - ) - - case type_ { - 0 -> - decode.success(ResolvedTextChannel(id:, name:, current_user_permissions:)) - 2 -> - decode.success(ResolvedVoiceChannel(id:, name:, current_user_permissions:)) - 4 -> - decode.success(ResolvedCategoryChannel( - id:, - name:, - current_user_permissions:, - )) - 5 -> - decode.success(ResolvedAnnouncementChannel( - id:, - name:, - current_user_permissions:, - )) - 10 | 11 | 12 -> { - use metadata <- decode.field("thread_metadata", thread.metadata_decoder()) - use parent_id <- decode.field("parent_id", decode.string) - case type_ { - 10 -> - decode.success(ResolvedAnnouncementThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - 11 -> - decode.success(ResolvedPublicThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - 12 -> - decode.success(ResolvedPrivateThread( - id:, - name:, - current_user_permissions:, - metadata:, - parent_id:, - )) - _ -> decode.failure(ResolvedTextChannel("", "", []), "ResolvedChannel") - } - } - 13 -> - decode.success(ResolvedStageChannel(id:, name:, current_user_permissions:)) - 15 -> - decode.success(ResolvedForumChannel(id:, name:, current_user_permissions:)) - 16 -> - decode.success(ResolvedMediaChannel(id:, name:, current_user_permissions:)) - _ -> decode.failure(ResolvedTextChannel("", "", []), "ResolvedChannel") - } -} - -@internal -pub fn resolved_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, guild_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, resolved_channel_decoder())), - ) - use messages <- decode.optional_field( - "messages", - None, - decode.optional(decode.dict(decode.string, decoder())), - ) - use attachments <- decode.optional_field( - "attachments", - None, - decode.optional(decode.dict(decode.string, attachment.decoder())), - ) - - decode.success(Resolved( - users:, - members:, - roles:, - channels:, - messages:, - attachments:, - )) -} - -@internal -pub fn component_decoder() -> decode.Decoder(Component) { - use type_ <- decode.field("type", decode.int) - case type_ { - 1 -> { - use action_row <- decode.then(action_row.decoder()) - decode.success(ActionRow(action_row)) - } - 9 -> { - use section <- decode.then(section.decoder()) - decode.success(Section(section)) - } - 10 -> { - use text_display <- decode.then(text_display.decoder()) - decode.success(TextDisplay(text_display)) - } - 12 -> { - use media_gallery <- decode.then(media_gallery.decoder()) - decode.success(MediaGallery(media_gallery)) - } - 13 -> { - use file <- decode.then(component_file.decoder()) - decode.success(File(file)) - } - 14 -> { - use separator <- decode.then(separator.decoder()) - decode.success(Separator(separator)) - } - 17 -> { - use container <- decode.then(container.decoder()) - decode.success(Container(container)) - } - _ -> decode.failure(ActionRow(action_row.ActionRow(None, [])), "Component") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let content = case create.content { - Some(content) -> [#("content", json.string(content))] - None -> [] - } - - let nonce = case create.nonce { - Some(nonce) -> [#("nonce", nonce_to_json(nonce))] - None -> [] - } - - let is_tts = case create.is_tts { - Some(tts) -> [#("tts", json.bool(tts))] - None -> [] - } - - let embeds = case create.embeds { - Some(embeds) -> [#("embeds", json.array(embeds, of: embed.to_json))] - None -> [] - } - - let allowed_mentions = case create.allowed_mentions { - Some(allowed_mentions) -> [ - #("allowed_mentions", allowed_mentions.to_json(allowed_mentions)), - ] - None -> [] - } - - let message_reference = case create.message_reference { - Some(reference) -> [ - #("message_reference", message_reference.to_json(reference)), - ] - None -> [] - } - - let components = case create.components { - Some(components) -> [ - #("components", json.array(components, of: component_to_json)), - ] - None -> [] - } - - let sticker_ids = case create.sticker_ids { - Some(ids) -> [#("sticker_ids", json.array(ids, json.string))] - None -> [] - } - - let attachments = case create.attachments { - Some(attachments) -> [ - #("attachments", json.array(attachments, of: attachment.create_to_json)), - ] - None -> [] - } - - let flags = case create.flags { - Some(flags) -> [#("flags", flags.to_json(flags, bits_create_flags()))] - None -> [] - } - - let enforce_nonce = case create.enforce_nonce { - Some(enforce_nonce) -> [#("enforce_nonce", json.bool(enforce_nonce))] - None -> [] - } - - let poll = case create.poll { - Some(poll) -> [#("poll", poll.create_to_json(poll))] - None -> [] - } - - [ - content, - nonce, - is_tts, - embeds, - allowed_mentions, - message_reference, - components, - sticker_ids, - attachments, - flags, - enforce_nonce, - poll, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn nonce_to_json(nonce: Nonce) -> Json { - case nonce { - IntNonce(nonce) -> json.int(nonce) - StringNonce(nonce) -> json.string(nonce) - } -} - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - json.object( - [ - modification.to_json(modify.content, "content", json.string), - modification.to_json(modify.embeds, "embdeds", json.array( - _, - embed.to_json, - )), - modification.to_json(modify.flags, "flags", flags.to_json( - _, - bits_modify_flags(), - )), - modification.to_json( - modify.allowed_mentions, - "allowed_mentions", - allowed_mentions.to_json, - ), - modification.to_json(modify.components, "components", json.array( - _, - component_to_json, - )), - modification.to_json(modify.attachments, "attachments", json.array( - _, - modify_attachment_to_json, - )), - ] - |> list.flatten, - ) -} - -@internal -pub fn modify_attachment_to_json(modify_attachment: ModifyAttachment) -> Json { - case modify_attachment { - ExistingAttachment(id:) -> json.object([#("id", json.string(id))]) - NewAttachment(create) -> attachment.create_to_json(create) - } -} - -@internal -pub fn component_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) -> component_file.to_json(file) - Separator(separator) -> separator.to_json(separator) - Container(container) -> container.to_json(container) - } -} - -@internal -pub fn start_thread_in_forum_or_media_to_json( - start_thread: StartThreadInForumOrMedia, -) -> 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_json(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_json(limit)), - ] - None -> [] - } - - let message = [ - #( - "message", - start_thread_in_forum_or_media_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 -} - -@internal -pub fn start_thread_in_forum_or_media_message_to_json( - message: StartThreadInForumOrMediaMessage, -) -> 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 -> [] - } - - 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 -> [] - } - - 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(bits_start_thread_in_forum_or_media_message_flags()) - |> json.int, - ), - ] - None -> [] - } - - [ - content, - embeds, - allowed_mentions, - components, - sticker_ids, - attachments, - flags, - ] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn pin( - client: grom.Client, - in channel_id: String, - id message_id: String, - reason reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Put, - "/channels/" <> channel_id <> "/messages/pins/" <> message_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn unpin( - client: grom.Client, - from channel_id: String, - id message_id: String, - reason reason: Option(String), -) -> Result(Nil, grom.Error) { - use _response <- result.try( - client - |> rest.new_request( - http.Delete, - "/channels/" <> channel_id <> "/messages/pins/" <> message_id, - ) - |> rest.with_reason(reason) - |> rest.execute, - ) - - Ok(Nil) -} - -pub fn get( - client: grom.Client, - in channel_id: String, - id message_id: String, -) -> Result(Message, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" <> channel_id <> "/messages/" <> message_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn create( - client: grom.Client, - in channel_id: String, - using create: Create, -) -> Result(Message, grom.Error) { - let json = create |> create_to_json - - let request = case create.files { - Some(files) -> { - client - |> rest.new_multipart_request( - http.Post, - "/channels/" <> channel_id <> "/messages", - json, - files, - ) - } - None -> { - client - |> rest.new_request(http.Post, "/channels/" <> channel_id <> "/messages") - |> request.set_body(json |> json.to_string |> bit_array.from_string) - } - } - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create() -> Create { - Create( - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) -} - -pub fn crosspost( - client: grom.Client, - from channel_id: String, - id message_id: String, -) -> Result(Message, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/channels/" <> channel_id <> "/messages/" <> message_id <> "/crosspost", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_reactions( - client: grom.Client, - in channel_id: String, - on message_id: String, - emoji emoji_id: String, - using query: List(GetReactionsQuery), -) -> Result(List(User), grom.Error) { - let query = - query - |> list.map(fn(single_query) { - case single_query { - Type(type_) -> #("type", reaction.type_to_int(type_) |> int.to_string) - AfterUserId(id) -> #("after", id) - Limit(limit) -> #("limit", int.to_string(limit)) - } - }) - - let emoji = uri.percent_encode(emoji_id) - - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" - <> channel_id - <> "/messages/" - <> message_id - <> "/reactions/" - <> emoji, - ) - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: user.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete_all_reactions( - client: grom.Client, - in channel_id: String, - from message_id: String, -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/channels/" <> channel_id <> "/messages/" <> message_id <> "/reactions", - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn delete_all_reactions_for_emoji( - client: grom.Client, - in channel_id: String, - from message_id: String, - emoji emoji_id: String, -) -> Result(Nil, grom.Error) { - let emoji = uri.percent_encode(emoji_id) - - client - |> rest.new_request( - http.Delete, - "/channels/" - <> channel_id - <> "/messages/" - <> message_id - <> "/reactions/" - <> emoji, - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn modify( - client: grom.Client, - in channel_id: String, - id message_id: String, - using modify: Modify, -) -> Result(Message, grom.Error) { - let json = modify |> modify_to_json - - let request = case modify.files { - New(files) -> - client - |> rest.new_multipart_request( - http.Patch, - "/channels/" <> channel_id <> "/messages/" <> message_id, - json, - files, - ) - _ -> - client - |> rest.new_request( - http.Patch, - "/channels/" <> channel_id <> "/messages/" <> message_id, - ) - |> request.set_body(json |> json.to_string |> bit_array.from_string) - } - - use response <- result.try(rest.execute_bytes(request)) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify(Skip, Skip, Skip, Skip, Skip, Skip, Skip) -} - -pub fn delete( - client: grom.Client, - in channel_id: String, - id message_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request( - http.Delete, - "/channels/" <> channel_id <> "/messages/" <> message_id, - ) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} - -pub fn end_poll( - client: grom.Client, - in channel_id: String, - id message_id: String, -) -> Result(Message, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Post, - "/channels/" <> channel_id <> "/polls/" <> message_id <> "/expire", - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn start_thread_in_forum_or_media( - client: grom.Client, - in channel_id: String, - with start_thread: StartThreadInForumOrMedia, - because reason: Option(String), -) -> Result(Thread, grom.Error) { - use response <- result.try(case start_thread.files { - Some(files) -> { - client - |> rest.new_multipart_request( - http.Post, - "/channels/" <> channel_id <> "/threads", - start_thread_in_forum_or_media_to_json(start_thread), - files, - ) - |> rest.with_reason(reason) - |> rest.execute_bytes - } - None -> { - let json = start_thread |> start_thread_in_forum_or_media_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(grom.CouldNotDecode) -} - -pub fn new_start_thread_in_forum_or_media( - name: String, - message: StartThreadInForumOrMediaMessage, -) -> StartThreadInForumOrMedia { - StartThreadInForumOrMedia(name, None, None, message, None, None) -} - -pub fn new_start_thread_in_forum_or_media_message() -> StartThreadInForumOrMediaMessage { - StartThreadInForumOrMediaMessage(None, None, None, None, None, None, None) -} diff --git a/src/grom/message/activity.gleam b/src/grom/message/activity.gleam deleted file mode 100644 index c7932a0..0000000 --- a/src/grom/message/activity.gleam +++ /dev/null @@ -1,31 +0,0 @@ -import gleam/dynamic/decode -import gleam/option.{type Option, None} - -// TYPES ----------------------------------------------------------------------- - -pub type Activity { - JoinActivity(party_id: Option(String)) - SpectateActivity(party_id: Option(String)) - ListenActivity(party_id: Option(String)) - JoinRequestActivity(party_id: Option(String)) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Activity) { - use type_ <- decode.field("type", decode.int) - use party_id <- decode.optional_field( - "party_id", - None, - decode.optional(decode.string), - ) - - case type_ { - 1 -> decode.success(JoinActivity(party_id:)) - 2 -> decode.success(SpectateActivity(party_id:)) - 3 -> decode.success(ListenActivity(party_id:)) - 5 -> decode.success(JoinRequestActivity(party_id:)) - _ -> decode.failure(JoinActivity(None), "Activity") - } -} diff --git a/src/grom/message/allowed_mentions.gleam b/src/grom/message/allowed_mentions.gleam deleted file mode 100644 index f1e5526..0000000 --- a/src/grom/message/allowed_mentions.gleam +++ /dev/null @@ -1,106 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type AllowedMentions { - AllowedMentions( - types: Option(List(Type)), - roles_ids: Option(List(String)), - users_ids: Option(List(String)), - replied_user: Option(Bool), - ) -} - -pub type Type { - Roles - Users - Everyone -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(AllowedMentions) { - use types <- decode.optional_field( - "parse", - None, - decode.optional(decode.list(type_decoder())), - ) - use roles_ids <- decode.optional_field( - "roles", - None, - decode.optional(decode.list(decode.string)), - ) - - use users_ids <- decode.optional_field( - "users", - None, - decode.optional(decode.list(decode.string)), - ) - use replied_user <- decode.optional_field( - "replied_user", - None, - decode.optional(decode.bool), - ) - - decode.success(AllowedMentions(types:, roles_ids:, users_ids:, replied_user:)) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.string) - case variant { - "roles" -> decode.success(Roles) - "users" -> decode.success(Users) - "everyone" -> decode.success(Everyone) - _ -> decode.failure(Roles, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(allowed_mentions: AllowedMentions) -> Json { - let types = case allowed_mentions.types { - Some(types) -> [#("parse", json.array(types, type_to_json))] - None -> [] - } - - let roles_ids = case allowed_mentions.roles_ids { - Some(ids) -> [#("roles", json.array(ids, json.string))] - None -> [] - } - - let users_ids = case allowed_mentions.users_ids { - Some(ids) -> [#("users", json.array(ids, json.string))] - None -> [] - } - - let replied_user = case allowed_mentions.replied_user { - Some(allowed) -> [#("replied_user", json.bool(allowed))] - None -> [] - } - - [types, roles_ids, users_ids, replied_user] - |> list.flatten - |> json.object -} - -@internal -pub fn type_to_json(type_: Type) -> Json { - case type_ { - Users -> "users" - Roles -> "roles" - Everyone -> "everyone" - } - |> json.string -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new() -> AllowedMentions { - AllowedMentions(None, None, None, None) -} diff --git a/src/grom/message/attachment.gleam b/src/grom/message/attachment.gleam deleted file mode 100644 index 0dcac78..0000000 --- a/src/grom/message/attachment.gleam +++ /dev/null @@ -1,142 +0,0 @@ -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/internal/base64 -import grom/internal/flags -import grom/internal/time_duration - -// TYPES ----------------------------------------------------------------------- - -pub type Attachment { - Attachment( - id: String, - filename: String, - title: Option(String), - description: Option(String), - content_type: Option(String), - size_bytes: Int, - url: String, - proxy_url: String, - height: Option(Int), - width: Option(Int), - is_ephemeral: Option(Bool), - duration: Duration, - waveform: Option(BitArray), - flags: Option(List(Flag)), - ) -} - -pub opaque type Create { - Create(filename: String, description: Option(String)) -} - -pub type Flag { - IsRemix -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [#(int.bitwise_shift_left(1, 2), IsRemix)] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Attachment) { - use id <- decode.field("id", decode.string) - use filename <- decode.field("filename", decode.string) - use title <- decode.optional_field( - "title", - None, - decode.optional(decode.string), - ) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - use content_type <- decode.optional_field( - "content_type", - None, - decode.optional(decode.string), - ) - use size_bytes <- decode.field("size", decode.int) - use url <- decode.field("url", decode.string) - use proxy_url <- decode.field("proxy_url", decode.string) - use height <- decode.optional_field( - "height", - None, - decode.optional(decode.int), - ) - use width <- decode.optional_field("width", None, decode.optional(decode.int)) - use is_ephemeral <- decode.optional_field( - "ephemeral", - None, - decode.optional(decode.bool), - ) - use duration <- decode.field( - "duration", - time_duration.from_float_seconds_decoder(), - ) - use waveform <- decode.field("waveform", decode.optional(base64.decoder())) - use flags <- decode.field( - "flags", - decode.optional(flags.decoder(bits_flags())), - ) - decode.success(Attachment( - id:, - filename:, - title:, - description:, - content_type:, - size_bytes:, - url:, - proxy_url:, - height:, - width:, - is_ephemeral:, - duration:, - waveform:, - flags:, - )) -} - -// this is cursed and shouldn't exist -@internal -pub fn create_decoder() -> decode.Decoder(Create) { - use filename <- decode.field("filename", decode.string) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - - decode.success(Create(filename:, description:)) -} - -// 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new_create(filename: String) -> Create { - Create(filename, description: None) -} diff --git a/src/grom/message/call.gleam b/src/grom/message/call.gleam deleted file mode 100644 index e14598c..0000000 --- a/src/grom/message/call.gleam +++ /dev/null @@ -1,23 +0,0 @@ -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import gleam/time/timestamp.{type Timestamp} -import grom/internal/time_rfc3339 - -// TYPES ----------------------------------------------------------------------- - -pub type Call { - Call(participants: List(String), ended_timestamp: Option(Timestamp)) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Call) { - use participants <- decode.field("participants", decode.list(decode.string)) - use ended_timestamp <- decode.optional_field( - "ended_timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - decode.success(Call(participants:, ended_timestamp:)) -} diff --git a/src/grom/message/embed.gleam b/src/grom/message/embed.gleam deleted file mode 100644 index 042a19b..0000000 --- a/src/grom/message/embed.gleam +++ /dev/null @@ -1,522 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/time/timestamp.{type Timestamp} -import grom/internal/time_rfc3339 - -// TYPES ----------------------------------------------------------------------- - -pub type Embed { - Embed( - title: Option(String), - type_: Option(Type), - description: Option(String), - url: Option(String), - timestamp: Option(Timestamp), - color: Option(Int), - footer: Option(Footer), - image: Option(Image), - thumbnail: Option(Image), - video: Option(Video), - provider: Option(Provider), - author: Option(Author), - fields: Option(List(Field)), - ) -} - -pub type Type { - Rich - ImageEmbed - VideoEmbed - Gifv - Article - Link - PollResult - Unknown -} - -pub type Footer { - Footer(text: String, icon_url: Option(String), proxy_icon_url: Option(String)) -} - -pub type Image { - Image( - url: String, - proxy_url: Option(String), - height: Option(Int), - width: Option(Int), - ) -} - -pub type Video { - Video( - url: Option(String), - proxy_url: Option(String), - height: Option(Int), - width: Option(Int), - ) -} - -pub type Provider { - Provider(name: Option(String), url: Option(String)) -} - -pub type Author { - Author( - name: String, - url: Option(String), - icon_url: Option(String), - proxy_icon_url: Option(String), - ) -} - -pub type Field { - Field(name: String, value: String, is_inline: Option(Bool)) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Embed) { - use title <- decode.optional_field( - "title", - None, - decode.optional(decode.string), - ) - use type_ <- decode.optional_field( - "type", - None, - decode.optional(type_decoder()), - ) - use description <- decode.optional_field( - "description", - None, - decode.optional(decode.string), - ) - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - use timestamp <- decode.optional_field( - "timestamp", - None, - decode.optional(time_rfc3339.decoder()), - ) - use color <- decode.optional_field("color", None, decode.optional(decode.int)) - use footer <- decode.optional_field( - "footer", - None, - decode.optional(footer_decoder()), - ) - use image <- decode.optional_field( - "image", - None, - decode.optional(image_decoder()), - ) - use thumbnail <- decode.optional_field( - "thumbnail", - None, - decode.optional(image_decoder()), - ) - use video <- decode.optional_field( - "video", - None, - decode.optional(video_decoder()), - ) - use provider <- decode.optional_field( - "provider", - None, - decode.optional(provider_decoder()), - ) - use author <- decode.optional_field( - "author", - None, - decode.optional(author_decoder()), - ) - use fields <- decode.optional_field( - "fields", - None, - decode.optional(decode.list(field_decoder())), - ) - decode.success(Embed( - title:, - type_:, - description:, - url:, - timestamp:, - color:, - footer:, - image:, - thumbnail:, - video:, - provider:, - author:, - fields:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.string) - case variant { - "rich" -> decode.success(Rich) - "image" -> decode.success(ImageEmbed) - "video" -> decode.success(VideoEmbed) - "gifv" -> decode.success(Gifv) - "article" -> decode.success(Article) - "link" -> decode.success(Link) - "poll_result" -> decode.success(PollResult) - _ -> decode.success(Unknown) - } -} - -@internal -pub fn footer_decoder() -> decode.Decoder(Footer) { - use text <- decode.field("text", decode.string) - use icon_url <- decode.optional_field( - "icon_url", - None, - decode.optional(decode.string), - ) - use proxy_icon_url <- decode.optional_field( - "proxy_icon_url", - None, - decode.optional(decode.string), - ) - decode.success(Footer(text:, icon_url:, proxy_icon_url:)) -} - -@internal -pub fn image_decoder() -> decode.Decoder(Image) { - use url <- decode.field("url", decode.string) - use proxy_url <- decode.optional_field( - "proxy_url", - None, - decode.optional(decode.string), - ) - use height <- decode.optional_field( - "height", - None, - decode.optional(decode.int), - ) - use width <- decode.optional_field("width", None, decode.optional(decode.int)) - decode.success(Image(url:, proxy_url:, height:, width:)) -} - -@internal -pub fn video_decoder() -> decode.Decoder(Video) { - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - use proxy_url <- decode.optional_field( - "proxy_url", - None, - decode.optional(decode.string), - ) - use height <- decode.optional_field( - "height", - None, - decode.optional(decode.int), - ) - use width <- decode.optional_field("width", None, decode.optional(decode.int)) - decode.success(Video(url:, proxy_url:, height:, width:)) -} - -@internal -pub fn provider_decoder() -> decode.Decoder(Provider) { - use name <- decode.optional_field( - "name", - None, - decode.optional(decode.string), - ) - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - decode.success(Provider(name:, url:)) -} - -@internal -pub fn author_decoder() -> decode.Decoder(Author) { - use name <- decode.field("name", decode.string) - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - use icon_url <- decode.optional_field( - "icon_url", - None, - decode.optional(decode.string), - ) - use proxy_icon_url <- decode.optional_field( - "proxy_icon_url", - None, - decode.optional(decode.string), - ) - decode.success(Author(name:, url:, icon_url:, proxy_icon_url:)) -} - -@internal -pub fn field_decoder() -> decode.Decoder(Field) { - use name <- decode.field("name", decode.string) - use value <- decode.field("value", decode.string) - use is_inline <- decode.optional_field( - "inline", - None, - decode.optional(decode.bool), - ) - 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" - Unknown -> "unknown" - } - |> 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 -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn new() -> Embed { - Embed( - None, - Some(Rich), - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) -} - -pub fn new_author(named name: String) -> Author { - Author(name, None, None, None) -} - -pub fn new_field(named name: String, value value: String) -> Field { - Field(name, value, None) -} - -pub fn new_footer(containing text: String) -> Footer { - Footer(text, None, None) -} - -pub fn new_image(url url: String) -> Image { - Image(url, None, None, None) -} - -pub fn new_video() -> Video { - Video(None, None, None, None) -} diff --git a/src/grom/message/interaction_metadata.gleam b/src/grom/message/interaction_metadata.gleam deleted file mode 100644 index c8591ff..0000000 --- a/src/grom/message/interaction_metadata.gleam +++ /dev/null @@ -1,83 +0,0 @@ -import gleam/dict.{type Dict} -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import grom/application -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type InteractionMetadata { - InteractionMetadata( - id: String, - type_: Type, - user: User, - authorizing_integration_owners: Dict( - application.InstallationContext, - String, - ), - original_response_message_id: Option(String), - target_user: Option(User), - target_message_id: Option(String), - ) -} - -pub type Type { - Ping - ApplicationCommand - MessageComponent - ApplicationCommandAutocomplete - ModalSubmit -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(InteractionMetadata) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", type_decoder()) - use user <- decode.field("user", user.decoder()) - use authorizing_integration_owners <- decode.field( - "authorizing_integration_owners", - decode.dict( - application.installation_context_string_decoder(), - decode.string, - ), - ) - use original_response_message_id <- decode.optional_field( - "original_response_message_id", - None, - decode.optional(decode.string), - ) - use target_user <- decode.optional_field( - "target_user", - None, - decode.optional(user.decoder()), - ) - use target_message_id <- decode.optional_field( - "target_message_id", - None, - decode.optional(decode.string), - ) - decode.success(InteractionMetadata( - id:, - type_:, - user:, - authorizing_integration_owners:, - original_response_message_id:, - target_user:, - target_message_id:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Ping) - 2 -> decode.success(ApplicationCommand) - 3 -> decode.success(MessageComponent) - 4 -> decode.success(ApplicationCommandAutocomplete) - 5 -> decode.success(ModalSubmit) - _ -> decode.failure(Ping, "Type") - } -} diff --git a/src/grom/message/message_reference.gleam b/src/grom/message/message_reference.gleam deleted file mode 100644 index ec797e8..0000000 --- a/src/grom/message/message_reference.gleam +++ /dev/null @@ -1,105 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} - -// TYPES ----------------------------------------------------------------------- - -pub type MessageReference { - MessageReference( - type_: Type, - message_id: Option(String), - channel_id: Option(String), - guild_id: Option(String), - fail_if_not_exists: Option(Bool), - ) -} - -pub type Type { - Default - Forward -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(MessageReference) { - use type_ <- decode.optional_field("type", Default, type_decoder()) - use message_id <- decode.optional_field( - "message_id", - None, - decode.optional(decode.string), - ) - use channel_id <- decode.optional_field( - "channel_id", - None, - decode.optional(decode.string), - ) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use fail_if_not_exists <- decode.optional_field( - "fail_if_not_exists", - None, - decode.optional(decode.bool), - ) - decode.success(MessageReference( - type_:, - message_id:, - channel_id:, - guild_id:, - fail_if_not_exists:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Default) - 1 -> decode.success(Forward) - _ -> decode.failure(Default, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(reference: MessageReference) -> Json { - let type_ = [#("type", type_to_json(reference.type_))] - - let message_id = case reference.message_id { - Some(id) -> [#("message_id", json.string(id))] - None -> [] - } - - let channel_id = case reference.channel_id { - Some(id) -> [#("channel_id", json.string(id))] - None -> [] - } - - let guild_id = case reference.guild_id { - Some(id) -> [#("guild_id", json.string(id))] - None -> [] - } - - let fail_if_not_exists = case reference.fail_if_not_exists { - Some(fail) -> [#("fail_if_not_exists", json.bool(fail))] - None -> [] - } - - [type_, message_id, channel_id, guild_id, fail_if_not_exists] - |> list.flatten - |> json.object -} - -@internal -pub fn type_to_json(type_: Type) -> Json { - case type_ { - Default -> 0 - Forward -> 1 - } - |> json.int -} diff --git a/src/grom/message/poll.gleam b/src/grom/message/poll.gleam deleted file mode 100644 index fb95976..0000000 --- a/src/grom/message/poll.gleam +++ /dev/null @@ -1,322 +0,0 @@ -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 gleam/time/timestamp.{type Timestamp} -import grom -import grom/emoji.{type Emoji} -import grom/internal/rest -import grom/internal/time_duration -import grom/internal/time_rfc3339 -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Poll { - Poll( - question: Media, - answers: List(Answer), - expiry: Option(Timestamp), - allows_multiselect: Bool, - layout_type: LayoutType, - results: Option(Results), - ) -} - -pub type Media { - Media(text: Option(String), emoji: Option(Emoji)) -} - -pub type Answer { - Answer(answer_id: Int, poll_media: Media) -} - -pub type Results { - Results(is_finalized: Bool, answer_counts: List(AnswerCount)) -} - -pub type AnswerCount { - AnswerCount(id: Int, count: Int, current_user_voted: Bool) -} - -pub type LayoutType { - Default -} - -pub type Create { - Create( - question: CreateQuestion, - answers: List(CreateAnswer), - duration: Option(Duration), - allow_multiselect: Bool, - layout_type: Option(LayoutType), - ) -} - -pub type CreateQuestion { - CreateQuestion(text: Option(String)) -} - -pub type CreateAnswer { - CreateAnswer(media: Media) -} - -pub type GetAnswerVotersQuery { - AfterUserId(String) - Limit(Int) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Poll) { - use question <- decode.field("question", media_decoder()) - use answers <- decode.field("answers", decode.list(answer_decoder())) - use expiry <- decode.field("expiry", decode.optional(time_rfc3339.decoder())) - use allows_multiselect <- decode.field("allow_multiselect", decode.bool) - use layout_type <- decode.field("layout_type", layout_type_decoder()) - use results <- decode.optional_field( - "results", - None, - decode.optional(results_decoder()), - ) - decode.success(Poll( - question:, - answers:, - expiry:, - allows_multiselect:, - layout_type:, - results:, - )) -} - -@internal -pub fn media_decoder() -> decode.Decoder(Media) { - use text <- decode.optional_field( - "text", - None, - decode.optional(decode.string), - ) - use emoji <- decode.optional_field( - "emoji", - None, - decode.optional(emoji.decoder()), - ) - decode.success(Media(text:, emoji:)) -} - -@internal -pub fn answer_decoder() -> decode.Decoder(Answer) { - use answer_id <- decode.field("answer_id", decode.int) - use poll_media <- decode.field("poll_media", media_decoder()) - decode.success(Answer(answer_id:, poll_media:)) -} - -@internal -pub fn layout_type_decoder() -> decode.Decoder(LayoutType) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Default) - _ -> decode.failure(Default, "LayoutType") - } -} - -@internal -pub fn results_decoder() -> decode.Decoder(Results) { - use is_finalized <- decode.field("is_finalized", decode.bool) - use answer_counts <- decode.field( - "answer_counts", - decode.list(answer_count_decoder()), - ) - decode.success(Results(is_finalized:, answer_counts:)) -} - -@internal -pub fn answer_count_decoder() -> decode.Decoder(AnswerCount) { - use id <- decode.field("id", decode.int) - use count <- decode.field("count", decode.int) - use current_user_voted <- decode.field("me_voted", decode.bool) - decode.success(AnswerCount(id:, count:, current_user_voted:)) -} - -// this is cursed and shouldn't exist -@internal -pub fn create_decoder() -> decode.Decoder(Create) { - use question <- decode.field("question", create_question_decoder()) - use answers <- decode.field( - "answers", - decode.list(of: create_answer_decoder()), - ) - use duration <- decode.optional_field( - "duration", - None, - decode.optional(time_duration.from_int_hours_decoder()), - ) - use allow_multiselect <- decode.optional_field( - "allow_multiselect", - False, - decode.bool, - ) - use layout_type <- decode.optional_field( - "layout_type", - None, - decode.optional(layout_type_decoder()), - ) - - decode.success(Create( - question:, - answers:, - duration:, - allow_multiselect:, - layout_type:, - )) -} - -@internal -pub fn create_question_decoder() -> decode.Decoder(CreateQuestion) { - // what was i thinking - // why is a question's text optional - // actually what was discord thinking - use text <- decode.optional_field( - "text", - None, - decode.optional(decode.string), - ) - decode.success(CreateQuestion(text)) -} - -@internal -pub fn create_answer_decoder() -> decode.Decoder(CreateAnswer) { - use media <- decode.field("media", media_decoder()) - decode.success(CreateAnswer(media)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - let question = [#("question", create_question_to_json(create.question))] - - let answers = [ - #("answers", json.array(create.answers, create_answer_to_json)), - ] - - let duration = case create.duration { - Some(duration) -> [#("duration", time_duration.to_int_hours_json(duration))] - None -> [] - } - - let allow_multiselect = [ - #("allow_multiselect", json.bool(create.allow_multiselect)), - ] - - let layout_type = case create.layout_type { - Some(type_) -> [#("layout_type", layout_type_to_json(type_))] - None -> [] - } - - [question, answers, duration, allow_multiselect, layout_type] - |> list.flatten - |> json.object -} - -@internal -pub fn create_question_to_json(question: CreateQuestion) -> Json { - json.object(case question.text { - Some(text) -> [#("text", json.string(text))] - None -> [] - }) -} - -@internal -pub fn create_answer_to_json(answer: CreateAnswer) -> Json { - json.object([#("poll_media", media_to_json(answer.media))]) -} - -@internal -pub fn media_to_json(media: Media) -> Json { - let text = case media.text { - Some(text) -> [#("text", json.string(text))] - None -> [] - } - - let emoji = case media.emoji { - Some(emoji) -> [#("emoji", emoji.to_json(emoji))] - None -> [] - } - - [text, emoji] - |> list.flatten - |> json.object -} - -@internal -pub fn layout_type_to_json(layout_type: LayoutType) -> Json { - case layout_type { - Default -> 1 - } - |> json.int -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_voters_for_answer( - client: grom.Client, - in channel_id: String, - on message_id: String, - regarding answer_id: String, - using query: List(GetAnswerVotersQuery), -) -> Result(List(User), grom.Error) { - let query = - list.map(query, fn(single_query) { - case single_query { - AfterUserId(id) -> #("after", id) - Limit(limit) -> #("limit", int.to_string(limit)) - } - }) - - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/channels/" - <> channel_id - <> "/polls/" - <> message_id - <> "/answers/" - <> answer_id, - ) - |> request.set_query(query) - |> rest.execute, - ) - - let response_decoder = { - use users <- decode.field("users", decode.list(of: user.decoder())) - decode.success(users) - } - - response.body - |> json.parse(using: response_decoder) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create( - asking question: CreateQuestion, - answers answers: List(CreateAnswer), - allowing_multiselect allow_multiselect: Bool, -) -> Create { - Create(question, answers, None, allow_multiselect, None) -} - -pub fn new_create_question() -> CreateQuestion { - CreateQuestion(None) -} - -pub fn new_media() -> Media { - Media(None, None) -} diff --git a/src/grom/message/reaction.gleam b/src/grom/message/reaction.gleam deleted file mode 100644 index fc9d6df..0000000 --- a/src/grom/message/reaction.gleam +++ /dev/null @@ -1,149 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/result -import gleam/uri -import grom -import grom/emoji.{type Emoji} -import grom/internal/rest - -// TYPES ----------------------------------------------------------------------- - -pub type Reaction { - Reaction( - count: Int, - count_details: CountDetails, - current_user_reacted: Bool, - current_user_burst_reacted: Bool, - emoji: Emoji, - burst_colors: List(Int), - ) -} - -pub type Type { - Normal - Burst -} - -pub type CountDetails { - CountDetails(burst: Int, normal: Int) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Reaction) { - use count <- decode.field("count", decode.int) - use count_details <- decode.field("count_details", count_details_decoder()) - use current_user_reacted <- decode.field("me", decode.bool) - use current_user_burst_reacted <- decode.field("me_burst", decode.bool) - use emoji <- decode.field("emoji", emoji.decoder()) - use burst_colors <- decode.field("burst_colors", decode.list(decode.int)) - decode.success(Reaction( - count:, - count_details:, - current_user_reacted:, - current_user_burst_reacted:, - emoji:, - burst_colors:, - )) -} - -@internal -pub fn count_details_decoder() -> decode.Decoder(CountDetails) { - use burst <- decode.field("burst", decode.int) - use normal <- decode.field("normal", decode.int) - decode.success(CountDetails(burst:, normal:)) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Normal) - 1 -> decode.success(Burst) - _ -> decode.failure(Normal, "Type") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn type_to_int(type_: Type) -> Int { - case type_ { - Normal -> 0 - Burst -> 1 - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn create( - client: grom.Client, - in channel_id: String, - on message_id: String, - emoji emoji_id: String, -) -> Result(Nil, grom.Error) { - let emoji = uri.percent_encode(emoji_id) - - client - |> rest.new_request( - http.Put, - "/channels/" - <> channel_id - <> "/messages/" - <> message_id - <> "/reactions/" - <> emoji - <> "/@me", - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn delete_own( - client: grom.Client, - in channel_id: String, - from message_id: String, - emoji emoji_id: String, -) -> Result(Nil, grom.Error) { - let emoji = uri.percent_encode(emoji_id) - - client - |> rest.new_request( - http.Delete, - "/channels/" - <> channel_id - <> "/messages/" - <> message_id - <> "/reactions/" - <> emoji - <> "/@me", - ) - |> rest.execute - |> result.replace(Nil) -} - -pub fn delete_users( - client: grom.Client, - in channel_id: String, - from message_id: String, - emoji emoji_id: String, - id user_id: String, -) -> Result(Nil, grom.Error) { - let emoji = uri.percent_encode(emoji_id) - - client - |> rest.new_request( - http.Delete, - "/channels/" - <> channel_id - <> "/messages/" - <> message_id - <> "/reactions/" - <> emoji - <> "/" - <> user_id, - ) - |> rest.execute - |> result.replace(Nil) -} diff --git a/src/grom/modal.gleam b/src/grom/modal.gleam deleted file mode 100644 index 756b73b..0000000 --- a/src/grom/modal.gleam +++ /dev/null @@ -1,20 +0,0 @@ -import gleam/json.{type Json} -import grom/component/label.{type Label} -import grom/component/text_display.{type TextDisplay} - -// TYPES ----------------------------------------------------------------------- - -pub type Component { - TextDisplay(TextDisplay) - Label(Label) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn component_to_json(component: Component) -> Json { - case component { - TextDisplay(text_display) -> text_display.to_json(text_display) - Label(label) -> label.to_json(label) - } -} diff --git a/src/grom/modification.gleam b/src/grom/modification.gleam deleted file mode 100644 index 24e40ad..0000000 --- a/src/grom/modification.gleam +++ /dev/null @@ -1,50 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/option.{None, Some} - -// TYPES ----------------------------------------------------------------------- - -/// Sometimes, you'll find yourself passing this type into a `modify` function. -/// This is because Gleam really has no way of determining whether to skip something, leaving it unchanged -/// or to send `null`, deleting that object from Discord's databases. -pub type Modification(a) { - /// Will send `a`. - New(a) - /// Will send `null`. - Delete - /// Will not send anything. - Skip -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder(of inner: decode.Decoder(a)) -> decode.Decoder(Modification(a)) { - let exists_decoder = { - use value <- decode.then(decode.optional(inner)) - case value { - Some(value) -> New(value) - None -> Delete - } - |> decode.success - } - - let not_set_decoder = decode.success(Skip) - - decode.one_of(exists_decoder, or: [not_set_decoder]) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json( - modification: Modification(a), - key: String, - success_encoder: fn(a) -> Json, -) -> List(#(String, Json)) { - case modification { - New(value) -> [#(key, success_encoder(value))] - Delete -> [#(key, json.null())] - Skip -> [] - } -} diff --git a/src/grom/permission.gleam b/src/grom/permission.gleam deleted file mode 100644 index 0807332..0000000 --- a/src/grom/permission.gleam +++ /dev/null @@ -1,149 +0,0 @@ -import gleam/dynamic/decode -import gleam/int -import gleam/json.{type Json} -import gleam/list -import grom/internal/flags - -// TYPES ----------------------------------------------------------------------- - -pub type Permission { - CreateInstantInvite - KickMembers - BanMembers - Administrator - ManageChannels - ManageGuild - AddReactions - ViewAuditLog - PrioritySpeaker - Stream - ViewChannel - SendMessages - SendTtsMessages - ManageMessages - EmbedLinks - AttachFiles - ReadMessageHistory - MentionEveryone - UseExternalEmojis - ViewGuildInsights - Connect - Speak - MuteMembers - DeafenMembers - MoveMembers - UseVoiceActivityDetection - ChangeNickname - ManageNicknames - ManageRoles - ManageWebhooks - ManageGuildExpressions - UseApplicationCommands - RequestToSpeak - ManageEvents - ManageThreads - CreatePublicThreads - CreatePrivateThreads - UseExternalStickers - SendMessagesInThreads - UseEmbeddedActivities - ModerateMembers - ViewCreatorMonetizationAnalytics - UseSoundboard - CreateGuildExpressions - CreateEvents - UseExternalSounds - SendVoiceMessages - SendPolls - UseExternalApps - PinMessages - BypassSlowmode -} - -// FLAGS ----------------------------------------------------------------------- - -fn permissions_bits() -> List(#(Int, Permission)) { - [ - #(int.bitwise_shift_left(1, 0), CreateInstantInvite), - #(int.bitwise_shift_left(1, 1), KickMembers), - #(int.bitwise_shift_left(1, 2), BanMembers), - #(int.bitwise_shift_left(1, 3), Administrator), - #(int.bitwise_shift_left(1, 4), ManageChannels), - #(int.bitwise_shift_left(1, 5), ManageGuild), - #(int.bitwise_shift_left(1, 6), AddReactions), - #(int.bitwise_shift_left(1, 7), ViewAuditLog), - #(int.bitwise_shift_left(1, 8), PrioritySpeaker), - #(int.bitwise_shift_left(1, 9), Stream), - #(int.bitwise_shift_left(1, 10), ViewChannel), - #(int.bitwise_shift_left(1, 11), SendMessages), - #(int.bitwise_shift_left(1, 12), SendTtsMessages), - #(int.bitwise_shift_left(1, 13), ManageMessages), - #(int.bitwise_shift_left(1, 14), EmbedLinks), - #(int.bitwise_shift_left(1, 15), AttachFiles), - #(int.bitwise_shift_left(1, 16), ReadMessageHistory), - #(int.bitwise_shift_left(1, 17), MentionEveryone), - #(int.bitwise_shift_left(1, 18), UseExternalEmojis), - #(int.bitwise_shift_left(1, 19), ViewGuildInsights), - #(int.bitwise_shift_left(1, 20), Connect), - #(int.bitwise_shift_left(1, 21), Speak), - #(int.bitwise_shift_left(1, 22), MuteMembers), - #(int.bitwise_shift_left(1, 23), DeafenMembers), - #(int.bitwise_shift_left(1, 24), MoveMembers), - #(int.bitwise_shift_left(1, 25), UseVoiceActivityDetection), - #(int.bitwise_shift_left(1, 26), ChangeNickname), - #(int.bitwise_shift_left(1, 27), ManageNicknames), - #(int.bitwise_shift_left(1, 28), ManageRoles), - #(int.bitwise_shift_left(1, 29), ManageWebhooks), - #(int.bitwise_shift_left(1, 30), ManageGuildExpressions), - #(int.bitwise_shift_left(1, 31), UseApplicationCommands), - #(int.bitwise_shift_left(1, 32), RequestToSpeak), - #(int.bitwise_shift_left(1, 33), ManageEvents), - #(int.bitwise_shift_left(1, 34), ManageThreads), - #(int.bitwise_shift_left(1, 35), CreatePublicThreads), - #(int.bitwise_shift_left(1, 36), CreatePrivateThreads), - #(int.bitwise_shift_left(1, 37), UseExternalStickers), - #(int.bitwise_shift_left(1, 38), SendMessagesInThreads), - #(int.bitwise_shift_left(1, 39), UseEmbeddedActivities), - #(int.bitwise_shift_left(1, 40), ModerateMembers), - #(int.bitwise_shift_left(1, 41), ViewCreatorMonetizationAnalytics), - #(int.bitwise_shift_left(1, 42), UseSoundboard), - #(int.bitwise_shift_left(1, 43), CreateGuildExpressions), - #(int.bitwise_shift_left(1, 44), CreateEvents), - #(int.bitwise_shift_left(1, 45), UseExternalSounds), - #(int.bitwise_shift_left(1, 46), SendVoiceMessages), - #(int.bitwise_shift_left(1, 49), SendPolls), - #(int.bitwise_shift_left(1, 50), UseExternalApps), - #(int.bitwise_shift_left(1, 51), PinMessages), - #(int.bitwise_shift_left(1, 52), BypassSlowmode), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(List(Permission)) { - use permissions <- decode.then(decode.string) - let permissions = permissions |> int.parse - - case permissions { - Ok(permissions) -> { - permissions_bits() - |> list.filter_map(fn(item) { - let #(bit, permission) = item - case int.bitwise_and(permissions, bit) != 0 { - True -> Ok(permission) - False -> Error(Nil) - } - }) - |> decode.success - } - Error(_) -> decode.failure([], "Permission") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(permissions: List(Permission)) -> Json { - json.string(permissions |> flags.to_int(permissions_bits()) |> int.to_string) -} diff --git a/src/grom/sku.gleam b/src/grom/sku.gleam deleted file mode 100644 index d5e9bcf..0000000 --- a/src/grom/sku.gleam +++ /dev/null @@ -1,133 +0,0 @@ -//// A SKU is an offer on Discord. -//// -//// For example, if you're offering a premium subscription for your bot, you must create an SKU. - -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/int -import gleam/json -import gleam/list -import gleam/result -import grom -import grom/internal/flags -import grom/internal/rest -import grom/subscription.{type Subscription} - -// TYPES ----------------------------------------------------------------------- - -pub type Sku { - Sku( - id: String, - type_: Type, - application_id: String, - name: String, - slug: String, - flags: List(Flag), - ) -} - -pub type Type { - Durable - Consumable - Subscription - SubscriptionGroup -} - -pub type Flag { - Available - GuildSubscription - UserSubscription -} - -pub type GetSubscriptionsQuery { - AfterId(String) - BeforeId(String) - Limit(Int) -} - -// FLAGS ----------------------------------------------------------------------- - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 2), Available), - #(int.bitwise_shift_left(1, 7), GuildSubscription), - #(int.bitwise_shift_left(1, 8), UserSubscription), - ] -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Sku) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", type_decoder()) - use application_id <- decode.field("application_id", decode.string) - use name <- decode.field("name", decode.string) - use slug <- decode.field("slug", decode.string) - use flags <- decode.field("flags", flags.decoder(bits_flags())) - decode.success(Sku(id:, type_:, application_id:, name:, slug:, flags:)) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 2 -> decode.success(Durable) - 3 -> decode.success(Consumable) - 5 -> decode.success(Subscription) - 6 -> decode.success(SubscriptionGroup) - _ -> decode.failure(Durable, "Type") - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get_subscriptions( - client: grom.Client, - sku sku_id: String, - for user_id: String, - using query: List(GetSubscriptionsQuery), -) -> Result(List(Subscription), grom.Error) { - let query = - query - |> list.map(fn(single_query) { - case single_query { - AfterId(id) -> #("after", id) - BeforeId(id) -> #("before", id) - Limit(limit) -> #("limit", int.to_string(limit)) - } - }) - |> list.append([#("user_id", user_id)]) - - use response <- result.try( - client - |> rest.new_request(http.Get, "/skus/" <> sku_id <> "/subscriptions") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: subscription.decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_subscription( - client: grom.Client, - sku sku_id: String, - id subscription_id: String, -) -> Result(Subscription, grom.Error) { - use response <- result.try( - client - |> rest.new_request( - http.Get, - "/skus/" <> sku_id <> "/subscriptions/" <> subscription_id, - ) - |> rest.execute, - ) - - response.body - |> json.parse(using: subscription.decoder()) - |> result.map_error(grom.CouldNotDecode) -} diff --git a/src/grom/snowflake.gleam b/src/grom/snowflake.gleam deleted file mode 100644 index bef4f54..0000000 --- a/src/grom/snowflake.gleam +++ /dev/null @@ -1,18 +0,0 @@ -import gleam/int -import gleam/result -import gleam/time/timestamp.{type Timestamp} - -const discord_epoch = 1_420_070_400_000 - -pub fn get_creation_timestamp(of snowflake: String) -> Result(Timestamp, Nil) { - use integer <- result.map(int.parse(snowflake)) - - let milliseconds = - integer - |> int.bitwise_shift_right(22) - |> int.add(discord_epoch) - - let seconds = milliseconds / 1000 - - timestamp.from_unix_seconds(seconds) -} diff --git a/src/grom/soundboard.gleam b/src/grom/soundboard.gleam deleted file mode 100644 index 2a9551e..0000000 --- a/src/grom/soundboard.gleam +++ /dev/null @@ -1,152 +0,0 @@ -import gleam/bit_array -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/internal/rest -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Sound { - Sound( - name: String, - id: String, - /// From `0.0` to `1.0`. - volume: Float, - emoji_id: Option(String), - emoji_name: Option(String), - guild_id: Option(String), - is_available: Bool, - creator: Option(User), - ) -} - -pub opaque type Data { - Data(String) -} - -pub type ContentType { - Mpeg - Ogg -} - -pub type SoundId { - StringId(String) - IntId(Int) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn sound_decoder() -> decode.Decoder(Sound) { - use name <- decode.field("name", decode.string) - use id <- decode.field("sound_id", decode.string) - use volume <- decode.field("volume", decode.float) - use emoji_id <- decode.field("emoji_id", decode.optional(decode.string)) - use emoji_name <- decode.field("emoji_name", decode.optional(decode.string)) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use is_available <- decode.field("available", decode.bool) - use creator <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - decode.success(Sound( - name:, - id:, - volume:, - emoji_id:, - emoji_name:, - guild_id:, - is_available:, - creator:, - )) -} - -@internal -pub fn sound_id_decoder() -> decode.Decoder(SoundId) { - let string_decoder = decode.map(decode.string, StringId) - let int_decoder = decode.map(decode.int, IntId) - decode.one_of(string_decoder, or: [int_decoder]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn send_sound( - client: grom.Client, - in channel_id: String, - id sound_id: String, - source_guild guild_id: Option(String), -) -> Result(Nil, grom.Error) { - let json = - json.object( - [ - [#("sound_id", json.string(sound_id))], - case guild_id { - Some(id) -> [#("source_guild_id", json.string(id))] - None -> [] - }, - ] - |> list.flatten, - ) - |> json.to_string - - client - |> rest.new_request( - http.Post, - "/channels/" <> channel_id <> "/send-soundboard-sound", - ) - |> request.set_body(json) - |> rest.execute - |> result.replace(Nil) -} - -pub fn get_default_sounds( - client: grom.Client, -) -> Result(List(Sound), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/soundboard-default-sounds") - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: sound_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn data_from_bit_array( - bit_array: BitArray, - content_type: ContentType, -) -> Data { - let content_type = case content_type { - Mpeg -> "audio/mpeg" - Ogg -> "audio/ogg" - } - - let data = bit_array.base64_encode(bit_array, False) - - Data("data:" <> content_type <> ";base64," <> data) -} - -@internal -pub fn data_to_base64(image: Data) -> String { - let Data(base64) = image - base64 -} - -@internal -pub fn data_to_json(image: Data) -> Json { - image - |> data_to_base64 - |> json.string -} diff --git a/src/grom/stage_instance.gleam b/src/grom/stage_instance.gleam deleted file mode 100644 index a04fdb2..0000000 --- a/src/grom/stage_instance.gleam +++ /dev/null @@ -1,204 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import grom -import grom/internal/rest - -// TYPES ----------------------------------------------------------------------- - -pub type StageInstance { - StageInstance( - id: String, - guild_id: String, - channel_id: String, - topic: String, - privacy_level: PrivacyLevel, - scheduled_event_id: Option(String), - ) -} - -pub type PrivacyLevel { - GuildOnly -} - -pub type Create { - Create( - channel_id: String, - topic: String, - privacy_level: PrivacyLevel, - send_start_notification: Bool, - scheduled_event_id: Option(String), - ) -} - -pub type Modify { - Modify(topic: Option(String), privacy_level: Option(PrivacyLevel)) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(StageInstance) { - use id <- decode.field("id", decode.string) - use guild_id <- decode.field("guild_id", decode.string) - use channel_id <- decode.field("channel_id", decode.string) - use topic <- decode.field("topic", decode.string) - use privacy_level <- decode.field("privacy_level", privacy_level_decoder()) - use scheduled_event_id <- decode.field( - "guild_scheduled_event_id", - decode.optional(decode.string), - ) - decode.success(StageInstance( - id:, - guild_id:, - channel_id:, - topic:, - privacy_level:, - scheduled_event_id:, - )) -} - -@internal -pub fn privacy_level_decoder() -> decode.Decoder(PrivacyLevel) { - use variant <- decode.then(decode.int) - case variant { - 2 -> decode.success(GuildOnly) - _ -> decode.failure(GuildOnly, "PrivacyLevel") - } -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn create_to_json(create: Create) -> Json { - json.object( - [ - [#("channel_id", json.string(create.channel_id))], - [#("topic", json.string(create.topic))], - [#("privacy_level", privacy_level_to_json(create.privacy_level))], - [#("send_start_notification", json.bool(create.send_start_notification))], - case create.scheduled_event_id { - Some(id) -> [#("guild_scheduled_event_id", json.string(id))] - None -> [] - }, - ] - |> list.flatten, - ) -} - -@internal -pub fn privacy_level_to_json(privacy_level: PrivacyLevel) -> Json { - case privacy_level { - GuildOnly -> 2 - } - |> json.int -} - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let topic = case modify.topic { - Some(topic) -> [#("topic", json.string(topic))] - None -> [] - } - - let privacy_level = case modify.privacy_level { - Some(level) -> [#("privacy_level", privacy_level_to_json(level))] - None -> [] - } - - [topic, privacy_level] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn create( - client: grom.Client, - using create: Create, - because reason: Option(String), -) -> Result(StageInstance, grom.Error) { - let json = create |> create_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Post, "/stage-instances") - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_create( - in channel_id: String, - about topic: String, - send_start_notification send_start_notification: Bool, -) -> Create { - Create( - channel_id:, - topic:, - privacy_level: GuildOnly, - send_start_notification:, - scheduled_event_id: None, - ) -} - -pub fn get( - client: grom.Client, - for channel_id: String, -) -> Result(StageInstance, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/stage-instances/" <> channel_id) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - in channel_id: String, - using modify: Modify, - because reason: Option(String), -) -> Result(StageInstance, grom.Error) { - let json = modify |> modify_to_json |> json.to_string - - use response <- result.try( - client - |> rest.new_request(http.Patch, "/stage-instances/" <> channel_id) - |> rest.with_reason(reason) - |> request.set_body(json) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn delete( - client: grom.Client, - from channel_id: String, - because reason: Option(String), -) -> Result(Nil, grom.Error) { - client - |> rest.new_request(http.Delete, "/stage-instances/" <> channel_id) - |> rest.with_reason(reason) - |> rest.execute - |> result.replace(Nil) -} - -pub fn new_modify() -> Modify { - Modify(None, None) -} diff --git a/src/grom/sticker.gleam b/src/grom/sticker.gleam deleted file mode 100644 index 5ae4314..0000000 --- a/src/grom/sticker.gleam +++ /dev/null @@ -1,246 +0,0 @@ -import gleam/dynamic/decode -import gleam/http -import gleam/http/request.{type Request} -import gleam/json -import gleam/option.{type Option, None} -import gleam/result -import grom -import grom/internal/rest -import grom/user.{type User} - -// TYPES ---------------------------------------------------------------------- - -pub type Sticker { - Sticker( - id: String, - pack_id: Option(String), - name: String, - description: Option(String), - tags: String, - type_: Type, - format_type: ContentType, - is_available: Option(Bool), - guild_id: Option(String), - user: Option(User), - sort_value: Option(Int), - ) -} - -pub type Item { - Item(id: String, name: String, format_type: ContentType) -} - -pub type Pack { - Pack( - id: String, - stickers: List(Sticker), - name: String, - sku_id: String, - cover_sticker_id: Option(String), - description: String, - banner_asset_id: Option(String), - ) -} - -pub type Type { - Standard - Guild -} - -pub type ContentType { - Png - Apng - Lottie - Gif -} - -pub type File { - File(name: String, content_type: ContentType, content: BitArray) -} - -// DECODERS ------------------------------------------------------------------- - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Standard) - 2 -> decode.success(Guild) - _ -> decode.failure(Standard, "Type") - } -} - -@internal -pub fn format_type_decoder() -> decode.Decoder(ContentType) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Png) - 2 -> decode.success(Apng) - 3 -> decode.success(Lottie) - 4 -> decode.success(Gif) - _ -> decode.failure(Png, "FormatType") - } -} - -@internal -pub fn decoder() -> decode.Decoder(Sticker) { - use id <- decode.field("id", decode.string) - use pack_id <- decode.optional_field( - "pack_id", - None, - decode.optional(decode.string), - ) - use name <- decode.field("name", decode.string) - use description <- decode.field("description", decode.optional(decode.string)) - use tags <- decode.field("tags", decode.string) - use type_ <- decode.field("type", type_decoder()) - use format_type <- decode.field("format_type", format_type_decoder()) - use is_available <- decode.field("available", decode.optional(decode.bool)) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use user <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - use sort_value <- decode.optional_field( - "sort_value", - None, - decode.optional(decode.int), - ) - decode.success(Sticker( - id:, - pack_id:, - name:, - description:, - tags:, - type_:, - format_type:, - is_available:, - guild_id:, - user:, - sort_value:, - )) -} - -@internal -pub fn pack_decoder() -> decode.Decoder(Pack) { - use id <- decode.field("id", decode.string) - use stickers <- decode.field("stickers", decode.list(of: decoder())) - use name <- decode.field("name", decode.string) - use sku_id <- decode.field("sku_id", decode.string) - use cover_sticker_id <- decode.optional_field( - "cover_sticker_id", - None, - decode.optional(decode.string), - ) - use description <- decode.field("description", decode.string) - use banner_asset_id <- decode.optional_field( - "banner_asset_id", - None, - decode.optional(decode.string), - ) - - decode.success(Pack( - id:, - stickers:, - name:, - sku_id:, - cover_sticker_id:, - description:, - banner_asset_id:, - )) -} - -@internal -pub fn item_decoder() -> decode.Decoder(Item) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use format_type <- decode.field("format_type", format_type_decoder()) - decode.success(Item(id:, name:, format_type:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn content_type_to_string(content_type: ContentType) -> String { - case content_type { - Png -> "image/png" - Apng -> "image/apng" - Gif -> "image/gif" - Lottie -> "video/lottie+json" - } -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get( - client: grom.Client, - id sticker_id: String, -) -> Result(Sticker, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/stickers/" <> sticker_id) - |> rest.execute, - ) - - response.body - |> json.parse(using: decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_packs(client: grom.Client) -> Result(List(Pack), grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/sticker-packs") - |> rest.execute, - ) - - let response_decoder = { - use packs <- decode.field("sticker_packs", decode.list(of: pack_decoder())) - decode.success(packs) - } - - response.body - |> json.parse(using: response_decoder) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_pack( - client: grom.Client, - id pack_id: String, -) -> Result(Pack, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/sticker-packs/" <> pack_id) - |> rest.execute, - ) - - response.body - |> json.parse(using: pack_decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -/// You CANNOT convert between formats. If the sticker you're requesting has a content type of PNG, you must request it as a PNG. -/// Additionally, please note that Lottie files are text (JSON), not binary. -pub fn request(id id: String, format format: ContentType) -> Request(String) { - let extension = case format { - Png -> ".png" - Apng -> ".png" - Lottie -> ".json" - Gif -> ".gif" - } - - let request = - rest.new_cdn_request(to: "/stickers/" <> id <> extension, query: []) - - case format { - Gif -> - request - |> request.set_host("media.discordapp.net") - _ -> request - } -} diff --git a/src/grom/subscription.gleam b/src/grom/subscription.gleam deleted file mode 100644 index c319aa8..0000000 --- a/src/grom/subscription.gleam +++ /dev/null @@ -1,85 +0,0 @@ -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import gleam/time/timestamp.{type Timestamp} -import grom/internal/time_rfc3339 - -// TYPES ----------------------------------------------------------------------- - -pub type Subscription { - Subscription( - id: String, - user_id: String, - sku_ids: List(String), - entitlement_ids: List(String), - renewal_sku_ids: List(String), - current_period_start: Timestamp, - current_period_end: Timestamp, - status: Status, - canceled_at: Option(Timestamp), - country: Option(String), - ) -} - -pub type Status { - Active - Ending - Inactive -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Subscription) { - use id <- decode.field("id", decode.string) - use user_id <- decode.field("user_id", decode.string) - use sku_ids <- decode.field("sku_ids", decode.list(decode.string)) - use entitlement_ids <- decode.field( - "entitlement_ids", - decode.list(decode.string), - ) - use renewal_sku_ids <- decode.field( - "renewal_sku_ids", - decode.list(decode.string), - ) - use current_period_start <- decode.field( - "current_period_start", - time_rfc3339.decoder(), - ) - use current_period_end <- decode.field( - "current_period_end", - time_rfc3339.decoder(), - ) - use status <- decode.field("status", status_decoder()) - use canceled_at <- decode.field( - "canceled_at", - decode.optional(time_rfc3339.decoder()), - ) - use country <- decode.optional_field( - "country", - None, - decode.optional(decode.string), - ) - decode.success(Subscription( - id:, - user_id:, - sku_ids:, - entitlement_ids:, - renewal_sku_ids:, - current_period_start:, - current_period_end:, - status:, - canceled_at:, - country:, - )) -} - -@internal -pub fn status_decoder() -> decode.Decoder(Status) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Active) - 1 -> decode.success(Ending) - 2 -> decode.success(Inactive) - _ -> decode.failure(Active, "Status") - } -} diff --git a/src/grom/user.gleam b/src/grom/user.gleam deleted file mode 100644 index 8c2ddbb..0000000 --- a/src/grom/user.gleam +++ /dev/null @@ -1,504 +0,0 @@ -import gleam/dynamic/decode -import gleam/http/request.{type Request} -import gleam/int -import gleam/json.{type Json} -import gleam/list -import gleam/option.{type Option, None, Some} -import gleam/result -import gleam/string -import grom/internal/flags -import grom/internal/rest - -// TYPES ---------------------------------------------------------------------- - -pub type User { - User( - id: String, - username: String, - discriminator: String, - global_name: Option(String), - avatar_hash: Option(String), - is_bot: Option(Bool), - is_system: Option(Bool), - is_mfa_enabled: Option(Bool), - banner_hash: Option(String), - accent_color: Option(Int), - locale: Option(String), - flags: Option(List(Flag)), - premium_type: Option(PremiumType), - public_flags: Option(List(Flag)), - avatar_decoration_data: Option(AvatarDecorationData), - collectibles: Option(Collectibles), - primary_guild: Option(PrimaryGuild), - ) -} - -pub type AvatarDecorationData { - AvatarDecorationData(asset: String, sku_id: String) -} - -pub type PremiumType { - NoPremium - NitroClassic - Nitro - NitroBasic -} - -pub type Flag { - Staff - Partner - Hypesquad - BugHunterLevel1 - HypesquadBravery - HypesquadBrilliance - HypesquadBalance - PremiumEarlySupporter - TeamPseudoUser - BugHunterLevel2 - VerifiedBot - VerifiedDeveloper - CertifiedModerator - BotHttpInteractions - ActiveDeveloper -} - -pub type Collectibles { - Collectibles(nameplate: Option(Nameplate)) -} - -pub type Nameplate { - Nameplate(sku_id: String, asset: String, label: String, palette: String) -} - -pub type PrimaryGuild { - PrimaryGuild( - id: Option(String), - is_enabled: Option(Bool), - tag: Option(String), - badge_hash: Option(String), - ) -} - -pub type BannerFormat { - PngBanner - JpegBanner - WebpBanner - GifBanner -} - -pub type AvatarFormat { - PngAvatar - JpegAvatar - WebpAvatar - GifAvatar -} - -// CONSTANTS ------------------------------------------------------------------ - -@internal -pub fn bits_flags() -> List(#(Int, Flag)) { - [ - #(int.bitwise_shift_left(1, 0), Staff), - #(int.bitwise_shift_left(1, 1), Partner), - #(int.bitwise_shift_left(1, 2), Hypesquad), - #(int.bitwise_shift_left(1, 3), BugHunterLevel1), - #(int.bitwise_shift_left(1, 6), HypesquadBravery), - #(int.bitwise_shift_left(1, 7), HypesquadBrilliance), - #(int.bitwise_shift_left(1, 8), HypesquadBalance), - #(int.bitwise_shift_left(1, 9), PremiumEarlySupporter), - #(int.bitwise_shift_left(1, 10), TeamPseudoUser), - #(int.bitwise_shift_left(1, 14), BugHunterLevel2), - #(int.bitwise_shift_left(1, 16), VerifiedBot), - #(int.bitwise_shift_left(1, 17), VerifiedDeveloper), - #(int.bitwise_shift_left(1, 18), CertifiedModerator), - #(int.bitwise_shift_left(1, 19), BotHttpInteractions), - #(int.bitwise_shift_left(1, 22), ActiveDeveloper), - ] -} - -// DECODERS ------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(User) { - use id <- decode.field("id", decode.string) - use username <- decode.field("username", decode.string) - use discriminator <- decode.field("discriminator", decode.string) - use global_name <- decode.field("global_name", decode.optional(decode.string)) - use avatar_hash <- decode.field("avatar", decode.optional(decode.string)) - use is_bot <- decode.optional_field("bot", None, decode.optional(decode.bool)) - use is_system <- decode.optional_field( - "system", - None, - decode.optional(decode.bool), - ) - use is_mfa_enabled <- decode.optional_field( - "mfa_enabled", - None, - decode.optional(decode.bool), - ) - use banner_hash <- decode.optional_field( - "banner", - None, - decode.optional(decode.string), - ) - use accent_color <- decode.optional_field( - "accent_color", - None, - decode.optional(decode.int), - ) - use locale <- decode.optional_field( - "locale", - None, - decode.optional(decode.string), - ) - use flags <- decode.optional_field( - "flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use premium_type <- decode.optional_field( - "premium_type", - None, - decode.optional(premium_type_decoder()), - ) - use public_flags <- decode.optional_field( - "public_flags", - None, - decode.optional(flags.decoder(bits_flags())), - ) - use avatar_decoration_data <- decode.optional_field( - "avatar_decoration_data", - None, - decode.optional(avatar_decoration_data_decoder()), - ) - use collectibles <- decode.optional_field( - "collectibles", - None, - decode.optional(collectibles_decoder()), - ) - use primary_guild <- decode.optional_field( - "primary_guild", - None, - decode.optional(primary_guild_decoder()), - ) - decode.success(User( - id:, - username:, - discriminator:, - global_name:, - avatar_hash:, - is_bot:, - is_system:, - is_mfa_enabled:, - banner_hash:, - accent_color:, - locale:, - flags:, - premium_type:, - public_flags:, - avatar_decoration_data:, - collectibles:, - primary_guild:, - )) -} - -@internal -pub fn avatar_decoration_data_decoder() -> decode.Decoder(AvatarDecorationData) { - use asset <- decode.field("asset", decode.string) - use sku_id <- decode.field("sku_id", decode.string) - decode.success(AvatarDecorationData(asset:, sku_id:)) -} - -@internal -pub fn premium_type_decoder() -> decode.Decoder(PremiumType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(NoPremium) - 1 -> decode.success(NitroClassic) - 2 -> decode.success(Nitro) - 3 -> decode.success(NitroBasic) - _ -> decode.failure(NoPremium, "PremiumType") - } -} - -@internal -pub fn collectibles_decoder() -> decode.Decoder(Collectibles) { - use nameplate <- decode.optional_field( - "nameplate", - None, - decode.optional(nameplate_decoder()), - ) - - decode.success(Collectibles(nameplate:)) -} - -@internal -pub fn nameplate_decoder() -> decode.Decoder(Nameplate) { - use sku_id <- decode.field("sku_id", decode.string) - use asset <- decode.field("asset", decode.string) - use label <- decode.field("label", decode.string) - use palette <- decode.field("palette", decode.string) - - decode.success(Nameplate(sku_id:, asset:, label:, palette:)) -} - -@internal -pub fn primary_guild_decoder() -> decode.Decoder(PrimaryGuild) { - use id <- decode.field("identity_guild_id", decode.optional(decode.string)) - use is_enabled <- decode.field( - "identity_enabled", - decode.optional(decode.bool), - ) - use tag <- decode.field("tag", decode.optional(decode.string)) - use badge_hash <- decode.field("badge", decode.optional(decode.string)) - - decode.success(PrimaryGuild(id:, is_enabled:, tag:, badge_hash:)) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn to_json(user: User) -> Json { - let id = [#("id", json.string(user.id))] - - let username = [#("username", json.string(user.username))] - - let discriminator = [#("discriminator", json.string(user.discriminator))] - - let global_name = [ - #("global_name", json.nullable(user.global_name, json.string)), - ] - - let avatar_hash = [#("avatar", json.nullable(user.avatar_hash, json.string))] - - let is_bot = case user.is_bot { - Some(bot) -> [#("bot", json.bool(bot))] - None -> [] - } - - let is_system = case user.is_system { - Some(system) -> [#("system", json.bool(system))] - None -> [] - } - - let is_mfa_enabled = case user.is_mfa_enabled { - Some(mfa_enabled) -> [#("mfa_enabled", json.bool(mfa_enabled))] - None -> [] - } - - let banner_hash = case user.banner_hash { - Some(banner) -> [#("banner", json.string(banner))] - None -> [] - } - - let accent_color = case user.accent_color { - Some(color) -> [#("accent_color", json.int(color))] - None -> [] - } - - let locale = case user.locale { - Some(locale) -> [#("locale", json.string(locale))] - None -> [] - } - - let flags = case user.flags { - Some(flags) -> [#("flags", flags.to_json(flags, bits_flags()))] - None -> [] - } - - let premium_type = case user.premium_type { - Some(type_) -> [#("premium_type", premium_type_to_json(type_))] - None -> [] - } - - let public_flags = case user.public_flags { - Some(flags) -> [#("public_flags", flags.to_json(flags, bits_flags()))] - None -> [] - } - - let avatar_decoration_data = case user.avatar_decoration_data { - Some(data) -> [ - #("avatar_decoration_data", avatar_decoration_data_to_json(data)), - ] - None -> [] - } - - let collectibles = case user.collectibles { - Some(collectibles) -> [ - #("collectibles", collectibles_to_json(collectibles)), - ] - None -> [] - } - - let primary_guild = case user.primary_guild { - Some(guild) -> [#("primary_guild", primary_guild_to_json(guild))] - None -> [] - } - - [ - id, - username, - discriminator, - global_name, - avatar_hash, - is_bot, - is_system, - is_mfa_enabled, - banner_hash, - accent_color, - locale, - flags, - premium_type, - public_flags, - avatar_decoration_data, - collectibles, - primary_guild, - ] - |> list.flatten - |> json.object -} - -@internal -pub fn premium_type_to_json(premium_type: PremiumType) -> Json { - case premium_type { - NoPremium -> 0 - NitroClassic -> 1 - Nitro -> 2 - NitroBasic -> 3 - } - |> json.int -} - -@internal -pub fn avatar_decoration_data_to_json(data: AvatarDecorationData) -> Json { - json.object([ - #("asset", json.string(data.asset)), - #("sku_id", json.string(data.sku_id)), - ]) -} - -@internal -pub fn collectibles_to_json(collectibles: Collectibles) -> Json { - let nameplate = case collectibles.nameplate { - Some(nameplate) -> [#("nameplate", nameplate_to_json(nameplate))] - None -> [] - } - - [nameplate] - |> list.flatten - |> json.object -} - -@internal -pub fn nameplate_to_json(nameplate: Nameplate) -> Json { - json.object([ - #("sku_id", json.string(nameplate.sku_id)), - #("asset", json.string(nameplate.asset)), - #("label", json.string(nameplate.label)), - #("palette", json.string(nameplate.palette)), - ]) -} - -@internal -pub fn primary_guild_to_json(primary_guild: PrimaryGuild) -> Json { - json.object([ - #("identity_guild_id", json.nullable(primary_guild.id, json.string)), - #("identity_enabled", json.nullable(primary_guild.is_enabled, json.bool)), - #("tag", json.nullable(primary_guild.tag, json.string)), - #("badge", json.nullable(primary_guild.badge_hash, json.string)), - ]) -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -/// Used to get the default user avatar. -/// If it returns an error - you got lucky - the user's ID/discriminator somehow isn't an integer. -pub fn index(of user: User) -> Result(Int, Nil) { - case user.discriminator { - "0" -> { - use id <- result.map(int.parse(user.id)) - int.bitwise_shift_right(id, 22) % 6 - } - _ -> { - use discriminator <- result.map(int.parse(user.discriminator)) - discriminator % 5 - } - } -} - -/// If `format == GifBanner` and the banner isn't animated, will fallback to static WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn banner_request( - id id: String, - hash banner: String, - format format: BannerFormat, -) -> Request(String) { - let is_animated = - banner - |> string.starts_with("a_") - - let extension = case format, is_animated { - PngBanner, _ -> ".png" - JpegBanner, _ -> ".jpg" - WebpBanner, _ -> ".webp" - GifBanner, True -> ".gif" - GifBanner, False -> ".webp" - } - - let query = case format, is_animated { - WebpBanner, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request( - to: "/banners/" <> id <> "/" <> banner <> extension, - query:, - ) -} - -/// Use `index(user)` to get the index. -/// Returns the HTTP GET request to the default avatar as a PNG. -pub fn default_avatar_request(index index: Int) -> Request(String) { - rest.new_cdn_request( - "/embed/avatars/" <> int.to_string(index) <> ".png", - query: [], - ) -} - -/// If `format == GifAvatar` and the avatar isn't animated, will fallback to static WEBP. -/// You can request animated WEBPs too, in which case, if it isn't animated, it will fallback to static WEBP. -pub fn avatar_request( - id id: String, - hash avatar: String, - format format: AvatarFormat, -) -> Request(String) { - let is_animated = - avatar - |> string.starts_with("a_") - - let extension = case format, is_animated { - PngAvatar, _ -> ".png" - JpegAvatar, _ -> ".jpg" - WebpAvatar, _ -> ".webp" - GifAvatar, True -> ".gif" - GifAvatar, False -> ".webp" - } - - let query = case format, is_animated { - WebpAvatar, True -> [#("animated", "true")] - _, _ -> [] - } - - rest.new_cdn_request( - to: "/avatars/" <> id <> "/" <> avatar <> extension, - query:, - ) -} - -/// Returns a HTTP GET request to the avatar decoration as a PNG. -pub fn avatar_decoration_request(asset asset: String) -> Request(String) { - rest.new_cdn_request( - to: "/avatar-decoration-presets/" <> asset <> ".png", - query: [], - ) -} diff --git a/src/grom/user/current_user.gleam b/src/grom/user/current_user.gleam deleted file mode 100644 index a462daa..0000000 --- a/src/grom/user/current_user.gleam +++ /dev/null @@ -1,191 +0,0 @@ -import gleam/bool -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/string -import grom -import grom/guild -import grom/guild_member.{type GuildMember} -import grom/image -import grom/internal/rest -import grom/permission.{type Permission} -import grom/user.{type User} - -// TYPES ---------------------------------------------------------------------- - -pub type Modify { - Modify( - username: Option(String), - avatar: Option(image.Data), - banner: Option(image.Data), - ) -} - -pub type GetGuildsQuery { - GetGuildsBeforeId(String) - GetGuildsAfterId(String) - GetGuildsLimit(Int) - /// Refers to the approximate_member and approximate_presence counts - GetGuildsWithCounts(Bool) -} - -pub type PartialGuild { - PartialGuild( - id: String, - name: String, - icon_hash: Option(String), - banner_hash: Option(String), - is_current_user_owner: Bool, - current_user_permissions: List(Permission), - features: List(guild.Feature), - approximate_member_count: Option(Int), - approximate_presence_count: Option(Int), - ) -} - -// DECODERS ------------------------------------------------------------------- - -fn partial_guild_decoder() -> decode.Decoder(PartialGuild) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use icon_hash <- decode.field("icon", decode.optional(decode.string)) - use banner_hash <- decode.field("banner", decode.optional(decode.string)) - use is_current_user_owner <- decode.field("owner", decode.bool) - use current_user_permissions <- decode.field( - "permissions", - permission.decoder(), - ) - use features <- decode.field("features", decode.list(guild.feature_decoder())) - use approximate_member_count <- decode.optional_field( - "approximate_member_count", - None, - decode.optional(decode.int), - ) - use approximate_presence_count <- decode.optional_field( - "approximate_presence_count", - None, - decode.optional(decode.int), - ) - decode.success(PartialGuild( - id:, - name:, - icon_hash:, - banner_hash:, - is_current_user_owner:, - current_user_permissions:, - features:, - approximate_member_count:, - approximate_presence_count:, - )) -} - -// ENCODERS -------------------------------------------------------------------- - -@internal -pub fn modify_to_json(modify: Modify) -> Json { - let Modify(username:, avatar:, banner:) = modify - let username = case username { - Some(name) -> [#("username", json.string(name))] - None -> [] - } - - let avatar = case avatar { - Some(image) -> [#("avatar", image.to_json(image))] - None -> [] - } - - let banner = case banner { - Some(image) -> [#("banner", image.to_json(image))] - None -> [] - } - - [username, avatar, banner] - |> list.flatten - |> json.object -} - -// PUBLIC API FUNCTIONS -------------------------------------------------------- - -pub fn get(client: grom.Client) -> Result(User, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/users/@me") - |> rest.execute, - ) - - response.body - |> json.parse(using: user.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn modify( - client: grom.Client, - with data: Modify, -) -> Result(User, grom.Error) { - let json = data |> modify_to_json - use response <- result.try( - client - |> rest.new_request(http.Patch, "/users/@me") - |> request.set_body(json |> json.to_string) - |> rest.execute, - ) - - response.body - |> json.parse(using: user.decoder()) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn new_modify() -> Modify { - Modify(username: None, avatar: None, banner: None) -} - -pub fn get_guilds( - client: grom.Client, - with query: List(GetGuildsQuery), -) -> Result(List(PartialGuild), grom.Error) { - let query = - list.map(query, fn(parameter) { - case parameter { - GetGuildsBeforeId(id) -> #("before", id) - GetGuildsAfterId(id) -> #("after", id) - GetGuildsLimit(limit) -> #("limit", limit |> int.to_string) - GetGuildsWithCounts(with_counts) -> #( - "with_counts", - with_counts - |> bool.to_string - |> string.lowercase, - ) - } - }) - - use response <- result.try( - client - |> rest.new_request(http.Get, "/users/@me/guilds") - |> request.set_query(query) - |> rest.execute, - ) - - response.body - |> json.parse(using: decode.list(of: partial_guild_decoder())) - |> result.map_error(grom.CouldNotDecode) -} - -pub fn get_as_member( - client: grom.Client, - guild_id: String, -) -> Result(GuildMember, grom.Error) { - use response <- result.try( - client - |> rest.new_request(http.Get, "/users/@me/guilds/" <> guild_id <> "/member") - |> rest.execute, - ) - - response.body - |> json.parse(using: guild_member.decoder()) - |> result.map_error(grom.CouldNotDecode) -} diff --git a/src/grom/voice.gleam b/src/grom/voice.gleam deleted file mode 100644 index 98e0deb..0000000 --- a/src/grom/voice.gleam +++ /dev/null @@ -1,101 +0,0 @@ -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import gleam/time/timestamp.{type Timestamp} -import grom/guild_member.{type GuildMember} -import grom/internal/time_rfc3339 - -pub type State { - State( - guild_id: Option(String), - channel_id: Option(String), - user_id: String, - member: Option(GuildMember), - session_id: String, - is_deaf: Bool, - is_mute: Bool, - is_self_deaf: Bool, - is_self_mute: Bool, - is_streaming: Bool, - is_sharing_camera: Bool, - is_suppressed: Bool, - request_to_speak_timestamp: Option(Timestamp), - ) -} - -@internal -pub fn state_decoder() -> decode.Decoder(State) { - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use channel_id <- decode.field("channel_id", decode.optional(decode.string)) - use user_id <- decode.field("user_id", decode.string) - use member <- decode.optional_field( - "member", - None, - decode.optional(guild_member.decoder()), - ) - use session_id <- decode.field("session_id", decode.string) - use is_deaf <- decode.field("deaf", decode.bool) - use is_mute <- decode.field("mute", decode.bool) - use is_self_deaf <- decode.field("self_deaf", decode.bool) - use is_self_mute <- decode.field("self_mute", decode.bool) - use is_streaming <- decode.optional_field("self_stream", False, decode.bool) - use is_sharing_camera <- decode.field("self_video", decode.bool) - use is_suppressed <- decode.field("suppress", decode.bool) - use request_to_speak_timestamp <- decode.field( - "request_to_speak_timestamp", - decode.optional(time_rfc3339.decoder()), - ) - decode.success(State( - guild_id:, - channel_id:, - user_id:, - member:, - session_id:, - is_deaf:, - is_mute:, - is_self_deaf:, - is_self_mute:, - is_streaming:, - is_sharing_camera:, - is_suppressed:, - request_to_speak_timestamp:, - )) -} - -pub type Region { - Region( - id: String, - name: String, - is_optimal: Bool, - is_deprecated: Bool, - is_custom: Bool, - ) -} - -@internal -pub fn region_decoder() -> decode.Decoder(Region) { - use id <- decode.field("id", decode.string) - use name <- decode.field("name", decode.string) - use is_optimal <- decode.field("optimal", decode.bool) - use is_deprecated <- decode.field("deprecated", decode.bool) - use is_custom <- decode.field("custom", decode.bool) - decode.success(Region(id:, name:, is_optimal:, is_deprecated:, is_custom:)) -} - -pub type AnimationType { - PremiumAnimation - BasicAnimation -} - -@internal -pub fn animation_type_decoder() -> decode.Decoder(AnimationType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(PremiumAnimation) - 1 -> decode.success(BasicAnimation) - _ -> decode.failure(PremiumAnimation, "AnimationType") - } -} diff --git a/src/grom/webhook.gleam b/src/grom/webhook.gleam deleted file mode 100644 index 77659e6..0000000 --- a/src/grom/webhook.gleam +++ /dev/null @@ -1,96 +0,0 @@ -import gleam/dynamic/decode -import gleam/option.{type Option, None} -import grom/channel.{type Channel} -import grom/guild.{type Guild} -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type Webhook { - Webhook( - id: String, - type_: Type, - guild_id: Option(String), - channel_id: Option(String), - user: Option(User), - name: Option(String), - avatar_hash: Option(String), - token: Option(String), - application_id: Option(String), - source_guild: Option(Guild), - source_channel: Option(Channel), - url: Option(String), - ) -} - -pub type Type { - Incoming - ChannelFollower - Application -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn decoder() -> decode.Decoder(Webhook) { - use id <- decode.field("id", decode.string) - use type_ <- decode.field("type", type_decoder()) - use guild_id <- decode.optional_field( - "guild_id", - None, - decode.optional(decode.string), - ) - use channel_id <- decode.field("channel_id", decode.optional(decode.string)) - use user <- decode.optional_field( - "user", - None, - decode.optional(user.decoder()), - ) - use name <- decode.field("name", decode.optional(decode.string)) - use avatar_hash <- decode.field("avatar", decode.optional(decode.string)) - use token <- decode.optional_field( - "token", - None, - decode.optional(decode.string), - ) - use application_id <- decode.field( - "application_id", - decode.optional(decode.string), - ) - use source_guild <- decode.optional_field( - "source_guild", - None, - decode.optional(guild.decoder()), - ) - use source_channel <- decode.optional_field( - "source_channel", - None, - decode.optional(channel.decoder()), - ) - use url <- decode.optional_field("url", None, decode.optional(decode.string)) - decode.success(Webhook( - id:, - type_:, - guild_id:, - channel_id:, - user:, - name:, - avatar_hash:, - token:, - application_id:, - source_guild:, - source_channel:, - url:, - )) -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.int) - case variant { - 1 -> decode.success(Incoming) - 2 -> decode.success(ChannelFollower) - 3 -> decode.success(Application) - _ -> decode.failure(Incoming, "Type") - } -} diff --git a/src/grom/webhook_event.gleam b/src/grom/webhook_event.gleam deleted file mode 100644 index 0dfaf59..0000000 --- a/src/grom/webhook_event.gleam +++ /dev/null @@ -1,143 +0,0 @@ -import gleam/dynamic/decode -import gleam/json.{type Json} -import gleam/option.{type Option, None} -import gleam/time/timestamp.{type Timestamp} -import grom/entitlement.{type Entitlement} -import grom/guild.{type Guild} -import grom/internal/time_rfc3339 -import grom/user.{type User} - -// TYPES ----------------------------------------------------------------------- - -pub type WebhookEvent { - WebhookEvent( - version: Int, - application_id: String, - type_: WebhookType, - event: Option(Body), - ) -} - -pub type WebhookType { - Ping - Event -} - -pub type Type { - ApplicationAuthorized - EntitlementCreate -} - -pub type Body { - Body(type_: Type, timestamp: Timestamp, data: Option(Data)) -} - -pub type Data { - ApplicationAuthorizedEvent( - integration_type: Option(IntegrationType), - user: User, - scopes: List(String), - guild: Option(Guild), - ) - EntitlementCreateEvent(Entitlement) -} - -pub type IntegrationType { - GuildInstall - UserInstall -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn webhook_event_decoder() -> decode.Decoder(WebhookEvent) { - use version <- decode.field("version", decode.int) - use application_id <- decode.field("application_id", decode.string) - use type_ <- decode.field("type", webhook_type_decoder()) - use event <- decode.field("event", decode.optional(body_decoder())) - decode.success(WebhookEvent(version:, application_id:, type_:, event:)) -} - -@internal -pub fn webhook_type_decoder() -> decode.Decoder(WebhookType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(Ping) - 1 -> decode.success(Event) - _ -> decode.failure(Ping, "WebhookType") - } -} - -@internal -pub fn type_decoder() -> decode.Decoder(Type) { - use variant <- decode.then(decode.string) - case variant { - "APPLICATION_AUTHORIZED" -> decode.success(ApplicationAuthorized) - "ENTITLEMENT_CREATE" -> decode.success(EntitlementCreate) - _ -> decode.failure(ApplicationAuthorized, "Type") - } -} - -@internal -pub fn integration_type_decoder() -> decode.Decoder(IntegrationType) { - use variant <- decode.then(decode.int) - case variant { - 0 -> decode.success(GuildInstall) - 1 -> decode.success(UserInstall) - _ -> decode.failure(GuildInstall, "IntegrationType") - } -} - -@internal -pub fn data_decoder() -> decode.Decoder(Data) { - decode.one_of( - { - use integration_type <- decode.optional_field( - "integration_type", - None, - decode.optional(integration_type_decoder()), - ) - use user <- decode.field("user", user.decoder()) - use scopes <- decode.field("scopes", decode.list(decode.string)) - use guild <- decode.optional_field( - "guild", - None, - decode.optional(guild.decoder()), - ) - decode.success(ApplicationAuthorizedEvent( - integration_type:, - user:, - scopes:, - guild:, - )) - }, - or: [ - { - use entitlement <- decode.then(entitlement.decoder()) - decode.success(EntitlementCreateEvent(entitlement)) - }, - ], - ) -} - -@internal -pub fn body_decoder() -> decode.Decoder(Body) { - use type_ <- decode.field("type", type_decoder()) - use timestamp <- decode.field("timestamp", time_rfc3339.decoder()) - use data <- decode.optional_field( - "data", - None, - decode.optional(data_decoder()), - ) - decode.success(Body(type_:, timestamp:, data:)) -} - -// DECODERS -------------------------------------------------------------------- - -@internal -pub fn type_encode(type_: Type) -> Json { - case type_ { - ApplicationAuthorized -> json.string("APPLICATION_AUTHORIZED") - EntitlementCreate -> json.string("ENTITLEMENT_CREATE") - } -} -- 2.51.2