diff --git a/.formatter.exs b/.formatter.exs index d2cda26..8e782f4 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,4 +1,13 @@ # Used by "mix format" [ - inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], + locals_without_parens: [ + field: 2, + field: 3, + hydrate: 2, + discord_struct: 1, + include_fields: 1, + endpoint: 3, + endpoint: 4 + ] ] diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..0c55a23 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,14 @@ +name: ci +on: [push, pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + elixir-version: "1.18" + otp-version: "27" + - run: mix deps.get + - run: mix test + - run: mix dexcord.coverage diff --git a/README.md b/README.md index e0fabad..939dc7a 100644 --- a/README.md +++ b/README.md @@ -112,18 +112,21 @@ whether prefix or slash routing also fires. ### 1. Raw handler events Every gateway dispatch event reaches your `Dexcord.Handler` as a -`{event_name, data}` tuple, where `data` is the raw string-keyed payload map -Discord sent (no atom keys, no structs): +`{event_name, data}` tuple. `data` is decoded exactly once, in the dispatcher, +into a typed model **struct** with typed fields and **integer** snowflake ids - +`%Dexcord.Message{}`, `%Dexcord.Guild{}`, `%Dexcord.Interaction{}`, and friends +(a malformed payload that fails to decode falls back to the raw string-keyed map +so dispatch never stalls): ```elixir defmodule MyBot.Handler do use Dexcord.Handler - def handle_event({:MESSAGE_CREATE, msg}) do - IO.puts("#{msg["author"]["username"]}: #{msg["content"]}") + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}) do + IO.puts("#{msg.author.username}: #{msg.content}") end - def handle_event({:PRESENCE_UPDATE, presence}) do + def handle_event({:PRESENCE_UPDATE, %Dexcord.Presence{} = presence}) do # only fires if cache_presences: true and presences are being cached end end @@ -144,28 +147,30 @@ handler's `MESSAGE_CREATE` clause: defmodule MyBot.Commands do use Dexcord.Prefix.Router - def handle_command("ping", _args, msg) do - Dexcord.Api.create_message(msg["channel_id"], "pong") + def handle_command("ping", _args, %Dexcord.Message{} = msg) do + Dexcord.Message.reply(msg, "pong") end - def handle_command("echo", args, msg) do - Dexcord.Api.create_message(msg["channel_id"], Enum.join(args, " ")) + def handle_command("echo", args, %Dexcord.Message{} = msg) do + Dexcord.Api.send(msg.channel_id, Enum.join(args, " ")) end end defmodule MyBot.Handler do use Dexcord.Handler - def handle_event({:MESSAGE_CREATE, msg}) do + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}) do Dexcord.Prefix.dispatch(msg, prefix: "!", to: MyBot.Commands) end end ``` -`dispatch/2` skips messages authored by a bot (including the bot's own -messages, checked both via the payload's `author.bot` flag and against the -cached bot user id) so a command that replies in-channel can't recursively -trigger itself. `Dexcord.Prefix.parse/2` is exposed separately as a pure +The router receives the decoded `%Dexcord.Message{}`, so command handlers read +typed fields (`msg.channel_id`, `msg.author.id`) and reply with +`Dexcord.Message.reply/2` or `Dexcord.Api.send/2`. `dispatch/2` skips messages +authored by a bot (including the bot's own messages, checked both via the +`author.bot` flag and against the cached bot user id) so a command that replies +in-channel can't recursively trigger itself. `Dexcord.Prefix.parse/2` is exposed separately as a pure function if you want the prefix/command/args split without the bot-author check or the router dispatch. @@ -327,13 +332,20 @@ limiting - `Dexcord.Api.Ratelimit` learns each route's bucket from response headers and makes the calling process sleep as needed; you never manage rate limits yourself. -Typed endpoints (thin wrappers, string-keyed request/response maps): +Typed endpoints take string-keyed request maps (or a bare binary where a +`content`/`name` wrap is natural) and **decode their responses into model +structs** - `{:ok, %Dexcord.Message{}}`, `{:ok, %Dexcord.User{}}`, +`{:ok, %Dexcord.Channel{}}` (the concrete per-type struct), a list of structs +for list endpoints, or `{:error, %Dexcord.Api.Error{}}`: + +```elixir +{:ok, %Dexcord.Message{} = sent} = Dexcord.Api.create_message(channel_id, "hi") +{:ok, %Dexcord.User{} = me} = Dexcord.Api.get_current_user() -``` get_gateway_bot/0 get_current_user/0 get_current_application/0 get_user/1 get_channel/1 get_guild/1 create_dm/1 create_message/2 edit_message/3 -delete_message/2 create_reaction/3 +delete_message/2 create_reaction/3 get_channel_messages/2 create_interaction_response/3 edit_original_interaction_response/3 create_followup_message/3 @@ -341,8 +353,15 @@ bulk_overwrite_global_commands/2 bulk_overwrite_guild_commands/3 ``` -For anything not covered, `Dexcord.Api.request/4` is the escape hatch every -typed endpoint is built on: +`Dexcord.Api.send/2` is the ergonomic front door over `create_message`: it +accepts anything `Dexcord.Messageable` - a channel or thread struct, a +`%Dexcord.Message{}` (posts to its channel), a `%Dexcord.User{}`/`%Dexcord.Member{}` +(lazily opening and caching a DM), or a bare integer channel id - and applies the +configured `allowed_mentions` default. List endpoints also come as lazy streams +(`message_history/2`, `guild_members_stream/2`, ...) that page on demand. + +For anything without a typed wrapper, `Dexcord.Api.request/4` is the escape hatch +every typed endpoint is built on - it stays string-keyed both ways: ```elixir Dexcord.Api.request(:patch, "/guilds/#{guild_id}", %{"name" => "New Name"}) @@ -422,16 +441,19 @@ running untouched. | Nostrum | Dexcord | |---|---| | `Nostrum.Consumer` `handle_event/1` callback | `Dexcord.Handler` `handle_event/1` callback (`use Dexcord.Handler`) | -| `Nostrum.Api.*` | `Dexcord.Api.*` (typed endpoints + `request/4` escape hatch) | +| `Nostrum.Api.*` | `Dexcord.Api.*` (typed struct returns + `request/4` escape hatch) | | `Nostrum.Cache.*` (several cache modules) | `Dexcord.Cache` (one module, one ETS table per entity type) | -| `%Nostrum.Struct.Message{}` etc. (atom-keyed structs) | string-keyed maps everywhere - `msg["content"]`, not `msg.content` | +| `%Nostrum.Struct.Message{}` etc. (atom-keyed structs) | `%Dexcord.Message{}` etc. (typed structs, integer snowflake ids) - `msg.content`, `msg.author.id` | +| snowflake ids parsed to integers | snowflake ids **are** integers on every decoded struct | | Auto-starting `:nostrum` application | you add `{Dexcord, opts}` to your own supervision tree | -The biggest day-to-day adjustment is the lack of structs: every payload - -messages, guilds, members, interactions - is the raw string-keyed map -Discord sent over the wire. This costs `msg["content"]` instead of -`msg.content`, but means Dexcord never has to guess a struct shape ahead of a -Discord API change, and never mints atoms from wire data. +Like Nostrum, Dexcord hands your handler decoded **structs** with typed fields +and integer ids, so day-to-day access is `msg.content` / `msg.author.id`, not +map indexing. The escape hatch below the model layer - `Dexcord.Api.request/4` - +stays string-keyed both ways for endpoints without a typed wrapper. A full +worked port (READY backfill, MESSAGE_CREATE flow, thread cache reads, integer +snowflakes, REST, hydration) lives in +[`docs/alamedya-migration-v2.md`](docs/alamedya-migration-v2.md). ## Reliability design diff --git a/docs/alamedya-migration-v2.md b/docs/alamedya-migration-v2.md new file mode 100644 index 0000000..a4b862d --- /dev/null +++ b/docs/alamedya-migration-v2.md @@ -0,0 +1,482 @@ +# Migrating Alamedya from Nostrum to Dexcord (v2 — the typed contract) + +This is the v2 migration guide, rewritten against Dexcord's **typed struct +contract**. It supersedes the older internal notes: every gateway payload now +arrives as a decoded model **struct** (`%Dexcord.Message{}`, `%Dexcord.Guild{}`, +`%Dexcord.Interaction{}`, …) with **integer** snowflake ids and typed nested +fields — not the raw string-keyed maps the first draft assumed. + +It is self-contained. It is written for a Claude thread (or engineer) with **no +prior context** on either codebase. Follow it top to bottom. + +- **Source app:** Alamedya — a Phoenix app whose Discord side currently runs on + Nostrum behind a hand-rolled discord.py bridge. +- **Target library:** Dexcord — a reliability-first, single-shard Discord library + that owns its own gateway (resume-over-reidentify, zombie detection, + crash-surviving sessions). + +## Why this migration exists (read this first) + +Alamedya today runs Discord in a dual-path hack: Nostrum's own gateway shard is +disabled, and a **discord.py proxy** connects the real gateway and POSTs every +raw payload into a Phoenix controller that re-injects it through Nostrum's +internal dispatch. That contraption exists only because Nostrum's gateway drops +the websocket (laptop sleep / flaky network) and never recovers. + +Dexcord was built specifically to survive that. So the migration **cuts the +Python bridge entirely** and lets Dexcord connect the gateway directly — the +intended end state. Alamedya's Discord footprint is small and message-shaped: it +consumes `READY` and `MESSAGE_CREATE`, sends messages, reads channel history, +creates one thread, and reads guild threads from cache. Nothing Dexcord lacks by +design blocks it. + +## What changed since the first draft: string maps → typed structs + +The single biggest thing to internalize: **Dexcord decodes each payload exactly +once, in the dispatcher, into a struct, before your handler runs.** The first +migration draft told you to reach into raw maps (`msg["author"]["id"]`) and to +`String.to_integer/1` every id at the handler boundary. **Delete all of that.** +The struct already has typed fields and integer ids: + +| First-draft assumption (raw maps) | v2 reality (typed structs) | +|---|---| +| `msg["content"]` | `msg.content` | +| `msg["author"]["id"]` (a string) | `msg.author.id` (an integer) | +| `String.to_integer(msg["channel_id"])` | `msg.channel_id` (already an integer) | +| `Dexcord.Cache.threads(gid)` returns maps | returns `[%Dexcord.Thread{}]` | +| `Dexcord.Api.create_message/2` returns `{:ok, map}` | returns `{:ok, %Dexcord.Message{}}` | + +The rest of this guide walks every Discord touchpoint Alamedya has, `before` +(Nostrum / raw-map style) → `after` (typed). + +## Prerequisites + +**Elixir must be `>= 1.18`.** Dexcord uses the built-in `JSON` module and ships +no Jason. Bump `mix.exs` and confirm `elixir --version` reports ≥1.18 wherever +Alamedya builds and runs. Keep Jason as a dep — Phoenix still uses it. + +Depend on Dexcord via `path:` (or `git:`), drop the Nostrum dep, `mix deps.get`. + +--- + +## 1. Boot / config + +Dexcord is a library you add as **one child** to your own supervision tree; the +event handler is a plain module, not a supervised process. The typed contract +adds one boot-time knob worth setting up front: an `allowed_mentions` **default** +that every `Dexcord.Api.send/2` and `Dexcord.Message.reply/2` merges under. A bot +that should never accidentally `@everyone` sets `parse: []` once, here, instead +of threading `allowed_mentions` through every send. + +**Before** (Nostrum auto-starts from application config): + +```elixir +# config/config.exs +config :nostrum, + gateway_intents: :all, + num_shards: :manual + +# config/runtime.exs +config :nostrum, token: System.get_env("DISCORD_TOKEN") +``` + +**After** (typed child spec; no application config for the gateway): + +```elixir +# lib/alamedya/application.ex +children = [ + # ... Repo, PubSub, Endpoint, your own supervisors ... + + {Dexcord, + token: System.fetch_env!("DISCORD_TOKEN"), + handler: AlamedyaDiscord.Handler, + intents: :all, + cache_presences: false, + request_guild_members: false, + # Field-wise default: per-send values override key-by-key; unset keys fall + # back to this. `parse: []` suppresses every mention type unless a send opts + # back in. + allowed_mentions: [parse: []]} +] +``` + +`Dexcord.child_spec/1` validates these eagerly and raises `ArgumentError` on +anything missing or malformed, so a bad token or unknown intent fails at boot, +not at first use. `:allowed_mentions` accepts a keyword list, a map, or a +`%Dexcord.AllowedMentions{}`. + +--- + +## 2. READY backfill + +`READY` arrives as a typed `%Dexcord.Events.Ready{}`. Its `guilds` are +**stubs** — `%Dexcord.UnavailableGuild{}` (just an `id`, plus an `unavailable` +flag) — because at `READY` time the guild objects have not been sent yet. The +full `%Dexcord.Guild{}` for each arrives in a subsequent `GUILD_CREATE`, which +Dexcord's dispatcher folds into the cache **before** your handler sees it. + +So: do READY-time bootstrapping that only needs ids (locks, per-channel history +backfill by channel id) in the `READY` clause; do anything that needs full guild +state off `GUILD_CREATE` (or just read it from `Dexcord.Cache` on demand). + +**Before** (Nostrum: `msg` is a struct-ish payload, ids integers already but the +guild list shape is Nostrum's): + +```elixir +def handle_event({:READY, msg, _ws_state}) do + Logger.info("READY! #{inspect(msg)}") + Reminder.Scheduler.lock() + backfill_channels() + Reminder.Scheduler.unlock() +end +``` + +**After** (2-tuple; typed struct; stub guilds): + +```elixir +def handle_event({:READY, %Dexcord.Events.Ready{user: me, guilds: guilds}}) do + Logger.info("READY as #{me.username} (#{me.id}); #{length(guilds)} guild stub(s)") + Reminder.Scheduler.lock() + backfill_channels() + Reminder.Scheduler.unlock() +end + +# Full guild objects land here (cache is already populated when this runs). +def handle_event({:GUILD_CREATE, %Dexcord.Guild{} = guild}) do + Logger.debug("GUILD_CREATE #{guild.name} (#{guild.id}) fully cached") + :ok +end +``` + +Alamedya has no dedicated `GUILD_CREATE` logic — the `use Dexcord.Handler` +catch-all absorbs it, and the dispatcher still caches guilds/threads first. You +only need a `GUILD_CREATE` clause if you want to *react* to it. + +--- + +## 3. MESSAGE_CREATE flow + +The core loop. `MESSAGE_CREATE` arrives as `%Dexcord.Message{}` with typed +fields: `msg.content` (string), `msg.author` (a `%Dexcord.User{}`), +`msg.author.bot` (boolean), `msg.channel_id`/`msg.author.id` (**integers**), +`msg.mentions` (a list of `%Dexcord.User{}`), and `msg.webhook_id` (an integer +when the message came from a webhook — in which case `msg.author` is a synthetic +webhook user, so a `webhook_id`-first guard is the clean way to skip those). + +Replying is a one-liner: `Dexcord.Message.reply(msg, "…")` sets +`message_reference` to the source message and routes through the same send funnel +(so your `allowed_mentions` default from §1 applies). `mention_author: false` +suppresses the reply ping. + +The handler module below is the canonical shape. **It is mirrored verbatim as a +compiled test module in `test/dexcord/migration_guide_samples_test.exs` — keep +the two in sync.** + +**Before** (raw maps, manual `String.to_integer`, `get_in`): + +```elixir +def handle_event({:MESSAGE_CREATE, msg, _ws_state}) do + author_id = String.to_integer(msg["author"]["id"]) + is_bot = get_in(msg, ["author", "bot"]) == true + + cond do + is_bot -> :ignore + author_id == @self_id -> :ignore + msg["content"] == "ping!" -> + Nostrum.Api.create_message!(msg["channel_id"], "helo") + true -> :ignore + end +end +``` + +**After** (typed struct routing; verbatim shared sample): + +```elixir +defmodule AlamedyaDiscord.Handler do + # KEEP IN SYNC: mirrored verbatim in + # test/dexcord/migration_guide_samples_test.exs (§3 of docs/alamedya-migration-v2.md). + use Dexcord.Handler + + @self_id 1_135_637_126_222_987_365 + + # Webhook messages carry a `webhook_id` and a synthetic author — skip them + # first, before touching `author.bot`. + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{webhook_id: id}}) when not is_nil(id), + do: :ignore + + # Any bot (including ourselves) — never react. + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{author: %Dexcord.User{bot: true}}}), + do: :ignore + + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{author: %Dexcord.User{id: @self_id}}}), + do: :ignore + + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}) do + cond do + mentions_self?(msg) -> Dexcord.Message.reply(msg, "you rang?") + msg.content == "ping!" -> Dexcord.Message.reply(msg, "helo") + true -> :ignore + end + end + + defp mentions_self?(%Dexcord.Message{mentions: mentions}), + do: Enum.any?(mentions, fn %Dexcord.User{id: id} -> id == @self_id end) +end +``` + +Note there is no `String.to_integer`, no `get_in`, and no map indexing anywhere: +the struct is already typed, and `@self_id` (an integer literal) compares +directly against `msg.author.id` (an integer). This is the whole point of the +typed contract. + +--- + +## 4. Thread cache reads + +Alamedya reads guild threads from cache to decide routing. Dexcord caches +threads as `%Dexcord.Thread{}` structs and exposes them per-guild via +`Dexcord.Cache.threads/1` (a **list**, not Nostrum's threads map). Thread fields +are typed: `thread.parent_id` (integer), `thread.thread_metadata` (a +`%Dexcord.ThreadMetadata{}` with `.archived`, `.locked`, …). `Dexcord.Cache.channel/1` +returns the concrete per-type struct (`%Dexcord.TextChannel{}`, +`%Dexcord.Thread{}`, …), so you can pattern-match the channel type directly. + +**Before** (Nostrum GuildCache threads map, string compare): + +```elixir +maybe_thread = + Nostrum.Cache.GuildCache.get!(guild_id).threads + |> Map.values() + |> Enum.find(fn t -> t.id == channel_id end) + +archived? = maybe_thread && maybe_thread.thread_metadata.archived +``` + +**After** (typed list from the cache, integer compare, typed nested metadata): + +```elixir +maybe_thread = + Dexcord.Cache.threads(msg.guild_id) + |> Enum.find(fn %Dexcord.Thread{} = t -> t.id == msg.channel_id end) + +archived? = + case maybe_thread do + %Dexcord.Thread{thread_metadata: %Dexcord.ThreadMetadata{archived: a}} -> a + _ -> false + end + +# parent channel of the thread, if we want it: +parent = + with %Dexcord.Thread{parent_id: pid} <- maybe_thread, + {:ok, channel} <- Dexcord.Cache.channel(pid), + do: channel, else: (_ -> nil) +``` + +Creating a thread and posting into it stays a straight REST pair, now returning a +typed channel: + +```elixir +{:ok, %Dexcord.Thread{} = thread} = + Dexcord.Api.start_thread_with_message(msg.channel_id, msg.id, "request") + +Dexcord.Api.create_message(thread.id, "helo from the new thread") +``` + +--- + +## 5. Integer snowflakes + +The old draft's `String.to_integer(msg["author"]["id"])` dance is **gone**. Every +id on a decoded struct is already an `integer`. That means: + +- Comparisons against integer constants (`@self_id`, a config-mapped channel id) + just work — no coercion. +- Interpolating an id into a string (`"member #{msg.author.id}"`) just works. +- An `:integer` Ecto column (`discord_message_id`) takes `msg.id` directly. + +The **only** place you convert is the app boundary — an id that arrives as a +string from *outside* Discord (an env var, a DB row, a web request). Use +`Dexcord.Snowflake.cast/1` there, exactly once: + +**Before** (coerce on every access, everywhere): + +```elixir +channel_id = String.to_integer(msg["channel_id"]) +mapped = Application.get_env(:alamedya, :reminders)[:discord_mapping] # integer values +if channel_id == mapped[:mins_30], do: ... +``` + +**After** (struct id is already an integer; cast only the external config value): + +```elixir +# discord_mapping values come from env/config as strings — normalize once, at load: +mapping = + :alamedya + |> Application.get_env(:reminders) + |> Keyword.fetch!(:discord_mapping) + |> Map.new(fn {bucket, raw_id} -> {bucket, Dexcord.Snowflake.cast!(raw_id)} end) + +# thereafter compare directly — both integers: +if msg.channel_id == mapping[:mins_30], do: Reminder.add(:mins_30, msg) +``` + +`Dexcord.Snowflake.cast/1` returns `{:ok, integer} | :error`; `cast!/1` raises on +a non-snowflake. It accepts an integer (passthrough), a decimal string, or a +struct carrying an `:id`, so it is safe to call on "whatever id you have." + +--- + +## 6. Slash commands + +Alamedya doesn't use slash commands today, but the typed contract makes them +cheap enough to add, so here's the shape. Interactions arrive as +`%Dexcord.Interaction{}` with a typed, polymorphic `data` field: for an +application command it's a `%Dexcord.Interaction.ApplicationCommandData{}` whose +`.name` is the command name and whose `.options` are typed. `interaction.token` +and `interaction.id` are what the response helpers need; resolved-data maps are +keyed by **integer** ids. + +A `Dexcord.Slash` module declares `commands/0` and handles routed interactions; +`Dexcord.Slash.respond/2` (unchanged call shape) sends the immediate response. + +```elixir +defmodule AlamedyaDiscord.Slash do + use Dexcord.Slash + + def commands, do: [%{name: "ping", description: "Replies with pong."}] + + # `name` is the command name; `itx` is the full %Dexcord.Interaction{}. + def handle_interaction("ping", itx) do + Dexcord.Slash.respond(itx, "pong") + end +end +``` + +Wire it up with `slash: AlamedyaDiscord.Slash` (and, in dev, +`slash_guild_ids: [dev_guild_id]` for instant registration) in the child spec +from §1. `respond/2` takes a binary (used as `content`) or a map +(`%{content: "…", ephemeral: true}`); `respond_later/1`, `followup/2`, and +`edit_response/2` cover the deferred flow. + +--- + +## 7. REST calls + +`Dexcord.Api` typed endpoints return **typed structs** on success and the +unchanged `{:error, %Dexcord.Api.Error{}}` on failure: + +```elixir +{:ok, %Dexcord.Message{} = sent} = Dexcord.Api.create_message(channel_id, "hi") +{:ok, %Dexcord.User{} = me} = Dexcord.Api.get_current_user() +{:error, %Dexcord.Api.Error{status: 403}} = Dexcord.Api.get_channel(forbidden_id) +``` + +For anything without a typed wrapper, `Dexcord.Api.request/4` is the escape hatch +(string-keyed request/response maps): + +```elixir +Dexcord.Api.request(:patch, "/guilds/#{guild_id}", %{"name" => "New Name"}) +``` + +**Hand-rolled pagination loops become streams.** The old draft read channel +history with an explicit `get_channel_messages(id, 50, {:after, cursor})` loop. +Dexcord ships lazy streams that page for you and only fetch as far as you consume: + +**Before** (manual cursor loop): + +```elixir +msgs = + Nostrum.Api.get_channel_messages!(channel_id, 50, {:after, last_id}) + |> Enum.reverse() +``` + +**After** (lazy stream; `after:` flips to oldest→newest, `limit:` caps the total): + +```elixir +msgs = + Dexcord.Api.message_history(channel_id, after: last_id, limit: 50) + |> Enum.to_list() +# each element is a %Dexcord.Message{}; take/2 stops fetching once satisfied. +``` + +`message_history/2`, `guild_members_stream/2`, `guild_bans_stream/2`, and +`audit_log_stream/2` are all lazy `Stream`s. The Nostrum-compatible +`Dexcord.Api.get_channel_messages/3` locator arity +(`get_channel_messages(id, limit, {:after, cursor})`) still exists if you want a +single explicit page instead of a stream. + +`Dexcord.Api.send/2` is the ergonomic front door over `create_message`: it +accepts anything `Dexcord.Messageable` — a channel struct, a `%Dexcord.Thread{}`, +a `%Dexcord.Message{}` (posts to its channel), a `%Dexcord.User{}`/`%Dexcord.Member{}` +(opens and caches a DM lazily), or a bare integer channel id — and applies the +`allowed_mentions` default from §1. + +--- + +## 8. Hydration + +Envelope events (reactions, message deletes) carry only **ids**, not the related +objects — a `%Dexcord.Events.ReactionAdd{}` has `user_id`, `channel_id`, +`guild_id` but leaves `user`, `channel`, `guild` as `nil`. `Dexcord.Cache.fill/1` +best-effort fills those slots from the cache (ETS only — no HTTP, safe on the hot +path). A cache miss leaves that slot `nil`; already-filled slots are untouched +(idempotent). + +**Before** (Nostrum: look each id up in a separate cache module by hand): + +```elixir +def handle_event({:MESSAGE_REACTION_ADD, reaction, _ws_state}) do + user = Nostrum.Cache.UserCache.get!(reaction.user_id) + channel = Nostrum.Cache.ChannelCache.get!(reaction.channel_id) + handle_reaction(reaction, user, channel) +end +``` + +**After** (one `fill/1` call hydrates every declared slot): + +```elixir +def handle_event({:MESSAGE_REACTION_ADD, %Dexcord.Events.ReactionAdd{} = reaction}) do + reaction = Dexcord.Cache.fill(reaction) + # reaction.user :: %Dexcord.User{} | nil, reaction.channel :: channel struct | nil, + # reaction.guild :: %Dexcord.Guild{} | nil — each nil on a cache miss. + handle_reaction(reaction) +end +``` + +Because `fill/1` never blocks on the network, treat a `nil` slot as "not cached +right now" and fall back to a typed REST call (`Dexcord.Api.get_user/1`, …) only +when you actually need that object. + +--- + +## Verification + +From the Alamedya checkout after the migration: + +1. **Clean compile with Nostrum gone** — proves no lingering `Nostrum.*` + references or struct matches, and no leftover `String.to_integer` on ids that + are now integers: + + ```sh + mix compile --warnings-as-errors + grep -rn "Nostrum" lib/ config/ # expect zero hits + ``` + +2. **Tests** (if the app has any touching this code): `mix test`. + +3. **End-to-end — the real acceptance test** (the failure the whole migration + retires): start Alamedya with `DISCORD_TOKEN` set and **no** discord.py + running. Confirm it connects via Dexcord's own gateway (a `READY` log), post a + message in a mapped channel (a reminder persists), @-mention the bot (it opens + a `"request"` thread and replies), then **suspend the machine / drop the + network, wait, and wake it** → confirm events resume via a RESUME, repeatedly. + That last step is the exact Nostrum failure the bridge was working around; + Dexcord must handle it natively. + +## Rollback + +The migration is a single branch. If E2E fails, `git checkout` back, restore the +`config :nostrum` block and the bridge, and re-run the discord.py proxy. Nothing +in the DB schema changes, so there is no data migration to reverse. + + diff --git a/docs/design-plans/2026-07-04-api-surface.md b/docs/design-plans/2026-07-04-api-surface.md new file mode 100644 index 0000000..a78d99c --- /dev/null +++ b/docs/design-plans/2026-07-04-api-surface.md @@ -0,0 +1,388 @@ +# Dexcord Full API Surface Design + +## Summary + +Dexcord's current API surface is a thin, map-based wrapper: REST calls return raw string-keyed maps, gateway events arrive as maps, and callers do their own field access and type coercion. This design replaces that contract end-to-end with generated structs. Three compile-time DSLs (`Dexcord.Struct` for object shapes, `Dexcord.Enum` for open integer enums, `Dexcord.Flags` for bitfields) let each of the ~60 Discord object types and ~180 REST endpoints be declared once — as a field list or a route line — and have the struct definition, type spec, decoder, encoder, and (for endpoints) the callable function itself generated from that single declaration. This keeps the "one source of truth per shape" property that hand-written triple declarations (struct + type + decoder, written separately) tend to lose over time, which is the specific failure mode being designed away from Nostrum's approach. + +The rest of the design follows from making that decode step happen exactly once, in one place: gateway JSON is turned into typed structs in the dispatcher before it ever reaches a handler or the cache, and REST responses are decoded the same way before being returned to the caller. Decoding is built to degrade rather than crash — unrecognized fields, enum values, and events fall back to sensible defaults or raw passthrough instead of raising, so a single malformed payload can't take down the dispatch loop. On top of the typed core sits a discord.py-flavored ergonomics layer (protocol-based message sending, reply helpers, mention rendering, an embed builder) intended to make the typed API pleasant to use rather than just type-safe. Work is sequenced across seven phases — DSLs first, then models, then events, then the REST layer and its coverage tooling, then wiring it all into the live gateway/cache/slash-command paths, finishing with the ergonomics layer and a migration guide. + +## Definition of Done + +1. **Typed REST surface**: `Dexcord.Api` covers all mainstream v10 bot endpoint groups (channels, messages, guilds, members, roles, reactions, threads, webhooks, invites, interactions, app commands, emojis, stickers, polls, auto-mod, scheduled events, audit logs) via a minimal-boilerplate endpoint-definition layer (macros or similar). Monetization, soundboard, stage instances, and templates stay on `request/4`, which remains public as the escape hatch. +2. **Deep structs**: real structs with `Dexcord.Snowflake` IDs everywhere — API returns, gateway event payloads handed to `handle_event`, and cache reads. No back-compat with the map contract. +3. **discord.py-flavored ergonomics**: struct-centric helper functions on model modules (`Dexcord.Message.reply/2`-style), accepting structs or snowflakes where sensible. +4. **Tests**: machinery tested hard (endpoint macro layer, decode engine, Snowflake, partial/null/unknown-field edge cases) + representative per-group fake-server round-trips; suite stays green and hermetic. +5. **New alamedya migration guide**: a fresh guide (not an update of the existing internal `alamedya-migration.md`, which remains uncommitted) written against the new struct contract. + +## Acceptance Criteria + +### api-surface.AC1: Typed REST surface +- **api-surface.AC1.1 Success:** A DSL-declared endpoint generates a function that issues the correct method + interpolated path (FakeRest wire assertion) and returns `{:ok, decoded_struct}` +- **api-surface.AC1.2 Success:** `query:` params are URL-encoded when provided, omitted entirely when not +- **api-surface.AC1.3 Success:** `reason: true` endpoints send `X-Audit-Log-Reason` (URL-encoded) when `:reason` is passed, and no header otherwise +- **api-surface.AC1.4 Success:** `files: true` endpoints encode multipart with `payload_json` + `files[n]` parts referenced by attachment `id` +- **api-surface.AC1.5 Success:** Snowflake arguments accept integer, decimal string, or struct-with-`.id`, all producing the identical request path +- **api-surface.AC1.6 Failure:** A 4xx/5xx response decodes to `{:error, %Dexcord.Api.Error{}}` with status/code/message intact +- **api-surface.AC1.7 Success:** `Dexcord.Api.request/4` remains public with its current raw-map contract +- **api-surface.AC1.8 Success:** `mix dexcord.coverage` reports zero missing in-scope endpoints against the pinned spec +- **api-surface.AC1.9 Failure:** Removing a route-table entry makes `mix dexcord.coverage` exit non-zero +- **api-surface.AC1.10 Success:** New route shapes get bounded bucket keys (invite codes collapse; no raw tokens in ETS) + +### api-surface.AC2: Deep structs & decode engine +- **api-surface.AC2.1 Success:** `from_map/1` decodes declared fields including nested structs and lists +- **api-surface.AC2.2 Success:** Unknown JSON keys are ignored; decoding mints zero atoms from wire data +- **api-surface.AC2.3 Failure:** Wrong-shaped field values (list where map expected, junk scalars) decode to `nil`/default/raw-value — never raise +- **api-surface.AC2.4 Success:** Unknown enum int → `{:unknown, n}`; `encode(decode(n))` is lossless for known and unknown values +- **api-surface.AC2.5 Success:** Flags fields keep the raw integer; `has?/2`, `to_list/1`, `from_list/1` correct; unknown bits survive round-trip in the int +- **api-surface.AC2.6 Success:** `tristate:` fields decode to `:absent | nil | value` +- **api-surface.AC2.7 Success:** `:presence` fields decode key-presence to boolean (present-with-null → `true`) +- **api-surface.AC2.8 Success:** `merge_map/2` updates only keys present in the partial map +- **api-surface.AC2.9 Success:** `to_map/1`/body encoding: snowflakes → strings, enums → ints, `DateTime` → ISO8601; absent keyword key = omitted field, `nil` = JSON null +- **api-surface.AC2.10 Success:** Golden GUILD_CREATE decodes with typed channels/roles/members/threads +- **api-surface.AC2.11 Success:** MESSAGE_CREATE's member-without-user → `%PartialMember{}`, author a full `%User{}` +- **api-surface.AC2.12 Success:** Webhook-authored message decodes (synthetic author, no member) +- **api-surface.AC2.13 Success:** `referenced_message`: absent → `:absent`, null → `nil`, present → `%Message{}` +- **api-surface.AC2.14 Success:** Channel factory maps all 15 documented types to their structs; unknown type → `%UnknownChannel{raw: map}` +- **api-surface.AC2.15 Success:** INTERACTION_CREATE decodes in both guild (member + permissions) and DM (user) variants +- **api-surface.AC2.16 Success:** Resolved interaction data decodes to partial structs (member w/o user, 4-field channel) +- **api-surface.AC2.17 Success:** Every atom in `EventNames.all/0` has decode coverage — full object, envelope struct, or documented passthrough (test iterates the list) +- **api-surface.AC2.18 Success:** Handler receives `{atom, typed_payload}`; unknown events arrive `{{:unknown_event, name}, raw_map}` +- **api-surface.AC2.19 Failure:** A malformed event payload logs, falls back to raw delivery, and dispatch of subsequent events continues +- **api-surface.AC2.20 Success:** Cache stores and returns structs keyed by integer snowflakes +- **api-surface.AC2.21 Success:** GUILD_UPDATE merge preserves gateway-only fields (`joined_at` survives) +- **api-surface.AC2.22 Success:** GUILD_DELETE cascade purge works against struct-valued tables + +### api-surface.AC3: Ergonomics +- **api-surface.AC3.1 Success:** `Api.send/2` accepts channel structs, threads, and bare snowflakes via `Messageable`, hitting the right channel route +- **api-surface.AC3.2 Success:** `Api.send/2` to `%User{}`/`%Member{}` creates the DM once, then reuses the cached DM channel (second send makes no create-DM call) +- **api-surface.AC3.3 Success:** `Message.reply/2` sets `message_reference` to the source message; `mention_author:` flows into allowed_mentions +- **api-surface.AC3.4 Success:** `mention/1` + `String.Chars` render `<@id>`/`<#id>`/`<@&id>`; `PartialEmoji.to_string/1` renders send format +- **api-surface.AC3.5 Success:** Config-level `allowed_mentions` default applies to sends; per-send values merge field-wise over it +- **api-surface.AC3.6 Success:** `Embed` builder chain produces a valid wire map +- **api-surface.AC3.7 Success:** Pagination streams lazily page across multiple FakeRest hits +- **api-surface.AC3.8 Success:** `Cache.fill/1` hydrates declared slots from cache; cache miss leaves slot `nil`; never issues HTTP + +### api-surface.AC4: Suite health +- **api-surface.AC4.1 Success:** Full suite green and hermetic (no external network) throughout +- **api-surface.AC4.2 Success:** DSL machinery (Struct/Enum/Flags/endpoint) each has a dedicated exhaustive test file +- **api-surface.AC4.3 Success:** `mix dexcord.coverage` runs in CI and gates regressions + +### api-surface.AC5: Migration guide +- **api-surface.AC5.1 Success:** `docs/alamedya-migration-v2.md` exists, covers every Discord touchpoint in alamedya (READY backfill, MESSAGE_CREATE flow, thread cache reads, integer snowflakes vs the old string dance), and its code samples are valid against the final API + +## Glossary + +- **Discord Gateway**: Discord's persistent WebSocket connection that pushes real-time events (message created, member joined, etc.) to a bot, as opposed to the REST API which is request/response. +- **Snowflake**: Discord's ID format — a 64-bit integer that encodes a creation timestamp, making IDs sortable by time. This design narrows Dexcord's representation from "integer or string" to integer-only. +- **Struct** (Elixir): A compile-time-checked, fixed-field data record built on top of Elixir maps. `Dexcord.Struct` here is a DSL that generates these (plus their type spec and (de)serialization functions) from one field declaration. +- **DSL (domain-specific language)**: A small, purpose-built syntax (implemented via Elixir macros) for declaring something — here, object shapes (`Dexcord.Struct`), enums (`Dexcord.Enum`), bitfields (`Dexcord.Flags`), and REST routes (the endpoint DSL) — that expands into ordinary code at compile time. +- **Atom / atomization**: Elixir atoms are fixed constants stored in a global, never-garbage-collected table; converting untrusted external strings into atoms can exhaust that table and crash the VM. "Zero atoms minted from wire data" means decoding matches on string keys directly rather than converting them to atoms. +- **Open integer enum**: An enum that must tolerate values Discord adds later. `Dexcord.Enum` decodes recognized ints to atoms and unrecognized ones to `{:unknown, n}`, and can re-encode either losslessly. +- **Bitfield / Flags**: An integer where each bit is an independent boolean (e.g., Discord permissions). `Dexcord.Flags` keeps the raw integer (so unknown bits aren't lost) while offering helpers to test/list/build named flags. +- **Tristate field**: A field with three meaningfully different states — absent from the payload, explicitly `null`, or a real value — rather than the usual two (present or `nil`). Used where "not present" and "present but null" mean different things (e.g., a message that isn't a reply vs. a reply whose original was deleted). +- **Presence field**: A field where only *whether the key exists* in the JSON matters, not its value — key present (even if null) decodes to `true`. +- **Hydration slot / `hydrate`**: A declared field on a gateway "envelope" struct (e.g., just a `user_id`) that can be optionally filled in later from the cache (e.g., the full `User`) via `Cache.fill/1`, without ever making a network call. +- **Envelope struct**: A per-event struct used for gateway events that don't carry a full Discord object (e.g., `MESSAGE_DELETE` only carries IDs), as opposed to "full-object" events like `MESSAGE_CREATE` that decode directly to the model struct. +- **Partial struct**: A distinct struct for a shape Discord documents as genuinely different from (usually a subset of) the full object — e.g., a guild member payload that lacks the nested user — used only where the shape truly differs, not merely where a field is sometimes missing. +- **Messageable (protocol)**: An Elixir protocol implemented by every "thing you can send a message to" (channels, threads, users, messages, raw IDs) so `Api.send/2` can resolve any of them to the right destination and route. +- **discord.py**: A popular Python Discord bot library whose API taste (naming, `get`/`fetch` conventions, ergonomics) this design explicitly imitates. +- **Nostrum**: An existing Elixir Discord library whose design choices (integer snowflakes, lenient casting, typed lists) are partly kept and partly deliberately rejected (its atom-minting and field-drift problems). +- **ETS (Erlang Term Storage)**: The in-memory key-value store the cache is built on; tables here are `:public` with `read_concurrency: true` so many processes can read concurrently while one process writes. +- **Single-writer cache**: An architecture where only the dispatcher process ever writes to the cache (in gateway event order), while any process can read — avoiding write races without needing locks. +- **Dispatcher**: The process that receives decoded gateway events, writes them to cache, and hands them off to handler code; the design's decode step happens here, once, before anything else sees the event. +- **Ratelimit bucket / `route_key`**: Discord rate-limits are tracked per "bucket," derived from a request's route with variable segments (IDs, tokens) collapsed so buckets don't multiply unboundedly or leak sensitive values (like invite codes) into ETS keys. +- **Multipart / `payload_json`**: The HTTP encoding Discord requires for endpoints that accept file uploads alongside JSON — the JSON body goes in a `payload_json` part, files in separate numbered parts. +- **`X-Audit-Log-Reason` header**: An HTTP header Discord uses to attach a human-readable reason to moderation actions in the guild's audit log. +- **OpenAPI spec (`discord-api-spec`)**: Discord's official, auto-generated machine-readable description of its REST API (published as a "public preview"), used here only as a checklist to verify endpoint coverage, not as codegen input. +- **`mix` task**: A command-line task in Elixir's build tool (`mix`); `mix dexcord.coverage` is a custom one that checks the declared routes against the pinned spec. +- **Golden fixture**: A checked-in, real (or realistic) sample payload used as a stable, exact-match test input — e.g., a captured `GUILD_CREATE` payload used to verify decoding. +- **FakeRest / FakeGateway**: This codebase's existing test doubles that simulate Discord's REST API and gateway connection so tests run hermetically, without real network calls. +- **`persistent_term`**: An Erlang/OTP storage mechanism for rarely-changing global data with very fast reads, used here to hold validated bot configuration. +- **Behaviour + `@before_compile`**: An Elixir pattern where a module declares a contract (`behaviour`) that implementers must satisfy, combined with a compile-time hook (`@before_compile`) that injects fallback/catch-all code into the implementing module. +- **Keyword list**: An Elixir list of `{atom, value}` pairs (written `key: value, ...`), Elixir's idiomatic way of passing optional named arguments — used here for endpoint bodies, where an omitted key means "don't send this field" versus an explicit `nil`. +- **Components V2**: A newer generation of Discord's interactive message-component system (buttons, selects, etc.) alongside the original component set. + +## Architecture + +### Overview + +Three compile-time DSLs generate everything from single declarations; a decode pipeline converts raw gateway/REST JSON (string-keyed maps from the built-in `JSON` module) into structs exactly once per event; the REST surface is declared as a route table that expands into typed functions. Discord's official OpenAPI spec (`discord/discord-api-spec`) is **not** codegen input — it is a coverage checklist consumed by a dev-only mix task. + +Prior art: discord.py sets the taste (model taxonomy, tolerance posture, get/fetch semantics, Messageable); Nostrum's casting vocabulary is kept (`{:struct, M}`/`{:list, T}` type specs, nil passthrough, lenient scalar fallback, integer snowflakes) while its regretted mechanics are dropped (global payload atomization, triple-declared fields, raw-int enums). + +### The three DSLs + +**`Dexcord.Struct`** (`lib/dexcord/struct.ex`) — a `field` declaration DSL. One declaration per field generates, at compile time: + +- `defstruct` with defaults +- `@type t` (generated from the same field table — cannot drift) +- `from_map/1` — decodes a string-keyed map; **matches string keys directly** (no atomization pass, zero atoms minted from wire data) +- `to_map/1` — encodes back to a JSON-ready map (snowflakes → strings, enums → ints, flags → int, `DateTime` → ISO8601) +- `merge_map/2` — `(struct, raw_partial_map) → struct`, updating **only keys present in the map** (correct partial-update semantics for the cache) +- `fill/2` hydration support for declared `hydrate` slots (see Cache integration) + +Field types: `:snowflake`, `:string`, `:integer`, `:boolean`, `:datetime` (ISO8601 → `DateTime.t()`), `{:struct, Module}`, `{:list, type}`, `{:enum, Module}`, `{:flags, Module}`, plus modifiers `default:`, `tristate: true` (decodes to `:absent | nil | value`; for `Message.referenced_message` absent = not a reply, null = deleted referent), and `:presence` (key-presence boolean; for `RoleTags.premium_subscriber` where present-null means true). + +```elixir +defmodule Dexcord.Message do + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :channel_id, :snowflake + field :author, {:struct, Dexcord.User} + field :content, :string + field :timestamp, :datetime + field :embeds, {:list, {:struct, Dexcord.Embed}}, default: [] + field :type, {:enum, Dexcord.MessageType} + field :flags, {:flags, Dexcord.MessageFlags} + field :referenced_message, {:struct, __MODULE__}, tristate: true + end +end +``` + +**`Dexcord.Enum`** (`lib/dexcord/enum.ex`) — open integer enums. One table generates `@type t :: :atom | ... | {:unknown, integer()}`, `decode/1` (unknown int → `{:unknown, n}`), `encode/1` (lossless round-trip, `{:unknown, n}` → `n`). + +**`Dexcord.Flags`** (`lib/dexcord/flags.ex`) — bitfields. Struct fields keep the **raw integer** (lossless, forward-compatible); the module generates `has?/2`, `to_list/1` (unknown bits omitted from the list only, never from the int), `from_list/1`, `all/0`. Used for `Dexcord.Permissions`, `Dexcord.MessageFlags`, and friends; `Dexcord.Intents` remains as-is. + +### Decode semantics (the tolerance contract) + +Decode must never crash the dispatcher: + +- Generated `from_map/1` guards shapes (`when is_map/is_list`); surprises fall through to `nil`/default; failed scalar casts keep the raw value. +- Unknown JSON keys are never inspected — forward compatible by construction. +- Unknown enum int → `{:unknown, n}`. Unknown channel `type` → `%Dexcord.UnknownChannel{type: {:unknown, n}, raw: map}`. Unknown gateway event → `{{:unknown_event, name}, raw_map}` (unchanged from today). +- Absent and JSON-null both decode to `nil`, except fields explicitly declared `tristate:` or `:presence`. +- Encode direction: request bodies come from keyword opts; **absent key = omit field, `nil` value = JSON null** — the PATCH tri-state maps onto keyword-list key presence with no sentinel. + +### Snowflake (breaking rework) + +`Dexcord.Snowflake.t()` becomes `0..0xFFFF_FFFF_FFFF_FFFF` — a plain integer (Nostrum-proven, discord.py-identical). Module: `cast/1`, `cast!/1`, `dump/1` (→ string for wire), `defguard is_snowflake/1`, existing `from_datetime/1`, `to_datetime/1`, `to_unix/1` (the missing `:error` path in `to_unix/1` gets fixed). Wire strings normalize to int once, at decode. Ids sort chronologically, interpolate, and pattern-match naturally. + +### Models + +~60 model modules under `lib/dexcord/model/`, **flat module names** (`Dexcord.Message`, `Dexcord.User`, `Dexcord.Guild`, `Dexcord.TextChannel` — directory ≠ namespace, deliberately). + +**Channels are distinct structs** chosen by a factory (`lib/dexcord/model/channel.ex`, `Dexcord.Channel.from_map/1` dispatching on `"type"`): `TextChannel`, `VoiceChannel`, `CategoryChannel`, `AnnouncementChannel`, `ForumChannel`, `MediaChannel`, `StageChannel`, `DMChannel`, `GroupDMChannel`, `Thread` (types 10/11/12), `UnknownChannel` (fallback). Shared fields come from a common declaration helper; capability is protocol membership (`CategoryChannel` is not `Messageable` — wrong sends fail at match time). + +**Partials are first-class** where Discord documents a genuinely distinct shape (the rule: different provenance → different struct; same shape with an occasionally-missing field → `nil`): + +- `Dexcord.PartialEmoji` (reactions/components/poll media; `to_string/1` gives send format) +- `Dexcord.PartialMember` (gateway member-without-user; interaction resolved members) +- `Dexcord.PartialChannel` (interaction resolved channels) +- `Dexcord.PartialGuild` (user-guilds list), `Dexcord.UnavailableGuild` (READY stubs) +- `Dexcord.MessageSnapshot` (forwards) + +No `PartialMessage`/`Object` equivalents — the id-accepting API layer makes them unnecessary. + +### Gateway events + +`Dexcord.Events` (`lib/dexcord/events.ex` + `lib/dexcord/events/*.ex`): a generated table maps event atom → payload type. Full-object events decode to the model struct (`{:MESSAGE_CREATE, %Dexcord.Message{}}`); envelope events get per-event structs (`{:MESSAGE_DELETE, %Dexcord.Events.MessageDelete{}}`, `{:MESSAGE_REACTION_ADD, %Dexcord.Events.ReactionAdd{}}`, …) so the handler always receives exactly what the gateway sent, typed. Handler contract: `{event_atom, typed_payload}` or `{{:unknown_event, name}, raw_map}` — the 2-tuple shape is unchanged. + +Envelope structs declare **hydration slots**: + +```elixir +discord_struct do + field :user_id, :snowflake + field :channel_id, :snowflake + field :guild_id, :snowflake + field :emoji, {:struct, Dexcord.PartialEmoji} + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild +end +``` + +`Dexcord.Cache.fill/1` populates the slots from cache — best-effort (miss → slot stays `nil`), never blocks, never issues HTTP. Hydration is invoked only by user handler code, never by the dispatcher. + +### REST surface + +**Endpoint DSL** (`lib/dexcord/api/endpoint.ex`): ~12 group modules (`lib/dexcord/api/channels.ex`, `messages.ex`, `guilds.ex`, `members.ex`, `roles.ex`, `webhooks.ex`, `invites.ex`, `interactions.ex`, `commands.ex`, `emojis_stickers.ex`, `moderation.ex`, `misc.ex` — exact grouping settled in implementation planning) declare routes: + +```elixir +endpoint :create_message, :post, "/channels/:channel_id/messages", + body: true, files: true, returns: Dexcord.Message + +endpoint :get_guild_audit_log, :get, "/guilds/:guild_id/audit-logs", + query: [:user_id, :action_type, :before, :after, :limit], + returns: Dexcord.AuditLog + +endpoint :delete_role, :delete, "/guilds/:guild_id/roles/:role_id", + reason: true, returns: nil +``` + +Each expands to a function with `@spec`, `@doc` (method + path + docs link), path interpolation (URL-encoded segments), query building, body encoding, response decoding into `returns:`. Conventions: + +- Coverage target: **~180 endpoints** across the in-scope groups (channels, messages/reactions/pins, guilds/members/roles/bans, users, webhooks, invites, interaction responses/followups, application commands, emojis, stickers, polls, auto-moderation, scheduled events, audit logs, gateway/application info). Excluded (escape hatch): monetization, soundboard, stage instances, guild templates, deprecated pin routes, Bearer-only endpoints. +- Return contract: `{:ok, struct | [struct] | nil}` | `{:error, %Dexcord.Api.Error{}}`. `request/4` stays public and map-based (the break-glass). +- `reason: true` → `:reason` opt → `X-Audit-Log-Reason` header (hand-flagged on the ~60 mutating endpoints; the OpenAPI spec doesn't model reason headers). +- `files: true` → multipart `payload_json` + `files[n]` encoding — new machinery alongside `request/4` in `lib/dexcord/api.ex`. +- Snowflake arguments accept int, decimal string, or a struct with an `.id` field. Bodies accept keyword lists, maps, or structs (`to_map/1`). +- Naming: Discord docs names, except "Modify X" → `edit_x`. +- Sugar arities (e.g. `create_message(channel_id, "text")`) are hand-written over the generated base. +- `Dexcord.Api` (`lib/dexcord/api.ex`) remains the flat front door: generated `defdelegate`s to the group modules + `request/4`. + +**Ratelimit**: `Dexcord.Api.Ratelimit.route_key/2` (`lib/dexcord/api/ratelimit.ex`) gains `templatize` clauses for new route shapes — invite codes (non-snowflake segment must collapse), `sticker-packs`, application-emoji routes — so buckets neither over-share nor grow unbounded ETS keys. Message-delete's separate bucket family and webhook/interaction token digests already exist and are kept. + +**Coverage tooling**: `mix dexcord.coverage` diffs the compiled route table against a checked-in snapshot of `discord-api-spec`'s `openapi.json` (`priv/discord_openapi.json` + a refresh task), reporting endpoints in the spec but missing from Dexcord (excluded groups allowlisted). Runs in CI; fails on regression. + +### Ergonomics layer + +- `Dexcord.Messageable` protocol (`lib/dexcord/messageable.ex`): `resolve/1 → {:channel, id} | {:dm_user, id}`; implemented for TextChannel, VoiceChannel, Thread, DMChannel, User, Member, Message, Interaction, and bare snowflakes. One `Dexcord.Api.send/2` funnel; `{:dm_user, id}` performs the create-DM dance with the DM channel id cached. +- `Dexcord.Message.reply/2` (sets `message_reference`; `mention_author:` opt). +- `mention/1` on User/Member/Role/channel structs; `String.Chars` impls; `created_at/1` wherever there's an id; `Dexcord.Util.format_dt/2` (`` markup). +- `Dexcord.Embed` pipe-friendly builder; `Dexcord.Permissions` (`has?/2`, `to_list/1`, `from_list/1`) plus `Dexcord.Guild.member_permissions/2` computing role + overwrite resolution. +- `allowed_mentions`: app-level default in `Dexcord.Config`, per-send field-wise merge, `mention_author:` sugar. +- Pagination: `Dexcord.Api.message_history/2` and friends (members, bans, audit log) return lazy `Stream`s that page transparently. + +### Data flow + +``` +Gateway (JSON.decode!) [unchanged] + → Dispatcher: Events.decode(name, raw) [decode ONCE] + → Cache.handle_dispatch(name, decoded, raw) [structs stored; merge_map for partial updates] + → handler Task: handle_event({name, decoded}) + → Slash.dispatch(%Interaction{}, module) [typed interactions] +``` + +Cache stores structs keyed by **integer snowflakes** (tables and single-writer discipline unchanged, `lib/dexcord/cache.ex`); GUILD_CREATE explodes typed children into the child tables; UPDATE events use `merge_map/2` so gateway-only fields (e.g. `joined_at`) survive partial updates. Reads return structs. A decode failure for one event logs, falls back to delivering the raw map, and never kills dispatch. + +## Existing Patterns + +Followed unchanged: + +- **Single-writer cache via Dispatcher** (`lib/dexcord/dispatcher.ex:35-51`, `lib/dexcord/cache.ex`): cache writes stay inline in the dispatcher process, in gateway order; tables stay `:public`, `read_concurrency: true`, reads straight from ETS. Only the stored values (structs, not maps) and key types (integers, not strings) change. +- **Behaviour + `@before_compile` catch-all injection** (`Dexcord.Handler`, `Dexcord.Slash`, `Dexcord.Prefix.Router`): kept as-is; the new DSLs follow the same macro hygiene conventions. +- **Pure logic in standalone testable modules** (`Gateway.Payload`, `EventNames`, `Snowflake`, `Intents`): the DSLs, `Events` table, and endpoint expansion follow this. +- **Config**: validated bot config in `:persistent_term` via `Dexcord.Config`; tuning knobs via `Application.get_env` (preserves the EnvSandbox test pattern). `allowed_mentions` default joins the validated config. +- **Test harness** (`test/support/`): EnvSandbox + FakeRest + FakeGateway patterns are the template for all new tests; `api_integration_test.exs` is the model for endpoint round-trips. +- **`Dexcord.Api.Error`** stays the error struct; `request/4` keeps its exact contract. +- **Moduledoc style**: substantial `@moduledoc` with design rationale, `@spec` on all public functions, `@doc false` for internals. + +Deliberate divergences: + +- **The map contract is removed** at all four seams (handler events, cache values, Api returns, Slash interactions). This is the point of the design; no compatibility layer. v0.1's "minimal typed REST" locked decision is consciously revisited. +- **Macro DSLs are a new pattern** in this codebase. Justification: ~130 shapes × 3 hand-written declarations each is the alternative, and it is the approach Nostrum demonstrably regrets (field drift, atomization hedges). +- **`Dexcord.Snowflake.t()` narrows** from `integer() | String.t()` to integer-only. Justification: one canonical representation ends the string/int comparison bugs (alamedya's `String.to_integer` dance). + +## Implementation Phases + + +### Phase 1: Foundations — DSLs and Snowflake +**Goal:** The three code-generating DSLs and the integer Snowflake exist, fully tested; everything later is declarations. + +**Components:** +- `Dexcord.Struct` DSL in `lib/dexcord/struct.ex` — `discord_struct`/`field`/`hydrate` macros generating `defstruct`, `@type t`, `from_map/1`, `to_map/1`, `merge_map/2`; field types `:snowflake`, `:string`, `:integer`, `:boolean`, `:datetime`, `{:struct, M}`, `{:list, t}`, `{:enum, M}`, `{:flags, M}`; modifiers `default:`, `tristate:`, `:presence` +- `Dexcord.Enum` DSL in `lib/dexcord/enum.ex` — `decode/1`/`encode/1` with `{:unknown, n}` round-trip +- `Dexcord.Flags` DSL in `lib/dexcord/flags.ex` — raw-int storage, `has?/2`, `to_list/1`, `from_list/1`, `all/0` +- Reworked `Dexcord.Snowflake` in `lib/dexcord/snowflake.ex` — integer `t()`, `cast/1`, `cast!/1`, `dump/1`, `is_snowflake/1` guard, fixed `to_unix/1` +- Fixture modules + exhaustive DSL tests in `test/dexcord/struct_test.exs`, `enum_test.exs`, `flags_test.exs`, updated `snowflake_test.exs` + +**Dependencies:** None. + +**Done when:** DSL fixture modules compile with correct `defstruct`/`@type`; decode/encode/merge/tristate/presence/lenient-fallback behaviors all pass (`api-surface.AC2.1–AC2.9`); suite green. + + + +### Phase 2: Core models +**Goal:** The message-path object graph decodes from real payloads. + +**Components:** +- Models in `lib/dexcord/model/`: `User`, `Member`, `PartialMember`, `Message`, `Attachment`, `Embed` (+ sub-objects), `Reaction`, `Emoji`, `PartialEmoji`, `Role` (+ `RoleTags` with presence-booleans), `Guild`, `Channel` factory + `TextChannel`/`VoiceChannel`/`CategoryChannel`/`AnnouncementChannel`/`ForumChannel`/`MediaChannel`/`StageChannel`/`DMChannel`/`GroupDMChannel`/`Thread`/`UnknownChannel`, `UnavailableGuild`, `PartialGuild`, `VoiceState`, `Presence`, `Sticker`, `StickerItem`, `MessageSnapshot` +- Enum/flag tables: `ChannelType`, `MessageType`, `MessageFlags`, `Permissions`, `UserFlags`, and companions the above need +- Golden payload fixtures in `test/fixtures/` (GUILD_CREATE, MESSAGE_CREATE with member+mentions, DM message, forum/thread channels, webhook-authored message, reply with deleted referent) + field-level decode tests + +**Dependencies:** Phase 1. + +**Done when:** Golden fixtures decode field-correct, including the notorious corners (`api-surface.AC2.10–AC2.14`); channel factory dispatches all 15 documented types + unknown fallback. + + + +### Phase 3: Remaining models and event envelopes +**Goal:** Every in-scope object and gateway event has a typed home. + +**Components:** +- Remaining models in `lib/dexcord/model/`: `Interaction` (+ data variants, `ResolvedData` with `PartialChannel`/resolved partials), `ApplicationCommand` (+ options tree), component structs (ActionRow/Button/selects/TextInput + Components V2 set), `Webhook`, `Invite` (+ metadata), `AuditLog` (+ entries/changes), `AutoModerationRule` (+ triggers/actions), `GuildScheduledEvent` (+ recurrence), `Poll` (+ media/answers/results), `Application`, `Team`, `WelcomeScreen`, `Onboarding`, `Integration`, `Ban`, `Widget` objects, `ThreadMember`, `FollowedChannel`, `VoiceRegion`, `Connection` +- `Dexcord.Events` decode table in `lib/dexcord/events.ex`; envelope structs with `hydrate` slots in `lib/dexcord/events/*.ex` for every non-full-object event in `Dexcord.EventNames` (reactions, deletes, bulk delete, member add/remove/update, role events, typing, thread list sync, poll votes, …) +- Golden payload fixtures: INTERACTION_CREATE (slash + component + modal, guild and DM), reaction add, audit log entry, scheduled event + +**Dependencies:** Phase 2. + +**Done when:** `Events.decode/2` covers every atom in `Dexcord.EventNames.all/0` (full-object, envelope, or documented raw passthrough); interaction fixtures decode including resolved partials (`api-surface.AC2.15–AC2.17`). + + + +### Phase 4: Endpoint DSL and Api core +**Goal:** The endpoint machinery works end-to-end; the existing 16 endpoints are reborn as declarations. + +**Components:** +- Endpoint DSL in `lib/dexcord/api/endpoint.ex` — route declaration → function generation (`@spec`, docs, path interpolation with URL-encoded segments, `query:` building, `body:` encoding from keyword/map/struct, `reason:` header, `returns:` decoding) +- Multipart machinery in `lib/dexcord/api.ex` — `payload_json` + `files[n]` encoding for `files: true` endpoints +- `Ratelimit.route_key/2` extensions in `lib/dexcord/api/ratelimit.ex` — invite-code collapse, `sticker-packs`, application-emoji clauses +- Existing 16 endpoints redeclared via the DSL in group modules; `Dexcord.Api` facade with generated delegates + `request/4` unchanged +- Endpoint-machinery tests against FakeRest (path/query/body/reason/multipart on the wire; struct decode on return; error passthrough) + +**Dependencies:** Phases 1–2 (Phase 3 models only needed for later groups). + +**Done when:** All 16 existing endpoints pass FakeRest round-trips returning structs (`api-surface.AC1.1–AC1.7`); multipart and reason-header wire format verified; ratelimit templatizer tests cover new clauses. + + + +### Phase 5: Full endpoint rollout and coverage gate +**Goal:** ~180 endpoints declared; coverage is mechanically enforced. + +**Components:** +- All in-scope endpoint declarations across the group modules in `lib/dexcord/api/` +- Sugar arities (`create_message/2` with binary, `get_channel_messages/3` locator form, etc.) +- `mix dexcord.coverage` task in `lib/mix/tasks/dexcord.coverage.ex` + pinned `priv/discord_openapi.json` + refresh task; CI wiring +- Representative per-group FakeRest tests (one round-trip per group minimum, plus every special shape: top-level-array bodies, `wait=true` webhooks, `with_response` callbacks, binary widget.png, no-auth routes) + +**Dependencies:** Phases 3–4. + +**Done when:** `mix dexcord.coverage` reports zero missing in-scope endpoints and runs in CI (`api-surface.AC1.8–AC1.10`); representative tests green. + + + +### Phase 6: Gateway, cache, and slash integration +**Goal:** The typed contract is live end-to-end; the map contract is gone. + +**Components:** +- `Dexcord.Dispatcher` decodes once via `Events.decode/2`; delivers `{name, typed}` to handler Tasks; decode-failure fallback to raw with logging +- `Dexcord.Cache` stores structs keyed by integer snowflakes; typed GUILD_CREATE explosion; `merge_map/2` on UPDATE events; reads return structs; `Dexcord.Cache.fill/1` hydration +- `Dexcord.Slash` typed: `%Interaction{}` into callbacks, struct-or-map `commands/0`, response helpers accepting keyword/map/struct bodies +- `Dexcord.Prefix` internals matching on `%Message{}` +- Updated integration tests: `cache_integration_test.exs`, `cache_cascade_test.exs`, `gateway_integration_test.exs`, `slash_test.exs`, `ergonomics_integration_test.exs`, cache-merge scenarios, `Cache.fill` round-trips + +**Dependencies:** Phase 3 (models/events); independent of Phases 4–5. + +**Done when:** FakeGateway end-to-end flows deliver typed events, cache returns structs surviving partial updates, hydration fills from cache (`api-surface.AC2.18–AC2.22`, `api-surface.AC3.8`); no map-shaped reads remain. + + + +### Phase 7: Ergonomics, docs, and the alamedya guide +**Goal:** The discord.py taste layer; the library is adoptable. + +**Components:** +- `Dexcord.Messageable` protocol in `lib/dexcord/messageable.ex` + `Dexcord.Api.send/2` funnel with create-DM handling +- `Dexcord.Message.reply/2`; `mention/1` + `String.Chars` + `created_at/1` across models; `Dexcord.Util.format_dt/2` +- `Dexcord.Embed` builder; `Dexcord.Guild.member_permissions/2`; `allowed_mentions` config default + merge +- Pagination streams (`message_history/2`, member/ban/audit-log streams) +- New migration guide `docs/alamedya-migration-v2.md` written against the struct contract; README + moduledoc refresh +- Ergonomics tests (protocol dispatch incl. DM dance, reply reference wiring, permission computation, allowed-mentions merge, stream pagination against FakeRest) + +**Dependencies:** Phases 4–6. + +**Done when:** Ergonomics ACs pass (`api-surface.AC3.1–AC3.7`); guide exists and compiles against the final API (`api-surface.AC5.1`); full suite green, `mix dexcord.coverage` green (`api-surface.AC4.1–AC4.3`). + + +## Additional Considerations + +**Decode failure blast radius:** one malformed event must never take down dispatch — `Events.decode/2` rescues per-event, logs at warning with the event name (never the full payload at warn level), and falls back to `{{name}, raw_map}` delivery so a handler can still act. Cache skips writes for events whose decode failed rather than storing half-decoded structs. + +**GUILD_CREATE size:** decode of a large guild (channels + members + presences) happens once, inline in the dispatcher, replacing today's raw-map writes. The generated `from_map/1` is a single traversal with no atomization pass; if profiling during Phase 6 shows dispatcher lag on very large guilds, the escape valve is decoding guild children lazily per-table (design permits; not built preemptively). + +**Spec drift:** `mix dexcord.coverage` compares against a *pinned* `openapi.json`; refreshing the pin is a deliberate act (`mix dexcord.coverage.refresh`), so upstream spec churn never breaks CI spontaneously. The spec is "public preview" — docs win on conflict, and reason-header support is hand-flagged because the spec omits it entirely. + +**Registrar/global-wipe guards:** the Registrar's empty-command-list guard and retry containment (`lib/dexcord/slash/registrar.ex`) are untouched; `commands/0` returning structs is normalized to maps before the existing logic. + +**Out of scope, still reachable:** monetization, soundboard, stage instances, guild templates, and Bearer-only endpoints remain on `request/4`. New Discord surface lands first as an `{:unknown, n}` enum value, an unmatched map key (silently ignored), or a missing endpoint flagged by the coverage task — all non-breaking. diff --git a/docs/test-plans/2026-07-05-api-surface.md b/docs/test-plans/2026-07-05-api-surface.md new file mode 100644 index 0000000..99ce4ea --- /dev/null +++ b/docs/test-plans/2026-07-05-api-surface.md @@ -0,0 +1,84 @@ +# Human Test Plan — dexcord Full API Surface (2026-07-05-api-surface) + +Coverage validation: **PASS** — 44/44 acceptance criteria covered by automated tests +(633 tests, 0 failures, 1 excluded `:flaky` with a deterministic default-suite twin). +This plan covers the residue automation cannot prove: hermeticity, CI enablement, +and migration-guide editorial quality, plus an optional live end-to-end. + +## Prerequisites + +- Elixir 1.18 / OTP 27, `mix deps.get` clean. +- `mix test` green from the repo root (baseline: 633 tests, 0 failures, 1 excluded). +- A Gitea repo with push access for the CI checks. +- Optional: a real Discord bot token + a throwaway test guild for the live end-to-end. + +## Phase A: Suite Hermeticity (HV-1 — AC4.1) + +Automated coverage proves the suite is green; this proves green also means *no external network*. + +| Step | Action | Expected | +|------|--------|----------| +| A1 | Add `127.0.0.1 discord.com` (and `docs.discord.com`) to `/etc/hosts`, or run `sudo unshare -n mix test` from repo root | Full suite still exits 0 with the same pass count (633/0/1) | +| A2 | `rg -n 'discord\.com|githubusercontent' test/` | Hits only fixtures, doc-link strings, and the deliberately network-only `dexcord.coverage.refresh` task — never a live call from a test | +| A3 | `rg -n 'coverage\.refresh' test/ .gitea/` | No match — the network-only refresh task is never invoked by tests or CI | +| A4 | Revert `/etc/hosts` | Clean state restored | + +## Phase B: CI Gate Actually Runs & Bites (HV-2 — AC4.3) + +| Step | Action | Expected | +|------|--------|----------| +| B1 | Confirm `.gitea/workflows/ci.yml` lists `- run: mix test` and `- run: mix dexcord.coverage` | Both steps present | +| B2 | In Gitea repo settings, enable **Actions** | Actions enabled (owner infra step; no CI configured today) | +| B3 | Push a normal commit (or open a PR) | Pipeline triggers; both `mix test` and `mix dexcord.coverage` run green | +| B4 | On a throwaway branch, delete one in-scope `endpoint` declaration line (e.g. `get_channel` in `lib/dexcord/api/channels.ex`), push | The `mix dexcord.coverage` step turns the pipeline **red** (non-zero exit, route reported missing) | +| B5 | Delete the throwaway branch | Gate proven to bite; no residue | + +## Phase C: Migration Guide Completeness & Prose (HV-3 — AC5.1) + +The sample-validity half is automated (`migration_guide_samples_test.exs`); this covers +editorial completeness against the real old app. + +| Step | Action | Expected | +|------|--------|----------| +| C1 | Read the old `alamedya-migration.md` (untracked, repo root) and extract its Discord touchpoint checklist | A concrete list, esp. the old string↔int snowflake dance and thread/cache reads specific to alamedya | +| C2 | Open `docs/alamedya-migration-v2.md` and confirm all 8 mandated sections exist | §1 Boot/config, §2 READY backfill, §3 MESSAGE_CREATE, §4 Thread cache reads, §5 Integer snowflakes, §6 Slash commands, §7 REST calls, §8 Hydration | +| C3 | For each old touchpoint from C1, find its before→after mapping in v2 | Nothing dropped; each maps to a v2 section | +| C4 | Confirm the `KEEP IN SYNC` comment sits in both the guide §3 handler and `test/dexcord/migration_guide_samples_test.exs` | Present in both | +| C5 | Read for prose quality | Accurate, adoptable, no stale map-shaped examples | + +## End-to-End: Live Bot Round-Trip (optional; spans Phases 2/6/7) + +Validates decode → dispatch → cache → send against real Discord, which the fakes only approximate. + +1. Configure a real bot token + `allowed_mentions` default per guide §1; boot the app against a test guild. +2. On READY, call `Dexcord.Cache.guilds()` → expect the test guild present; after `GUILD_CREATE`, `Dexcord.Cache.channels(guild_id)` populated with typed channel structs (`%Dexcord.TextChannel{}` etc.). +3. From another account, post `ping!` in a visible channel → the bot replies as a threaded reply (`message_reference` set to your message). +4. `@mention` the bot → it replies. +5. Post as a webhook (or another bot) → the bot sends nothing (webhook/bot authors skipped). +6. `Dexcord.Api.message_history(channel_id) |> Enum.take(5)` in `iex` → exactly 5 `%Dexcord.Message{}` in descending id order, one page fetched. +7. Register a slash command, invoke it → handler receives a typed `%Dexcord.Interaction{}`; `Dexcord.Slash.respond/2` posts a visible reply. + +## Human Verification Required + +| Criterion | Why Manual | Steps | +|-----------|------------|-------| +| AC4.1 (hermeticity) | "green suite" ≠ "no egress" — a mis-stubbed fake could pass while calling out | Phase A | +| AC4.3 (CI runs) | A checked-in YAML can't assert the server executes it; Gitea Actions enablement is owner infra | Phase B | +| AC5.1 (guide completeness/prose) | Coverage-completeness and readability are editorial judgments | Phase C | + +## Traceability + +| Acceptance Criterion | Automated Test | Manual Step | +|----------------------|----------------|-------------| +| AC1.1–1.7 | endpoint_macro_test, endpoint_test, api_facade_test | E2E steps 2, 6 | +| AC1.8–1.10 | coverage_test, ratelimit_test | Phase B (B4 exercises the gate) | +| AC2.1–2.9 | struct_test, enum_test, flags_test, endpoint_test | — (pure) | +| AC2.10–2.17 | model_guild/message/channel/interaction/role_test, events_test | E2E step 2 | +| AC2.18–2.19 | gateway_integration_test | E2E steps 3–5 | +| AC2.20–2.22, AC3.8 | cache_test, cache_fill_test, cache_cascade_test | E2E step 2 | +| AC3.1–3.6 | send_test, messageable_test, ergonomics_helpers_test, model_message_parts_test | E2E steps 3–4 | +| AC3.7 | pagination_test | E2E step 6 | +| AC4.1 | full `mix test` | Phase A | +| AC4.2 | struct/enum/flags/endpoint test files (async categorization) | — | +| AC4.3 | coverage_test + `.gitea/workflows/ci.yml` | Phase B | +| AC5.1 | migration_guide_samples_test | Phase C | diff --git a/lib/dexcord.ex b/lib/dexcord.ex index bb97fbc..df31e9c 100644 --- a/lib/dexcord.ex +++ b/lib/dexcord.ex @@ -81,6 +81,8 @@ defmodule Dexcord do slash_guild_ids = Keyword.get(opts, :slash_guild_ids) validate_slash_guild_ids(slash_guild_ids) + allowed_mentions = validate_allowed_mentions(Keyword.get(opts, :allowed_mentions)) + %{ token: token, handler: handler, @@ -90,7 +92,8 @@ defmodule Dexcord do request_guild_members: request_guild_members, slash: slash, slash_guild_ids: slash_guild_ids, - gateway_url: gateway_url + gateway_url: gateway_url, + allowed_mentions: allowed_mentions } end @@ -153,6 +156,21 @@ defmodule Dexcord do "got: #{inspect(other)}" end + # Accepts a keyword, a map, or a `%Dexcord.AllowedMentions{}`, normalized to a + # string-keyed wire map (only explicitly-provided keys). Absent -> nil. + defp validate_allowed_mentions(nil), do: nil + + defp validate_allowed_mentions(spec) + when is_list(spec) or is_map(spec) do + Dexcord.AllowedMentions.normalize(spec) + end + + defp validate_allowed_mentions(other) do + raise ArgumentError, + "Dexcord :allowed_mentions must be a keyword list, map, or %Dexcord.AllowedMentions{}, " <> + "got: #{inspect(other)}" + end + defp boolean_opt(opts, key, default) do value = Keyword.get(opts, key, default) diff --git a/lib/dexcord/api.ex b/lib/dexcord/api.ex index 1f8aeff..99bbc46 100644 --- a/lib/dexcord/api.ex +++ b/lib/dexcord/api.ex @@ -3,7 +3,11 @@ defmodule Dexcord.Api do REST client over Finch, governed by `Dexcord.Api.Ratelimit`. `request/4` is the generic escape hatch and the only place that talks to the - network; the ~16 typed endpoints below are thin wrappers over it. Every request + network; the typed endpoint surface is declared with the + `Dexcord.Api.Endpoint` DSL across `Dexcord.Api.Channels`, + `Dexcord.Api.Messages`, `Dexcord.Api.Users`, `Dexcord.Api.Interactions`, + `Dexcord.Api.Commands`, and `Dexcord.Api.Misc`, then re-exported here as + generated delegates (`Dexcord.Api.get_channel/1` and friends). Every request flows through the limiter: acquire a token (sleeping in the caller's process while `{:wait, ms}`), issue the request, feed the response headers back for bucket learning, and on a 429 honor `retry_after` / the global scope and retry @@ -17,6 +21,21 @@ defmodule Dexcord.Api do The base URL defaults to `https://discord.com/api/v10` and is overridable with `config :dexcord, :api_base_url` (used by the integration tests to point at a fake server). + + ## Internal multipart form + + As an internal detail used by the endpoint layer, `request/4` also accepts a + body of the shape `{:multipart, payload_map, files}` where `files` is a list of + `%{filename: String.t(), data: binary(), content_type: String.t()}`. It is + encoded as `multipart/form-data` with a `payload_json` part plus one `files[n]` + part per file (see "Uploading Files" in the Discord reference). This form is not + part of the public contract; external callers should pass plain maps/lists. + + A second internal shape, `{:form_multipart, fields_map, file}`, encodes plain + form-field multipart (Discord's "Create Guild Sticker"): each `fields_map` key + becomes its own bare form part and `file` (a `%{filename, data, content_type}` + map) rides in a single part named `file` — NOT the `payload_json`/`files[n]` + shape above. """ alias Dexcord.Api.Error @@ -54,7 +73,12 @@ defmodule Dexcord.Api do when the internal waits would exceed `:api_deadline_ms`. For a non-JSON error body a bounded snippet of the raw body is kept in `:message`. """ - @spec request(atom(), String.t(), map() | list() | nil, keyword()) :: + @spec request( + atom(), + String.t(), + map() | list() | {:multipart, map(), list()} | {:form_multipart, map(), map()} | nil, + keyword() + ) :: {:ok, map()} | {:ok, nil} | {:ok, {:raw, binary()}} | {:error, Error.t()} def request(method, path, body \\ nil, opts \\ []) do route = Ratelimit.route_key(method, path) @@ -169,11 +193,89 @@ defmodule Dexcord.Api do nil -> {headers, nil} + {:multipart, payload_map, files} -> + build_multipart(payload_map, files, headers) + + {:form_multipart, fields, file} -> + build_form_multipart(fields, file, headers) + body -> {[{"content-type", "application/json"} | headers], JSON.encode!(body)} end end + # Hand-rolled `multipart/form-data` per the Discord "Uploading Files" reference: + # a `payload_json` part carrying the JSON body, followed by one `files[n]` part + # per attachment (referenced from the payload's `attachments` array by index). + defp build_multipart(payload_map, files, headers) do + payload_part = [ + "Content-Disposition: form-data; name=\"payload_json\"\r\n", + "Content-Type: application/json\r\n\r\n", + JSON.encode!(payload_map) + ] + + file_parts = + files + |> Enum.with_index() + |> Enum.map(fn {%{filename: filename, data: data, content_type: ctype}, n} -> + file_part("files[#{n}]", filename, ctype, data) + end) + + multipart_wrap([payload_part | file_parts], headers) + end + + # Plain form-field `multipart/form-data` (Discord "Create Guild Sticker"): each + # payload key is its own form-data part carrying a bare scalar value, plus a + # single file part named literally `file` — NOT the `payload_json` + `files[n]` + # shape. `file` is a `%{filename, data, content_type}` map. + defp build_form_multipart( + fields, + %{filename: filename, data: data, content_type: ctype}, + headers + ) do + field_parts = + Enum.map(fields, fn {key, value} -> + [ + "Content-Disposition: form-data; name=\"", + to_string(key), + "\"\r\n\r\n", + to_string(value) + ] + end) + + multipart_wrap(field_parts ++ [file_part("file", filename, ctype, data)], headers) + end + + # A file part's iodata: content-disposition (with filename) + content-type + data. + defp file_part(name, filename, content_type, data) do + [ + "Content-Disposition: form-data; name=\"", + name, + "\"; filename=\"", + escape_filename(filename), + "\"\r\n", + "Content-Type: ", + content_type, + "\r\n\r\n", + data + ] + end + + # Wraps a list of part iodatas (each the bytes AFTER the boundary line) in a + # fresh random boundary and returns `{headers, body}`. + defp multipart_wrap(parts, headers) do + boundary = + "dexcord" <> Base.url_encode64(:crypto.strong_rand_bytes(12), padding: false) + + framed = Enum.map(parts, fn part -> ["--", boundary, "\r\n", part, "\r\n"] end) + body = IO.iodata_to_binary([framed, "--", boundary, "--\r\n"]) + + {[{"content-type", "multipart/form-data; boundary=" <> boundary} | headers], body} + end + + # Quotes, backslashes and CR/LF in a filename would corrupt the part header. + defp escape_filename(name), do: String.replace(name, ~r/["\\\r\n]/, "_") + defp base_url, do: Application.get_env(:dexcord, :api_base_url, @default_base_url) defp deadline_ms, do: Application.get_env(:dexcord, :api_deadline_ms, @default_deadline_ms) @@ -273,185 +375,313 @@ defmodule Dexcord.Api do end end - # Builds the query string for `get_channel_messages/2`: `:limit` plus at most - # one anchor (`:before` > `:after` > `:around`), or `""` when neither is given. - defp message_query(opts) do - anchor = - Enum.find_value([:before, :after, :around], fn k -> - case Keyword.get(opts, k) do - nil -> nil - id -> {k, id} + # --- Ergonomic send funnel --------------------------------------------- + + @doc """ + Sends a message to anything `Dexcord.Messageable`: channels, threads, + messages (their channel), users/members (lazy DM), or a bare channel id. + + The target is resolved through `Dexcord.Messageable.resolve/1` — a + non-sendable value (a category/forum/media/directory channel) raises + `Protocol.UndefinedError` here, before any HTTP. A user/member target opens + (and caches) a DM channel on first use; see `Dexcord.Cache.dm_channel/1`. + + `body` follows `Dexcord.Api.Messages.create_message/2`: a binary (wrapped as + `content`), or a keyword/map/struct body. + """ + @spec send(Dexcord.Messageable.t(), term(), keyword()) :: + {:ok, Dexcord.Message.t()} | {:error, Error.t()} + def send(target, body, opts \\ []) do + body = apply_allowed_mentions_default(body) + + case Dexcord.Messageable.resolve(target) do + {:channel, channel_id} -> + Dexcord.Api.Messages.create_message(channel_id, body, opts) + + {:dm_user, user_id} -> + with {:ok, channel_id} <- dm_channel_id(user_id) do + Dexcord.Api.Messages.create_message(channel_id, body, opts) end - end) - - params = - [] - |> maybe_param(:limit, Keyword.get(opts, :limit)) - |> maybe_param(anchor && elem(anchor, 0), anchor && elem(anchor, 1)) - - case params do - [] -> "" - _ -> "?" <> URI.encode_query(params) end end - defp maybe_param(params, _key, nil), do: params - defp maybe_param(params, key, value), do: [{key, to_string(value)} | params] + # Applies the configured `allowed_mentions` default (if any) with a field-wise + # merge under any per-send value (per-send wins). Only the `send`/`reply` + # ergonomics funnel does this — the raw `create_message` endpoint stays + # mechanical. The body is normalized to a string-keyed map here so the merge + # (and `create_message`'s own encode) both see the same shape. + # + # Skipped entirely when the config key is unset AND the body carries no + # allowed_mentions, so an absent field keeps Discord's own defaults. + defp apply_allowed_mentions_default(body) do + default = Dexcord.Config.get(:allowed_mentions) + normalized = Dexcord.Api.Endpoint.encode_body(body, %{binary_wrap: :content}) - defp maybe_put(map, _key, nil), do: map - defp maybe_put(map, key, value), do: Map.put(map, key, value) + case normalized do + %{} = map -> + if is_nil(default) and not Map.has_key?(map, "allowed_mentions") do + map + else + Map.update(map, "allowed_mentions", default, fn per_send -> + Dexcord.AllowedMentions.merge(default, Dexcord.AllowedMentions.normalize(per_send)) + end) + end - # --- Typed endpoints ---------------------------------------------------- - - @doc "GET /gateway/bot - recommended gateway url + shard/session-start info." - @spec get_gateway_bot() :: {:ok, map()} | {:error, Error.t()} - def get_gateway_bot, do: request(:get, "/gateway/bot") - - @doc "GET /users/@me - the current bot user." - @spec get_current_user() :: {:ok, map()} | {:error, Error.t()} - def get_current_user, do: request(:get, "/users/@me") - - @doc "GET /oauth2/applications/@me - the current application object." - @spec get_current_application() :: {:ok, map()} | {:error, Error.t()} - def get_current_application, do: request(:get, "/oauth2/applications/@me") - - @doc "GET /users/:user_id - a user." - @spec get_user(id()) :: {:ok, map()} | {:error, Error.t()} - def get_user(user_id), do: request(:get, "/users/#{user_id}") - - @doc "GET /channels/:channel_id - a channel." - @spec get_channel(id()) :: {:ok, map()} | {:error, Error.t()} - def get_channel(channel_id), do: request(:get, "/channels/#{channel_id}") - - @doc "GET /guilds/:guild_id - a guild." - @spec get_guild(id()) :: {:ok, map()} | {:error, Error.t()} - def get_guild(guild_id), do: request(:get, "/guilds/#{guild_id}") - - @doc "POST /users/@me/channels - open (or fetch) a DM channel with a user." - @spec create_dm(id()) :: {:ok, map()} | {:error, Error.t()} - def create_dm(user_id) do - request(:post, "/users/@me/channels", %{"recipient_id" => to_string(user_id)}) + other -> + other + end end - @doc """ - GET /channels/:channel_id/messages - a channel's messages, newest first. + # Resolve a user id to a DM channel id, opening (and caching) the DM on a miss. + defp dm_channel_id(user_id) do + case Dexcord.Cache.dm_channel(user_id) do + {:ok, channel_id} -> + {:ok, channel_id} - `opts` is a keyword list; `:limit` caps the count and at most one anchor of - `:before` / `:after` / `:around` (that precedence) selects the window. All are - encoded into the query string. - """ - @spec get_channel_messages(id(), keyword()) :: {:ok, [map()]} | {:error, Error.t()} - def get_channel_messages(channel_id, opts \\ []) do - request(:get, "/channels/#{channel_id}/messages" <> message_query(opts)) + :error -> + with {:ok, %Dexcord.DMChannel{id: id}} <- Dexcord.Api.Users.create_dm(user_id) do + Dexcord.Cache.put_dm_channel(user_id, id) + {:ok, id} + end + end end - @doc """ - GET /channels/:channel_id/messages - `Nostrum.Api`-compatible arity. + # --- Lazy pagination streams ------------------------------------------- + # + # Each returns a lazy `Stream` over `Dexcord.Api.Paginate`: a page is fetched + # only when the consumer walks that far, so `Stream.take/2` on a fresh stream + # makes exactly one wire hit. A page-fetch `{:error, _}` raises + # `Dexcord.Api.Paginate.PageError` mid-stream (streams cannot carry a tagged + # tuple), which the caller can `rescue`. - `locator` is `{:before, id}` / `{:after, id}` / `{:around, id}` or `{}` for no - anchor; it is normalized onto the keyword form of `get_channel_messages/2`. + @history_page_size 100 + @members_page_size 1000 + @bans_page_size 1000 + @audit_log_page_size 100 + + @doc """ + Streams a channel's message history, resolving `messageable` through + `Dexcord.Messageable`. + + By default it pages newest→oldest using the `before:` anchor (cursor = the + last message's id). Passing `after: id` flips to oldest→newest paging with the + `after:` anchor (mirroring discord.py's `oldest_first`, which defaults true iff + `after` is given). `limit: n` caps the total number of messages yielded. + + The stream is lazy — pages are fetched on demand — and raises + `Dexcord.Api.Paginate.PageError` if a page request fails. """ - @spec get_channel_messages(id(), pos_integer(), {atom(), id()} | {}) :: - {:ok, [map()]} | {:error, Error.t()} - def get_channel_messages(channel_id, limit, locator) do - opts = - case locator do - {a, id} when a in [:before, :after, :around] -> [{:limit, limit}, {a, id}] - {} -> [limit: limit] + @spec message_history(Dexcord.Messageable.t(), keyword()) :: Enumerable.t() + def message_history(messageable, opts \\ []) do + {:channel, channel_id} = Dexcord.Messageable.resolve(messageable) + + stream = + case Keyword.fetch(opts, :after) do + {:ok, after_id} -> + Dexcord.Api.Paginate.stream( + after_id, + @history_page_size, + fn cursor -> + Dexcord.Api.Messages.get_channel_messages(channel_id, + limit: @history_page_size, + after: cursor + ) + end, + fn last -> last.id end + ) + + :error -> + Dexcord.Api.Paginate.stream( + nil, + @history_page_size, + fn + nil -> + Dexcord.Api.Messages.get_channel_messages(channel_id, limit: @history_page_size) + + cursor -> + Dexcord.Api.Messages.get_channel_messages(channel_id, + limit: @history_page_size, + before: cursor + ) + end, + fn last -> last.id end + ) end - get_channel_messages(channel_id, opts) - end - - @doc "POST /channels/:channel_id/messages - send a message." - @spec create_message(id(), String.t() | map()) :: {:ok, map()} | {:error, Error.t()} - def create_message(channel_id, content) when is_binary(content) do - create_message(channel_id, %{"content" => content}) - end - - def create_message(channel_id, %{} = params) do - request(:post, "/channels/#{channel_id}/messages", params) - end - - @doc "PATCH /channels/:channel_id/messages/:message_id - edit a message." - @spec edit_message(id(), id(), String.t() | map()) :: {:ok, map()} | {:error, Error.t()} - def edit_message(channel_id, message_id, content) when is_binary(content) do - edit_message(channel_id, message_id, %{"content" => content}) - end - - def edit_message(channel_id, message_id, %{} = params) do - request(:patch, "/channels/#{channel_id}/messages/#{message_id}", params) - end - - @doc "DELETE /channels/:channel_id/messages/:message_id - delete a message." - @spec delete_message(id(), id()) :: {:ok, nil} | {:error, Error.t()} - def delete_message(channel_id, message_id) do - request(:delete, "/channels/#{channel_id}/messages/#{message_id}") + maybe_take(stream, Keyword.get(opts, :limit)) end @doc """ - PUT /channels/:channel_id/messages/:message_id/reactions/:emoji/@me - react as - the bot. `emoji` is a unicode emoji or a `name:id` custom emoji; it is - URL-encoded for you. + Streams a guild's members ascending, paging with the `after:` anchor (cursor = + the last member's user id), 1000 per page. `limit: n` caps the total. + Lazy; raises `Dexcord.Api.Paginate.PageError` on a failed page. """ - @spec create_reaction(id(), id(), String.t()) :: {:ok, nil} | {:error, Error.t()} - def create_reaction(channel_id, message_id, emoji) do - encoded = URI.encode(emoji, &URI.char_unreserved?/1) - request(:put, "/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded}/@me") + @spec guild_members_stream(Dexcord.Guild.t() | Dexcord.Snowflake.t(), keyword()) :: + Enumerable.t() + def guild_members_stream(guild_or_id, opts \\ []) do + guild_id = resolve_guild_id(guild_or_id) + after0 = Keyword.get(opts, :after, 0) + + stream = + Dexcord.Api.Paginate.stream( + after0, + @members_page_size, + fn cursor -> + Dexcord.Api.Members.list_guild_members(guild_id, + limit: @members_page_size, + after: cursor + ) + end, + fn last -> member_user_id(last) end + ) + + maybe_take(stream, Keyword.get(opts, :limit)) end @doc """ - POST /channels/:channel_id/messages/:message_id/threads - start a thread from - an existing message. - - `opts` may carry `:auto_archive_duration` and `:rate_limit_per_user`; each is - omitted from the body when `nil`. + Streams a guild's bans ascending, paging with the `after:` anchor (cursor = + the last ban's user id), 1000 per page. `limit: n` caps the total. + Lazy; raises `Dexcord.Api.Paginate.PageError` on a failed page. """ - @spec start_thread_with_message(id(), id(), String.t(), keyword()) :: - {:ok, map()} | {:error, Error.t()} - def start_thread_with_message(channel_id, message_id, name, opts \\ []) when is_binary(name) do - body = - %{"name" => name} - |> maybe_put("auto_archive_duration", Keyword.get(opts, :auto_archive_duration)) - |> maybe_put("rate_limit_per_user", Keyword.get(opts, :rate_limit_per_user)) + @spec guild_bans_stream(Dexcord.Guild.t() | Dexcord.Snowflake.t(), keyword()) :: Enumerable.t() + def guild_bans_stream(guild_or_id, opts \\ []) do + guild_id = resolve_guild_id(guild_or_id) + after0 = Keyword.get(opts, :after, 0) - request(:post, "/channels/#{channel_id}/messages/#{message_id}/threads", body) + stream = + Dexcord.Api.Paginate.stream( + after0, + @bans_page_size, + fn cursor -> + Dexcord.Api.Members.get_guild_bans(guild_id, limit: @bans_page_size, after: cursor) + end, + fn last -> last.user.id end + ) + + maybe_take(stream, Keyword.get(opts, :limit)) end - @doc "POST /interactions/:interaction_id/:token/callback - respond to an interaction." - @spec create_interaction_response(id(), String.t(), map()) :: - {:ok, nil} | {:ok, map()} | {:error, Error.t()} - def create_interaction_response(interaction_id, token, response) do - request(:post, "/interactions/#{interaction_id}/#{token}/callback", response) + @doc """ + Streams a guild's audit-log entries descending, paging with the `before:` + anchor (cursor = the last entry's id), 100 per page. + + The `get_guild_audit_log` endpoint returns a `%Dexcord.AuditLog{}` container; + this stream yields the `audit_log_entries` out of it (the referenced + users/webhooks/etc. on the container are not threaded through). `limit: n` + caps the total. Lazy; raises `Dexcord.Api.Paginate.PageError` on a failed page. + """ + @spec audit_log_stream(Dexcord.Guild.t() | Dexcord.Snowflake.t(), keyword()) :: Enumerable.t() + def audit_log_stream(guild_or_id, opts \\ []) do + guild_id = resolve_guild_id(guild_or_id) + + stream = + Dexcord.Api.Paginate.stream( + nil, + @audit_log_page_size, + fn + nil -> + with {:ok, %Dexcord.AuditLog{} = al} <- + Dexcord.Api.Guilds.get_guild_audit_log(guild_id, limit: @audit_log_page_size), + do: {:ok, al.audit_log_entries} + + cursor -> + with {:ok, %Dexcord.AuditLog{} = al} <- + Dexcord.Api.Guilds.get_guild_audit_log(guild_id, + limit: @audit_log_page_size, + before: cursor + ), + do: {:ok, al.audit_log_entries} + end, + fn last -> last.id end + ) + + maybe_take(stream, Keyword.get(opts, :limit)) end - @doc "PATCH /webhooks/:application_id/:token/messages/@original - edit the original response." - @spec edit_original_interaction_response(id(), String.t(), map()) :: - {:ok, map()} | {:error, Error.t()} - def edit_original_interaction_response(application_id, token, body) do - request(:patch, "/webhooks/#{application_id}/#{token}/messages/@original", body) + defp maybe_take(stream, nil), do: stream + defp maybe_take(stream, limit) when is_integer(limit), do: Stream.take(stream, limit) + + defp resolve_guild_id(%{id: id}) when is_integer(id), do: id + defp resolve_guild_id(id) when is_integer(id), do: id + defp resolve_guild_id(other), do: Dexcord.Snowflake.cast!(other) + + defp member_user_id(%{user_id: user_id, user: user}), do: user_id || (user && user.id) + + # --- Generated endpoint facade ----------------------------------------- + # + # The typed endpoint surface lives in the group modules below (declared with + # the `Dexcord.Api.Endpoint` DSL). Each public endpoint head is re-exported + # here as a `defdelegate`, so `Dexcord.Api.` keeps working for every + # internal and external caller. + # + # The delegate set is derived from STATIC, compile-time-stable data — each + # group's `__endpoints__/0` route table — NOT from `module_info(:exports)`. + # `module_info/1` is an auto-generated function; the Elixir compiler's tracer + # does not record calls to it as compile-time dependencies, so reading a + # group's export table established no compile-order edge and raced under + # parallel/incremental compilation, silently dropping delegates (an incremental + # recompile could leave `Dexcord.Api.get_gateway_bot/0` undefined). `__endpoints__/0` + # is an ordinary remote call: it IS tracked, forcing every group fully compiled + # before this module, and its contents are deterministic. + # + # Each generated endpoint head ends in `opts \\ []`, so it exports at two + # arities — `base` and `base + 1`, where `base = path_param_count + (body? 1 : 0)`. + # That reproduces the old "default-arity heads are separate exports" behavior + # (e.g. both `create_message/2` and `create_message/3`) without introspection. + + @endpoint_groups [ + Dexcord.Api.Channels, + Dexcord.Api.Messages, + Dexcord.Api.Guilds, + Dexcord.Api.Members, + Dexcord.Api.Roles, + Dexcord.Api.Webhooks, + Dexcord.Api.Invites, + Dexcord.Api.Users, + Dexcord.Api.Interactions, + Dexcord.Api.Commands, + Dexcord.Api.EmojisStickers, + Dexcord.Api.Moderation, + Dexcord.Api.Misc + ] + + @doc false + def __endpoint_groups__, do: @endpoint_groups + + # Hand-written sugar heads whose arities are NOT reproduced by the + # `__endpoints__/0` derivation, listed explicitly as `{group, name, arity}`. + # `Dexcord.Api.Users.create_dm/1,2` is already covered: its spec lives in the + # `__endpoints__/0` table (body: true, 0 path params -> arities 1 and 2). Only + # `Dexcord.Api.Messages.get_channel_messages/3` (the Nostrum-compatible locator + # arity, a plain `def` with no `endpoint/4` spec) needs to be named here. + @facade_sugar [ + {Dexcord.Api.Messages, :get_channel_messages, 3} + ] + + @facade_exports ( + endpoint_exports = + for group <- @endpoint_groups, + spec <- group.__endpoints__(), + params = Enum.count(spec.segments, &match?({:param, _}, &1)), + base = params + if(spec.body, do: 1, else: 0), + arity <- [base, base + 1] do + {group, spec.name, arity} + end + + (endpoint_exports ++ @facade_sugar) + |> Enum.uniq_by(fn {_group, name, arity} -> {name, arity} end) + ) + + @doc false + # The statically-derived facade delegate set as a MapSet of `{name, arity}`. + # This is the single source of truth `api_facade_test.exs` asserts against, so + # a dropped delegate fails the guard deterministically rather than flakily. + def __facade_exports__ do + MapSet.new(for {_group, name, arity} <- @facade_exports, do: {name, arity}) end - @doc "POST /webhooks/:application_id/:token - send an interaction followup message." - @spec create_followup_message(id(), String.t(), map()) :: - {:ok, map()} | {:error, Error.t()} - def create_followup_message(application_id, token, body) do - request(:post, "/webhooks/#{application_id}/#{token}", body) + for {group, name, arity} <- @facade_exports do + args = Macro.generate_arguments(arity, __MODULE__) + defdelegate unquote(name)(unquote_splicing(args)), to: group end - - @doc "PUT /applications/:application_id/commands - bulk overwrite global commands." - @spec bulk_overwrite_global_commands(id(), [map()]) :: {:ok, map()} | {:error, Error.t()} - def bulk_overwrite_global_commands(application_id, commands) when is_list(commands) do - request(:put, "/applications/#{application_id}/commands", commands) - end - - @doc "PUT /applications/:application_id/guilds/:guild_id/commands - bulk overwrite guild commands." - @spec bulk_overwrite_guild_commands(id(), id(), [map()]) :: - {:ok, map()} | {:error, Error.t()} - def bulk_overwrite_guild_commands(application_id, guild_id, commands) when is_list(commands) do - request(:put, "/applications/#{application_id}/guilds/#{guild_id}/commands", commands) - end - - @typedoc "A Discord snowflake id, as a string or integer." - @type id :: String.t() | integer() end diff --git a/lib/dexcord/api/channels.ex b/lib/dexcord/api/channels.ex new file mode 100644 index 0000000..48a1fa0 --- /dev/null +++ b/lib/dexcord/api/channels.ex @@ -0,0 +1,104 @@ +defmodule Dexcord.Api.Channels do + @moduledoc """ + Channel endpoints, declared with the `Dexcord.Api.Endpoint` DSL. + + Every function here is also re-exported from `Dexcord.Api` as a delegate, so + callers may use either `Dexcord.Api.get_channel/1` or + `Dexcord.Api.Channels.get_channel/1`. + + `get_channel/1` returns the polymorphic `Dexcord.Channel` (the concrete + struct for the channel's `type`); `start_thread_with_message/4` returns the + created thread channel and honors an audit-log `:reason` opt. + """ + use Dexcord.Api.Endpoint + + endpoint :get_channel, :get, "/channels/:channel_id", returns: Dexcord.Channel + + endpoint :start_thread_with_message, + :post, + "/channels/:channel_id/messages/:message_id/threads", + body: true, + binary_wrap: :name, + reason: true, + returns: Dexcord.Channel + + endpoint :edit_channel, :patch, "/channels/:channel_id", + body: true, + reason: true, + returns: Dexcord.Channel + + endpoint :delete_channel, :delete, "/channels/:channel_id", + reason: true, + returns: Dexcord.Channel + + endpoint :set_voice_channel_status, :put, "/channels/:channel_id/voice-status", + body: true, + reason: true, + returns: nil + + endpoint :edit_channel_permissions, :put, "/channels/:channel_id/permissions/:overwrite_id", + body: true, + reason: true, + returns: nil + + endpoint :delete_channel_permission, :delete, "/channels/:channel_id/permissions/:overwrite_id", + reason: true, + returns: nil + + endpoint :get_channel_invites, :get, "/channels/:channel_id/invites", returns: [Dexcord.Invite] + + endpoint :create_channel_invite, :post, "/channels/:channel_id/invites", + body: true, + reason: true, + returns: Dexcord.Invite + + endpoint :follow_announcement_channel, :post, "/channels/:channel_id/followers", + body: true, + reason: true, + returns: Dexcord.FollowedChannel + + endpoint :trigger_typing, :post, "/channels/:channel_id/typing", returns: nil + + endpoint :group_dm_add_recipient, :put, "/channels/:channel_id/recipients/:user_id", + body: true, + returns: nil + + endpoint :group_dm_remove_recipient, :delete, "/channels/:channel_id/recipients/:user_id", + returns: nil + + endpoint :start_thread, :post, "/channels/:channel_id/threads", + body: true, + binary_wrap: :name, + files: true, + reason: true, + returns: Dexcord.Channel + + endpoint :join_thread, :put, "/channels/:channel_id/thread-members/@me", returns: nil + endpoint :add_thread_member, :put, "/channels/:channel_id/thread-members/:user_id", returns: nil + endpoint :leave_thread, :delete, "/channels/:channel_id/thread-members/@me", returns: nil + + endpoint :remove_thread_member, :delete, "/channels/:channel_id/thread-members/:user_id", + returns: nil + + endpoint :get_thread_member, :get, "/channels/:channel_id/thread-members/:user_id", + query: [:with_member], + returns: Dexcord.ThreadMember + + endpoint :list_thread_members, :get, "/channels/:channel_id/thread-members", + query: [:with_member, :after, :limit], + returns: [Dexcord.ThreadMember] + + endpoint :list_public_archived_threads, :get, "/channels/:channel_id/threads/archived/public", + query: [:before, :limit], + returns: :raw + + endpoint :list_private_archived_threads, :get, "/channels/:channel_id/threads/archived/private", + query: [:before, :limit], + returns: :raw + + endpoint :list_joined_private_archived_threads, + :get, + "/channels/:channel_id/users/@me/threads/archived/private", + query: [:before, :limit], + returns: :raw +end diff --git a/lib/dexcord/api/commands.ex b/lib/dexcord/api/commands.ex new file mode 100644 index 0000000..fe88f92 --- /dev/null +++ b/lib/dexcord/api/commands.ex @@ -0,0 +1,75 @@ +defmodule Dexcord.Api.Commands do + @moduledoc """ + Application (slash) command endpoints, declared with the + `Dexcord.Api.Endpoint` DSL. + + Both bulk-overwrite endpoints take a top-level list body (each element a + command definition map/struct) and return `[Dexcord.ApplicationCommand]`. + """ + use Dexcord.Api.Endpoint + + endpoint :bulk_overwrite_global_commands, + :put, + "/applications/:application_id/commands", + body: true, + returns: [Dexcord.ApplicationCommand] + + endpoint :bulk_overwrite_guild_commands, + :put, + "/applications/:application_id/guilds/:guild_id/commands", + body: true, + returns: [Dexcord.ApplicationCommand] + + endpoint :get_global_commands, :get, "/applications/:application_id/commands", + query: [:with_localizations], + returns: [Dexcord.ApplicationCommand] + + endpoint :create_global_command, :post, "/applications/:application_id/commands", + body: true, + returns: Dexcord.ApplicationCommand + + endpoint :get_global_command, :get, "/applications/:application_id/commands/:command_id", + returns: Dexcord.ApplicationCommand + + endpoint :edit_global_command, :patch, "/applications/:application_id/commands/:command_id", + body: true, + returns: Dexcord.ApplicationCommand + + endpoint :delete_global_command, :delete, "/applications/:application_id/commands/:command_id", + returns: nil + + endpoint :get_guild_commands, :get, "/applications/:application_id/guilds/:guild_id/commands", + query: [:with_localizations], + returns: [Dexcord.ApplicationCommand] + + endpoint :create_guild_command, + :post, + "/applications/:application_id/guilds/:guild_id/commands", + body: true, + returns: Dexcord.ApplicationCommand + + endpoint :get_guild_command, + :get, + "/applications/:application_id/guilds/:guild_id/commands/:command_id", + returns: Dexcord.ApplicationCommand + + endpoint :edit_guild_command, + :patch, + "/applications/:application_id/guilds/:guild_id/commands/:command_id", + body: true, + returns: Dexcord.ApplicationCommand + + endpoint :delete_guild_command, + :delete, + "/applications/:application_id/guilds/:guild_id/commands/:command_id", returns: nil + + endpoint :get_guild_command_permissions, + :get, + "/applications/:application_id/guilds/:guild_id/commands/permissions", + returns: [Dexcord.GuildApplicationCommandPermissions] + + endpoint :get_command_permissions, + :get, + "/applications/:application_id/guilds/:guild_id/commands/:command_id/permissions", + returns: Dexcord.GuildApplicationCommandPermissions +end diff --git a/lib/dexcord/api/coverage.ex b/lib/dexcord/api/coverage.ex new file mode 100644 index 0000000..5aadaf7 --- /dev/null +++ b/lib/dexcord/api/coverage.ex @@ -0,0 +1,86 @@ +defmodule Dexcord.Api.Coverage do + @moduledoc """ + Diffs the compiled endpoint route table against a pinned snapshot of + Discord's official OpenAPI spec (`priv/discord_openapi.json`). + + The spec is a coverage CHECKLIST, not codegen input: it is refreshed only + by the deliberate `mix dexcord.coverage.refresh`, so upstream churn never + breaks CI spontaneously. Docs win over spec on conflict; out-of-scope + routes live in `priv/coverage_allowlist.exs`. + """ + + @spec_path Path.join(:code.priv_dir(:dexcord), "discord_openapi.json") + @allowlist_path Path.join(:code.priv_dir(:dexcord), "coverage_allowlist.exs") + + @doc "Returns {:ok, %{declared: n, spec: n, allowlisted: n}} or {:missing, [route]}." + def check(declared \\ declared_routes()) do + spec = spec_routes() + allowlist = allowlist() + + missing = + spec + |> Enum.reject(&(&1 in declared)) + |> Enum.reject(&allowlisted?(&1, allowlist)) + |> Enum.sort() + + case missing do + [] -> + {:ok, %{declared: length(declared), spec: length(spec), allowlisted: length(allowlist)}} + + missing -> + {:missing, missing} + end + end + + @doc "All declared routes as normalized \"VERB /path/*\" strings." + def declared_routes do + for group <- Dexcord.Api.__endpoint_groups__(), + spec <- group.__endpoints__(), + uniq: true do + normalize(spec.method, Enum.map(spec.segments, &segment_to_string/1)) + end + end + + @doc "All spec routes as normalized strings." + def spec_routes do + %{"paths" => paths} = @spec_path |> File.read!() |> JSON.decode!() + + for {path, operations} <- paths, + {method, _op} <- operations, + method in ~w(get post put patch delete), + uniq: true do + segments = + path + |> String.split("/", trim: true) + |> Enum.map(fn + "{" <> _ -> "*" + literal -> literal + end) + + normalize(method, segments) + end + end + + defp segment_to_string({:param, _}), do: "*" + defp segment_to_string(literal), do: literal + + defp normalize(method, segments) do + String.upcase(to_string(method)) <> " /" <> Enum.join(segments, "/") + end + + defp allowlist do + {list, _} = Code.eval_file(@allowlist_path) + list + end + + # Entries are exact normalized routes or prefix patterns ending in "*". + defp allowlisted?(route, allowlist) do + Enum.any?(allowlist, fn pattern -> + if String.ends_with?(pattern, "*") do + String.starts_with?(route, String.trim_trailing(pattern, "*")) + else + route == pattern + end + end) + end +end diff --git a/lib/dexcord/api/emojis_stickers.ex b/lib/dexcord/api/emojis_stickers.ex new file mode 100644 index 0000000..ae5636a --- /dev/null +++ b/lib/dexcord/api/emojis_stickers.ex @@ -0,0 +1,69 @@ +defmodule Dexcord.Api.EmojisStickers do + @moduledoc """ + Emoji and sticker endpoints, declared with the `Dexcord.Api.Endpoint` DSL — + mirrors the Discord "Emoji" and "Sticker" resource pages. + + Emoji create/edit bodies carry base64 `image` data in plain JSON (no + multipart). `create_guild_sticker` is the exception: it uses plain form-field + multipart (`files: :form`) with `name`/`description`/`tags` parts plus a single + `file` part. `list_application_emojis` and the sticker-pack listings return + `{"items": [...]}` / pack envelopes with no model (`:raw`). + """ + use Dexcord.Api.Endpoint + + endpoint :list_guild_emojis, :get, "/guilds/:guild_id/emojis", returns: [Dexcord.Emoji] + endpoint :get_guild_emoji, :get, "/guilds/:guild_id/emojis/:emoji_id", returns: Dexcord.Emoji + + endpoint :create_guild_emoji, :post, "/guilds/:guild_id/emojis", + body: true, + reason: true, + returns: Dexcord.Emoji + + endpoint :edit_guild_emoji, :patch, "/guilds/:guild_id/emojis/:emoji_id", + body: true, + reason: true, + returns: Dexcord.Emoji + + endpoint :delete_guild_emoji, :delete, "/guilds/:guild_id/emojis/:emoji_id", + reason: true, + returns: nil + + endpoint :list_application_emojis, :get, "/applications/:application_id/emojis", returns: :raw + + endpoint :get_application_emoji, :get, "/applications/:application_id/emojis/:emoji_id", + returns: Dexcord.Emoji + + endpoint :create_application_emoji, :post, "/applications/:application_id/emojis", + body: true, + returns: Dexcord.Emoji + + endpoint :edit_application_emoji, :patch, "/applications/:application_id/emojis/:emoji_id", + body: true, + returns: Dexcord.Emoji + + endpoint :delete_application_emoji, :delete, "/applications/:application_id/emojis/:emoji_id", + returns: nil + + endpoint :get_sticker, :get, "/stickers/:sticker_id", returns: Dexcord.Sticker + endpoint :list_sticker_packs, :get, "/sticker-packs", returns: :raw + endpoint :get_sticker_pack, :get, "/sticker-packs/:pack_id", returns: :raw + endpoint :list_guild_stickers, :get, "/guilds/:guild_id/stickers", returns: [Dexcord.Sticker] + + endpoint :get_guild_sticker, :get, "/guilds/:guild_id/stickers/:sticker_id", + returns: Dexcord.Sticker + + endpoint :create_guild_sticker, :post, "/guilds/:guild_id/stickers", + body: true, + files: :form, + reason: true, + returns: Dexcord.Sticker + + endpoint :edit_guild_sticker, :patch, "/guilds/:guild_id/stickers/:sticker_id", + body: true, + reason: true, + returns: Dexcord.Sticker + + endpoint :delete_guild_sticker, :delete, "/guilds/:guild_id/stickers/:sticker_id", + reason: true, + returns: nil +end diff --git a/lib/dexcord/api/endpoint.ex b/lib/dexcord/api/endpoint.ex new file mode 100644 index 0000000..14f4b80 --- /dev/null +++ b/lib/dexcord/api/endpoint.ex @@ -0,0 +1,336 @@ +defmodule Dexcord.Api.Endpoint do + @moduledoc """ + Route-declaration DSL for the Discord REST surface (see `endpoint/4`) + plus the runtime engine behind every generated endpoint function. + + A group module `use`s this module and declares its routes with `endpoint/4`; + each declaration compiles to a spec map (the compile-time route table lives + at `__endpoints__/0`) and a documented function that funnels its arguments + through `run/4`. `run/4` builds the path, query string, headers, and body and + hands everything to `Dexcord.Api.request/4`, inheriting its ratelimit and + 429-retry guarantees rather than reimplementing them. + + ## Generated `@spec`s + + This layer deliberately does **not** emit per-endpoint `@spec`s. The `@doc` + string carries the route; there is no dialyzer in this repo (no `dialyxir` + dep), so specs on the ~180 generated heads would add macro complexity with no + verification leverage. + """ + + alias Dexcord.Snowflake + + @type snowflake_arg :: Dexcord.Snowflake.t() | String.t() | struct() + @type body_arg :: keyword() | map() | struct() | [map() | struct()] + + # --- macro --- + + @doc false + defmacro __using__(_opts) do + quote do + import Dexcord.Api.Endpoint, only: [endpoint: 3, endpoint: 4] + Module.register_attribute(__MODULE__, :dexcord_endpoints, accumulate: true) + @before_compile Dexcord.Api.Endpoint + end + end + + @doc false + defmacro __before_compile__(env) do + endpoints = + env.module |> Module.get_attribute(:dexcord_endpoints) |> Enum.reverse() + + quote do + @doc false + def __endpoints__, do: unquote(Macro.escape(endpoints)) + end + end + + @doc """ + Declares a REST endpoint, generating a documented function plus a spec entry + in the module's `__endpoints__/0` route table. + + `name` is the generated function name, `method` the HTTP verb atom, and `path` + the route with `:param` placeholders (e.g. `"/channels/:channel_id/messages"`). + Each placeholder becomes a leading positional argument; `body: true` adds a + `body` argument before the trailing `opts` keyword list. + + ## Options + + * `:query` - allowed query-parameter keys (atoms), read from `opts` + * `:body` - `true` if the endpoint takes a body argument + * `:binary_wrap` - key (atom) a binary body is wrapped under + * `:files` - `true` if `opts[:files]` triggers multipart encoding + * `:reason` - `true` if `opts[:reason]` sends `X-Audit-Log-Reason` + * `:returns` - `module | [module] | nil | :raw` decode target (default `:raw`) + * `:docs` - a Discord docs URL appended to the generated `@doc` + """ + defmacro endpoint(name, method, path, opts \\ []) do + segments = parse_path(path) + params = for {:param, p} <- segments, do: p + returns_ast = Keyword.get(opts, :returns, :raw) + + spec = %{ + name: name, + method: method, + path: path, + segments: segments, + query: Keyword.get(opts, :query, []), + body: Keyword.get(opts, :body, false), + binary_wrap: Keyword.get(opts, :binary_wrap), + files: Keyword.get(opts, :files, false), + reason: Keyword.get(opts, :reason, false) + } + + param_vars = Enum.map(params, &Macro.var(&1, __MODULE__)) + + doc_string = + "`#{method |> to_string() |> String.upcase()} #{path}`" <> + case Keyword.get(opts, :docs) do + nil -> "" + url -> "\n\nDiscord docs: #{url}" + end + + if spec.body do + quote do + @dexcord_endpoints Map.put( + unquote(Macro.escape(spec)), + :returns, + unquote(returns_ast) + ) + @doc unquote(doc_string) + def unquote(name)(unquote_splicing(param_vars), body, opts \\ []) do + Dexcord.Api.Endpoint.run( + Map.put(unquote(Macro.escape(spec)), :returns, unquote(returns_ast)), + [unquote_splicing(param_vars)], + body, + opts + ) + end + end + else + quote do + @dexcord_endpoints Map.put( + unquote(Macro.escape(spec)), + :returns, + unquote(returns_ast) + ) + @doc unquote(doc_string) + def unquote(name)(unquote_splicing(param_vars), opts \\ []) do + Dexcord.Api.Endpoint.run( + Map.put(unquote(Macro.escape(spec)), :returns, unquote(returns_ast)), + [unquote_splicing(param_vars)], + nil, + opts + ) + end + end + end + end + + @doc false + def parse_path(path) do + path + |> String.split("/", trim: true) + |> Enum.map(fn + ":" <> param -> {:param, String.to_atom(param)} + literal -> literal + end) + end + + # --- runtime --- + + @doc false + def run(spec, path_args, body, opts) do + with {:ok, path} <- build_path(spec, path_args) do + full_path = path <> build_query(spec.query, opts) + encoded_body = prepare_body(spec, body, opts) + req_opts = reason_opts(spec, opts) + + Dexcord.Api.request(spec.method, full_path, encoded_body, req_opts) + |> decode_response(spec.returns) + end + end + + # --- path --- + + @doc false + def build_path(spec, path_args) do + params = for {:param, p} <- spec.segments, do: p + + values = + Enum.zip(params, path_args) + |> Map.new(fn {p, arg} -> {p, encode_segment(p, arg)} end) + + case Enum.find(values, fn {_p, v} -> v == :error end) do + {p, :error} -> + {:error, + %Dexcord.Api.Error{ + status: nil, + code: nil, + message: "invalid value for path parameter :#{p}", + errors: nil + }} + + nil -> + segments = + Enum.map(spec.segments, fn + {:param, p} -> values[p] + literal -> literal + end) + + {:ok, "/" <> Enum.join(segments, "/")} + end + end + + # *_id params take snowflake-ish args; everything else is a URL-encoded string. + defp encode_segment(param, arg) do + if String.ends_with?(Atom.to_string(param), "_id") do + case Snowflake.cast(arg) do + {:ok, int} -> Integer.to_string(int) + :error -> :error + end + else + case arg do + arg when is_binary(arg) -> URI.encode(arg, &URI.char_unreserved?/1) + arg when is_integer(arg) -> Integer.to_string(arg) + _ -> :error + end + end + end + + # --- query --- + + @doc false + def build_query(allowed, opts) do + pairs = + for key <- allowed, + {:ok, value} <- [Keyword.fetch(opts, key)], + value != nil, + do: {key, query_value(value)} + + case pairs do + [] -> "" + pairs -> "?" <> URI.encode_query(pairs) + end + end + + defp query_value(%{id: id}) when is_integer(id), do: Integer.to_string(id) + defp query_value(true), do: "true" + defp query_value(false), do: "false" + defp query_value(v) when is_integer(v), do: Integer.to_string(v) + defp query_value(v) when is_binary(v), do: v + defp query_value(v), do: to_string(v) + + # --- body --- + + @doc false + def prepare_body(%{body: false}, _body, _opts), do: nil + + def prepare_body(spec, body, opts) do + payload = encode_body(body, spec) + + case {spec.files, Keyword.get(opts, :files)} do + {true, [_ | _] = files} -> + normalized = Enum.map(files, &normalize_file/1) + + {:multipart, attach_ids(payload, normalized), + Enum.map(normalized, &Map.delete(&1, :description))} + + # Plain form-field multipart (sticker create): exactly one file, and every + # payload value goes on the wire as a bare string form part. + {:form, [file]} -> + {:form_multipart, stringify_values(payload), + Map.delete(normalize_file(file), :description)} + + _ -> + payload + end + end + + # Form-field parts carry bare scalars, so non-binary values are stringified. + defp stringify_values(payload) when is_map(payload), + do: Map.new(payload, fn {k, v} -> {k, to_string(v)} end) + + # A nil payload is legitimate (payload || %{} semantics); any other non-map + # is a caller error and must raise rather than silently drop the form fields. + defp stringify_values(nil), do: %{} + + @doc false + def encode_body(nil, _spec), do: nil + + def encode_body(body, %{binary_wrap: key}) when is_binary(body) and key != nil, + do: %{Atom.to_string(key) => body} + + def encode_body(body, _spec) when is_list(body) do + if Keyword.keyword?(body) do + # Keyword contract (AC2.9): absent key = omitted; explicit nil = JSON null. + Map.new(body, fn {k, v} -> {Atom.to_string(k), encode_body_value(v)} end) + else + # top-level array bodies (e.g. bulk overwrite) + Enum.map(body, &encode_body(&1, %{binary_wrap: nil})) + end + end + + def encode_body(%_{} = struct, _spec), do: struct.__struct__.to_map(struct) + def encode_body(body, _spec) when is_map(body), do: body + + defp encode_body_value(%DateTime{} = dt), do: DateTime.to_iso8601(dt) + defp encode_body_value(%_{} = struct), do: struct.__struct__.to_map(struct) + defp encode_body_value(list) when is_list(list), do: Enum.map(list, &encode_body_value/1) + defp encode_body_value(v), do: v + + defp normalize_file(%{filename: f, data: d} = file) when is_binary(f) and is_binary(d) do + %{ + filename: f, + data: d, + content_type: Map.get(file, :content_type, "application/octet-stream"), + description: Map.get(file, :description) + } + end + + # payload_json's attachments array references files[n] by id == n. + defp attach_ids(payload, files) do + declared = + files + |> Enum.with_index() + |> Enum.map(fn {file, n} -> + %{"id" => n, "filename" => file.filename} + |> then(fn m -> + if file.description, do: Map.put(m, "description", file.description), else: m + end) + end) + + Map.update(payload || %{}, "attachments", declared, fn existing -> existing ++ declared end) + end + + # --- reason header --- + + defp reason_opts(%{reason: true}, opts) do + case Keyword.get(opts, :reason) do + nil -> [] + reason -> [audit_log_reason: URI.encode(reason, &URI.char_unreserved?/1)] + end + end + + defp reason_opts(_spec, _opts), do: [] + + # --- response decode --- + + @doc false + def decode_response({:error, _} = err, _returns), do: err + def decode_response({:ok, nil}, _returns), do: {:ok, nil} + def decode_response({:ok, {:raw, _}} = raw, _returns), do: raw + def decode_response(ok, :raw), do: ok + # `returns: nil` endpoints normalize to `{:ok, nil}` even when the server sends + # a non-204 JSON body (otherwise the raw map would leak through the passthrough + # clause, since the atom-mod clause's `mod != nil` guard rightly skips nil). + def decode_response({:ok, _}, nil), do: {:ok, nil} + + def decode_response({:ok, list}, [mod]) when is_list(list), + do: {:ok, Enum.map(list, &mod.from_map/1)} + + def decode_response({:ok, map}, mod) when is_atom(mod) and mod != nil and is_map(map), + do: {:ok, mod.from_map(map)} + + def decode_response(ok, _returns), do: ok +end diff --git a/lib/dexcord/api/guilds.ex b/lib/dexcord/api/guilds.ex new file mode 100644 index 0000000..b4160a1 --- /dev/null +++ b/lib/dexcord/api/guilds.ex @@ -0,0 +1,96 @@ +defmodule Dexcord.Api.Guilds do + @moduledoc """ + Guild endpoints, declared with the `Dexcord.Api.Endpoint` DSL — mirrors the + Discord "Guild" resource page (plus the guild widget / welcome-screen / + onboarding / prune / audit-log sub-resources). + + `get_guild/1,2` moved here from `Dexcord.Api.Misc`; the `Dexcord.Api` facade + delegate follows the group automatically. `get_guild_preview/1` decodes into + `Dexcord.Guild` (a lenient subset shape). Shapes with no model + (`get_guild_widget_image`, `get_guild_vanity_url`, prune counts, incident + actions, active-thread listings) stay `:raw`. + """ + use Dexcord.Api.Endpoint + + endpoint :get_guild, :get, "/guilds/:guild_id", query: [:with_counts], returns: Dexcord.Guild + endpoint :get_guild_preview, :get, "/guilds/:guild_id/preview", returns: Dexcord.Guild + + endpoint :edit_guild, :patch, "/guilds/:guild_id", + body: true, + reason: true, + returns: Dexcord.Guild + + endpoint :get_guild_channels, :get, "/guilds/:guild_id/channels", returns: [Dexcord.Channel] + + endpoint :create_guild_channel, :post, "/guilds/:guild_id/channels", + body: true, + binary_wrap: :name, + reason: true, + returns: Dexcord.Channel + + endpoint :edit_guild_channel_positions, :patch, "/guilds/:guild_id/channels", + body: true, + returns: nil + + endpoint :list_active_guild_threads, :get, "/guilds/:guild_id/threads/active", returns: :raw + endpoint :get_guild_invites, :get, "/guilds/:guild_id/invites", returns: [Dexcord.Invite] + + endpoint :get_guild_integrations, :get, "/guilds/:guild_id/integrations", + returns: [Dexcord.Integration] + + endpoint :delete_guild_integration, :delete, "/guilds/:guild_id/integrations/:integration_id", + reason: true, + returns: nil + + endpoint :get_guild_widget_settings, :get, "/guilds/:guild_id/widget", + returns: Dexcord.GuildWidgetSettings + + endpoint :edit_guild_widget, :patch, "/guilds/:guild_id/widget", + body: true, + reason: true, + returns: Dexcord.GuildWidgetSettings + + endpoint :get_guild_widget, :get, "/guilds/:guild_id/widget.json", returns: Dexcord.GuildWidget + + endpoint :get_guild_widget_image, :get, "/guilds/:guild_id/widget.png", + query: [:style], + returns: :raw + + endpoint :get_guild_vanity_url, :get, "/guilds/:guild_id/vanity-url", returns: :raw + + endpoint :get_guild_welcome_screen, :get, "/guilds/:guild_id/welcome-screen", + returns: Dexcord.WelcomeScreen + + endpoint :edit_guild_welcome_screen, :patch, "/guilds/:guild_id/welcome-screen", + body: true, + reason: true, + returns: Dexcord.WelcomeScreen + + endpoint :get_guild_onboarding, :get, "/guilds/:guild_id/onboarding", + returns: Dexcord.GuildOnboarding + + endpoint :edit_guild_onboarding, :put, "/guilds/:guild_id/onboarding", + body: true, + reason: true, + returns: Dexcord.GuildOnboarding + + endpoint :get_guild_prune_count, :get, "/guilds/:guild_id/prune", + query: [:days, :include_roles], + returns: :raw + + endpoint :begin_guild_prune, :post, "/guilds/:guild_id/prune", + body: true, + reason: true, + returns: :raw + + endpoint :get_guild_voice_regions, :get, "/guilds/:guild_id/regions", + returns: [Dexcord.VoiceRegion] + + endpoint :edit_guild_incident_actions, :put, "/guilds/:guild_id/incident-actions", + body: true, + returns: :raw + + endpoint :get_guild_audit_log, :get, "/guilds/:guild_id/audit-logs", + query: [:user_id, :action_type, :before, :after, :limit], + returns: Dexcord.AuditLog +end diff --git a/lib/dexcord/api/interactions.ex b/lib/dexcord/api/interactions.ex new file mode 100644 index 0000000..87034da --- /dev/null +++ b/lib/dexcord/api/interactions.ex @@ -0,0 +1,56 @@ +defmodule Dexcord.Api.Interactions do + @moduledoc """ + Interaction response endpoints, declared with the `Dexcord.Api.Endpoint` DSL. + + `create_interaction_response/3` stays `:raw` — a `204` becomes `{:ok, nil}` + naturally, and `query: [:with_response]` lets the caller opt into the richer + callback-resource body. The followup/edit endpoints return a `Dexcord.Message`. + + These routes are token-authed (`:token` is the interaction/application token); + each collapses to its own digested ratelimit bucket, so they are isolated from + the global bot rate limit (see `Dexcord.Api.Ratelimit.route_key/2`). + """ + use Dexcord.Api.Endpoint + + endpoint :create_interaction_response, + :post, + "/interactions/:interaction_id/:token/callback", + body: true, + files: true, + query: [:with_response], + returns: :raw + + endpoint :edit_original_interaction_response, + :patch, + "/webhooks/:application_id/:token/messages/@original", + body: true, + files: true, + returns: Dexcord.Message + + endpoint :create_followup_message, :post, "/webhooks/:application_id/:token", + body: true, + files: true, + returns: Dexcord.Message + + endpoint :get_original_interaction_response, + :get, + "/webhooks/:application_id/:token/messages/@original", returns: Dexcord.Message + + endpoint :delete_original_interaction_response, + :delete, + "/webhooks/:application_id/:token/messages/@original", returns: nil + + endpoint :get_followup_message, :get, "/webhooks/:application_id/:token/messages/:message_id", + returns: Dexcord.Message + + endpoint :edit_followup_message, + :patch, + "/webhooks/:application_id/:token/messages/:message_id", + body: true, + files: true, + returns: Dexcord.Message + + endpoint :delete_followup_message, + :delete, + "/webhooks/:application_id/:token/messages/:message_id", returns: nil +end diff --git a/lib/dexcord/api/invites.ex b/lib/dexcord/api/invites.ex new file mode 100644 index 0000000..3b5df31 --- /dev/null +++ b/lib/dexcord/api/invites.ex @@ -0,0 +1,17 @@ +defmodule Dexcord.Api.Invites do + @moduledoc """ + Invite endpoints, declared with the `Dexcord.Api.Endpoint` DSL — mirrors the + Discord "Invite" resource page. + + Invite codes are non-numeric path params; the ratelimit layer collapses them + to a bounded placeholder so codes never land in ETS + (see `Dexcord.Api.Ratelimit.route_key/2`). + """ + use Dexcord.Api.Endpoint + + endpoint :get_invite, :get, "/invites/:invite_code", + query: [:with_counts, :guild_scheduled_event_id], + returns: Dexcord.Invite + + endpoint :delete_invite, :delete, "/invites/:invite_code", reason: true, returns: Dexcord.Invite +end diff --git a/lib/dexcord/api/members.ex b/lib/dexcord/api/members.ex new file mode 100644 index 0000000..f63eb42 --- /dev/null +++ b/lib/dexcord/api/members.ex @@ -0,0 +1,81 @@ +defmodule Dexcord.Api.Members do + @moduledoc """ + Guild member, ban, and voice-state endpoints, declared with the + `Dexcord.Api.Endpoint` DSL — mirrors the member/ban/voice sections of the + Discord "Guild" resource page. + + `bulk_guild_ban` returns the `{banned_users, failed_users}` envelope with no + model (`:raw`). The `edit_*_voice_state` endpoints return `nil` (204). + """ + use Dexcord.Api.Endpoint + + endpoint :get_guild_member, :get, "/guilds/:guild_id/members/:user_id", returns: Dexcord.Member + + endpoint :list_guild_members, :get, "/guilds/:guild_id/members", + query: [:limit, :after], + returns: [Dexcord.Member] + + endpoint :search_guild_members, :get, "/guilds/:guild_id/members/search", + query: [:query, :limit], + returns: [Dexcord.Member] + + endpoint :add_guild_member, :put, "/guilds/:guild_id/members/:user_id", + body: true, + returns: Dexcord.Member + + endpoint :edit_guild_member, :patch, "/guilds/:guild_id/members/:user_id", + body: true, + reason: true, + returns: Dexcord.Member + + endpoint :edit_current_member, :patch, "/guilds/:guild_id/members/@me", + body: true, + reason: true, + returns: Dexcord.Member + + endpoint :add_guild_member_role, :put, "/guilds/:guild_id/members/:user_id/roles/:role_id", + reason: true, + returns: nil + + endpoint :remove_guild_member_role, + :delete, + "/guilds/:guild_id/members/:user_id/roles/:role_id", reason: true, returns: nil + + endpoint :remove_guild_member, :delete, "/guilds/:guild_id/members/:user_id", + reason: true, + returns: nil + + endpoint :get_guild_bans, :get, "/guilds/:guild_id/bans", + query: [:limit, :before, :after], + returns: [Dexcord.Ban] + + endpoint :get_guild_ban, :get, "/guilds/:guild_id/bans/:user_id", returns: Dexcord.Ban + + endpoint :create_guild_ban, :put, "/guilds/:guild_id/bans/:user_id", + body: true, + reason: true, + returns: nil + + endpoint :remove_guild_ban, :delete, "/guilds/:guild_id/bans/:user_id", + reason: true, + returns: nil + + endpoint :bulk_guild_ban, :post, "/guilds/:guild_id/bulk-ban", + body: true, + reason: true, + returns: :raw + + endpoint :get_current_user_voice_state, :get, "/guilds/:guild_id/voice-states/@me", + returns: Dexcord.VoiceState + + endpoint :get_user_voice_state, :get, "/guilds/:guild_id/voice-states/:user_id", + returns: Dexcord.VoiceState + + endpoint :edit_current_user_voice_state, :patch, "/guilds/:guild_id/voice-states/@me", + body: true, + returns: nil + + endpoint :edit_user_voice_state, :patch, "/guilds/:guild_id/voice-states/:user_id", + body: true, + returns: nil +end diff --git a/lib/dexcord/api/messages.ex b/lib/dexcord/api/messages.ex new file mode 100644 index 0000000..732b46a --- /dev/null +++ b/lib/dexcord/api/messages.ex @@ -0,0 +1,142 @@ +defmodule Dexcord.Api.Messages do + @moduledoc """ + Message + reaction endpoints, declared with the `Dexcord.Api.Endpoint` DSL. + + `create_message/2,3` and `edit_message/3,4` accept a binary (wrapped as + `content`), a keyword/map body, and `files:` for attachments; both return a + `Dexcord.Message`. `get_channel_messages/2` reads `:limit` plus any of + `:before` / `:after` / `:around` from its opts and encodes them into the query + string. + + ## Anchor precedence + + Unlike the pre-DSL surface (which collapsed multiple anchors with a fixed + `before` > `after` > `around` precedence), `get_channel_messages/2` now sends + every anchor it is given. Discord expects exactly one; pass exactly one. The + Nostrum-compatible `get_channel_messages/3` locator arity enforces this by + construction. + """ + use Dexcord.Api.Endpoint + + endpoint :get_channel_messages, :get, "/channels/:channel_id/messages", + query: [:limit, :before, :after, :around], + returns: [Dexcord.Message] + + endpoint :create_message, :post, "/channels/:channel_id/messages", + body: true, + binary_wrap: :content, + files: true, + returns: Dexcord.Message + + endpoint :edit_message, :patch, "/channels/:channel_id/messages/:message_id", + body: true, + binary_wrap: :content, + files: true, + returns: Dexcord.Message + + endpoint :delete_message, :delete, "/channels/:channel_id/messages/:message_id", + reason: true, + returns: nil + + endpoint :create_reaction, + :put, + "/channels/:channel_id/messages/:message_id/reactions/:emoji/@me", + returns: nil + + endpoint :get_channel_message, :get, "/channels/:channel_id/messages/:message_id", + returns: Dexcord.Message + + endpoint :crosspost_message, :post, "/channels/:channel_id/messages/:message_id/crosspost", + returns: Dexcord.Message + + endpoint :delete_own_reaction, + :delete, + "/channels/:channel_id/messages/:message_id/reactions/:emoji/@me", returns: nil + + endpoint :delete_user_reaction, + :delete, + "/channels/:channel_id/messages/:message_id/reactions/:emoji/:user_id", returns: nil + + endpoint :get_reactions, :get, "/channels/:channel_id/messages/:message_id/reactions/:emoji", + query: [:type, :after, :limit], + returns: [Dexcord.User] + + endpoint :delete_all_reactions, :delete, "/channels/:channel_id/messages/:message_id/reactions", + returns: nil + + endpoint :delete_all_reactions_for_emoji, + :delete, + "/channels/:channel_id/messages/:message_id/reactions/:emoji", returns: nil + + endpoint :bulk_delete_messages, :post, "/channels/:channel_id/messages/bulk-delete", + body: true, + reason: true, + returns: nil + + endpoint :get_channel_pins, :get, "/channels/:channel_id/messages/pins", + query: [:before, :limit], + returns: :raw + + endpoint :pin_message, :put, "/channels/:channel_id/messages/pins/:message_id", + reason: true, + returns: nil + + endpoint :unpin_message, :delete, "/channels/:channel_id/messages/pins/:message_id", + reason: true, + returns: nil + + endpoint :search_guild_messages, :get, "/guilds/:guild_id/messages/search", + query: [ + :limit, + :offset, + :max_id, + :min_id, + :slop, + :content, + :channel_id, + :author_type, + :author_id, + :mentions, + :mentions_role_id, + :mention_everyone, + :replied_to_user_id, + :replied_to_message_id, + :pinned, + :has, + :embed_type, + :embed_provider, + :link_hostname, + :attachment_filename, + :attachment_extension, + :sort_by, + :sort_order, + :include_nsfw + ], + returns: :raw + + endpoint :get_answer_voters, :get, "/channels/:channel_id/polls/:message_id/answers/:answer_id", + query: [:after, :limit], + returns: :raw + + endpoint :end_poll, :post, "/channels/:channel_id/polls/:message_id/expire", + returns: Dexcord.Message + + @doc """ + Nostrum-compatible locator arity for `get_channel_messages/2`. + + `locator` is `{:before, id}` / `{:after, id}` / `{:around, id}` or `{}` for no + anchor; it is normalized onto the keyword form. + """ + def get_channel_messages(channel_id, limit, locator) when is_integer(limit) do + opts = + case locator do + {} -> + [limit: limit] + + {anchor, id} when anchor in [:before, :after, :around] -> + [{:limit, limit}, {anchor, id}] + end + + get_channel_messages(channel_id, opts) + end +end diff --git a/lib/dexcord/api/misc.ex b/lib/dexcord/api/misc.ex new file mode 100644 index 0000000..4dbd324 --- /dev/null +++ b/lib/dexcord/api/misc.ex @@ -0,0 +1,31 @@ +defmodule Dexcord.Api.Misc do + @moduledoc """ + Gateway / application-info endpoints, declared with the + `Dexcord.Api.Endpoint` DSL. + + `get_gateway/0` and `get_gateway_bot/0` stay `:raw` — their + `url` / `shards` / `session_start_limit` shapes have no model and the gateway + reads them as raw maps. + + `get_current_application/0` hits the docs-canonical `GET /applications/@me`; + the older `GET /oauth2/applications/@me` route is preserved as + `get_current_bot_application/0`. Both return a `Dexcord.ApplicationInfo`. + + `get_guild` moved to `Dexcord.Api.Guilds` — the `Dexcord.Api` facade delegate + follows the group automatically. + """ + use Dexcord.Api.Endpoint + + endpoint :get_gateway, :get, "/gateway", returns: :raw + endpoint :get_gateway_bot, :get, "/gateway/bot", returns: :raw + endpoint :get_current_application, :get, "/applications/@me", returns: Dexcord.ApplicationInfo + + endpoint :edit_current_application, :patch, "/applications/@me", + body: true, + returns: Dexcord.ApplicationInfo + + endpoint :get_current_bot_application, :get, "/oauth2/applications/@me", + returns: Dexcord.ApplicationInfo + + endpoint :list_voice_regions, :get, "/voice/regions", returns: [Dexcord.VoiceRegion] +end diff --git a/lib/dexcord/api/moderation.ex b/lib/dexcord/api/moderation.ex new file mode 100644 index 0000000..d719287 --- /dev/null +++ b/lib/dexcord/api/moderation.ex @@ -0,0 +1,69 @@ +defmodule Dexcord.Api.Moderation do + @moduledoc """ + Auto-moderation and guild-scheduled-event endpoints, declared with the + `Dexcord.Api.Endpoint` DSL — mirrors the Discord "Auto Moderation" and + "Guild Scheduled Event" resource pages. + + `delete_guild_scheduled_event` deliberately has NO `reason:` support (the docs + are asymmetric — create/edit take a reason, delete does not). + """ + use Dexcord.Api.Endpoint + + endpoint :list_auto_moderation_rules, :get, "/guilds/:guild_id/auto-moderation/rules", + returns: [Dexcord.AutoModerationRule] + + endpoint :get_auto_moderation_rule, + :get, + "/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id", + returns: Dexcord.AutoModerationRule + + endpoint :create_auto_moderation_rule, :post, "/guilds/:guild_id/auto-moderation/rules", + body: true, + reason: true, + returns: Dexcord.AutoModerationRule + + endpoint :edit_auto_moderation_rule, + :patch, + "/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id", + body: true, + reason: true, + returns: Dexcord.AutoModerationRule + + endpoint :delete_auto_moderation_rule, + :delete, + "/guilds/:guild_id/auto-moderation/rules/:auto_moderation_rule_id", + reason: true, + returns: nil + + endpoint :list_guild_scheduled_events, :get, "/guilds/:guild_id/scheduled-events", + query: [:with_user_count], + returns: [Dexcord.GuildScheduledEvent] + + endpoint :create_guild_scheduled_event, :post, "/guilds/:guild_id/scheduled-events", + body: true, + reason: true, + returns: Dexcord.GuildScheduledEvent + + endpoint :get_guild_scheduled_event, + :get, + "/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id", + query: [:with_user_count], + returns: Dexcord.GuildScheduledEvent + + endpoint :edit_guild_scheduled_event, + :patch, + "/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id", + body: true, + reason: true, + returns: Dexcord.GuildScheduledEvent + + endpoint :delete_guild_scheduled_event, + :delete, + "/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id", returns: nil + + endpoint :get_guild_scheduled_event_users, + :get, + "/guilds/:guild_id/scheduled-events/:guild_scheduled_event_id/users", + query: [:limit, :with_member, :before, :after], + returns: [Dexcord.GuildScheduledEventUser] +end diff --git a/lib/dexcord/api/paginate.ex b/lib/dexcord/api/paginate.ex new file mode 100644 index 0000000..29f28c6 --- /dev/null +++ b/lib/dexcord/api/paginate.ex @@ -0,0 +1,54 @@ +defmodule Dexcord.Api.Paginate do + @moduledoc false + # Lazy cursor pagination over Discord's list endpoints. Each page is fetched + # only when the stream is consumed that far (so `Stream.take/2` on a fresh + # stream makes exactly one wire hit). A page shorter than the page size ends + # the stream; an `{:error, _}` from the fetcher raises `PageError` — a stream + # cannot carry a tagged-tuple failure mid-flow, so the error surfaces as a + # raise the caller can `rescue`. + + defmodule PageError do + @moduledoc """ + Raised when a page fetch inside a pagination stream returns + `{:error, _}`. The underlying error is on the `:error` field. + """ + defexception [:error] + + @impl true + def message(%{error: e}), do: "pagination request failed: #{inspect(e)}" + end + + @doc false + # `fetch_page.(cursor)` -> `{:ok, items}` | `{:error, e}`; + # `next.(last_item)` -> the cursor for the following page. + # + # `initial_cursor` seeds the first fetch (may be `nil` for an anchorless first + # page). The stream halts on the first page whose length is < `page_size`. + @spec stream(term(), pos_integer(), (term() -> {:ok, list()} | {:error, term()}), (term() -> + term())) :: + Enumerable.t() + def stream(initial_cursor, page_size, fetch_page, next) + when is_integer(page_size) and page_size > 0 and is_function(fetch_page, 1) and + is_function(next, 1) do + Stream.resource( + fn -> {initial_cursor, :go} end, + fn + {_cursor, :halt} -> + {:halt, nil} + + {cursor, :go} -> + case fetch_page.(cursor) do + {:ok, items} when length(items) < page_size -> + {items, {cursor, :halt}} + + {:ok, items} -> + {items, {next.(List.last(items)), :go}} + + {:error, error} -> + raise PageError, error: error + end + end, + fn _ -> :ok end + ) + end +end diff --git a/lib/dexcord/api/ratelimit.ex b/lib/dexcord/api/ratelimit.ex index a5a0877..6109b7b 100644 --- a/lib/dexcord/api/ratelimit.ex +++ b/lib/dexcord/api/ratelimit.ex @@ -108,6 +108,9 @@ defmodule Dexcord.Api.Ratelimit do template = "/" <> Enum.join(templatize(segments), "/") base = verb <> " " <> template + # As of the 2026-07 docs the separate message-delete bucket is no longer + # documented, but this suffix only splits buckets more finely (safe) - kept + # to avoid 429s should the server-side behavior still exist. if method == :delete and String.ends_with?(template, "/messages/:id") do base <> " [message-delete]" else @@ -138,6 +141,11 @@ defmodule Dexcord.Api.Ratelimit do defp templatize(["reactions", _emoji | rest]), do: ["reactions", ":id" | templatize(rest)] + # invite codes are non-numeric, so they'd otherwise stay literal - collapse + # them to keep bucket keys bounded and keep invite codes out of ETS. + defp templatize(["invites", _code | rest]), + do: ["invites", ":code" | templatize(rest)] + defp templatize([seg | rest]), do: [templatize_one(seg) | templatize(rest)] defp templatize([]), do: [] diff --git a/lib/dexcord/api/roles.ex b/lib/dexcord/api/roles.ex new file mode 100644 index 0000000..7581e6d --- /dev/null +++ b/lib/dexcord/api/roles.ex @@ -0,0 +1,37 @@ +defmodule Dexcord.Api.Roles do + @moduledoc """ + Guild role endpoints, declared with the `Dexcord.Api.Endpoint` DSL — mirrors + the role section of the Discord "Guild" resource page. + + `get_guild_role_member_counts` returns a `{role_id => count}` map with no + model (`:raw`). `edit_guild_role_positions` takes a top-level list body and + returns the full `[Dexcord.Role]` list. + """ + use Dexcord.Api.Endpoint + + endpoint :get_guild_roles, :get, "/guilds/:guild_id/roles", returns: [Dexcord.Role] + endpoint :get_guild_role, :get, "/guilds/:guild_id/roles/:role_id", returns: Dexcord.Role + + endpoint :get_guild_role_member_counts, :get, "/guilds/:guild_id/roles/member-counts", + returns: :raw + + endpoint :create_guild_role, :post, "/guilds/:guild_id/roles", + body: true, + binary_wrap: :name, + reason: true, + returns: Dexcord.Role + + endpoint :edit_guild_role_positions, :patch, "/guilds/:guild_id/roles", + body: true, + reason: true, + returns: [Dexcord.Role] + + endpoint :edit_guild_role, :patch, "/guilds/:guild_id/roles/:role_id", + body: true, + reason: true, + returns: Dexcord.Role + + endpoint :delete_guild_role, :delete, "/guilds/:guild_id/roles/:role_id", + reason: true, + returns: nil +end diff --git a/lib/dexcord/api/users.ex b/lib/dexcord/api/users.ex new file mode 100644 index 0000000..99a32df --- /dev/null +++ b/lib/dexcord/api/users.ex @@ -0,0 +1,76 @@ +defmodule Dexcord.Api.Users do + @moduledoc """ + User + DM endpoints, declared with the `Dexcord.Api.Endpoint` DSL. + + `create_dm/1` keeps its historical shape: given a snowflake-ish `user_id` it + opens (or fetches) a DM channel. It is registered by hand rather than via + `endpoint/4` because the generated `create_dm(body, opts \\ [])` head would + collide with the snowflake sugar's `create_dm/1`; the manual spec still lands + in the `__endpoints__/0` route table so coverage tooling sees it. + """ + use Dexcord.Api.Endpoint + + endpoint :get_current_user, :get, "/users/@me", returns: Dexcord.User + endpoint :get_user, :get, "/users/:user_id", returns: Dexcord.User + + endpoint :edit_current_user, :patch, "/users/@me", body: true, returns: Dexcord.User + + endpoint :get_current_user_guilds, :get, "/users/@me/guilds", + query: [:before, :after, :limit, :with_counts], + returns: [Dexcord.PartialGuild] + + endpoint :get_current_user_guild_member, :get, "/users/@me/guilds/:guild_id/member", + returns: Dexcord.Member + + endpoint :leave_guild, :delete, "/users/@me/guilds/:guild_id", returns: nil + + endpoint :get_current_user_connections, :get, "/users/@me/connections", + returns: [Dexcord.Connection] + + @create_dm_spec %{ + name: :create_dm, + method: :post, + path: "/users/@me/channels", + segments: ["users", "@me", "channels"], + query: [], + body: true, + binary_wrap: nil, + files: false, + reason: false, + returns: Dexcord.Channel + } + @dexcord_endpoints @create_dm_spec + + @doc """ + `POST /users/@me/channels` — open (or fetch) a DM channel. + + Accepts a snowflake-ish `user_id` (wrapped as `recipient_id`) or an explicit + keyword/map body. + """ + def create_dm(user_id_or_body, opts \\ []) + + def create_dm(body, opts) when is_list(body) or is_map(body) do + Dexcord.Api.Endpoint.run(@create_dm_spec, [], body, opts) + end + + def create_dm(user_id, _opts) do + case Dexcord.Snowflake.cast(user_id) do + {:ok, id} -> + Dexcord.Api.Endpoint.run( + @create_dm_spec, + [], + [recipient_id: Integer.to_string(id)], + [] + ) + + :error -> + {:error, + %Dexcord.Api.Error{ + status: nil, + code: nil, + message: "invalid value for path parameter :user_id", + errors: nil + }} + end + end +end diff --git a/lib/dexcord/api/webhooks.ex b/lib/dexcord/api/webhooks.ex new file mode 100644 index 0000000..a4873a3 --- /dev/null +++ b/lib/dexcord/api/webhooks.ex @@ -0,0 +1,79 @@ +defmodule Dexcord.Api.Webhooks do + @moduledoc """ + Webhook endpoints, declared with the `Dexcord.Api.Endpoint` DSL — mirrors the + Discord "Webhook" resource page. + + `execute_webhook` returns a `Dexcord.Message` only when called with + `wait: true` (otherwise Discord replies 204 and the passthrough yields + `{:ok, nil}`). The Slack/GitHub-compat executes have no model (`:raw`). The + `*_with_token` variants are token-authed; their tokens are digested out of the + ratelimit bucket key (see `Dexcord.Api.Ratelimit.route_key/2`). + """ + use Dexcord.Api.Endpoint + + endpoint :create_webhook, :post, "/channels/:channel_id/webhooks", + body: true, + binary_wrap: :name, + reason: true, + returns: Dexcord.Webhook + + endpoint :get_channel_webhooks, :get, "/channels/:channel_id/webhooks", + returns: [Dexcord.Webhook] + + endpoint :get_guild_webhooks, :get, "/guilds/:guild_id/webhooks", returns: [Dexcord.Webhook] + endpoint :get_webhook, :get, "/webhooks/:webhook_id", returns: Dexcord.Webhook + + endpoint :get_webhook_with_token, :get, "/webhooks/:webhook_id/:webhook_token", + returns: Dexcord.Webhook + + endpoint :edit_webhook, :patch, "/webhooks/:webhook_id", + body: true, + reason: true, + returns: Dexcord.Webhook + + endpoint :edit_webhook_with_token, :patch, "/webhooks/:webhook_id/:webhook_token", + body: true, + returns: Dexcord.Webhook + + endpoint :delete_webhook, :delete, "/webhooks/:webhook_id", reason: true, returns: nil + + endpoint :delete_webhook_with_token, :delete, "/webhooks/:webhook_id/:webhook_token", + returns: nil + + endpoint :execute_webhook, :post, "/webhooks/:webhook_id/:webhook_token", + body: true, + binary_wrap: :content, + files: true, + query: [:wait, :thread_id, :with_components], + returns: Dexcord.Message + + endpoint :execute_slack_webhook, :post, "/webhooks/:webhook_id/:webhook_token/slack", + body: true, + query: [:thread_id, :wait], + returns: :raw + + endpoint :execute_github_webhook, :post, "/webhooks/:webhook_id/:webhook_token/github", + body: true, + query: [:thread_id, :wait], + returns: :raw + + endpoint :get_webhook_message, + :get, + "/webhooks/:webhook_id/:webhook_token/messages/:message_id", + query: [:thread_id], + returns: Dexcord.Message + + endpoint :edit_webhook_message, + :patch, + "/webhooks/:webhook_id/:webhook_token/messages/:message_id", + body: true, + files: true, + query: [:thread_id, :with_components], + returns: Dexcord.Message + + endpoint :delete_webhook_message, + :delete, + "/webhooks/:webhook_id/:webhook_token/messages/:message_id", + query: [:thread_id], + returns: nil +end diff --git a/lib/dexcord/cache.ex b/lib/dexcord/cache.ex index 785b3f5..fc40524 100644 --- a/lib/dexcord/cache.ex +++ b/lib/dexcord/cache.ex @@ -10,7 +10,7 @@ defmodule Dexcord.Cache do ## Single writer, lock-free reads This GenServer only *creates* the tables (in `init/1`); it never writes to them - on the hot path. All writes happen through `handle_dispatch/3`, which the + on the hot path. All writes happen through `handle_dispatch/4`, which the `Dexcord.Dispatcher` calls **inline, in its own process, in gateway order** - making the Dispatcher the sole writer. Because there is exactly one writer, no locks are needed. Every table is `:public`, so reads (`guild/1`, `member/2`, ...) @@ -21,6 +21,17 @@ defmodule Dexcord.Cache do is always the source of truth; the cache is best-effort, and can briefly lag or hold a duplicate after a resume gap. + ### The one sanctioned writer exception: DM channels + + `:dexcord_dm_channels` is the single table written from OUTSIDE the Dispatcher — + the `Dexcord.Api.send/2` funnel calls `put_dm_channel/2` after lazily opening a + DM (`Dexcord.Cache.dm_channel/1` misses → `POST /users/@me/channels` → cache the + id). This breaks the single-writer rule on purpose, and it is safe: DM channel + ids are stable and idempotent (Discord returns the SAME channel for a given + recipient), so two processes racing to open a DM for the same user both compute + and store the same id — a harmless duplicate write, never a conflicting one. The + table is `:set`/`:public` like the rest, so reads stay lock-free. + ## Tables | Table | Type | Key | @@ -36,16 +47,20 @@ defmodule Dexcord.Cache do ### Key and value conventions - Snowflake ids are stored in table keys as the **raw strings Discord sends** - no - integer parsing. This keeps lookups cheap and avoids precision pitfalls, at the - cost that a caller must pass string ids (which is what every payload already - carries). Stored values are the original string-keyed payload maps (no structs, - no atom keys), with the large child arrays of a guild peeled off into their own - tables. The `ordered_set` tables use `{guild_id, x}` keys so a whole guild can be - listed with a key-prefix select and purged with a single `match_delete/2`. + Snowflake ids are stored in table keys as **integers** - the same representation + `Dexcord.Snowflake.cast/1` produces at decode time. Reads accept anything + castable (an integer, a decimal string, or a struct with an `:id`) and normalize + it, so a caller may pass whatever id they have. Stored values are decoded model + **structs** (`%Dexcord.Guild{}`, `%Dexcord.TextChannel{}`, `%Dexcord.Member{}`, + ...), with the large child arrays of a guild peeled off into their own tables. A + cached member's nested `user` is moved into the users table and the row keeps a + `user_id` back-reference. The `ordered_set` tables use `{guild_id, x}` keys so a + whole guild can be listed with a key-prefix select and purged with a single + `match_delete/2`. """ use GenServer + require Logger @me :dexcord_me @guilds :dexcord_guilds @@ -55,13 +70,15 @@ defmodule Dexcord.Cache do @roles :dexcord_roles @presences :dexcord_presences @voice_states :dexcord_voice_states + # Written by the `Dexcord.Api.send/2` funnel, not the Dispatcher — the one + # sanctioned exception to single-writer (see moduledoc). Key: user_id -> DM + # channel id. + @dm_channels :dexcord_dm_channels # Guild child collections lifted out of the guild row into their own tables. # `emojis` (and stickers) stay inline on the guild and are replaced wholesale. - @guild_child_arrays ~w(channels threads roles members presences voice_states) - - # Channel `type` values that denote a thread (public, private, announcement). - @thread_types [10, 11, 12] + @guild_child_fields [:channels, :threads, :roles, :members, :presences, :voice_states] + @guild_child_keys ~w(channels threads roles members presences voice_states) # --- lifecycle ---------------------------------------------------------- @@ -83,6 +100,7 @@ defmodule Dexcord.Cache do :ets.new(@roles, oset) :ets.new(@presences, oset) :ets.new(@voice_states, oset) + :ets.new(@dm_channels, set) {:ok, %{}} end @@ -90,260 +108,385 @@ defmodule Dexcord.Cache do # --- write path (called inline by the Dispatcher, single writer) --------- @doc """ - Folds one dispatch event into the cache. Called inline by `Dexcord.Dispatcher` - *before* the user handler runs, so the handler always observes a cache that - already reflects the event. + Folds one decoded dispatch event into the cache. Called inline by + `Dexcord.Dispatcher` *before* the user handler runs, so the handler always + observes a cache that already reflects the event. + + `decoded` is the typed payload from `Dexcord.Events.decode/2`; `raw` is the + original string-keyed wire map (needed by partial-update events that merge only + the keys Discord actually sent). Write clauses pattern-match the decoded struct + type, so a decode failure (which delivers the raw map as a fallback) simply + matches no write clause and is skipped. `config` is the resolved `Dexcord.Config` map; only `:cache_presences` is - consulted (presence writes are skipped entirely when it is falsey). Unknown or - uninteresting events - and any event whose data is not a map - are a no-op. - Returns `:ok`. + consulted (presence writes are skipped entirely when it is falsey). Returns `:ok`. """ - @spec handle_dispatch(atom() | {:unknown_event, String.t()}, term(), map()) :: :ok - def handle_dispatch(name, data, config) + @spec handle_dispatch(atom() | {:unknown_event, String.t()}, term(), term(), map()) :: :ok + def handle_dispatch(name, decoded, raw, config) - def handle_dispatch(_name, data, _config) when not is_map(data), do: :ok + def handle_dispatch(:READY, %Dexcord.Events.Ready{} = ready, _raw, _config) do + put_me(ready.user) - def handle_dispatch(:READY, data, _config) do - if user = data["user"], do: put_me(user) - - for g <- data["guilds"] || [], - is_map(g) and is_binary(g["id"]), - do: :ets.insert(@guilds, {g["id"], g}) + for %Dexcord.UnavailableGuild{id: id} = stub <- ready.guilds, not is_nil(id) do + :ets.insert(@guilds, {id, stub}) + end :ok end - def handle_dispatch(:GUILD_CREATE, guild, config) do - gid = guild["id"] - :ets.insert(@guilds, {gid, Map.drop(guild, @guild_child_arrays)}) + def handle_dispatch(:GUILD_CREATE, %Dexcord.Guild{} = guild, _raw, config) do + :ets.insert(@guilds, {guild.id, without_children(guild)}) - for ch <- guild["channels"] || [], do: put_channel(Map.put(ch, "guild_id", gid)) - for th <- guild["threads"] || [], do: put_channel(Map.put(th, "guild_id", gid)) + for ch <- guild.channels ++ guild.threads, do: put_guild_channel(ch, guild.id) - for role <- guild["roles"] || [], - is_binary(role["id"]), - do: :ets.insert(@roles, {{gid, role["id"]}, role}) + for %Dexcord.Role{id: rid} = role <- guild.roles, not is_nil(rid) do + :ets.insert(@roles, {{guild.id, rid}, role}) + end - for vs <- guild["voice_states"] || [], do: put_voice_state(gid, vs) - for m <- guild["members"] || [], do: put_member(gid, m) + for vs <- guild.voice_states, do: put_voice_state(guild.id, vs) + for m <- guild.members, do: put_member(guild.id, m) if cache_presences?(config) do - for p <- guild["presences"] || [], do: put_presence(gid, p) + for p <- guild.presences, do: put_presence(guild.id, p) end :ok end - def handle_dispatch(:GUILD_UPDATE, guild, _config) do - gid = guild["id"] - merged = Map.merge(existing(@guilds, gid), Map.drop(guild, @guild_child_arrays)) - :ets.insert(@guilds, {gid, merged}) + # GUILD_UPDATE carries no child arrays and omits gateway-only fields like + # `joined_at`; merge only the keys Discord actually sent so those survive + # (api-surface.AC2.21). A guild we've never seen is stored decoded-minus-children. + def handle_dispatch(:GUILD_UPDATE, %Dexcord.Guild{} = guild, raw, _config) do + partial = Map.drop(raw, @guild_child_keys) + + case :ets.lookup(@guilds, guild.id) do + [{_, %Dexcord.Guild{} = existing}] -> + :ets.insert(@guilds, {guild.id, Dexcord.Guild.merge_map(existing, partial)}) + + _ -> + :ets.insert(@guilds, {guild.id, without_children(guild)}) + end + :ok end - def handle_dispatch(:GUILD_DELETE, data, _config) do - gid = data["id"] + def handle_dispatch( + :GUILD_EMOJIS_UPDATE, + %Dexcord.Events.GuildEmojisUpdate{} = e, + _raw, + _config + ) do + update_guild(e.guild_id, fn g -> %{g | emojis: e.emojis} end) + end - if data["unavailable"] == true do + def handle_dispatch( + :GUILD_STICKERS_UPDATE, + %Dexcord.Events.GuildStickersUpdate{} = e, + _raw, + _config + ) do + update_guild(e.guild_id, fn g -> %{g | stickers: e.stickers} end) + end + + def handle_dispatch(:GUILD_DELETE, %Dexcord.UnavailableGuild{id: id} = stub, _raw, _config) + when not is_nil(id) do + if stub.unavailable do # Guild went unavailable (outage), not removed: keep a stub placeholder. - :ets.insert(@guilds, {gid, data}) + :ets.insert(@guilds, {id, stub}) else - :ets.delete(@guilds, gid) - :ets.match_delete(@members, {{gid, :_}, :_}) - :ets.match_delete(@roles, {{gid, :_}, :_}) - :ets.match_delete(@presences, {{gid, :_}, :_}) - :ets.match_delete(@voice_states, {{gid, :_}, :_}) - :ets.select_delete(@channels, [{{:_, %{"guild_id" => gid}}, [], [true]}]) + # The bot was removed (no `unavailable` key): cascade-purge every table. + # Tuple-keyed tables match on the guild-id prefix; channels are struct + # rows selected by their `guild_id` field (api-surface.AC2.22). + :ets.delete(@guilds, id) + :ets.match_delete(@members, {{id, :_}, :_}) + :ets.match_delete(@roles, {{id, :_}, :_}) + :ets.match_delete(@presences, {{id, :_}, :_}) + :ets.match_delete(@voice_states, {{id, :_}, :_}) + :ets.select_delete(@channels, [{{:_, %{guild_id: id}}, [], [true]}]) end :ok end - def handle_dispatch(name, ch, _config) - when name in [:CHANNEL_CREATE, :CHANNEL_UPDATE, :THREAD_CREATE, :THREAD_UPDATE] do - put_channel(ch) + def handle_dispatch(name, channel, _raw, _config) + when name in [:CHANNEL_CREATE, :CHANNEL_UPDATE, :THREAD_CREATE, :THREAD_UPDATE] and + is_struct(channel) and not is_struct(channel, Dexcord.UnknownChannel) do + put_channel(channel) :ok end - def handle_dispatch(name, ch, _config) when name in [:CHANNEL_DELETE, :THREAD_DELETE] do - if id = ch["id"], do: :ets.delete(@channels, id) + def handle_dispatch(:CHANNEL_DELETE, channel, _raw, _config) + when is_struct(channel) and not is_struct(channel, Dexcord.UnknownChannel) do + if id = Map.get(channel, :id), do: :ets.delete(@channels, id) :ok end - def handle_dispatch(name, member, _config) - when name in [:GUILD_MEMBER_ADD, :GUILD_MEMBER_UPDATE] do - put_member(member["guild_id"], member) + def handle_dispatch(:THREAD_DELETE, %Dexcord.Events.ThreadDelete{id: id}, _raw, _config) + when not is_nil(id) do + :ets.delete(@channels, id) :ok end - def handle_dispatch(:GUILD_MEMBER_REMOVE, data, _config) do - with gid when is_binary(gid) <- data["guild_id"], - %{"id" => uid} <- data["user"] do - :ets.delete(@members, {gid, uid}) + def handle_dispatch(:GUILD_MEMBER_ADD, %Dexcord.Member{} = member, _raw, _config) do + put_member(member.guild_id, member) + :ok + end + + # GUILD_MEMBER_UPDATE is a partial: merge only the keys it sent into the cached + # row (keeping `user: nil`/`user_id`), and refresh the separate user record. + def handle_dispatch( + :GUILD_MEMBER_UPDATE, + %Dexcord.Events.GuildMemberUpdate{guild_id: gid, user: %Dexcord.User{id: uid} = user}, + raw, + _config + ) + when not is_nil(gid) and not is_nil(uid) do + upsert_user(user) + partial = Map.delete(raw, "user") + + merged = + case :ets.lookup(@members, {gid, uid}) do + [{_, %Dexcord.Member{} = existing}] -> Dexcord.Member.merge_map(existing, partial) + _ -> Dexcord.Member.from_map(partial) + end + + :ets.insert(@members, {{gid, uid}, %{merged | user: nil, user_id: uid}}) + :ok + end + + def handle_dispatch( + :GUILD_MEMBER_REMOVE, + %Dexcord.Events.GuildMemberRemove{guild_id: gid, user: %Dexcord.User{id: uid}}, + _raw, + _config + ) + when not is_nil(gid) and not is_nil(uid) do + :ets.delete(@members, {gid, uid}) + :ok + end + + def handle_dispatch( + :GUILD_MEMBERS_CHUNK, + %Dexcord.Events.GuildMembersChunk{} = chunk, + _raw, + config + ) do + for m <- chunk.members, do: put_member(chunk.guild_id, m) + + if cache_presences?(config) do + for p <- chunk.presences, do: put_presence(chunk.guild_id, p) end :ok end - def handle_dispatch(:GUILD_MEMBERS_CHUNK, data, config) do - gid = data["guild_id"] - for m <- data["members"] || [], do: put_member(gid, m) + def handle_dispatch(name, %{guild_id: gid, role: %Dexcord.Role{id: rid} = role}, _raw, _config) + when name in [:GUILD_ROLE_CREATE, :GUILD_ROLE_UPDATE] and not is_nil(gid) and + not is_nil(rid) do + :ets.insert(@roles, {{gid, rid}, role}) + :ok + end - presences = data["presences"] + def handle_dispatch( + :GUILD_ROLE_DELETE, + %Dexcord.Events.GuildRoleDelete{guild_id: gid, role_id: rid}, + _raw, + _config + ) + when not is_nil(gid) and not is_nil(rid) do + :ets.delete(@roles, {gid, rid}) + :ok + end - if is_list(presences) and cache_presences?(config) do - for p <- presences, do: put_presence(gid, p) + def handle_dispatch(:PRESENCE_UPDATE, %Dexcord.Presence{} = presence, _raw, config) do + # The chattiest event under `:all`: when presences aren't cached we skip the + # write entirely rather than build-then-discard a term. + if cache_presences?(config), do: put_presence(presence.guild_id, presence) + :ok + end + + def handle_dispatch(:VOICE_STATE_UPDATE, %Dexcord.VoiceState{} = vs, _raw, _config) do + cond do + is_nil(vs.guild_id) or is_nil(vs.user_id) -> :ok + is_nil(vs.channel_id) -> :ets.delete(@voice_states, {vs.guild_id, vs.user_id}) + true -> :ets.insert(@voice_states, {{vs.guild_id, vs.user_id}, vs}) end :ok end - def handle_dispatch(name, data, _config) - when name in [:GUILD_ROLE_CREATE, :GUILD_ROLE_UPDATE] do - gid = data["guild_id"] + def handle_dispatch(:USER_UPDATE, %Dexcord.User{} = user, raw, _config) do + case :ets.lookup(@me, :me) do + [{_, %Dexcord.User{} = existing}] -> + :ets.insert(@me, {:me, Dexcord.User.merge_map(existing, raw)}) - case data["role"] do - %{"id" => rid} = role -> :ets.insert(@roles, {{gid, rid}, role}) + _ -> + :ets.insert(@me, {:me, user}) + end + + :ok + end + + # Opportunistic freshness: cache the (real, non-webhook) author and, when the + # message carries a guild member fragment, fold it into the members table. + # Webhook messages (`webhook_id` set) write nothing. + def handle_dispatch( + :MESSAGE_CREATE, + %Dexcord.Message{webhook_id: nil, author: %Dexcord.User{id: aid} = author} = message, + raw, + _config + ) + when is_integer(aid) do + merge_user(aid, author, raw["author"]) + + if message.member && message.guild_id do + gid = message.guild_id + partial = Map.delete(raw["member"] || %{}, "user") + + merged = + case :ets.lookup(@members, {gid, aid}) do + [{_, %Dexcord.Member{} = existing}] -> Dexcord.Member.merge_map(existing, partial) + _ -> Dexcord.Member.from_map(partial) + end + + :ets.insert(@members, {{gid, aid}, %{merged | user: nil, user_id: aid}}) + end + + :ok + end + + # Everything else - unknown events, events with no cache mapping, and any raw + # fallback map from a failed decode (which matches no struct clause) - is a no-op. + def handle_dispatch(_name, _decoded, _raw, _config), do: :ok + + # --- write helpers ------------------------------------------------------ + + defp without_children(%Dexcord.Guild{} = guild) do + Enum.reduce(@guild_child_fields, guild, fn field, acc -> Map.put(acc, field, []) end) + end + + # Apply `fun` to an existing guild row in place; a no-op if the guild is unknown. + defp update_guild(gid, fun) do + case :ets.lookup(@guilds, gid) do + [{_, %Dexcord.Guild{} = g}] -> :ets.insert(@guilds, {gid, fun.(g)}) _ -> :ok end :ok end - def handle_dispatch(:GUILD_ROLE_DELETE, data, _config) do - if is_binary(data["guild_id"]) and is_binary(data["role_id"]), - do: :ets.delete(@roles, {data["guild_id"], data["role_id"]}) - - :ok - end - - def handle_dispatch(:GUILD_EMOJIS_UPDATE, data, _config) do - gid = data["guild_id"] - - case :ets.lookup(@guilds, gid) do - [{_, g}] -> :ets.insert(@guilds, {gid, Map.put(g, "emojis", data["emojis"] || [])}) - [] -> :ok - end - - :ok - end - - def handle_dispatch(:PRESENCE_UPDATE, data, config) do - # The chattiest event under `:all`: when presences aren't cached we skip the - # write entirely rather than build-then-discard a term. - if cache_presences?(config), do: put_presence(data["guild_id"], data) - :ok - end - - def handle_dispatch(:VOICE_STATE_UPDATE, data, _config) do - gid = data["guild_id"] - uid = data["user_id"] - - cond do - is_nil(gid) or is_nil(uid) -> :ok - is_nil(data["channel_id"]) -> :ets.delete(@voice_states, {gid, uid}) - true -> :ets.insert(@voice_states, {{gid, uid}, data}) - end - - :ok - end - - def handle_dispatch(:USER_UPDATE, data, _config) do - put_me(data) - :ok - end - - def handle_dispatch(:MESSAGE_CREATE, data, _config) do - author = data["author"] - - # Opportunistic freshness: cache the (real, non-webhook) author and, when the - # message carries a guild member fragment, fold it into the members table. - if is_map(author) and is_binary(author["id"]) and is_nil(author["webhook_id"]) and - is_nil(data["webhook_id"]) do - upsert_user(author) - - with gid when is_binary(gid) <- data["guild_id"], - member when is_map(member) <- data["member"] do - uid = author["id"] - merged = existing(@members, {gid, uid}) |> Map.merge(member) |> Map.put("user_id", uid) - :ets.insert(@members, {{gid, uid}, merged}) - end - end - - :ok - end - - # Everything else (unknown events, events with no cache mapping) is a no-op. - def handle_dispatch(_name, _data, _config), do: :ok - - # --- write helpers ------------------------------------------------------ - - defp put_me(user) when is_map(user), - do: :ets.insert(@me, {:me, Map.merge(existing(@me, :me), user)}) - + defp put_me(%Dexcord.User{} = user), do: :ets.insert(@me, {:me, user}) defp put_me(_), do: :ok - defp put_channel(%{"id" => id} = ch) when is_binary(id), do: :ets.insert(@channels, {id, ch}) + # A guild's inline channels arrive without a `guild_id`; stamp it on the way in. + # `%Dexcord.UnknownChannel{}` has no `guild_id` field (and no stable id), so + # unknown-typed channels are a documented, bounded gap: they are not cached. + defp put_guild_channel(%Dexcord.UnknownChannel{}, _gid) do + Logger.debug("Dexcord.Cache: skipping unknown-typed channel in GUILD_CREATE fan-out") + :ok + end + + defp put_guild_channel(%_{} = channel, gid), do: put_channel(%{channel | guild_id: gid}) + + defp put_channel(%_{id: id} = channel) when not is_nil(id), + do: :ets.insert(@channels, {id, channel}) + defp put_channel(_), do: :ok # A member's `user` object is moved into the users table; the member row keeps - # only a `"user_id"` back-reference. Merges over any existing row so partial - # updates (GUILD_MEMBER_UPDATE, MESSAGE_CREATE fragments) don't drop fields. - defp put_member(gid, %{"user" => %{"id" => uid} = user} = member) when is_binary(gid) do + # only a `user_id` back-reference. Members without a nested user are skipped. + defp put_member(gid, %Dexcord.Member{user: %Dexcord.User{id: uid} = user} = member) + when not is_nil(gid) and not is_nil(uid) do upsert_user(user) + row = %{member | user: nil, user_id: uid} merged = - existing(@members, {gid, uid}) - |> Map.merge(Map.delete(member, "user")) - |> Map.put("user_id", uid) + case :ets.lookup(@members, {gid, uid}) do + [{_, %Dexcord.Member{} = existing}] -> merge_non_nil(existing, row) + _ -> row + end :ets.insert(@members, {{gid, uid}, merged}) end defp put_member(_gid, _member), do: :ok - defp put_voice_state(gid, %{"user_id" => uid} = vs) when is_binary(uid), - do: :ets.insert(@voice_states, {{gid, uid}, vs}) + defp put_voice_state(gid, %Dexcord.VoiceState{user_id: uid} = vs) + when not is_nil(gid) and not is_nil(uid), + do: :ets.insert(@voice_states, {{gid, uid}, vs}) defp put_voice_state(_gid, _vs), do: :ok - defp put_presence(gid, %{"user" => %{"id" => uid}} = presence) when is_binary(gid), - do: :ets.insert(@presences, {{gid, uid}, presence}) + # `presence.user` is `:raw` (Discord guarantees only its `id`); cast it here. + defp put_presence(gid, %Dexcord.Presence{user: user} = presence) when not is_nil(gid) do + case Dexcord.Snowflake.cast(user_id(user)) do + {:ok, uid} -> :ets.insert(@presences, {{gid, uid}, presence}) + :error -> :ok + end + end defp put_presence(_gid, _presence), do: :ok - defp upsert_user(%{"id" => uid} = user), - do: :ets.insert(@users, {uid, Map.merge(existing(@users, uid), user)}) + defp user_id(%{"id" => id}), do: id + defp user_id(_), do: nil + + defp upsert_user(%Dexcord.User{id: uid} = user) when not is_nil(uid) do + merged = + case :ets.lookup(@users, uid) do + [{_, %Dexcord.User{} = existing}] -> merge_non_nil(existing, user) + _ -> user + end + + :ets.insert(@users, {uid, merged}) + end defp upsert_user(_), do: :ok - defp existing(table, key) do - case :ets.lookup(table, key) do - [{_, v}] -> v - [] -> %{} - end + # Upsert a user, merging a raw partial fragment over any existing record so the + # fields the fragment lacks (e.g. a message author fragment vs. a full user) are + # preserved. Falls back to the decoded struct when there's no existing row. + defp merge_user(uid, %Dexcord.User{} = fallback, raw_fragment) do + merged = + case :ets.lookup(@users, uid) do + [{_, %Dexcord.User{} = existing}] -> Dexcord.User.merge_map(existing, raw_fragment || %{}) + _ -> fallback + end + + :ets.insert(@users, {uid, merged}) + end + + # Overlay `new` onto `existing`, keeping `existing`'s value wherever `new`'s is + # nil - so a fuller cached row is never clobbered by a sparser struct. + defp merge_non_nil(%module{} = existing, %module{} = new) do + Map.merge(existing, Map.from_struct(new), fn _k, ev, nv -> + if is_nil(nv), do: ev, else: nv + end) end # --- read API ----------------------------------------------------------- - @typedoc "A cached entity, as its original string-keyed payload map." - @type entity :: map() + @typedoc "A cached entity, as its decoded model struct." + @type entity :: struct() + + @typedoc "Any snowflake-castable id: an integer, a decimal string, or a struct with `:id`." + @type id :: Dexcord.Snowflake.t() | String.t() | struct() @doc "The bot's own user object (from READY / USER_UPDATE)." - @spec me() :: {:ok, entity()} | :error + @spec me() :: {:ok, Dexcord.User.t()} | :error def me, do: fetch(@me, :me) @doc "Bang variant of `me/0`; raises if unset." - @spec me!() :: entity() + @spec me!() :: Dexcord.User.t() def me!, do: unwrap(me(), :me) @doc "A guild by id." - @spec guild(String.t()) :: {:ok, entity()} | :error - def guild(id), do: fetch(@guilds, id) + @spec guild(id()) :: {:ok, entity()} | :error + def guild(id) do + with {:ok, id} <- Dexcord.Snowflake.cast(id), do: fetch(@guilds, id) + end @doc "Bang variant of `guild/1`." - @spec guild!(String.t()) :: entity() + @spec guild!(id()) :: entity() def guild!(id), do: unwrap(guild(id), id) @doc "All cached guilds (including unavailable stubs)." @@ -351,84 +494,158 @@ defmodule Dexcord.Cache do def guilds, do: all_values(@guilds) @doc "A channel (or thread, or DM) by id." - @spec channel(String.t()) :: {:ok, entity()} | :error - def channel(id), do: fetch(@channels, id) + @spec channel(id()) :: {:ok, entity()} | :error + def channel(id) do + with {:ok, id} <- Dexcord.Snowflake.cast(id), do: fetch(@channels, id) + end @doc "Bang variant of `channel/1`." - @spec channel!(String.t()) :: entity() + @spec channel!(id()) :: entity() def channel!(id), do: unwrap(channel(id), id) @doc "All cached channels/threads belonging to a guild." - @spec channels(String.t()) :: [entity()] + @spec channels(id()) :: [entity()] def channels(guild_id) do - @channels - |> :ets.select([{{:_, %{"guild_id" => guild_id}}, [], [:"$_"]}]) - |> Enum.map(&elem(&1, 1)) + case Dexcord.Snowflake.cast(guild_id) do + {:ok, gid} -> + @channels + |> :ets.select([{{:_, %{guild_id: gid}}, [], [:"$_"]}]) + |> Enum.map(&elem(&1, 1)) + + :error -> + [] + end end - @doc "All cached threads (channel types 10/11/12) belonging to a guild." - @spec threads(String.t()) :: [entity()] + @doc "All cached threads belonging to a guild." + @spec threads(id()) :: [entity()] def threads(guild_id) do - guild_id |> channels() |> Enum.filter(fn ch -> ch["type"] in @thread_types end) + guild_id |> channels() |> Enum.filter(&match?(%Dexcord.Thread{}, &1)) end @doc "A guild member by guild id + user id." - @spec member(String.t(), String.t()) :: {:ok, entity()} | :error - def member(guild_id, user_id), do: fetch(@members, {guild_id, user_id}) + @spec member(id(), id()) :: {:ok, entity()} | :error + def member(guild_id, user_id), do: pair_fetch(@members, guild_id, user_id) @doc "Bang variant of `member/2`." - @spec member!(String.t(), String.t()) :: entity() + @spec member!(id(), id()) :: entity() def member!(guild_id, user_id), do: unwrap(member(guild_id, user_id), {guild_id, user_id}) @doc "All cached members of a guild." - @spec members(String.t()) :: [entity()] + @spec members(id()) :: [entity()] def members(guild_id), do: prefix_values(@members, guild_id) @doc "A user by id." - @spec user(String.t()) :: {:ok, entity()} | :error - def user(id), do: fetch(@users, id) + @spec user(id()) :: {:ok, entity()} | :error + def user(id) do + with {:ok, id} <- Dexcord.Snowflake.cast(id), do: fetch(@users, id) + end @doc "Bang variant of `user/1`." - @spec user!(String.t()) :: entity() + @spec user!(id()) :: entity() def user!(id), do: unwrap(user(id), id) @doc "A role by guild id + role id." - @spec role(String.t(), String.t()) :: {:ok, entity()} | :error - def role(guild_id, role_id), do: fetch(@roles, {guild_id, role_id}) + @spec role(id(), id()) :: {:ok, entity()} | :error + def role(guild_id, role_id), do: pair_fetch(@roles, guild_id, role_id) @doc "Bang variant of `role/2`." - @spec role!(String.t(), String.t()) :: entity() + @spec role!(id(), id()) :: entity() def role!(guild_id, role_id), do: unwrap(role(guild_id, role_id), {guild_id, role_id}) @doc "All cached roles of a guild." - @spec roles(String.t()) :: [entity()] + @spec roles(id()) :: [entity()] def roles(guild_id), do: prefix_values(@roles, guild_id) @doc "A presence by guild id + user id (only populated when `cache_presences: true`)." - @spec presence(String.t(), String.t()) :: {:ok, entity()} | :error - def presence(guild_id, user_id), do: fetch(@presences, {guild_id, user_id}) + @spec presence(id(), id()) :: {:ok, entity()} | :error + def presence(guild_id, user_id), do: pair_fetch(@presences, guild_id, user_id) @doc "Bang variant of `presence/2`." - @spec presence!(String.t(), String.t()) :: entity() + @spec presence!(id(), id()) :: entity() def presence!(guild_id, user_id), do: unwrap(presence(guild_id, user_id), {guild_id, user_id}) @doc "All cached presences of a guild." - @spec presences(String.t()) :: [entity()] + @spec presences(id()) :: [entity()] def presences(guild_id), do: prefix_values(@presences, guild_id) @doc "A voice state by guild id + user id." - @spec voice_state(String.t(), String.t()) :: {:ok, entity()} | :error - def voice_state(guild_id, user_id), do: fetch(@voice_states, {guild_id, user_id}) + @spec voice_state(id(), id()) :: {:ok, entity()} | :error + def voice_state(guild_id, user_id), do: pair_fetch(@voice_states, guild_id, user_id) @doc "Bang variant of `voice_state/2`." - @spec voice_state!(String.t(), String.t()) :: entity() + @spec voice_state!(id(), id()) :: entity() def voice_state!(guild_id, user_id), do: unwrap(voice_state(guild_id, user_id), {guild_id, user_id}) @doc "All cached voice states of a guild." - @spec voice_states(String.t()) :: [entity()] + @spec voice_states(id()) :: [entity()] def voice_states(guild_id), do: prefix_values(@voice_states, guild_id) + # --- DM channel cache (written by the send funnel, see moduledoc) -------- + + @doc """ + The cached DM channel id for a user, if one has been opened this session. + + Populated lazily by `Dexcord.Api.send/2` the first time it DMs a user; a miss + is `:error` (the funnel then opens the DM and caches the result). + """ + @spec dm_channel(id()) :: {:ok, Dexcord.Snowflake.t()} | :error + def dm_channel(user_id) do + with {:ok, uid} <- Dexcord.Snowflake.cast(user_id), do: fetch(@dm_channels, uid) + end + + @doc false + # Written by the `Dexcord.Api.send/2` funnel — the sole sanctioned non-Dispatcher + # writer (see moduledoc). Idempotent: DM channel ids are stable per recipient, so + # racing writers store the same value. + @spec put_dm_channel(id(), Dexcord.Snowflake.t()) :: :ok + def put_dm_channel(user_id, channel_id) do + with {:ok, uid} <- Dexcord.Snowflake.cast(user_id) do + :ets.insert(@dm_channels, {uid, channel_id}) + end + + :ok + end + + # --- hydration ---------------------------------------------------------- + + @doc """ + Best-effort hydration of an envelope struct's declared `hydrate` slots from + the cache. Each slot is filled from ETS by the id in its `from` field; a cache + miss (or a nil source id) leaves that slot `nil`. A struct with no `hydrate` + declarations is returned unchanged. + + This function reads ETS **only** - it never issues HTTP and never blocks on the + network, so it is safe to call from any process on the hot path + (api-surface.AC3.8). It is meant to be called by **user handler code** when a + handler wants the related objects inline; the `Dexcord.Dispatcher` never calls + it (hydration is opt-in, not a cost paid on every event). + + Already-populated slots are left alone, so `fill/1` is idempotent. + """ + @spec fill(struct()) :: struct() + def fill(%module{} = event) do + if function_exported?(module, :__hydrations__, 0) do + Enum.reduce(module.__hydrations__(), event, fn h, acc -> + with nil <- Map.fetch!(acc, h.name), + id when not is_nil(id) <- Map.fetch!(acc, h.from), + {:ok, value} <- fill_lookup(h.type, id) do + Map.put(acc, h.name, value) + else + _ -> acc + end + end) + else + event + end + end + + defp fill_lookup(Dexcord.User, id), do: user(id) + defp fill_lookup(:channel, id), do: channel(id) + defp fill_lookup(Dexcord.Guild, id), do: guild(id) + defp fill_lookup(_type, _id), do: :error + # --- read helpers ------------------------------------------------------- defp cache_presences?(config), do: Map.get(config, :cache_presences, false) @@ -440,11 +657,21 @@ defmodule Dexcord.Cache do end end + defp pair_fetch(table, guild_id, x_id) do + with {:ok, gid} <- Dexcord.Snowflake.cast(guild_id), + {:ok, xid} <- Dexcord.Snowflake.cast(x_id), + do: fetch(table, {gid, xid}) + end + defp all_values(table), do: :ets.select(table, [{{:_, :"$1"}, [], [:"$1"]}]) # Per-guild listing of an ordered_set keyed by `{guild_id, x}`. - defp prefix_values(table, guild_id), - do: :ets.select(table, [{{{guild_id, :_}, :"$1"}, [], [:"$1"]}]) + defp prefix_values(table, guild_id) do + case Dexcord.Snowflake.cast(guild_id) do + {:ok, gid} -> :ets.select(table, [{{{gid, :_}, :"$1"}, [], [:"$1"]}]) + :error -> [] + end + end defp unwrap({:ok, value}, _key), do: value defp unwrap(:error, key), do: raise("Dexcord.Cache: no cached entry for #{inspect(key)}") diff --git a/lib/dexcord/config.ex b/lib/dexcord/config.ex index ca6095f..d3be8bb 100644 --- a/lib/dexcord/config.ex +++ b/lib/dexcord/config.ex @@ -44,10 +44,11 @@ defmodule Dexcord.Config do Stored under a dedicated `:persistent_term` key, separate from the config map, so it can be written after boot without republishing the whole config. """ - @spec put_application_id(String.t()) :: :ok - def put_application_id(id) when is_binary(id), do: :persistent_term.put(@app_id_key, id) + @spec put_application_id(Dexcord.Snowflake.t() | String.t()) :: :ok + def put_application_id(id) when is_binary(id) or is_integer(id), + do: :persistent_term.put(@app_id_key, id) @doc "Returns the cached application id, or `nil` if not yet resolved." - @spec application_id() :: String.t() | nil + @spec application_id() :: Dexcord.Snowflake.t() | String.t() | nil def application_id, do: :persistent_term.get(@app_id_key, nil) end diff --git a/lib/dexcord/dispatcher.ex b/lib/dexcord/dispatcher.ex index 2f6cbad..c352113 100644 --- a/lib/dexcord/dispatcher.ex +++ b/lib/dexcord/dispatcher.ex @@ -35,16 +35,21 @@ defmodule Dexcord.Dispatcher do def handle_cast({:dispatch, name, data}, state) do config = Dexcord.Config.get() + # Decode the wire payload exactly once, here, and feed the cache BOTH forms: + # the decoded struct drives inserts, the raw partial map drives merges. Decode + # never raises - a failure falls back to the raw map (see `Dexcord.Events`). + decoded = Dexcord.Events.decode(name, data) + # Cache write happens INLINE, in this (single-writer) process, BEFORE the # handler Task is spawned - so the handler always observes a cache that # already reflects this event, and cache writes stay in exact gateway order. - Dexcord.Cache.handle_dispatch(name, data, config) + Dexcord.Cache.handle_dispatch(name, decoded, data, config) maybe_request_members(name, data, config) - maybe_route_slash(name, data, config) + maybe_route_slash(name, decoded, data, config) Task.Supervisor.start_child(@task_supervisor, fn -> - config.handler.handle_event({name, data}) + config.handler.handle_event({name, decoded}) end) {:noreply, state} @@ -71,22 +76,44 @@ defmodule Dexcord.Dispatcher do defp maybe_request_members(_name, _data, _config), do: :ok # When a `slash:` module is configured, auto-route INTERACTION_CREATEs to it in - # a separate Task, IN ADDITION to the raw handler (which always receives the - # event). Only the interaction top-level `"type"`s handled by the slash layer are - # routed: 2 (application command), 3 (message component), 5 (modal submit). - # Type 1 (PING) never arrives over the gateway and type 4 (autocomplete) is left - # to the raw handler. - defp maybe_route_slash(:INTERACTION_CREATE, interaction, config) when is_map(interaction) do + # a separate Task, IN ADDITION to the handler (which always receives the event). + # Routing gates on the DECODED `%Dexcord.Interaction{}`: only the three top-level + # interaction types handled by the slash layer are routed - `:application_command` + # (2), `:message_component` (3), `:modal_submit` (5). Type 1 (PING) never arrives + # over the gateway and type 4 (`:application_command_autocomplete`) is left to the + # handler. + # + # If decode FELL BACK to the raw map (malformed interaction), the struct gate + # can't fire; we degrade to the old integer `"type"` gate and hand the RAW map to + # `Slash.dispatch/2`'s documented degraded head so a not-quite-decodable + # interaction still reaches the slash layer. A cleanly decoded interaction routes + # the typed `%Dexcord.Interaction{}` struct. + defp maybe_route_slash(:INTERACTION_CREATE, decoded, raw, config) do slash_mod = Map.get(config, :slash) - if slash_mod && interaction["type"] in [2, 3, 5] do - Task.Supervisor.start_child(@task_supervisor, fn -> - Dexcord.Slash.dispatch(interaction, slash_mod) - end) + cond do + is_nil(slash_mod) -> + :ok + + match?(%Dexcord.Interaction{}, decoded) and + decoded.type in [:application_command, :message_component, :modal_submit] -> + route_slash(decoded, slash_mod) + + is_map(raw) and raw["type"] in [2, 3, 5] -> + route_slash(raw, slash_mod) + + true -> + :ok end + end + + defp maybe_route_slash(_name, _decoded, _raw, _config), do: :ok + + defp route_slash(interaction, slash_mod) do + Task.Supervisor.start_child(@task_supervisor, fn -> + Dexcord.Slash.dispatch(interaction, slash_mod) + end) :ok end - - defp maybe_route_slash(_name, _data, _config), do: :ok end diff --git a/lib/dexcord/enum.ex b/lib/dexcord/enum.ex new file mode 100644 index 0000000..c2af224 --- /dev/null +++ b/lib/dexcord/enum.ex @@ -0,0 +1,63 @@ +defmodule Dexcord.Enum do + @moduledoc """ + A DSL for Discord's open integer enums. + + Discord adds enum values over time; decoding must not break when it does. + `decode/1` maps a recognized integer to its named atom and any other + integer to `{:unknown, n}`; `encode/1` reverses both losslessly. + + defmodule Dexcord.MessageType do + use Dexcord.Enum, default: 0, reply: 19 + end + + Dexcord.MessageType.decode(19) #=> :reply + Dexcord.MessageType.decode(999) #=> {:unknown, 999} + Dexcord.MessageType.encode({:unknown, 999}) #=> 999 + """ + + defmacro __using__(values) do + unless is_list(values) and values != [] and Keyword.keyword?(values) do + raise ArgumentError, + "use Dexcord.Enum expects a non-empty keyword list of name: integer pairs" + end + + names = Keyword.keys(values) + + type_union = + Enum.reduce(names, quote(do: {:unknown, integer()}), fn name, acc -> + {:|, [], [name, acc]} + end) + + decode_clauses = + for {name, int} <- values do + quote do + def decode(unquote(int)), do: unquote(name) + end + end + + encode_clauses = + for {name, int} <- values do + quote do + def encode(unquote(name)), do: unquote(int) + end + end + + quote do + @type t :: unquote(type_union) + + @doc "Decodes a wire integer; unrecognized values become `{:unknown, n}`." + @spec decode(integer()) :: t() + unquote_splicing(decode_clauses) + def decode(n) when is_integer(n), do: {:unknown, n} + + @doc "Encodes a named atom or `{:unknown, n}` back to the wire integer." + @spec encode(t()) :: integer() + unquote_splicing(encode_clauses) + def encode({:unknown, n}) when is_integer(n), do: n + + @doc "The full name => integer table." + @spec values() :: %{atom() => integer()} + def values, do: unquote({:%{}, [], values}) + end + end +end diff --git a/lib/dexcord/events.ex b/lib/dexcord/events.ex new file mode 100644 index 0000000..f33eb46 --- /dev/null +++ b/lib/dexcord/events.ex @@ -0,0 +1,139 @@ +defmodule Dexcord.Events do + @moduledoc """ + The gateway event decode table: maps every documented dispatch event atom + to its payload type and decodes raw payloads exactly once, in the + dispatcher. Decoding NEVER raises: failures log a warning (event name + only — payloads may be huge or sensitive) and fall back to the raw map. + """ + + require Logger + + @full_object %{ + GUILD_CREATE: Dexcord.Guild, + GUILD_UPDATE: Dexcord.Guild, + GUILD_DELETE: Dexcord.UnavailableGuild, + CHANNEL_CREATE: Dexcord.Channel, + CHANNEL_UPDATE: Dexcord.Channel, + CHANNEL_DELETE: Dexcord.Channel, + THREAD_CREATE: Dexcord.Channel, + THREAD_UPDATE: Dexcord.Channel, + THREAD_MEMBER_UPDATE: Dexcord.ThreadMember, + MESSAGE_CREATE: Dexcord.Message, + MESSAGE_UPDATE: Dexcord.Message, + GUILD_MEMBER_ADD: Dexcord.Member, + GUILD_AUDIT_LOG_ENTRY_CREATE: Dexcord.AuditLogEntry, + USER_UPDATE: Dexcord.User, + INTERACTION_CREATE: Dexcord.Interaction, + APPLICATION_COMMAND_PERMISSIONS_UPDATE: Dexcord.GuildApplicationCommandPermissions, + AUTO_MODERATION_RULE_CREATE: Dexcord.AutoModerationRule, + AUTO_MODERATION_RULE_UPDATE: Dexcord.AutoModerationRule, + AUTO_MODERATION_RULE_DELETE: Dexcord.AutoModerationRule, + GUILD_SCHEDULED_EVENT_CREATE: Dexcord.GuildScheduledEvent, + GUILD_SCHEDULED_EVENT_UPDATE: Dexcord.GuildScheduledEvent, + GUILD_SCHEDULED_EVENT_DELETE: Dexcord.GuildScheduledEvent, + INTEGRATION_CREATE: Dexcord.Integration, + INTEGRATION_UPDATE: Dexcord.Integration, + VOICE_STATE_UPDATE: Dexcord.VoiceState, + PRESENCE_UPDATE: Dexcord.Presence + } + + @envelopes %{ + READY: Dexcord.Events.Ready, + AUTO_MODERATION_ACTION_EXECUTION: Dexcord.Events.AutoModerationActionExecution, + CHANNEL_PINS_UPDATE: Dexcord.Events.ChannelPinsUpdate, + THREAD_DELETE: Dexcord.Events.ThreadDelete, + THREAD_LIST_SYNC: Dexcord.Events.ThreadListSync, + THREAD_MEMBERS_UPDATE: Dexcord.Events.ThreadMembersUpdate, + GUILD_BAN_ADD: Dexcord.Events.GuildBanAdd, + GUILD_BAN_REMOVE: Dexcord.Events.GuildBanRemove, + GUILD_EMOJIS_UPDATE: Dexcord.Events.GuildEmojisUpdate, + GUILD_STICKERS_UPDATE: Dexcord.Events.GuildStickersUpdate, + GUILD_INTEGRATIONS_UPDATE: Dexcord.Events.GuildIntegrationsUpdate, + GUILD_MEMBER_REMOVE: Dexcord.Events.GuildMemberRemove, + GUILD_MEMBER_UPDATE: Dexcord.Events.GuildMemberUpdate, + GUILD_MEMBERS_CHUNK: Dexcord.Events.GuildMembersChunk, + GUILD_ROLE_CREATE: Dexcord.Events.GuildRoleCreate, + GUILD_ROLE_UPDATE: Dexcord.Events.GuildRoleUpdate, + GUILD_ROLE_DELETE: Dexcord.Events.GuildRoleDelete, + GUILD_SCHEDULED_EVENT_USER_ADD: Dexcord.Events.GuildScheduledEventUserAdd, + GUILD_SCHEDULED_EVENT_USER_REMOVE: Dexcord.Events.GuildScheduledEventUserRemove, + INTEGRATION_DELETE: Dexcord.Events.IntegrationDelete, + INVITE_CREATE: Dexcord.Events.InviteCreate, + INVITE_DELETE: Dexcord.Events.InviteDelete, + MESSAGE_DELETE: Dexcord.Events.MessageDelete, + MESSAGE_DELETE_BULK: Dexcord.Events.MessageDeleteBulk, + MESSAGE_REACTION_ADD: Dexcord.Events.ReactionAdd, + MESSAGE_REACTION_REMOVE: Dexcord.Events.ReactionRemove, + MESSAGE_REACTION_REMOVE_ALL: Dexcord.Events.ReactionRemoveAll, + MESSAGE_REACTION_REMOVE_EMOJI: Dexcord.Events.ReactionRemoveEmoji, + MESSAGE_POLL_VOTE_ADD: Dexcord.Events.PollVoteAdd, + MESSAGE_POLL_VOTE_REMOVE: Dexcord.Events.PollVoteRemove, + TYPING_START: Dexcord.Events.TypingStart, + VOICE_CHANNEL_EFFECT_SEND: Dexcord.Events.VoiceChannelEffectSend, + VOICE_SERVER_UPDATE: Dexcord.Events.VoiceServerUpdate, + WEBHOOKS_UPDATE: Dexcord.Events.WebhooksUpdate + } + + # Documented raw passthrough: no-payload markers and out-of-scope groups + # (monetization, stage instances) whose consumers use the raw map. + @passthrough [ + :RESUMED, + :ENTITLEMENT_CREATE, + :ENTITLEMENT_UPDATE, + :ENTITLEMENT_DELETE, + :STAGE_INSTANCE_CREATE, + :STAGE_INSTANCE_UPDATE, + :STAGE_INSTANCE_DELETE + ] + + @doc "Payload classification for an event atom (used by tests and docs)." + @spec payload_type(atom()) :: + {:full, module()} | {:envelope, module()} | :passthrough | :unknown + def payload_type(name) do + cond do + is_map_key(@full_object, name) -> {:full, @full_object[name]} + is_map_key(@envelopes, name) -> {:envelope, @envelopes[name]} + name in @passthrough -> :passthrough + true -> :unknown + end + end + + @doc """ + Decodes a raw dispatch payload for `name`. Returns the decoded payload, + or the raw payload unchanged for passthrough/unknown events, or — if + decoding fails or produces nil — the raw payload (with a warning logged). + """ + @spec decode(atom() | {:unknown_event, String.t()}, term()) :: term() + def decode({:unknown_event, _name}, raw), do: raw + + def decode(name, raw) when is_atom(name) do + case payload_type(name) do + {:full, mod} -> safe_decode(mod, name, raw) + {:envelope, mod} -> safe_decode(mod, name, raw) + :passthrough -> raw + :unknown -> raw + end + end + + defp safe_decode(mod, name, raw) do + case mod.from_map(raw) do + nil -> + Logger.warning("Dexcord.Events: #{name} payload decoded to nil; delivering raw map") + raw + + decoded -> + decoded + end + rescue + # Deliberately untestable defensive backstop. Under the Phase 1 tolerance + # contract `from_map/1` never raises — a non-map decodes to nil and hits the + # nil→raw fallback above (which IS tested). This rescue exists only to keep + # dispatch alive if a future decoder bug ever violates that contract. + error -> + Logger.warning( + "Dexcord.Events: decode failed for #{name} (#{inspect(error.__struct__)}); delivering raw map" + ) + + raw + end +end diff --git a/lib/dexcord/events/channel_events.ex b/lib/dexcord/events/channel_events.ex new file mode 100644 index 0000000..566db62 --- /dev/null +++ b/lib/dexcord/events/channel_events.ex @@ -0,0 +1,50 @@ +defmodule Dexcord.Events.ChannelPinsUpdate do + @moduledoc "CHANNEL_PINS_UPDATE. https://docs.discord.com/developers/events/gateway-events#channel-pins-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :channel_id, :snowflake + field :last_pin_timestamp, :datetime, tristate: true + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ThreadDelete do + @moduledoc "THREAD_DELETE. https://docs.discord.com/developers/events/gateway-events#thread-delete" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :parent_id, :snowflake + field :type, {:enum, Dexcord.ChannelType} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ThreadListSync do + @moduledoc "THREAD_LIST_SYNC. https://docs.discord.com/developers/events/gateway-events#thread-list-sync" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :channel_ids, {:list, :snowflake}, default: [] + field :threads, {:list, {:struct, Dexcord.Channel}}, default: [] + field :members, {:list, {:struct, Dexcord.ThreadMember}}, default: [] + end +end + +defmodule Dexcord.Events.ThreadMembersUpdate do + @moduledoc "THREAD_MEMBERS_UPDATE. https://docs.discord.com/developers/events/gateway-events#thread-members-update" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :member_count, :integer + field :added_members, {:list, {:struct, Dexcord.ThreadMember}}, default: [] + field :removed_member_ids, {:list, :snowflake}, default: [] + end +end diff --git a/lib/dexcord/events/guild_events.ex b/lib/dexcord/events/guild_events.ex new file mode 100644 index 0000000..b7e262c --- /dev/null +++ b/lib/dexcord/events/guild_events.ex @@ -0,0 +1,166 @@ +defmodule Dexcord.Events.GuildBanAdd do + @moduledoc "GUILD_BAN_ADD. https://docs.discord.com/developers/events/gateway-events#guild-ban-add" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :user, {:struct, Dexcord.User} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildBanRemove do + @moduledoc "GUILD_BAN_REMOVE. https://docs.discord.com/developers/events/gateway-events#guild-ban-remove" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :user, {:struct, Dexcord.User} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildEmojisUpdate do + @moduledoc "GUILD_EMOJIS_UPDATE. https://docs.discord.com/developers/events/gateway-events#guild-emojis-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :emojis, {:list, {:struct, Dexcord.Emoji}}, default: [] + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildStickersUpdate do + @moduledoc "GUILD_STICKERS_UPDATE. https://docs.discord.com/developers/events/gateway-events#guild-stickers-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :stickers, {:list, {:struct, Dexcord.Sticker}}, default: [] + end +end + +defmodule Dexcord.Events.GuildIntegrationsUpdate do + @moduledoc "GUILD_INTEGRATIONS_UPDATE. https://docs.discord.com/developers/events/gateway-events#guild-integrations-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + end +end + +defmodule Dexcord.Events.GuildMemberRemove do + @moduledoc "GUILD_MEMBER_REMOVE. https://docs.discord.com/developers/events/gateway-events#guild-member-remove" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :user, {:struct, Dexcord.User} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildMemberUpdate do + @moduledoc """ + GUILD_MEMBER_UPDATE. + + This is NOT a `Dexcord.Member`: `joined_at` is nullable, there is no `flags`, + and several fields carry PATCH tristate semantics for the Phase 6 cache merge + (`:absent` means the key was not sent, distinct from an explicit `null`). + + https://docs.discord.com/developers/events/gateway-events#guild-member-update + """ + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :roles, {:list, :snowflake}, default: [] + field :user, {:struct, Dexcord.User} + field :nick, :string, tristate: true + field :avatar, :string + field :banner, :string + field :joined_at, :datetime + field :premium_since, :datetime, tristate: true + field :deaf, :boolean + field :mute, :boolean + field :pending, :boolean + field :communication_disabled_until, :datetime, tristate: true + field :avatar_decoration_data, :raw, tristate: true + field :collectibles, :raw, tristate: true + end +end + +defmodule Dexcord.Events.GuildMembersChunk do + @moduledoc "GUILD_MEMBERS_CHUNK. https://docs.discord.com/developers/events/gateway-events#guild-members-chunk" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :members, {:list, {:struct, Dexcord.Member}}, default: [] + field :chunk_index, :integer + field :chunk_count, :integer + field :not_found, {:list, :raw}, default: [] + field :presences, {:list, {:struct, Dexcord.Presence}}, default: [] + field :nonce, :string + end +end + +defmodule Dexcord.Events.GuildRoleCreate do + @moduledoc "GUILD_ROLE_CREATE. https://docs.discord.com/developers/events/gateway-events#guild-role-create" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :role, {:struct, Dexcord.Role} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildRoleUpdate do + @moduledoc "GUILD_ROLE_UPDATE. https://docs.discord.com/developers/events/gateway-events#guild-role-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :role, {:struct, Dexcord.Role} + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildRoleDelete do + @moduledoc "GUILD_ROLE_DELETE. https://docs.discord.com/developers/events/gateway-events#guild-role-delete" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :role_id, :snowflake + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildScheduledEventUserAdd do + @moduledoc "GUILD_SCHEDULED_EVENT_USER_ADD. https://docs.discord.com/developers/events/gateway-events#guild-scheduled-event-user-add" + use Dexcord.Struct + + discord_struct do + field :guild_scheduled_event_id, :snowflake + field :user_id, :snowflake + field :guild_id, :snowflake + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.GuildScheduledEventUserRemove do + @moduledoc "GUILD_SCHEDULED_EVENT_USER_REMOVE. https://docs.discord.com/developers/events/gateway-events#guild-scheduled-event-user-remove" + use Dexcord.Struct + + discord_struct do + field :guild_scheduled_event_id, :snowflake + field :user_id, :snowflake + field :guild_id, :snowflake + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end diff --git a/lib/dexcord/events/message_events.ex b/lib/dexcord/events/message_events.ex new file mode 100644 index 0000000..2a76676 --- /dev/null +++ b/lib/dexcord/events/message_events.ex @@ -0,0 +1,123 @@ +defmodule Dexcord.Events.MessageDelete do + @moduledoc "MESSAGE_DELETE. https://docs.discord.com/developers/events/gateway-events#message-delete" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :channel_id, :snowflake + field :guild_id, :snowflake + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.MessageDeleteBulk do + @moduledoc "MESSAGE_DELETE_BULK. https://docs.discord.com/developers/events/gateway-events#message-delete-bulk" + use Dexcord.Struct + + discord_struct do + field :ids, {:list, :snowflake}, default: [] + field :channel_id, :snowflake + field :guild_id, :snowflake + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ReactionAdd do + @moduledoc "MESSAGE_REACTION_ADD. https://docs.discord.com/developers/events/gateway-events#message-reaction-add" + use Dexcord.Struct + + discord_struct do + field :user_id, :snowflake + field :channel_id, :snowflake + field :message_id, :snowflake + field :guild_id, :snowflake + field :member, {:struct, Dexcord.Member} + field :emoji, {:struct, Dexcord.PartialEmoji} + field :message_author_id, :snowflake + field :burst, :boolean + field :burst_colors, {:list, :string}, default: [] + field :type, {:enum, Dexcord.ReactionType} + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ReactionRemove do + @moduledoc "MESSAGE_REACTION_REMOVE. https://docs.discord.com/developers/events/gateway-events#message-reaction-remove" + use Dexcord.Struct + + discord_struct do + field :user_id, :snowflake + field :channel_id, :snowflake + field :message_id, :snowflake + field :guild_id, :snowflake + field :emoji, {:struct, Dexcord.PartialEmoji} + field :burst, :boolean + field :type, {:enum, Dexcord.ReactionType} + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ReactionRemoveAll do + @moduledoc "MESSAGE_REACTION_REMOVE_ALL. https://docs.discord.com/developers/events/gateway-events#message-reaction-remove-all" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :message_id, :snowflake + field :guild_id, :snowflake + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.ReactionRemoveEmoji do + @moduledoc "MESSAGE_REACTION_REMOVE_EMOJI. https://docs.discord.com/developers/events/gateway-events#message-reaction-remove-emoji" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :guild_id, :snowflake + field :message_id, :snowflake + field :emoji, {:struct, Dexcord.PartialEmoji} + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.PollVoteAdd do + @moduledoc "MESSAGE_POLL_VOTE_ADD. https://docs.discord.com/developers/events/gateway-events#message-poll-vote-add" + use Dexcord.Struct + + discord_struct do + field :user_id, :snowflake + field :channel_id, :snowflake + field :message_id, :snowflake + field :guild_id, :snowflake + field :answer_id, :integer + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.PollVoteRemove do + @moduledoc "MESSAGE_POLL_VOTE_REMOVE. https://docs.discord.com/developers/events/gateway-events#message-poll-vote-remove" + use Dexcord.Struct + + discord_struct do + field :user_id, :snowflake + field :channel_id, :snowflake + field :message_id, :snowflake + field :guild_id, :snowflake + field :answer_id, :integer + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end diff --git a/lib/dexcord/events/misc_events.ex b/lib/dexcord/events/misc_events.ex new file mode 100644 index 0000000..eeded18 --- /dev/null +++ b/lib/dexcord/events/misc_events.ex @@ -0,0 +1,124 @@ +defmodule Dexcord.Events.AutoModerationActionExecution do + @moduledoc "AUTO_MODERATION_ACTION_EXECUTION. https://docs.discord.com/developers/events/gateway-events#auto-moderation-action-execution" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :action, {:struct, Dexcord.AutoModerationAction} + field :rule_id, :snowflake + field :rule_trigger_type, {:enum, Dexcord.AutoModerationTriggerType} + field :user_id, :snowflake + field :channel_id, :snowflake + field :message_id, :snowflake + field :alert_system_message_id, :snowflake + field :content, :string + field :matched_keyword, :string + field :matched_content, :string + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.IntegrationDelete do + @moduledoc "INTEGRATION_DELETE. https://docs.discord.com/developers/events/gateway-events#integration-delete" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :application_id, :snowflake + end +end + +defmodule Dexcord.Events.InviteCreate do + @moduledoc "INVITE_CREATE. https://docs.discord.com/developers/events/gateway-events#invite-create" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :code, :string + field :created_at, :datetime + field :guild_id, :snowflake + field :inviter, {:struct, Dexcord.User} + field :max_age, :integer + field :max_uses, :integer + field :target_type, {:enum, Dexcord.InviteTargetType} + field :target_user, {:struct, Dexcord.User} + field :target_application, :raw + field :temporary, :boolean + field :uses, :integer + field :expires_at, :datetime + field :role_ids, {:list, :snowflake}, default: [] + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.InviteDelete do + @moduledoc "INVITE_DELETE. https://docs.discord.com/developers/events/gateway-events#invite-delete" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :guild_id, :snowflake + field :code, :string + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.TypingStart do + @moduledoc "TYPING_START. `timestamp` is unix SECONDS (integer), NOT a datetime. https://docs.discord.com/developers/events/gateway-events#typing-start" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :guild_id, :snowflake + field :user_id, :snowflake + field :timestamp, :integer + field :member, {:struct, Dexcord.Member} + hydrate :user, from: :user_id, type: Dexcord.User + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end + +defmodule Dexcord.Events.VoiceChannelEffectSend do + @moduledoc "VOICE_CHANNEL_EFFECT_SEND. https://docs.discord.com/developers/events/gateway-events#voice-channel-effect-send" + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :guild_id, :snowflake + field :user_id, :snowflake + field :emoji, :raw + field :animation_type, {:enum, Dexcord.VoiceAnimationType} + field :animation_id, :integer + field :sound_id, :raw + field :sound_volume, :number + end +end + +defmodule Dexcord.Events.VoiceServerUpdate do + @moduledoc "VOICE_SERVER_UPDATE. A null `endpoint` means the server was lost; await reallocation. https://docs.discord.com/developers/events/gateway-events#voice-server-update" + use Dexcord.Struct + + discord_struct do + field :token, :string + field :guild_id, :snowflake + field :endpoint, :string + end +end + +defmodule Dexcord.Events.WebhooksUpdate do + @moduledoc "WEBHOOKS_UPDATE. https://docs.discord.com/developers/events/gateway-events#webhooks-update" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :channel_id, :snowflake + hydrate :channel, from: :channel_id, type: :channel + hydrate :guild, from: :guild_id, type: Dexcord.Guild + end +end diff --git a/lib/dexcord/events/ready.ex b/lib/dexcord/events/ready.ex new file mode 100644 index 0000000..9861d02 --- /dev/null +++ b/lib/dexcord/events/ready.ex @@ -0,0 +1,14 @@ +defmodule Dexcord.Events.Ready do + @moduledoc "The READY dispatch payload. https://docs.discord.com/developers/events/gateway-events#ready" + use Dexcord.Struct + + discord_struct do + field :v, :integer + field :user, {:struct, Dexcord.User} + field :guilds, {:list, {:struct, Dexcord.UnavailableGuild}}, default: [] + field :session_id, :string + field :resume_gateway_url, :string + field :shard, {:list, :integer} + field :application, :raw + end +end diff --git a/lib/dexcord/flags.ex b/lib/dexcord/flags.ex new file mode 100644 index 0000000..26fd96d --- /dev/null +++ b/lib/dexcord/flags.ex @@ -0,0 +1,60 @@ +defmodule Dexcord.Flags do + @moduledoc """ + A DSL for Discord bitfields. + + Struct fields declared `{:flags, Module}` store the **raw integer** so + unknown bits are never lost; the generated module provides named views: + + defmodule Dexcord.MessageFlags do + use Dexcord.Flags, crossposted: 0, suppress_embeds: 2 + end + + Dexcord.MessageFlags.has?(4, :suppress_embeds) #=> true + Dexcord.MessageFlags.to_list(5) #=> [:crossposted, :suppress_embeds] + Dexcord.MessageFlags.from_list([:crossposted]) #=> 1 + """ + + defmacro __using__(bits) do + unless is_list(bits) and bits != [] and Keyword.keyword?(bits) do + raise ArgumentError, + "use Dexcord.Flags expects a non-empty keyword list of name: bit_position pairs" + end + + flag_values = + for {name, pos} <- bits do + {name, Bitwise.bsl(1, pos)} + end + + flag_map = {:%{}, [], flag_values} + + quote do + @dexcord_flag_values unquote(flag_map) + + @doc "All named flags as a name => integer-value map." + @spec all() :: %{atom() => pos_integer()} + def all, do: @dexcord_flag_values + + @doc "Whether `int` has the named flag set." + @spec has?(integer(), atom()) :: boolean() + def has?(int, flag) when is_integer(int) and is_map_key(@dexcord_flag_values, flag), + do: Bitwise.band(int, @dexcord_flag_values[flag]) != 0 + + @doc "Named flags set in `int`, ordered by bit value. Unknown bits are omitted." + @spec to_list(integer()) :: [atom()] + def to_list(int) when is_integer(int) do + @dexcord_flag_values + |> Enum.filter(fn {_name, val} -> Bitwise.band(int, val) != 0 end) + |> Enum.sort_by(fn {_name, val} -> val end) + |> Enum.map(fn {name, _val} -> name end) + end + + @doc "Combines named flags into an integer. Raises `KeyError` on unknown names." + @spec from_list([atom()]) :: non_neg_integer() + def from_list(flags) when is_list(flags) do + Enum.reduce(flags, 0, fn flag, acc -> + Bitwise.bor(acc, Map.fetch!(@dexcord_flag_values, flag)) + end) + end + end + end +end diff --git a/lib/dexcord/handler.ex b/lib/dexcord/handler.ex index cb89823..9104a1d 100644 --- a/lib/dexcord/handler.ex +++ b/lib/dexcord/handler.ex @@ -7,18 +7,28 @@ defmodule Dexcord.Handler do injects a catch-all clause (via `@before_compile`) so unmatched events are silently ignored - the user only writes the clauses they want. - The argument is a `{name, data}` tuple where `name` is a dispatch event atom - (e.g. `:MESSAGE_CREATE`) or `{:unknown_event, name}` for undocumented events, - and `data` is the raw string-keyed payload map. + The argument is a `{name, payload}` tuple where `name` is a dispatch event atom + (e.g. `:MESSAGE_CREATE`) or `{:unknown_event, name}` for undocumented events. The + `payload` is decoded exactly once, in the dispatcher, and its shape depends on the + event: + + * **full-object events** (e.g. `:MESSAGE_CREATE`, `:GUILD_CREATE`) arrive as the + corresponding model struct - `%Dexcord.Message{}`, `%Dexcord.Guild{}`, etc. + * **envelope events** (e.g. `:GUILD_MEMBERS_CHUNK`, `:MESSAGE_REACTION_ADD`) + arrive as a `Dexcord.Events.*` struct. + * **passthrough, unknown, and decode-failure** payloads arrive as the raw + string-keyed map. A malformed payload that fails to decode logs a warning and + falls back to this raw form so dispatch never stalls. defmodule MyBot.Handler do use Dexcord.Handler - def handle_event({:MESSAGE_CREATE, msg}), do: IO.inspect(msg["content"]) + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}), + do: IO.inspect(msg.content) end """ - @callback handle_event({atom() | {:unknown_event, String.t()}, map()}) :: any() + @callback handle_event({atom() | {:unknown_event, String.t()}, term()}) :: any() defmacro __using__(_opts) do quote do diff --git a/lib/dexcord/messageable.ex b/lib/dexcord/messageable.ex new file mode 100644 index 0000000..dcbe82e --- /dev/null +++ b/lib/dexcord/messageable.ex @@ -0,0 +1,45 @@ +defprotocol Dexcord.Messageable do + @moduledoc """ + Anything a message can be sent to. `Dexcord.Api.send/2` resolves its target + through this protocol. Category/forum/media/directory channels deliberately + do NOT implement it — sending to one fails with `Protocol.UndefinedError` + at resolve time, before any HTTP. + """ + + @spec resolve(t) :: {:channel, Dexcord.Snowflake.t()} | {:dm_user, Dexcord.Snowflake.t()} + def resolve(target) +end + +defimpl Dexcord.Messageable, + for: [ + Dexcord.TextChannel, + Dexcord.AnnouncementChannel, + Dexcord.VoiceChannel, + Dexcord.StageChannel, + Dexcord.Thread, + Dexcord.DMChannel, + Dexcord.GroupDMChannel + ] do + def resolve(%{id: id}), do: {:channel, id} +end + +defimpl Dexcord.Messageable, for: Dexcord.Message do + def resolve(%{channel_id: id}), do: {:channel, id} +end + +defimpl Dexcord.Messageable, for: Dexcord.Interaction do + def resolve(%{channel_id: id}) when not is_nil(id), do: {:channel, id} +end + +defimpl Dexcord.Messageable, for: Dexcord.User do + def resolve(%{id: id}), do: {:dm_user, id} +end + +defimpl Dexcord.Messageable, for: Dexcord.Member do + def resolve(%{user: %Dexcord.User{id: id}}), do: {:dm_user, id} + def resolve(%{user_id: id}) when not is_nil(id), do: {:dm_user, id} +end + +defimpl Dexcord.Messageable, for: Integer do + def resolve(id) when id >= 0, do: {:channel, id} +end diff --git a/lib/dexcord/model/allowed_mentions.ex b/lib/dexcord/model/allowed_mentions.ex new file mode 100644 index 0000000..43dbc42 --- /dev/null +++ b/lib/dexcord/model/allowed_mentions.ex @@ -0,0 +1,34 @@ +defmodule Dexcord.AllowedMentions do + @moduledoc "Send-side allowed-mentions control. https://docs.discord.com/developers/resources/message" + use Dexcord.Struct + + discord_struct do + field :parse, {:list, :string}, default: [] + field :roles, {:list, :snowflake} + field :users, {:list, :snowflake} + field :replied_user, :boolean, default: false + end + + @doc false + # Normalizes any allowed-mentions spec to a string-keyed wire map carrying only + # explicitly-provided keys (an absent key means "not set" for merge/2). A struct + # goes through to_map/1, so every non-nil field is explicit. + @spec normalize(nil | struct() | keyword() | map()) :: map() | nil + def normalize(nil), do: nil + def normalize(%{__struct__: __MODULE__} = am), do: to_map(am) + def normalize(kw) when is_list(kw), do: Map.new(kw, fn {k, v} -> {to_string(k), v} end) + def normalize(map) when is_map(map), do: Map.new(map, fn {k, v} -> {to_string(k), v} end) + + @doc """ + Field-wise merge of two normalized allowed-mentions maps: keys present in + `per_send` win; `default` fills the rest (discord.py's documented contract). + + Callers own category-exclusivity — like discord.py, this happily builds + combinations Discord would reject (e.g. `parse: ["users"]` alongside an explicit + `users:` list); it does not validate that. + """ + @spec merge(map() | nil, map() | nil) :: map() | nil + def merge(nil, per_send), do: per_send + def merge(default, nil), do: default + def merge(default, per_send), do: Map.merge(default, per_send) +end diff --git a/lib/dexcord/model/application_command.ex b/lib/dexcord/model/application_command.ex new file mode 100644 index 0000000..fdfcabf --- /dev/null +++ b/lib/dexcord/model/application_command.ex @@ -0,0 +1,80 @@ +defmodule Dexcord.ApplicationCommand do + @moduledoc "An application command. https://docs.discord.com/developers/interactions/application-commands" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.ApplicationCommandType}, default: :chat_input + field :application_id, :snowflake + field :guild_id, :snowflake + field :name, :string + field :name_localizations, :raw + field :description, :string + field :description_localizations, :raw + field :options, {:list, {:struct, Dexcord.ApplicationCommand.Option}}, default: [] + field :default_member_permissions, {:flags, Dexcord.Permissions}, wire_string: true + field :dm_permission, :boolean + field :default_permission, :boolean + field :nsfw, :boolean, default: false + field :integration_types, {:list, {:enum, Dexcord.ApplicationIntegrationType}} + field :contexts, {:list, {:enum, Dexcord.InteractionContextType}} + field :version, :snowflake + field :handler, {:enum, Dexcord.EntryPointHandlerType} + end +end + +defmodule Dexcord.ApplicationCommand.Option do + @moduledoc "A command option (recursive — sub-command groups nest options)." + use Dexcord.Struct + + discord_struct do + field :type, {:enum, Dexcord.ApplicationCommandOptionType} + field :name, :string + field :name_localizations, :raw + field :description, :string + field :description_localizations, :raw + field :required, :boolean, default: false + field :choices, {:list, {:struct, Dexcord.ApplicationCommand.OptionChoice}}, default: [] + field :options, {:list, {:struct, __MODULE__}}, default: [] + field :channel_types, {:list, :integer} + field :min_value, :number + field :max_value, :number + field :min_length, :integer + field :max_length, :integer + field :autocomplete, :boolean + end +end + +defmodule Dexcord.ApplicationCommand.OptionChoice do + @moduledoc "A choice for a command option." + use Dexcord.Struct + + discord_struct do + field :name, :string + field :name_localizations, :raw + field :value, :raw + end +end + +defmodule Dexcord.GuildApplicationCommandPermissions do + @moduledoc "The permissions for a command in a guild." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :application_id, :snowflake + field :guild_id, :snowflake + field :permissions, {:list, {:struct, Dexcord.ApplicationCommandPermission}}, default: [] + end +end + +defmodule Dexcord.ApplicationCommandPermission do + @moduledoc "A single command permission override (role/user/channel target)." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.ApplicationCommandPermissionType} + field :permission, :boolean + end +end diff --git a/lib/dexcord/model/application_info.ex b/lib/dexcord/model/application_info.ex new file mode 100644 index 0000000..1d41d2b --- /dev/null +++ b/lib/dexcord/model/application_info.ex @@ -0,0 +1,56 @@ +defmodule Dexcord.InstallParams do + @moduledoc "OAuth2 install parameters for an application." + use Dexcord.Struct + + discord_struct do + field :scopes, {:list, :string}, default: [] + field :permissions, {:flags, Dexcord.Permissions}, wire_string: true + end +end + +defmodule Dexcord.ApplicationInfo do + @moduledoc """ + A Discord application object. + + Named `ApplicationInfo` (not `Application`) to avoid colliding with the OTP + application module `Dexcord.Application`. + + https://docs.discord.com/developers/resources/application + """ + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :icon, :string + field :description, :string + field :rpc_origins, {:list, :string}, default: [] + field :bot_public, :boolean + field :bot_require_code_grant, :boolean + field :bot, {:struct, Dexcord.User} + field :terms_of_service_url, :string + field :privacy_policy_url, :string + field :owner, {:struct, Dexcord.User} + field :verify_key, :string + field :team, {:struct, Dexcord.Team} + field :guild_id, :snowflake + field :guild, {:struct, Dexcord.PartialGuild} + field :primary_sku_id, :snowflake + field :slug, :string + field :cover_image, :string + field :flags, {:flags, Dexcord.ApplicationFlags} + field :approximate_guild_count, :integer + field :approximate_user_install_count, :integer + field :approximate_user_authorization_count, :integer + field :redirect_uris, {:list, :string}, default: [] + field :interactions_endpoint_url, :string + field :role_connections_verification_url, :string + field :event_webhooks_url, :string + field :event_webhooks_status, {:enum, Dexcord.EventWebhooksStatus} + field :event_webhooks_types, {:list, :string}, default: [] + field :tags, {:list, :string}, default: [] + field :install_params, {:struct, Dexcord.InstallParams} + field :integration_types_config, :raw + field :custom_install_url, :string + end +end diff --git a/lib/dexcord/model/attachment.ex b/lib/dexcord/model/attachment.ex new file mode 100644 index 0000000..e1c576f --- /dev/null +++ b/lib/dexcord/model/attachment.ex @@ -0,0 +1,26 @@ +defmodule Dexcord.Attachment do + @moduledoc "A message attachment. https://docs.discord.com/developers/resources/message" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :filename, :string + field :title, :string + field :description, :string + field :content_type, :string + field :size, :integer + field :url, :string + field :proxy_url, :string + field :height, :integer + field :width, :integer + field :placeholder, :string + field :placeholder_version, :integer + field :ephemeral, :boolean + field :duration_secs, :number + field :waveform, :string + field :flags, {:flags, Dexcord.AttachmentFlags} + field :clip_participants, {:list, {:struct, Dexcord.User}} + field :clip_created_at, :datetime + field :application, :raw + end +end diff --git a/lib/dexcord/model/audit_log.ex b/lib/dexcord/model/audit_log.ex new file mode 100644 index 0000000..33b159e --- /dev/null +++ b/lib/dexcord/model/audit_log.ex @@ -0,0 +1,71 @@ +defmodule Dexcord.AuditLogChange do + @moduledoc "A single changed key in an audit-log entry." + use Dexcord.Struct + + discord_struct do + field :new_value, :raw + field :old_value, :raw + field :key, :string + end +end + +defmodule Dexcord.AuditLogEntryInfo do + @moduledoc """ + Optional extra info for certain audit-log actions. + + Several numeric-looking fields (`count`, `delete_member_days`, + `members_removed`) are typed `:string` on purpose — Discord genuinely + sends them as strings on the wire. + """ + use Dexcord.Struct + + discord_struct do + field :application_id, :snowflake + field :auto_moderation_rule_name, :string + field :auto_moderation_rule_trigger_type, :string + field :channel_id, :snowflake + field :count, :string + field :delete_member_days, :string + field :id, :snowflake + field :members_removed, :string + field :message_id, :snowflake + field :role_name, :string + field :type, :string + field :integration_type, :string + field :status, :string + end +end + +defmodule Dexcord.AuditLogEntry do + @moduledoc "A single audit-log entry. https://docs.discord.com/developers/resources/audit-log" + use Dexcord.Struct + + discord_struct do + # String, not snowflake — can hold non-snowflake target ids. + field :target_id, :string + field :changes, {:list, {:struct, Dexcord.AuditLogChange}}, default: [] + field :user_id, :snowflake + field :id, :snowflake + field :action_type, {:enum, Dexcord.AuditLogEvent} + field :options, {:struct, Dexcord.AuditLogEntryInfo} + field :reason, :string + # Gateway extra from GUILD_AUDIT_LOG_ENTRY_CREATE. + field :guild_id, :snowflake + end +end + +defmodule Dexcord.AuditLog do + @moduledoc "A guild audit log. https://docs.discord.com/developers/resources/audit-log" + use Dexcord.Struct + + discord_struct do + field :application_commands, {:list, {:struct, Dexcord.ApplicationCommand}}, default: [] + field :audit_log_entries, {:list, {:struct, Dexcord.AuditLogEntry}}, default: [] + field :auto_moderation_rules, {:list, {:struct, Dexcord.AutoModerationRule}}, default: [] + field :guild_scheduled_events, {:list, {:struct, Dexcord.GuildScheduledEvent}}, default: [] + field :integrations, {:list, {:struct, Dexcord.Integration}}, default: [] + field :threads, {:list, {:struct, Dexcord.Channel}}, default: [] + field :users, {:list, {:struct, Dexcord.User}}, default: [] + field :webhooks, {:list, {:struct, Dexcord.Webhook}}, default: [] + end +end diff --git a/lib/dexcord/model/auto_moderation.ex b/lib/dexcord/model/auto_moderation.ex new file mode 100644 index 0000000..8e5378d --- /dev/null +++ b/lib/dexcord/model/auto_moderation.ex @@ -0,0 +1,53 @@ +defmodule Dexcord.AutoModerationTriggerMetadata do + @moduledoc "Additional data used to determine whether an auto-moderation rule triggers." + use Dexcord.Struct + + discord_struct do + field :keyword_filter, {:list, :string}, default: [] + field :regex_patterns, {:list, :string}, default: [] + field :presets, {:list, {:enum, Dexcord.KeywordPresetType}}, default: [] + field :allow_list, {:list, :string}, default: [] + field :mention_total_limit, :integer + field :mention_raid_protection_enabled, :boolean + end +end + +defmodule Dexcord.AutoModerationActionMetadata do + @moduledoc "Additional data used when an auto-moderation action is executed." + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :duration_seconds, :integer + field :custom_message, :string + end +end + +defmodule Dexcord.AutoModerationAction do + @moduledoc "An action taken when an auto-moderation rule is triggered." + use Dexcord.Struct + + discord_struct do + field :type, {:enum, Dexcord.AutoModerationActionType} + field :metadata, {:struct, Dexcord.AutoModerationActionMetadata} + end +end + +defmodule Dexcord.AutoModerationRule do + @moduledoc "An auto-moderation rule. https://docs.discord.com/developers/resources/auto-moderation" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :name, :string + field :creator_id, :snowflake + field :event_type, {:enum, Dexcord.AutoModerationEventType} + field :trigger_type, {:enum, Dexcord.AutoModerationTriggerType} + field :trigger_metadata, {:struct, Dexcord.AutoModerationTriggerMetadata} + field :actions, {:list, {:struct, Dexcord.AutoModerationAction}}, default: [] + field :enabled, :boolean + field :exempt_roles, {:list, :snowflake}, default: [] + field :exempt_channels, {:list, :snowflake}, default: [] + end +end diff --git a/lib/dexcord/model/channel.ex b/lib/dexcord/model/channel.ex new file mode 100644 index 0000000..fd67be4 --- /dev/null +++ b/lib/dexcord/model/channel.ex @@ -0,0 +1,329 @@ +defmodule Dexcord.Model.ChannelShared do + @moduledoc false + # Field group shared by the guild-channel structs (everything with a guild). + + def __included_fields__ do + [ + {:id, :snowflake, []}, + {:type, {:enum, Dexcord.ChannelType}, []}, + {:guild_id, :snowflake, []}, + {:name, :string, []}, + {:position, :integer, []}, + {:permission_overwrites, {:list, {:struct, Dexcord.Overwrite}}, [default: []]}, + {:parent_id, :snowflake, []}, + {:nsfw, :boolean, [default: false]}, + {:flags, {:flags, Dexcord.ChannelFlags}, []} + ] + end +end + +defmodule Dexcord.TextChannel do + @moduledoc "A guild text channel (type 0)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :topic, :string + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + field :last_pin_timestamp, :datetime + field :default_auto_archive_duration, :integer + field :default_thread_rate_limit_per_user, :integer + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.AnnouncementChannel do + @moduledoc "A guild announcement channel (type 5)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :topic, :string + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + field :last_pin_timestamp, :datetime + field :default_auto_archive_duration, :integer + field :default_thread_rate_limit_per_user, :integer + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.VoiceChannel do + @moduledoc "A guild voice channel (type 2)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :bitrate, :integer + field :user_limit, :integer + field :rtc_region, :string + field :video_quality_mode, {:enum, Dexcord.VideoQualityMode} + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.StageChannel do + @moduledoc "A guild stage voice channel (type 13)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :bitrate, :integer + field :user_limit, :integer + field :rtc_region, :string + field :video_quality_mode, {:enum, Dexcord.VideoQualityMode} + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.CategoryChannel do + @moduledoc "A guild category channel (type 4)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.DirectoryChannel do + @moduledoc "A guild directory channel (type 14)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.ForumChannel do + @moduledoc "A guild forum channel (type 15)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :topic, :string + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + field :default_auto_archive_duration, :integer + field :default_thread_rate_limit_per_user, :integer + field :available_tags, {:list, {:struct, Dexcord.ForumTag}}, default: [] + field :default_reaction_emoji, {:struct, Dexcord.DefaultReaction} + field :default_sort_order, {:enum, Dexcord.SortOrderType} + field :default_forum_layout, {:enum, Dexcord.ForumLayoutType} + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.MediaChannel do + @moduledoc "A guild media channel (type 16) — like a forum, without a layout mode." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.ChannelShared + field :topic, :string + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + field :default_auto_archive_duration, :integer + field :default_thread_rate_limit_per_user, :integer + field :available_tags, {:list, {:struct, Dexcord.ForumTag}}, default: [] + field :default_reaction_emoji, {:struct, Dexcord.DefaultReaction} + field :default_sort_order, {:enum, Dexcord.SortOrderType} + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.DMChannel do + @moduledoc "A one-to-one DM channel (type 1). No guild fields." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.ChannelType} + field :flags, {:flags, Dexcord.ChannelFlags} + field :last_message_id, :snowflake + field :recipients, {:list, {:struct, Dexcord.User}}, default: [] + field :last_pin_timestamp, :datetime + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.GroupDMChannel do + @moduledoc "A group DM channel (type 3)." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.ChannelType} + field :flags, {:flags, Dexcord.ChannelFlags} + field :last_message_id, :snowflake + field :recipients, {:list, {:struct, Dexcord.User}}, default: [] + field :last_pin_timestamp, :datetime + field :name, :string + field :icon, :string + field :owner_id, :snowflake + field :application_id, :snowflake + field :managed, :boolean + end + + @doc "The creation `DateTime` encoded in this channel's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.Thread do + @moduledoc "A thread channel (types 10 announcement, 11 public, 12 private)." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.ChannelType} + field :guild_id, :snowflake + field :name, :string + field :flags, {:flags, Dexcord.ChannelFlags} + field :parent_id, :snowflake + field :owner_id, :snowflake + field :last_message_id, :snowflake + field :rate_limit_per_user, :integer + field :message_count, :integer + field :member_count, :integer + field :total_message_sent, :integer + field :thread_metadata, {:struct, Dexcord.ThreadMetadata} + field :member, {:struct, Dexcord.ThreadMember} + field :applied_tags, {:list, :snowflake}, default: [] + field :last_pin_timestamp, :datetime + field :newly_created, :boolean, default: false + end + + @doc "The creation `DateTime` encoded in this thread's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.UnknownChannel do + @moduledoc "Fallback for channel types Dexcord doesn't recognize. Raw payload preserved." + defstruct [:type, :raw] + + @type t :: %__MODULE__{type: {:unknown, integer()} | nil, raw: map()} + + @doc false + def from_map(%{"type" => n} = map) when is_integer(n), + do: %__MODULE__{type: {:unknown, n}, raw: map} + + def from_map(map) when is_map(map), do: %__MODULE__{type: nil, raw: map} + def from_map(_), do: nil + + @doc false + # No __fields__/0 here (hand-written struct): re-encoding yields the raw + # payload verbatim. The Codec routes struct encodes through the struct's + # own to_map/1, so this keeps nested unknown channels encodable. + def to_map(%__MODULE__{raw: raw}), do: raw +end + +defmodule Dexcord.Channel do + @moduledoc """ + Channel factory: decodes a wire channel map into the struct for its type. + + Capability is struct identity — a `%Dexcord.CategoryChannel{}` is not + Messageable; sending to it fails at match time, by design. + """ + + @type_map %{ + 0 => Dexcord.TextChannel, + 1 => Dexcord.DMChannel, + 2 => Dexcord.VoiceChannel, + 3 => Dexcord.GroupDMChannel, + 4 => Dexcord.CategoryChannel, + 5 => Dexcord.AnnouncementChannel, + 10 => Dexcord.Thread, + 11 => Dexcord.Thread, + 12 => Dexcord.Thread, + 13 => Dexcord.StageChannel, + 14 => Dexcord.DirectoryChannel, + 15 => Dexcord.ForumChannel, + 16 => Dexcord.MediaChannel + } + + @type t :: + Dexcord.TextChannel.t() + | Dexcord.DMChannel.t() + | Dexcord.VoiceChannel.t() + | Dexcord.GroupDMChannel.t() + | Dexcord.CategoryChannel.t() + | Dexcord.AnnouncementChannel.t() + | Dexcord.Thread.t() + | Dexcord.StageChannel.t() + | Dexcord.DirectoryChannel.t() + | Dexcord.ForumChannel.t() + | Dexcord.MediaChannel.t() + | Dexcord.UnknownChannel.t() + + @doc "Decodes a wire channel map into the struct for its `\"type\"`." + @spec from_map(term()) :: t() | nil + def from_map(%{"type" => type} = map) when is_map_key(@type_map, type), + do: @type_map[type].from_map(map) + + def from_map(map) when is_map(map), do: Dexcord.UnknownChannel.from_map(map) + def from_map(_), do: nil + + @doc """ + The mention string (`<#id>`) for any channel struct with an integer `id` — + guild channels, threads, and DM/group-DM channels alike. + """ + @spec mention(%{id: Dexcord.Snowflake.t()}) :: String.t() + def mention(%{id: id}) when is_integer(id), do: "<##{id}>" + + @doc false + def __type_map__, do: @type_map +end + +# `String.Chars` for every channel struct: interpolating a channel produces a +# real `<#id>` mention (delegating to `Dexcord.Channel.mention/1`). +defimpl String.Chars, + for: [ + Dexcord.TextChannel, + Dexcord.AnnouncementChannel, + Dexcord.VoiceChannel, + Dexcord.StageChannel, + Dexcord.CategoryChannel, + Dexcord.DirectoryChannel, + Dexcord.ForumChannel, + Dexcord.MediaChannel, + Dexcord.DMChannel, + Dexcord.GroupDMChannel, + Dexcord.Thread + ] do + def to_string(channel), do: Dexcord.Channel.mention(channel) +end diff --git a/lib/dexcord/model/channel_parts.ex b/lib/dexcord/model/channel_parts.ex new file mode 100644 index 0000000..2ad7ba0 --- /dev/null +++ b/lib/dexcord/model/channel_parts.ex @@ -0,0 +1,50 @@ +defmodule Dexcord.ThreadMetadata do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :archived, :boolean + field :auto_archive_duration, :integer + field :archive_timestamp, :datetime + field :locked, :boolean + field :invitable, :boolean + field :create_timestamp, :datetime + end +end + +defmodule Dexcord.ThreadMember do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :user_id, :snowflake + field :join_timestamp, :datetime + field :flags, :integer + field :member, {:struct, Dexcord.Member} + field :guild_id, :snowflake + end +end + +defmodule Dexcord.ForumTag do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :moderated, :boolean + field :emoji_id, :snowflake + field :emoji_name, :string + end +end + +defmodule Dexcord.DefaultReaction do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :emoji_id, :snowflake + field :emoji_name, :string + end +end diff --git a/lib/dexcord/model/component.ex b/lib/dexcord/model/component.ex new file mode 100644 index 0000000..1e7b283 --- /dev/null +++ b/lib/dexcord/model/component.ex @@ -0,0 +1,383 @@ +defmodule Dexcord.Component.ActionRow do + @moduledoc "An action row (type 1): a container for a set of interactive components." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 1 + field :id, :integer + field :components, {:list, {:struct, Dexcord.Component}}, default: [] + end +end + +defmodule Dexcord.Component.Button do + @moduledoc "A button (type 2)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 2 + field :id, :integer + field :style, {:enum, Dexcord.ButtonStyle} + field :label, :string + field :emoji, {:struct, Dexcord.PartialEmoji} + field :custom_id, :string + field :sku_id, :snowflake + field :url, :string + field :disabled, :boolean, default: false + end +end + +defmodule Dexcord.Component.SelectOption do + @moduledoc "An option in a string select (type 3)." + use Dexcord.Struct + + discord_struct do + field :label, :string + field :value, :string + field :description, :string + field :emoji, {:struct, Dexcord.PartialEmoji} + field :default, :boolean + end +end + +defmodule Dexcord.Component.StringSelect do + @moduledoc "A string select menu (type 3)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 3 + field :id, :integer + field :custom_id, :string + field :options, {:list, {:struct, Dexcord.Component.SelectOption}}, default: [] + field :placeholder, :string + field :min_values, :integer + field :max_values, :integer + field :required, :boolean + field :disabled, :boolean, default: false + end +end + +defmodule Dexcord.Component.TextInput do + @moduledoc "A text input (type 4), used in modals." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 4 + field :id, :integer + field :custom_id, :string + field :style, {:enum, Dexcord.TextInputStyle} + field :min_length, :integer + field :max_length, :integer + field :required, :boolean + field :value, :string + field :placeholder, :string + end +end + +defmodule Dexcord.Component.SelectDefaultValue do + @moduledoc "A default value for an entity select (types 5–8)." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, :string + end +end + +defmodule Dexcord.Component.EntitySelect do + @moduledoc """ + An entity select menu — one struct for user (5), role (6), mentionable (7) + and channel (8) selects. They differ only by `type` and channel select's + extra `channel_types`. + """ + use Dexcord.Struct + + discord_struct do + field :type, :integer + field :id, :integer + field :custom_id, :string + field :channel_types, {:list, :integer} + field :placeholder, :string + field :default_values, {:list, {:struct, Dexcord.Component.SelectDefaultValue}}, default: [] + field :min_values, :integer + field :max_values, :integer + field :required, :boolean + field :disabled, :boolean, default: false + end +end + +defmodule Dexcord.Component.UnfurledMediaItem do + @moduledoc "An unfurled media item, referenced by Components V2 media components." + use Dexcord.Struct + + discord_struct do + field :url, :string + field :proxy_url, :string + field :height, :integer + field :width, :integer + field :placeholder, :string + field :placeholder_version, :integer + field :content_type, :string + field :flags, :integer + field :attachment_id, :snowflake + end +end + +defmodule Dexcord.Component.Section do + @moduledoc "A section (type 9): text components with an accessory." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 9 + field :id, :integer + field :components, {:list, {:struct, Dexcord.Component}}, default: [] + field :accessory, {:struct, Dexcord.Component} + end +end + +defmodule Dexcord.Component.TextDisplay do + @moduledoc "A text display (type 10)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 10 + field :id, :integer + field :content, :string + end +end + +defmodule Dexcord.Component.Thumbnail do + @moduledoc "A thumbnail (type 11), used as a section accessory." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 11 + field :id, :integer + field :media, {:struct, Dexcord.Component.UnfurledMediaItem} + field :description, :string + field :spoiler, :boolean, default: false + end +end + +defmodule Dexcord.Component.MediaGalleryItem do + @moduledoc "An item in a media gallery (type 12)." + use Dexcord.Struct + + discord_struct do + field :media, {:struct, Dexcord.Component.UnfurledMediaItem} + field :description, :string + field :spoiler, :boolean, default: false + end +end + +defmodule Dexcord.Component.MediaGallery do + @moduledoc "A media gallery (type 12)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 12 + field :id, :integer + field :items, {:list, {:struct, Dexcord.Component.MediaGalleryItem}}, default: [] + end +end + +defmodule Dexcord.Component.File do + @moduledoc "A file (type 13)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 13 + field :id, :integer + field :file, {:struct, Dexcord.Component.UnfurledMediaItem} + field :spoiler, :boolean, default: false + field :name, :string + field :size, :integer + end +end + +defmodule Dexcord.Component.Separator do + @moduledoc "A separator (type 14)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 14 + field :id, :integer + field :divider, :boolean, default: true + field :spacing, :integer + end +end + +defmodule Dexcord.Component.Container do + @moduledoc "A container (type 17): a Components V2 grouping with an accent color." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 17 + field :id, :integer + field :components, {:list, {:struct, Dexcord.Component}}, default: [] + field :accent_color, :integer + field :spoiler, :boolean, default: false + end +end + +defmodule Dexcord.Component.Label do + @moduledoc "A label (type 18): wraps a component with a label and description." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 18 + field :id, :integer + field :label, :string + field :description, :string + field :component, {:struct, Dexcord.Component} + end +end + +defmodule Dexcord.Component.FileUpload do + @moduledoc "A file upload (type 19)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 19 + field :id, :integer + field :custom_id, :string + field :min_values, :integer + field :max_values, :integer + field :required, :boolean + end +end + +defmodule Dexcord.Component.GroupOption do + @moduledoc "An option in a radio group (21) or checkbox group (22)." + use Dexcord.Struct + + discord_struct do + field :value, :string + field :label, :string + field :description, :string + field :default, :boolean + end +end + +defmodule Dexcord.Component.RadioGroup do + @moduledoc "A radio group (type 21)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 21 + field :id, :integer + field :custom_id, :string + field :options, {:list, {:struct, Dexcord.Component.GroupOption}}, default: [] + field :required, :boolean + end +end + +defmodule Dexcord.Component.CheckboxGroup do + @moduledoc "A checkbox group (type 22)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 22 + field :id, :integer + field :custom_id, :string + field :options, {:list, {:struct, Dexcord.Component.GroupOption}}, default: [] + field :min_values, :integer + field :max_values, :integer + field :required, :boolean + end +end + +defmodule Dexcord.Component.Checkbox do + @moduledoc "A checkbox (type 23)." + use Dexcord.Struct + + discord_struct do + field :type, :integer, default: 23 + field :id, :integer + field :custom_id, :string + field :default, :boolean + end +end + +defmodule Dexcord.Component.Unknown do + @moduledoc "Fallback for component types Dexcord doesn't recognize. Raw payload preserved." + defstruct [:type, :raw] + + @type t :: %__MODULE__{type: {:unknown, integer()} | nil, raw: map()} + + @doc false + def from_map(%{"type" => n} = map) when is_integer(n), + do: %__MODULE__{type: {:unknown, n}, raw: map} + + def from_map(map) when is_map(map), do: %__MODULE__{type: nil, raw: map} + def from_map(_), do: nil + + @doc false + # No __fields__/0 here (hand-written struct): re-encoding yields the raw + # payload verbatim. The Codec routes struct encodes through the struct's + # own to_map/1, so this keeps nested unknown components encodable. + def to_map(%__MODULE__{raw: raw}), do: raw +end + +defmodule Dexcord.Component do + @moduledoc """ + Component factory: decodes a wire component map into the struct for its type. + + Components dispatch on the integer `"type"` field. Types 15, 16 and 20 are + unassigned in the current docs (real gaps) and fall through to + `Dexcord.Component.Unknown` along with any other unrecognized type. + """ + + @type_map %{ + 1 => Dexcord.Component.ActionRow, + 2 => Dexcord.Component.Button, + 3 => Dexcord.Component.StringSelect, + 4 => Dexcord.Component.TextInput, + 5 => Dexcord.Component.EntitySelect, + 6 => Dexcord.Component.EntitySelect, + 7 => Dexcord.Component.EntitySelect, + 8 => Dexcord.Component.EntitySelect, + 9 => Dexcord.Component.Section, + 10 => Dexcord.Component.TextDisplay, + 11 => Dexcord.Component.Thumbnail, + 12 => Dexcord.Component.MediaGallery, + 13 => Dexcord.Component.File, + 14 => Dexcord.Component.Separator, + 17 => Dexcord.Component.Container, + 18 => Dexcord.Component.Label, + 19 => Dexcord.Component.FileUpload, + 21 => Dexcord.Component.RadioGroup, + 22 => Dexcord.Component.CheckboxGroup, + 23 => Dexcord.Component.Checkbox + } + + @type t :: + Dexcord.Component.ActionRow.t() + | Dexcord.Component.Button.t() + | Dexcord.Component.StringSelect.t() + | Dexcord.Component.TextInput.t() + | Dexcord.Component.EntitySelect.t() + | Dexcord.Component.Section.t() + | Dexcord.Component.TextDisplay.t() + | Dexcord.Component.Thumbnail.t() + | Dexcord.Component.MediaGallery.t() + | Dexcord.Component.File.t() + | Dexcord.Component.Separator.t() + | Dexcord.Component.Container.t() + | Dexcord.Component.Label.t() + | Dexcord.Component.FileUpload.t() + | Dexcord.Component.RadioGroup.t() + | Dexcord.Component.CheckboxGroup.t() + | Dexcord.Component.Checkbox.t() + | Dexcord.Component.Unknown.t() + + @doc "Decodes a wire component map into the struct for its `\"type\"`." + @spec from_map(term()) :: t() | nil + def from_map(%{"type" => type} = map) when is_map_key(@type_map, type), + do: @type_map[type].from_map(map) + + def from_map(map) when is_map(map), do: Dexcord.Component.Unknown.from_map(map) + def from_map(_), do: nil + + @doc false + def __type_map__, do: @type_map +end diff --git a/lib/dexcord/model/embed.ex b/lib/dexcord/model/embed.ex new file mode 100644 index 0000000..57710fd --- /dev/null +++ b/lib/dexcord/model/embed.ex @@ -0,0 +1,158 @@ +defmodule Dexcord.EmbedFooter do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :text, :string + field :icon_url, :string + field :proxy_icon_url, :string + end +end + +defmodule Dexcord.EmbedImage do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :url, :string + field :proxy_url, :string + field :height, :integer + field :width, :integer + field :content_type, :string + field :placeholder, :string + field :placeholder_version, :integer + field :description, :string + field :flags, :integer + end +end + +defmodule Dexcord.EmbedThumbnail do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :url, :string + field :proxy_url, :string + field :height, :integer + field :width, :integer + field :content_type, :string + field :placeholder, :string + field :placeholder_version, :integer + field :description, :string + field :flags, :integer + end +end + +defmodule Dexcord.EmbedVideo do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :url, :string + field :proxy_url, :string + field :height, :integer + field :width, :integer + field :content_type, :string + field :placeholder, :string + field :placeholder_version, :integer + field :description, :string + field :flags, :integer + end +end + +defmodule Dexcord.EmbedProvider do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :name, :string + field :url, :string + end +end + +defmodule Dexcord.EmbedAuthor do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :name, :string + field :url, :string + field :icon_url, :string + field :proxy_icon_url, :string + end +end + +defmodule Dexcord.EmbedField do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :name, :string + field :value, :string + field :inline, :boolean, default: false + end +end + +defmodule Dexcord.Embed do + @moduledoc "A message embed. https://docs.discord.com/developers/resources/message" + use Dexcord.Struct + + discord_struct do + field :title, :string + field :type, :string + field :description, :string + field :url, :string + field :timestamp, :datetime + field :color, :integer + field :footer, {:struct, Dexcord.EmbedFooter} + field :image, {:struct, Dexcord.EmbedImage} + field :thumbnail, {:struct, Dexcord.EmbedThumbnail} + field :video, {:struct, Dexcord.EmbedVideo} + field :provider, {:struct, Dexcord.EmbedProvider} + field :author, {:struct, Dexcord.EmbedAuthor} + field :fields, {:list, {:struct, Dexcord.EmbedField}}, default: [] + field :flags, :integer + end + + @doc "A new embed. `opts` seed struct fields directly (e.g. `title:`, `color:`)." + @spec new(keyword()) :: t() + def new(opts \\ []), do: struct!(__MODULE__, opts) + + @doc "Sets the embed title." + def title(embed, title), do: %{embed | title: title} + + @doc "Sets the embed description." + def description(embed, description), do: %{embed | description: description} + + @doc "Sets the embed url." + def url(embed, url), do: %{embed | url: url} + + @doc "Sets the embed color (an integer)." + def color(embed, color) when is_integer(color), do: %{embed | color: color} + + @doc "Sets the embed timestamp." + def timestamp(embed, %DateTime{} = dt), do: %{embed | timestamp: dt} + + @doc "Appends a field. `inline:` defaults to `false`." + def field(embed, name, value, opts \\ []) do + f = %Dexcord.EmbedField{name: name, value: value, inline: Keyword.get(opts, :inline, false)} + %{embed | fields: embed.fields ++ [f]} + end + + @doc "Sets the embed footer. `icon_url:` optional." + def footer(embed, text, opts \\ []), + do: %{embed | footer: %Dexcord.EmbedFooter{text: text, icon_url: opts[:icon_url]}} + + @doc "Sets the embed image by url." + def image(embed, url), do: %{embed | image: %Dexcord.EmbedImage{url: url}} + + @doc "Sets the embed thumbnail by url." + def thumbnail(embed, url), do: %{embed | thumbnail: %Dexcord.EmbedThumbnail{url: url}} + + @doc "Sets the embed author. `url:`/`icon_url:` optional." + def author(embed, name, opts \\ []), + do: %{ + embed + | author: %Dexcord.EmbedAuthor{name: name, url: opts[:url], icon_url: opts[:icon_url]} + } +end diff --git a/lib/dexcord/model/emoji.ex b/lib/dexcord/model/emoji.ex new file mode 100644 index 0000000..e2d1a02 --- /dev/null +++ b/lib/dexcord/model/emoji.ex @@ -0,0 +1,52 @@ +defmodule Dexcord.Emoji do + @moduledoc "A guild emoji. https://docs.discord.com/developers/resources/emoji" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :roles, {:list, :snowflake}, default: [] + field :user, {:struct, Dexcord.User} + field :require_colons, :boolean + field :managed, :boolean + field :animated, :boolean, default: false + field :available, :boolean + end + + @doc "The creation `DateTime` encoded in this emoji's id, or `:error` (unicode/nil id)." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defmodule Dexcord.PartialEmoji do + @moduledoc """ + The minimal emoji shape used in reactions, components, and poll media. + Unicode emoji: `id` nil, `name` the character. Custom: `id` set; `name` + may be nil when the emoji's data is unavailable (deleted custom emoji). + """ + use Dexcord.Struct + + # `to_string/1` below shadows the auto-imported `Kernel.to_string/1`. + import Kernel, except: [to_string: 1] + + discord_struct do + field :id, :snowflake + field :name, :string + field :animated, :boolean, default: false + end + + @doc """ + The send-format string for this emoji. + + Custom static: `<:name:id>`; animated: ``; unicode (nil id): the + raw `name` character. + """ + @spec to_string(t()) :: String.t() + def to_string(%{id: nil, name: name}), do: name + def to_string(%{id: id, name: name, animated: true}), do: "" + def to_string(%{id: id, name: name}), do: "<:#{name}:#{id}>" +end + +defimpl String.Chars, for: Dexcord.PartialEmoji do + def to_string(emoji), do: Dexcord.PartialEmoji.to_string(emoji) +end diff --git a/lib/dexcord/model/enums.ex b/lib/dexcord/model/enums.ex new file mode 100644 index 0000000..be24ce9 --- /dev/null +++ b/lib/dexcord/model/enums.ex @@ -0,0 +1,398 @@ +# Model enums — all `Dexcord.Enum` declarations for the Phase 2 object graph. +# +# Tables transcribed verbatim from official Discord docs source (2026-07-05). +# Gaps in the numbering are REAL (Discord removed/reserved values) and must +# not be "fixed". Real gaps: MessageType 13, 30, 33–35, 40–43, 45; +# ChannelType 6–9. + +defmodule Dexcord.ChannelType do + use Dexcord.Enum, + guild_text: 0, + dm: 1, + guild_voice: 2, + group_dm: 3, + guild_category: 4, + guild_announcement: 5, + announcement_thread: 10, + public_thread: 11, + private_thread: 12, + guild_stage_voice: 13, + guild_directory: 14, + guild_forum: 15, + guild_media: 16 +end + +defmodule Dexcord.MessageType do + use Dexcord.Enum, + default: 0, + recipient_add: 1, + recipient_remove: 2, + call: 3, + channel_name_change: 4, + channel_icon_change: 5, + channel_pinned_message: 6, + user_join: 7, + guild_boost: 8, + guild_boost_tier_1: 9, + guild_boost_tier_2: 10, + guild_boost_tier_3: 11, + channel_follow_add: 12, + guild_discovery_disqualified: 14, + guild_discovery_requalified: 15, + guild_discovery_grace_period_initial_warning: 16, + guild_discovery_grace_period_final_warning: 17, + thread_created: 18, + reply: 19, + chat_input_command: 20, + thread_starter_message: 21, + guild_invite_reminder: 22, + context_menu_command: 23, + auto_moderation_action: 24, + role_subscription_purchase: 25, + interaction_premium_upsell: 26, + stage_start: 27, + stage_end: 28, + stage_speaker: 29, + stage_topic: 31, + guild_application_premium_subscription: 32, + guild_incident_alert_mode_enabled: 36, + guild_incident_alert_mode_disabled: 37, + guild_incident_report_raid: 38, + guild_incident_report_false_alarm: 39, + purchase_notification: 44, + poll_result: 46 +end + +defmodule Dexcord.MessageReferenceType do + use Dexcord.Enum, default: 0, forward: 1 +end + +defmodule Dexcord.PremiumType do + use Dexcord.Enum, none: 0, nitro_classic: 1, nitro: 2, nitro_basic: 3 +end + +defmodule Dexcord.StickerType do + use Dexcord.Enum, standard: 1, guild: 2 +end + +defmodule Dexcord.StickerFormatType do + use Dexcord.Enum, png: 1, apng: 2, lottie: 3, gif: 4 +end + +defmodule Dexcord.VerificationLevel do + use Dexcord.Enum, none: 0, low: 1, medium: 2, high: 3, very_high: 4 +end + +defmodule Dexcord.DefaultMessageNotificationLevel do + use Dexcord.Enum, all_messages: 0, only_mentions: 1 +end + +defmodule Dexcord.ExplicitContentFilterLevel do + use Dexcord.Enum, disabled: 0, members_without_roles: 1, all_members: 2 +end + +defmodule Dexcord.MfaLevel do + use Dexcord.Enum, none: 0, elevated: 1 +end + +defmodule Dexcord.GuildNsfwLevel do + use Dexcord.Enum, default: 0, explicit: 1, safe: 2, age_restricted: 3 +end + +defmodule Dexcord.PremiumTier do + use Dexcord.Enum, none: 0, tier_1: 1, tier_2: 2, tier_3: 3 +end + +defmodule Dexcord.SortOrderType do + use Dexcord.Enum, latest_activity: 0, creation_date: 1 +end + +defmodule Dexcord.ForumLayoutType do + use Dexcord.Enum, not_set: 0, list_view: 1, gallery_view: 2 +end + +defmodule Dexcord.VideoQualityMode do + use Dexcord.Enum, auto: 1, full: 2 +end + +defmodule Dexcord.OverwriteType do + use Dexcord.Enum, role: 0, member: 1 +end + +defmodule Dexcord.ReactionType do + use Dexcord.Enum, normal: 0, burst: 1 +end + +defmodule Dexcord.InteractionType do + use Dexcord.Enum, + ping: 1, + application_command: 2, + message_component: 3, + application_command_autocomplete: 4, + modal_submit: 5 +end + +defmodule Dexcord.InteractionContextType do + use Dexcord.Enum, guild: 0, bot_dm: 1, private_channel: 2 +end + +defmodule Dexcord.ApplicationCommandType do + use Dexcord.Enum, chat_input: 1, user: 2, message: 3, primary_entry_point: 4 +end + +defmodule Dexcord.ApplicationCommandOptionType do + use Dexcord.Enum, + sub_command: 1, + sub_command_group: 2, + string: 3, + integer: 4, + boolean: 5, + user: 6, + channel: 7, + role: 8, + mentionable: 9, + number: 10, + attachment: 11 +end + +defmodule Dexcord.EntryPointHandlerType do + use Dexcord.Enum, app_handler: 1, discord_launch_activity: 2 +end + +defmodule Dexcord.ApplicationCommandPermissionType do + use Dexcord.Enum, role: 1, user: 2, channel: 3 +end + +# Component types 15, 16, 20 are unassigned in the current docs — real gaps. +defmodule Dexcord.ComponentType do + use Dexcord.Enum, + action_row: 1, + button: 2, + string_select: 3, + text_input: 4, + user_select: 5, + role_select: 6, + mentionable_select: 7, + channel_select: 8, + section: 9, + text_display: 10, + thumbnail: 11, + media_gallery: 12, + file: 13, + separator: 14, + container: 17, + label: 18, + file_upload: 19, + radio_group: 21, + checkbox_group: 22, + checkbox: 23 +end + +defmodule Dexcord.ButtonStyle do + use Dexcord.Enum, primary: 1, secondary: 2, success: 3, danger: 4, link: 5, premium: 6 +end + +defmodule Dexcord.TextInputStyle do + use Dexcord.Enum, short: 1, paragraph: 2 +end + +defmodule Dexcord.WebhookType do + use Dexcord.Enum, incoming: 1, channel_follower: 2, application: 3 +end + +defmodule Dexcord.InviteType do + use Dexcord.Enum, guild: 0, group_dm: 1, friend: 2 +end + +defmodule Dexcord.InviteTargetType do + use Dexcord.Enum, stream: 1, embedded_application: 2 +end + +# The complete AuditLogEvent table. The sparse ranges are REAL — Discord +# reserves whole numeric bands per object family, leaving large gaps. +defmodule Dexcord.AuditLogEvent do + use Dexcord.Enum, + guild_update: 1, + channel_create: 10, + channel_update: 11, + channel_delete: 12, + channel_overwrite_create: 13, + channel_overwrite_update: 14, + channel_overwrite_delete: 15, + member_kick: 20, + member_prune: 21, + member_ban_add: 22, + member_ban_remove: 23, + member_update: 24, + member_role_update: 25, + member_move: 26, + member_disconnect: 27, + bot_add: 28, + role_create: 30, + role_update: 31, + role_delete: 32, + invite_create: 40, + invite_update: 41, + invite_delete: 42, + webhook_create: 50, + webhook_update: 51, + webhook_delete: 52, + emoji_create: 60, + emoji_update: 61, + emoji_delete: 62, + message_delete: 72, + message_bulk_delete: 73, + message_pin: 74, + message_unpin: 75, + integration_create: 80, + integration_update: 81, + integration_delete: 82, + stage_instance_create: 83, + stage_instance_update: 84, + stage_instance_delete: 85, + sticker_create: 90, + sticker_update: 91, + sticker_delete: 92, + guild_scheduled_event_create: 100, + guild_scheduled_event_update: 101, + guild_scheduled_event_delete: 102, + thread_create: 110, + thread_update: 111, + thread_delete: 112, + application_command_permission_update: 121, + soundboard_sound_create: 130, + soundboard_sound_update: 131, + soundboard_sound_delete: 132, + auto_moderation_rule_create: 140, + auto_moderation_rule_update: 141, + auto_moderation_rule_delete: 142, + auto_moderation_block_message: 143, + auto_moderation_flag_to_channel: 144, + auto_moderation_user_communication_disabled: 145, + auto_moderation_quarantine_user: 146, + creator_monetization_request_created: 150, + creator_monetization_terms_accepted: 151, + onboarding_prompt_create: 163, + onboarding_prompt_update: 164, + onboarding_prompt_delete: 165, + onboarding_create: 166, + onboarding_update: 167, + home_settings_create: 190, + home_settings_update: 191, + voice_channel_status_create: 192, + voice_channel_status_delete: 193 +end + +# Trigger type 2 was removed — real gap. +defmodule Dexcord.AutoModerationTriggerType do + use Dexcord.Enum, keyword: 1, spam: 3, keyword_preset: 4, mention_spam: 5, member_profile: 6 +end + +defmodule Dexcord.KeywordPresetType do + use Dexcord.Enum, profanity: 1, sexual_content: 2, slurs: 3 +end + +defmodule Dexcord.AutoModerationEventType do + use Dexcord.Enum, message_send: 1, member_update: 2 +end + +defmodule Dexcord.AutoModerationActionType do + use Dexcord.Enum, + block_message: 1, + send_alert_message: 2, + timeout: 3, + block_member_interaction: 4 +end + +defmodule Dexcord.GuildScheduledEventPrivacyLevel do + use Dexcord.Enum, guild_only: 2 +end + +defmodule Dexcord.GuildScheduledEventEntityType do + use Dexcord.Enum, stage_instance: 1, voice: 2, external: 3 +end + +defmodule Dexcord.GuildScheduledEventStatus do + use Dexcord.Enum, scheduled: 1, active: 2, completed: 3, canceled: 4 +end + +defmodule Dexcord.RecurrenceRuleFrequency do + use Dexcord.Enum, yearly: 0, monthly: 1, weekly: 2, daily: 3 +end + +defmodule Dexcord.RecurrenceRuleWeekday do + use Dexcord.Enum, + monday: 0, + tuesday: 1, + wednesday: 2, + thursday: 3, + friday: 4, + saturday: 5, + sunday: 6 +end + +defmodule Dexcord.RecurrenceRuleMonth do + use Dexcord.Enum, + january: 1, + february: 2, + march: 3, + april: 4, + may: 5, + june: 6, + july: 7, + august: 8, + september: 9, + october: 10, + november: 11, + december: 12 +end + +defmodule Dexcord.PollLayoutType do + use Dexcord.Enum, default: 1 +end + +defmodule Dexcord.ApplicationIntegrationType do + use Dexcord.Enum, guild_install: 0, user_install: 1 +end + +defmodule Dexcord.EventWebhooksStatus do + use Dexcord.Enum, disabled: 1, enabled: 2, disabled_by_discord: 3 +end + +defmodule Dexcord.TeamMembershipState do + use Dexcord.Enum, invited: 1, accepted: 2 +end + +defmodule Dexcord.OnboardingMode do + use Dexcord.Enum, onboarding_default: 0, onboarding_advanced: 1 +end + +defmodule Dexcord.OnboardingPromptType do + use Dexcord.Enum, multiple_choice: 0, dropdown: 1 +end + +defmodule Dexcord.IntegrationExpireBehavior do + use Dexcord.Enum, remove_role: 0, kick: 1 +end + +defmodule Dexcord.ConnectionVisibility do + use Dexcord.Enum, none: 0, everyone: 1 +end + +defmodule Dexcord.ActivityType do + use Dexcord.Enum, + playing: 0, + streaming: 1, + listening: 2, + watching: 3, + custom: 4, + competing: 5 +end + +defmodule Dexcord.StatusDisplayType do + use Dexcord.Enum, name: 0, state: 1, details: 2 +end + +defmodule Dexcord.VoiceAnimationType do + use Dexcord.Enum, premium: 0, basic: 1 +end diff --git a/lib/dexcord/model/flags.ex b/lib/dexcord/model/flags.ex new file mode 100644 index 0000000..173b81e --- /dev/null +++ b/lib/dexcord/model/flags.ex @@ -0,0 +1,164 @@ +# Model flags — all `Dexcord.Flags` declarations (bit POSITIONS) for Phase 2. +# +# Tables transcribed verbatim from official Discord docs source (2026-07-05). +# Gaps in the bit positions are REAL and must not be "fixed". Real gaps: +# UserFlags bits 4–5, 11–13, 15, 20+; Permissions bit 47; GuildMemberFlags +# bit 8; AttachmentFlags bit 4; ChannelFlags bits 0, 2–3, 5–14. + +defmodule Dexcord.UserFlags do + use Dexcord.Flags, + staff: 0, + partner: 1, + hypesquad: 2, + bug_hunter_level_1: 3, + hypesquad_online_house_1: 6, + hypesquad_online_house_2: 7, + hypesquad_online_house_3: 8, + premium_early_supporter: 9, + team_pseudo_user: 10, + bug_hunter_level_2: 14, + verified_bot: 16, + verified_developer: 17, + certified_moderator: 18, + bot_http_interactions: 19 +end + +defmodule Dexcord.MessageFlags do + use Dexcord.Flags, + crossposted: 0, + is_crosspost: 1, + suppress_embeds: 2, + source_message_deleted: 3, + urgent: 4, + has_thread: 5, + ephemeral: 6, + loading: 7, + failed_to_mention_some_roles_in_thread: 8, + suppress_notifications: 12, + is_voice_message: 13, + has_snapshot: 14, + is_components_v2: 15 +end + +defmodule Dexcord.Permissions do + use Dexcord.Flags, + create_instant_invite: 0, + kick_members: 1, + ban_members: 2, + administrator: 3, + manage_channels: 4, + manage_guild: 5, + add_reactions: 6, + view_audit_log: 7, + priority_speaker: 8, + stream: 9, + view_channel: 10, + send_messages: 11, + send_tts_messages: 12, + manage_messages: 13, + embed_links: 14, + attach_files: 15, + read_message_history: 16, + mention_everyone: 17, + use_external_emojis: 18, + view_guild_insights: 19, + connect: 20, + speak: 21, + mute_members: 22, + deafen_members: 23, + move_members: 24, + use_vad: 25, + change_nickname: 26, + manage_nicknames: 27, + manage_roles: 28, + manage_webhooks: 29, + manage_guild_expressions: 30, + use_application_commands: 31, + request_to_speak: 32, + manage_events: 33, + manage_threads: 34, + create_public_threads: 35, + create_private_threads: 36, + use_external_stickers: 37, + send_messages_in_threads: 38, + use_embedded_activities: 39, + moderate_members: 40, + view_creator_monetization_analytics: 41, + use_soundboard: 42, + create_guild_expressions: 43, + create_events: 44, + use_external_sounds: 45, + send_voice_messages: 46, + set_voice_channel_status: 48, + send_polls: 49, + use_external_apps: 50, + pin_messages: 51, + bypass_slowmode: 52 +end + +defmodule Dexcord.SystemChannelFlags do + use Dexcord.Flags, + suppress_join_notifications: 0, + suppress_premium_subscriptions: 1, + suppress_guild_reminder_notifications: 2, + suppress_join_notification_replies: 3, + suppress_role_subscription_purchase_notifications: 4, + suppress_role_subscription_purchase_notification_replies: 5 +end + +defmodule Dexcord.GuildMemberFlags do + use Dexcord.Flags, + did_rejoin: 0, + completed_onboarding: 1, + bypasses_verification: 2, + started_onboarding: 3, + is_guest: 4, + started_home_actions: 5, + completed_home_actions: 6, + automod_quarantined_username: 7, + dm_settings_upsell_acknowledged: 9, + automod_quarantined_guild_tag: 10 +end + +defmodule Dexcord.AttachmentFlags do + use Dexcord.Flags, is_clip: 0, is_thumbnail: 1, is_remix: 2, is_spoiler: 3, is_animated: 5 +end + +defmodule Dexcord.ChannelFlags do + use Dexcord.Flags, pinned: 1, require_tag: 4, hide_media_download_options: 15 +end + +defmodule Dexcord.RoleFlags do + use Dexcord.Flags, in_prompt: 0 +end + +defmodule Dexcord.ApplicationFlags do + use Dexcord.Flags, + application_auto_moderation_rule_create_badge: 6, + gateway_presence: 12, + gateway_presence_limited: 13, + gateway_guild_members: 14, + gateway_guild_members_limited: 15, + verification_pending_guild_limit: 16, + embedded: 17, + gateway_message_content: 18, + gateway_message_content_limited: 19, + application_command_badge: 23 +end + +defmodule Dexcord.InviteFlags do + use Dexcord.Flags, is_guest_invite: 0 +end + +defmodule Dexcord.ActivityFlags do + use Dexcord.Flags, + instance: 0, + join: 1, + spectate: 2, + join_request: 3, + sync: 4, + play: 5, + party_privacy_friends: 6, + party_privacy_voice_channel: 7, + embedded: 8 +end diff --git a/lib/dexcord/model/guild.ex b/lib/dexcord/model/guild.ex new file mode 100644 index 0000000..f91be66 --- /dev/null +++ b/lib/dexcord/model/guild.ex @@ -0,0 +1,217 @@ +defmodule Dexcord.Guild do + @moduledoc "A guild. https://docs.discord.com/developers/resources/guild" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :icon, :string + field :splash, :string + field :discovery_splash, :string + field :owner_id, :snowflake + field :afk_channel_id, :snowflake + field :afk_timeout, :integer + field :widget_enabled, :boolean + field :widget_channel_id, :snowflake + field :verification_level, {:enum, Dexcord.VerificationLevel} + field :default_message_notifications, {:enum, Dexcord.DefaultMessageNotificationLevel} + field :explicit_content_filter, {:enum, Dexcord.ExplicitContentFilterLevel} + field :roles, {:list, {:struct, Dexcord.Role}}, default: [] + field :emojis, {:list, {:struct, Dexcord.Emoji}}, default: [] + field :features, {:list, :string}, default: [] + field :mfa_level, {:enum, Dexcord.MfaLevel} + field :application_id, :snowflake + field :system_channel_id, :snowflake + field :system_channel_flags, {:flags, Dexcord.SystemChannelFlags} + field :rules_channel_id, :snowflake + field :max_presences, :integer + field :max_members, :integer + field :vanity_url_code, :string + field :description, :string + field :banner, :string + field :premium_tier, {:enum, Dexcord.PremiumTier} + field :premium_subscription_count, :integer + field :preferred_locale, :string + field :public_updates_channel_id, :snowflake + field :max_video_channel_users, :integer + field :max_stage_video_channel_users, :integer + field :approximate_member_count, :integer + field :approximate_presence_count, :integer + field :welcome_screen, {:struct, Dexcord.WelcomeScreen} + field :nsfw_level, {:enum, Dexcord.GuildNsfwLevel} + field :stickers, {:list, {:struct, Dexcord.Sticker}}, default: [] + field :premium_progress_bar_enabled, :boolean + field :safety_alerts_channel_id, :snowflake + field :incidents_data, :raw + # --- GUILD_CREATE gateway extension fields --- + field :joined_at, :datetime + field :large, :boolean + field :unavailable, :boolean, default: false + field :member_count, :integer + field :voice_states, {:list, {:struct, Dexcord.VoiceState}}, default: [] + field :members, {:list, {:struct, Dexcord.Member}}, default: [] + field :channels, {:list, {:struct, Dexcord.Channel}}, default: [] + field :threads, {:list, {:struct, Dexcord.Channel}}, default: [] + field :presences, {:list, {:struct, Dexcord.Presence}}, default: [] + field :stage_instances, {:list, :raw}, default: [] + field :guild_scheduled_events, {:list, {:struct, Dexcord.GuildScheduledEvent}}, default: [] + field :soundboard_sounds, {:list, :raw}, default: [] + end + + @doc "The creation `DateTime` encoded in this guild's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) + + import Bitwise + + @all_permissions Dexcord.Permissions.all() |> Map.values() |> Enum.reduce(0, &Bitwise.bor/2) + @timeout_allowed Bitwise.bor( + Dexcord.Permissions.all()[:view_channel], + Dexcord.Permissions.all()[:read_message_history] + ) + + @doc """ + Computes a member's effective permissions in the guild (arity 2) or in a + specific channel (arity 3, applying permission overwrites and the timeout + rule). + + `guild.roles` must be populated — when reading from the cache, set them first: + `%{guild | roles: Dexcord.Cache.roles(guild.id)}`. Returns the raw permissions + integer; combine with `Dexcord.Permissions.has?/2`. + + Implements Discord's documented algorithm: owner → all; base = the @everyone + role (`role id == guild id`) OR'd with each member role; `ADMINISTRATOR` → all, + skipping overwrites entirely; otherwise channel overwrites in order — + @everyone (deny then allow), aggregated role overwrites (all denies OR'd, all + allows OR'd, deny before allow), then the member overwrite; finally the timeout + rule (a member whose `communication_disabled_until` is in the future keeps only + `VIEW_CHANNEL | READ_MESSAGE_HISTORY`, unless owner/administrator — a null or + past value is NOT a timeout). + """ + @spec member_permissions(t(), Dexcord.Member.t(), term()) :: non_neg_integer() + def member_permissions(guild, member, channel \\ nil) + + def member_permissions(%{} = guild, member, channel) do + user_id = member_user_id(member) + + cond do + guild.owner_id == user_id -> + @all_permissions + + true -> + base = base_permissions(guild, member) + + if has_flag?(base, :administrator) do + @all_permissions + else + base + |> apply_channel_overwrites(guild, member, user_id, channel) + |> apply_timeout(member) + end + end + end + + defp member_user_id(member), do: member.user_id || (member.user && member.user.id) + + defp has_flag?(perms, flag), do: Dexcord.Permissions.has?(perms, flag) + + # base = @everyone role perms OR'd with each member role's perms (unknown role + # ids contribute nothing). + defp base_permissions(guild, member) do + Enum.reduce(member.roles, role_permissions(guild, guild.id), fn role_id, acc -> + bor(acc, role_permissions(guild, role_id)) + end) + end + + defp role_permissions(guild, role_id) do + case Enum.find(guild.roles, fn role -> role.id == role_id end) do + nil -> 0 + role -> role.permissions || 0 + end + end + + defp apply_channel_overwrites(perms, _guild, _member, _user_id, nil), do: perms + + defp apply_channel_overwrites(perms, guild, member, user_id, channel) do + overwrites = channel.permission_overwrites || [] + + perms + |> apply_everyone_overwrite(overwrites, guild.id) + |> apply_role_overwrites(overwrites, member.roles) + |> apply_member_overwrite(overwrites, user_id) + end + + defp apply_everyone_overwrite(perms, overwrites, guild_id) do + case Enum.find(overwrites, fn ow -> ow.id == guild_id end) do + nil -> perms + ow -> apply_deny_allow(perms, ow.deny, ow.allow) + end + end + + # All role overwrites for the member's roles are aggregated: denies OR'd, allows + # OR'd, then applied deny-before-allow as a single tier. + defp apply_role_overwrites(perms, overwrites, member_roles) do + {deny, allow} = + overwrites + |> Enum.filter(fn ow -> ow.id in member_roles end) + |> Enum.reduce({0, 0}, fn ow, {deny, allow} -> + {bor(deny, ow.deny || 0), bor(allow, ow.allow || 0)} + end) + + apply_deny_allow(perms, deny, allow) + end + + defp apply_member_overwrite(perms, overwrites, user_id) do + case Enum.find(overwrites, fn ow -> ow.id == user_id end) do + nil -> perms + ow -> apply_deny_allow(perms, ow.deny, ow.allow) + end + end + + defp apply_deny_allow(perms, deny, allow) do + perms + |> band(bnot(deny || 0)) + |> bor(allow || 0) + end + + defp apply_timeout(perms, member) do + case member.communication_disabled_until do + %DateTime{} = cdu -> + if DateTime.compare(cdu, DateTime.utc_now()) == :gt do + band(perms, @timeout_allowed) + else + perms + end + + _ -> + perms + end + end +end + +defmodule Dexcord.UnavailableGuild do + @moduledoc "READY / outage guild stub. On GUILD_DELETE, `unavailable` absent means the bot was removed." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :unavailable, :presence + end +end + +defmodule Dexcord.PartialGuild do + @moduledoc "The GET /users/@me/guilds list shape." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :icon, :string + field :banner, :string + field :owner, :boolean + field :permissions, {:flags, Dexcord.Permissions}, wire_string: true + field :features, {:list, :string}, default: [] + field :approximate_member_count, :integer + field :approximate_presence_count, :integer + end +end diff --git a/lib/dexcord/model/guild_extras.ex b/lib/dexcord/model/guild_extras.ex new file mode 100644 index 0000000..0d03084 --- /dev/null +++ b/lib/dexcord/model/guild_extras.ex @@ -0,0 +1,149 @@ +defmodule Dexcord.WelcomeScreenChannel do + @moduledoc "A channel shown on a guild's welcome screen." + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :description, :string + field :emoji_id, :snowflake + field :emoji_name, :string + end +end + +defmodule Dexcord.WelcomeScreen do + @moduledoc "A guild welcome screen." + use Dexcord.Struct + + discord_struct do + field :description, :string + field :welcome_channels, {:list, {:struct, Dexcord.WelcomeScreenChannel}}, default: [] + end +end + +defmodule Dexcord.Ban do + @moduledoc "A guild ban." + use Dexcord.Struct + + discord_struct do + field :reason, :string + field :user, {:struct, Dexcord.User} + end +end + +defmodule Dexcord.GuildWidgetSettings do + @moduledoc "A guild's widget settings." + use Dexcord.Struct + + discord_struct do + field :enabled, :boolean + field :channel_id, :snowflake + end +end + +defmodule Dexcord.GuildWidget do + @moduledoc "A guild widget." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :instant_invite, :string + field :channels, {:list, :raw}, default: [] + field :members, {:list, :raw}, default: [] + field :presence_count, :integer + end +end + +defmodule Dexcord.OnboardingPromptOption do + @moduledoc "An option within an onboarding prompt." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :channel_ids, {:list, :snowflake}, default: [] + field :role_ids, {:list, :snowflake}, default: [] + field :emoji, {:struct, Dexcord.PartialEmoji} + field :emoji_id, :snowflake + field :emoji_name, :string + field :emoji_animated, :boolean + field :title, :string + field :description, :string + end +end + +defmodule Dexcord.OnboardingPrompt do + @moduledoc "A guild onboarding prompt." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.OnboardingPromptType} + field :options, {:list, {:struct, Dexcord.OnboardingPromptOption}}, default: [] + field :title, :string + field :single_select, :boolean + field :required, :boolean + field :in_onboarding, :boolean + end +end + +defmodule Dexcord.GuildOnboarding do + @moduledoc "A guild's onboarding flow. https://docs.discord.com/developers/resources/guild" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :prompts, {:list, {:struct, Dexcord.OnboardingPrompt}}, default: [] + field :default_channel_ids, {:list, :snowflake}, default: [] + field :enabled, :boolean + field :mode, {:enum, Dexcord.OnboardingMode} + end +end + +defmodule Dexcord.IntegrationAccount do + @moduledoc "An integration account. Note: `id` is a plain string, NOT a snowflake." + use Dexcord.Struct + + discord_struct do + field :id, :string + field :name, :string + end +end + +defmodule Dexcord.IntegrationApplication do + @moduledoc "The bot/OAuth2 application associated with an integration." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :icon, :string + field :description, :string + field :bot, {:struct, Dexcord.User} + end +end + +defmodule Dexcord.Integration do + @moduledoc "A guild integration. https://docs.discord.com/developers/resources/guild" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :type, :string + field :enabled, :boolean + field :syncing, :boolean + field :role_id, :snowflake + field :enable_emoticons, :boolean + field :expire_behavior, {:enum, Dexcord.IntegrationExpireBehavior} + field :expire_grace_period, :integer + field :user, {:struct, Dexcord.User} + field :account, {:struct, Dexcord.IntegrationAccount} + field :synced_at, :datetime + field :subscriber_count, :integer + field :revoked, :boolean + field :application, {:struct, Dexcord.IntegrationApplication} + field :scopes, {:list, :string}, default: [] + # Gateway extra from INTEGRATION_CREATE/UPDATE. + field :guild_id, :snowflake + end +end diff --git a/lib/dexcord/model/interaction.ex b/lib/dexcord/model/interaction.ex new file mode 100644 index 0000000..1985b14 --- /dev/null +++ b/lib/dexcord/model/interaction.ex @@ -0,0 +1,158 @@ +defmodule Dexcord.PartialChannel do + @moduledoc "The partial channel shape in interaction resolved data (current docs: 13 fields + thread_metadata)." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :type, {:enum, Dexcord.ChannelType} + field :permissions, {:flags, Dexcord.Permissions}, wire_string: true + field :last_message_id, :snowflake + field :last_pin_timestamp, :datetime + field :nsfw, :boolean, default: false + field :parent_id, :snowflake + field :guild_id, :snowflake + field :flags, {:flags, Dexcord.ChannelFlags} + field :rate_limit_per_user, :integer + field :topic, :string + field :position, :integer + field :thread_metadata, {:struct, Dexcord.ThreadMetadata} + end +end + +defmodule Dexcord.ResolvedData do + @moduledoc "Interaction resolved data: id-keyed maps of (partial) objects." + use Dexcord.Struct + + discord_struct do + field :users, {:id_map, {:struct, Dexcord.User}}, default: %{} + field :members, {:id_map, {:struct, Dexcord.PartialMember}}, default: %{} + field :roles, {:id_map, {:struct, Dexcord.Role}}, default: %{} + field :channels, {:id_map, {:struct, Dexcord.PartialChannel}}, default: %{} + field :messages, {:id_map, {:struct, Dexcord.Message}}, default: %{} + field :attachments, {:id_map, {:struct, Dexcord.Attachment}}, default: %{} + end +end + +defmodule Dexcord.Interaction.DataOption do + @moduledoc "A single application-command interaction data option (recursive)." + use Dexcord.Struct + + discord_struct do + field :name, :string + field :type, {:enum, Dexcord.ApplicationCommandOptionType} + field :value, :raw + field :options, {:list, {:struct, __MODULE__}}, default: [] + field :focused, :boolean + end +end + +defmodule Dexcord.Interaction.ApplicationCommandData do + @moduledoc "Interaction `data` for APPLICATION_COMMAND / autocomplete interactions." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :type, {:enum, Dexcord.ApplicationCommandType} + field :resolved, {:struct, Dexcord.ResolvedData} + field :options, {:list, {:struct, Dexcord.Interaction.DataOption}}, default: [] + field :guild_id, :snowflake + field :target_id, :snowflake + end +end + +defmodule Dexcord.Interaction.MessageComponentData do + @moduledoc "Interaction `data` for MESSAGE_COMPONENT interactions." + use Dexcord.Struct + + discord_struct do + field :custom_id, :string + field :component_type, {:enum, Dexcord.ComponentType} + field :values, {:list, :string}, default: [] + field :resolved, {:struct, Dexcord.ResolvedData} + end +end + +defmodule Dexcord.Interaction.ModalSubmitData do + @moduledoc """ + Interaction `data` for MODAL_SUBMIT interactions. `components` holds modal + RESPONSE shapes (not component definitions) — deliberately raw. + """ + use Dexcord.Struct + + discord_struct do + field :custom_id, :string + field :components, {:list, :raw}, default: [] + field :resolved, {:struct, Dexcord.ResolvedData} + end +end + +defmodule Dexcord.MessageInteraction do + @moduledoc """ + The deprecated `message.interaction` shape (superseded by + `message.interaction_metadata`, still delivered on messages). + """ + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.InteractionType} + field :name, :string + field :user, {:struct, Dexcord.User} + field :member, {:struct, Dexcord.PartialMember} + end +end + +defmodule Dexcord.Interaction do + @moduledoc "An interaction. https://docs.discord.com/developers/interactions/receiving-and-responding" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :application_id, :snowflake + field :type, {:enum, Dexcord.InteractionType} + field :data, :raw + field :guild, {:struct, Dexcord.Guild} + field :guild_id, :snowflake + field :channel, {:struct, Dexcord.PartialChannel} + field :channel_id, :snowflake + field :member, {:struct, Dexcord.Member} + field :user, {:struct, Dexcord.User} + field :token, :string + field :version, :integer + field :message, {:struct, Dexcord.Message} + field :app_permissions, {:flags, Dexcord.Permissions}, wire_string: true + field :locale, :string + field :guild_locale, :string + field :entitlements, :raw + field :authorizing_integration_owners, :raw + field :context, {:enum, Dexcord.InteractionContextType} + field :attachment_size_limit, :integer + end + + @data_variants %{ + application_command: Dexcord.Interaction.ApplicationCommandData, + application_command_autocomplete: Dexcord.Interaction.ApplicationCommandData, + message_component: Dexcord.Interaction.MessageComponentData, + modal_submit: Dexcord.Interaction.ModalSubmitData + } + + # Polymorphic `data` decode: the generated `from_map/1` (Task 1's + # `defoverridable`) is post-processed to swap the raw `data` map for its + # typed variant based on the interaction `type`. NOTE: match the decoded + # struct as a plain map guarded by `is_struct/2` — `defstruct` is injected + # by `@before_compile`, so `%__MODULE__{}` is not usable in the body. + def from_map(map) do + case super(map) do + %{data: raw} = interaction when is_struct(interaction, __MODULE__) and is_map(raw) -> + case Map.fetch(@data_variants, interaction.type) do + {:ok, mod} -> %{interaction | data: mod.from_map(raw)} + :error -> interaction + end + + other -> + other + end + end +end diff --git a/lib/dexcord/model/invite.ex b/lib/dexcord/model/invite.ex new file mode 100644 index 0000000..0f254f6 --- /dev/null +++ b/lib/dexcord/model/invite.ex @@ -0,0 +1,48 @@ +defmodule Dexcord.Model.InviteShared do + @moduledoc false + # Field group shared by Dexcord.Invite and Dexcord.InviteMetadata + # (InviteMetadata is an Invite plus the extra metadata fields). + + def __included_fields__ do + [ + {:type, {:enum, Dexcord.InviteType}, []}, + {:code, :string, []}, + {:guild, {:struct, Dexcord.PartialGuild}, []}, + {:channel, :raw, []}, + {:inviter, {:struct, Dexcord.User}, []}, + {:target_type, {:enum, Dexcord.InviteTargetType}, []}, + {:target_user, {:struct, Dexcord.User}, []}, + {:target_application, {:struct, Dexcord.ApplicationInfo}, []}, + {:approximate_presence_count, :integer, []}, + {:approximate_member_count, :integer, []}, + {:expires_at, :datetime, []}, + {:stage_instance, :raw, []}, + {:guild_scheduled_event, {:struct, Dexcord.GuildScheduledEvent}, []}, + {:flags, {:flags, Dexcord.InviteFlags}, []}, + {:roles, {:list, :raw}, [default: []]} + ] + end +end + +defmodule Dexcord.Invite do + @moduledoc "A guild/group invite. https://docs.discord.com/developers/resources/invite" + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.InviteShared + end +end + +defmodule Dexcord.InviteMetadata do + @moduledoc "An invite plus extra metadata (uses, max_uses, max_age, temporary, created_at)." + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.InviteShared + field :uses, :integer + field :max_uses, :integer + field :max_age, :integer + field :temporary, :boolean + field :created_at, :datetime + end +end diff --git a/lib/dexcord/model/member.ex b/lib/dexcord/model/member.ex new file mode 100644 index 0000000..d9b2245 --- /dev/null +++ b/lib/dexcord/model/member.ex @@ -0,0 +1,66 @@ +defmodule Dexcord.Model.MemberShared do + @moduledoc false + # Field group shared by Dexcord.Member and Dexcord.PartialMember. + + def __included_fields__ do + [ + {:guild_id, :snowflake, []}, + # Cache-populated back-reference to the nested user's id. Decodes `nil` from + # real payloads (no such wire key); `Dexcord.Cache` sets it on write. + {:user_id, :snowflake, []}, + {:nick, :string, []}, + {:avatar, :string, []}, + {:banner, :string, []}, + {:roles, {:list, :snowflake}, [default: []]}, + {:joined_at, :datetime, []}, + {:premium_since, :datetime, []}, + {:deaf, :boolean, []}, + {:mute, :boolean, []}, + {:flags, {:flags, Dexcord.GuildMemberFlags}, []}, + {:pending, :boolean, []}, + {:permissions, {:flags, Dexcord.Permissions}, [wire_string: true]}, + {:communication_disabled_until, :datetime, []}, + {:avatar_decoration_data, {:struct, Dexcord.AvatarDecorationData}, []}, + {:collectibles, :raw, []} + ] + end +end + +defmodule Dexcord.Member do + @moduledoc "A guild member (with its user). https://docs.discord.com/developers/resources/guild" + use Dexcord.Struct + + discord_struct do + field :user, {:struct, Dexcord.User} + include_fields Dexcord.Model.MemberShared + end + + @doc "The mention string for this member (`<@id>`), from `user_id` or the nested user." + @spec mention(t()) :: String.t() + def mention(%{} = member), do: "<@#{member_user_id(member)}>" + + @doc "The creation `DateTime` encoded in this member's user id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{} = member), + do: Dexcord.Snowflake.to_datetime(member_user_id(member)) + + defp member_user_id(%{user_id: user_id, user: user}) do + user_id || (user && user.id) + end +end + +defimpl String.Chars, for: Dexcord.Member do + def to_string(member), do: Dexcord.Member.mention(member) +end + +defmodule Dexcord.PartialMember do + @moduledoc """ + A guild member without the nested user — the shape Discord sends inside + MESSAGE_CREATE/UPDATE (`message.member`) and interaction resolved data. + """ + use Dexcord.Struct + + discord_struct do + include_fields Dexcord.Model.MemberShared + end +end diff --git a/lib/dexcord/model/message.ex b/lib/dexcord/model/message.ex new file mode 100644 index 0000000..80145c9 --- /dev/null +++ b/lib/dexcord/model/message.ex @@ -0,0 +1,132 @@ +defmodule Dexcord.Message do + @moduledoc "A Discord message. https://docs.discord.com/developers/resources/message" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :channel_id, :snowflake + field :guild_id, :snowflake + field :author, {:struct, Dexcord.User} + field :member, {:struct, Dexcord.PartialMember} + field :content, :string + field :timestamp, :datetime + field :edited_timestamp, :datetime + field :tts, :boolean + field :mention_everyone, :boolean + field :mentions, {:list, {:struct, Dexcord.User}}, default: [] + field :mention_roles, {:list, :snowflake}, default: [] + field :mention_channels, {:list, {:struct, Dexcord.ChannelMention}}, default: [] + field :attachments, {:list, {:struct, Dexcord.Attachment}}, default: [] + field :embeds, {:list, {:struct, Dexcord.Embed}}, default: [] + field :reactions, {:list, {:struct, Dexcord.Reaction}}, default: [] + field :nonce, :raw + field :pinned, :boolean + field :webhook_id, :snowflake + field :type, {:enum, Dexcord.MessageType} + field :channel_type, :integer + field :activity, :raw + field :application, :raw + field :application_id, :snowflake + field :flags, {:flags, Dexcord.MessageFlags} + field :message_reference, {:struct, Dexcord.MessageReference} + field :message_snapshots, {:list, {:struct, Dexcord.MessageSnapshot}} + field :referenced_message, {:struct, __MODULE__}, tristate: true + field :interaction_metadata, :raw + field :interaction, :raw + field :thread, {:struct, Dexcord.Channel} + field :components, {:list, {:struct, Dexcord.Component}}, default: [] + field :sticker_items, {:list, {:struct, Dexcord.StickerItem}}, default: [] + field :stickers, {:list, {:struct, Dexcord.Sticker}} + field :position, :integer + field :role_subscription_data, :raw + field :resolved, {:struct, Dexcord.ResolvedData} + field :poll, {:struct, Dexcord.Poll} + field :call, {:struct, Dexcord.MessageCall} + field :shared_client_theme, :raw + end + + @doc "The creation `DateTime` encoded in this message's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) + + @doc """ + Replies to this message. Sets `message_reference` to the source message and + routes through `Dexcord.Api.send/2` (so the allowed-mentions config default + merge applies). + + `mention_author: true | false` overrides `allowed_mentions.replied_user` + (mirroring discord.py) — it is applied AFTER the body is normalized, so it + wins over any `replied_user` a caller passed in the body. Everything else + behaves like `Dexcord.Api.send/3`. + """ + @spec reply(t(), term(), keyword()) :: + {:ok, t()} | {:error, Dexcord.Api.Error.t()} + def reply(msg, body, opts \\ []) + + def reply(%{id: id, channel_id: channel_id}, body, opts) do + {mention_author, opts} = Keyword.pop(opts, :mention_author) + + body = + body + |> Dexcord.Api.Endpoint.encode_body(%{binary_wrap: :content}) + |> Map.put("message_reference", %{"message_id" => Dexcord.Snowflake.dump(id)}) + |> apply_mention_author(mention_author) + + Dexcord.Api.send(channel_id, body, opts) + end + + defp apply_mention_author(body, nil), do: body + + defp apply_mention_author(body, flag) when is_boolean(flag) do + Map.update( + body, + "allowed_mentions", + %{"replied_user" => flag}, + &Map.put(&1, "replied_user", flag) + ) + end +end + +defmodule Dexcord.MessageReference do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :type, {:enum, Dexcord.MessageReferenceType} + field :message_id, :snowflake + field :channel_id, :snowflake + field :guild_id, :snowflake + field :fail_if_not_exists, :boolean + end +end + +defmodule Dexcord.MessageSnapshot do + @moduledoc "A forwarded-message snapshot (partial message; no author)." + use Dexcord.Struct + + discord_struct do + field :message, {:struct, Dexcord.Message} + end +end + +defmodule Dexcord.MessageCall do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :participants, {:list, :snowflake}, default: [] + field :ended_timestamp, :datetime + end +end + +defmodule Dexcord.ChannelMention do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :type, {:enum, Dexcord.ChannelType} + field :name, :string + end +end diff --git a/lib/dexcord/model/misc.ex b/lib/dexcord/model/misc.ex new file mode 100644 index 0000000..4f7ad9a --- /dev/null +++ b/lib/dexcord/model/misc.ex @@ -0,0 +1,67 @@ +defmodule Dexcord.FollowedChannel do + @moduledoc "A followed announcement channel." + use Dexcord.Struct + + discord_struct do + field :channel_id, :snowflake + field :webhook_id, :snowflake + end +end + +defmodule Dexcord.VoiceRegion do + @moduledoc "A voice region. https://docs.discord.com/developers/resources/voice" + use Dexcord.Struct + + discord_struct do + field :id, :string + field :name, :string + field :optimal, :boolean + field :deprecated, :boolean + field :custom, :boolean + end +end + +defmodule Dexcord.Connection do + @moduledoc "A user connection to an external service." + use Dexcord.Struct + + discord_struct do + field :id, :string + field :name, :string + field :type, :string + field :revoked, :boolean + field :integrations, :raw + field :verified, :boolean + field :friend_sync, :boolean + field :show_activity, :boolean + field :two_way_link, :boolean + field :visibility, {:enum, Dexcord.ConnectionVisibility} + end +end + +defmodule Dexcord.Activity do + @moduledoc "A presence activity. https://docs.discord.com/developers/topics/gateway-events#activity-object" + use Dexcord.Struct + + discord_struct do + field :name, :string + field :type, {:enum, Dexcord.ActivityType} + field :url, :string + # Unix milliseconds — NOT an ISO8601 datetime. + field :created_at, :integer + field :timestamps, :raw + field :application_id, :snowflake + field :status_display_type, {:enum, Dexcord.StatusDisplayType} + field :details, :string + field :details_url, :string + field :state, :string + field :state_url, :string + field :emoji, :raw + field :party, :raw + field :assets, :raw + field :secrets, :raw + field :instance, :boolean + field :flags, {:flags, Dexcord.ActivityFlags} + field :buttons, {:list, :string} + end +end diff --git a/lib/dexcord/model/overwrite.ex b/lib/dexcord/model/overwrite.ex new file mode 100644 index 0000000..e02606e --- /dev/null +++ b/lib/dexcord/model/overwrite.ex @@ -0,0 +1,11 @@ +defmodule Dexcord.Overwrite do + @moduledoc "A channel permission overwrite." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.OverwriteType} + field :allow, {:flags, Dexcord.Permissions}, wire_string: true + field :deny, {:flags, Dexcord.Permissions}, wire_string: true + end +end diff --git a/lib/dexcord/model/poll.ex b/lib/dexcord/model/poll.ex new file mode 100644 index 0000000..48ce6b2 --- /dev/null +++ b/lib/dexcord/model/poll.ex @@ -0,0 +1,55 @@ +defmodule Dexcord.PollMedia do + @moduledoc "The text/emoji content of a poll question or answer." + use Dexcord.Struct + + discord_struct do + field :text, :string + field :emoji, {:struct, Dexcord.PartialEmoji} + end +end + +defmodule Dexcord.PollAnswer do + @moduledoc "A single poll answer." + use Dexcord.Struct + + discord_struct do + field :answer_id, :integer + field :poll_media, {:struct, Dexcord.PollMedia} + end +end + +defmodule Dexcord.PollAnswerCount do + @moduledoc "The vote tally for one poll answer." + use Dexcord.Struct + + discord_struct do + field :id, :integer + field :count, :integer + field :me_voted, :boolean + end +end + +defmodule Dexcord.PollResults do + @moduledoc "The tallied results of a poll." + use Dexcord.Struct + + discord_struct do + field :is_finalized, :boolean + field :answer_counts, {:list, {:struct, Dexcord.PollAnswerCount}}, default: [] + end +end + +defmodule Dexcord.Poll do + @moduledoc "A message poll. https://docs.discord.com/developers/resources/poll" + use Dexcord.Struct + + discord_struct do + field :question, {:struct, Dexcord.PollMedia} + field :answers, {:list, {:struct, Dexcord.PollAnswer}}, default: [] + field :expiry, :datetime + field :allow_multiselect, :boolean + field :layout_type, {:enum, Dexcord.PollLayoutType} + # Tristate: absence does NOT mean "no votes" — Discord genuinely omits it. + field :results, {:struct, Dexcord.PollResults}, tristate: true + end +end diff --git a/lib/dexcord/model/presence.ex b/lib/dexcord/model/presence.ex new file mode 100644 index 0000000..9781381 --- /dev/null +++ b/lib/dexcord/model/presence.ex @@ -0,0 +1,23 @@ +defmodule Dexcord.Presence do + @moduledoc "A presence update. `user` is raw — Discord guarantees only its `id` key." + use Dexcord.Struct + + discord_struct do + field :user, :raw + field :guild_id, :snowflake + field :status, :string + field :activities, {:list, :raw}, default: [] + field :client_status, {:struct, Dexcord.ClientStatus} + end +end + +defmodule Dexcord.ClientStatus do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :desktop, :string + field :mobile, :string + field :web, :string + end +end diff --git a/lib/dexcord/model/reaction.ex b/lib/dexcord/model/reaction.ex new file mode 100644 index 0000000..fe3e383 --- /dev/null +++ b/lib/dexcord/model/reaction.ex @@ -0,0 +1,23 @@ +defmodule Dexcord.Reaction do + @moduledoc "A message reaction. https://docs.discord.com/developers/resources/message" + use Dexcord.Struct + + discord_struct do + field :count, :integer + field :count_details, {:struct, Dexcord.ReactionCountDetails} + field :me, :boolean + field :me_burst, :boolean + field :emoji, {:struct, Dexcord.PartialEmoji} + field :burst_colors, {:list, :string}, default: [] + end +end + +defmodule Dexcord.ReactionCountDetails do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :burst, :integer + field :normal, :integer + end +end diff --git a/lib/dexcord/model/role.ex b/lib/dexcord/model/role.ex new file mode 100644 index 0000000..3ff2779 --- /dev/null +++ b/lib/dexcord/model/role.ex @@ -0,0 +1,61 @@ +defmodule Dexcord.Role do + @moduledoc "A guild role. https://docs.discord.com/developers/topics/permissions" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :color, :integer + field :colors, {:struct, Dexcord.RoleColors} + field :hoist, :boolean + field :icon, :string + field :unicode_emoji, :string + field :position, :integer + field :permissions, {:flags, Dexcord.Permissions}, wire_string: true + field :managed, :boolean + field :mentionable, :boolean + field :tags, {:struct, Dexcord.RoleTags} + field :flags, {:flags, Dexcord.RoleFlags} + end + + @doc "The mention string for this role (`<@&id>`)." + @spec mention(t()) :: String.t() + def mention(%{id: id}), do: "<@&#{id}>" + + @doc "The creation `DateTime` encoded in this role's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defimpl String.Chars, for: Dexcord.Role do + def to_string(role), do: Dexcord.Role.mention(role) +end + +defmodule Dexcord.RoleColors do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :primary_color, :integer + field :secondary_color, :integer + field :tertiary_color, :integer + end +end + +defmodule Dexcord.RoleTags do + @moduledoc """ + Role tags. Docs: "Tags with type null represent booleans. They will be + present and set to null if they are 'true', and will be not present if + they are 'false'." — the three `:presence` fields below. + """ + use Dexcord.Struct + + discord_struct do + field :bot_id, :snowflake + field :integration_id, :snowflake + field :premium_subscriber, :presence + field :subscription_listing_id, :snowflake + field :available_for_purchase, :presence + field :guild_connections, :presence + end +end diff --git a/lib/dexcord/model/scheduled_event.ex b/lib/dexcord/model/scheduled_event.ex new file mode 100644 index 0000000..d23ea4d --- /dev/null +++ b/lib/dexcord/model/scheduled_event.ex @@ -0,0 +1,73 @@ +defmodule Dexcord.GuildScheduledEvent.EntityMetadata do + @moduledoc "Additional metadata for a guild scheduled event's entity (e.g. external location)." + use Dexcord.Struct + + discord_struct do + field :location, :string + end +end + +defmodule Dexcord.GuildScheduledEvent.NWeekday do + @moduledoc "An (n, weekday) pair for a monthly recurrence rule." + use Dexcord.Struct + + discord_struct do + field :n, :integer + field :day, {:enum, Dexcord.RecurrenceRuleWeekday} + end +end + +defmodule Dexcord.GuildScheduledEvent.RecurrenceRule do + @moduledoc "The recurrence rule for a repeating guild scheduled event." + use Dexcord.Struct + + discord_struct do + field :start, :datetime + # The JSON key is "end"; Atom.to_string(:end) == "end", so no special handling. + field :end, :datetime + field :frequency, {:enum, Dexcord.RecurrenceRuleFrequency} + field :interval, :integer + field :by_weekday, {:list, {:enum, Dexcord.RecurrenceRuleWeekday}}, default: [] + field :by_n_weekday, {:list, {:struct, Dexcord.GuildScheduledEvent.NWeekday}}, default: [] + field :by_month, {:list, {:enum, Dexcord.RecurrenceRuleMonth}}, default: [] + field :by_month_day, {:list, :integer}, default: [] + field :by_year_day, {:list, :integer}, default: [] + field :count, :integer + end +end + +defmodule Dexcord.GuildScheduledEvent do + @moduledoc "A guild scheduled event. https://docs.discord.com/developers/resources/guild-scheduled-event" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :guild_id, :snowflake + field :channel_id, :snowflake + field :creator_id, :snowflake + field :name, :string + field :description, :string + field :scheduled_start_time, :datetime + field :scheduled_end_time, :datetime + field :privacy_level, {:enum, Dexcord.GuildScheduledEventPrivacyLevel} + field :status, {:enum, Dexcord.GuildScheduledEventStatus} + field :entity_type, {:enum, Dexcord.GuildScheduledEventEntityType} + field :entity_id, :snowflake + field :entity_metadata, {:struct, Dexcord.GuildScheduledEvent.EntityMetadata} + field :creator, {:struct, Dexcord.User} + field :user_count, :integer + field :image, :string + field :recurrence_rule, {:struct, Dexcord.GuildScheduledEvent.RecurrenceRule} + end +end + +defmodule Dexcord.GuildScheduledEventUser do + @moduledoc "A user subscribed to a guild scheduled event." + use Dexcord.Struct + + discord_struct do + field :guild_scheduled_event_id, :snowflake + field :user, {:struct, Dexcord.User} + field :member, {:struct, Dexcord.Member} + end +end diff --git a/lib/dexcord/model/sticker.ex b/lib/dexcord/model/sticker.ex new file mode 100644 index 0000000..5cf270d --- /dev/null +++ b/lib/dexcord/model/sticker.ex @@ -0,0 +1,29 @@ +defmodule Dexcord.Sticker do + @moduledoc "A message sticker. https://docs.discord.com/developers/resources/sticker" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :pack_id, :snowflake + field :name, :string + field :description, :string + field :tags, :string + field :type, {:enum, Dexcord.StickerType} + field :format_type, {:enum, Dexcord.StickerFormatType} + field :available, :boolean + field :guild_id, :snowflake + field :user, {:struct, Dexcord.User} + field :sort_value, :integer + end +end + +defmodule Dexcord.StickerItem do + @moduledoc "The smallest amount of data required to render a sticker." + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :format_type, {:enum, Dexcord.StickerFormatType} + end +end diff --git a/lib/dexcord/model/team.ex b/lib/dexcord/model/team.ex new file mode 100644 index 0000000..443df14 --- /dev/null +++ b/lib/dexcord/model/team.ex @@ -0,0 +1,24 @@ +defmodule Dexcord.TeamMember do + @moduledoc "A member of a developer team." + use Dexcord.Struct + + discord_struct do + field :membership_state, {:enum, Dexcord.TeamMembershipState} + field :team_id, :snowflake + field :user, {:struct, Dexcord.User} + field :role, :string + end +end + +defmodule Dexcord.Team do + @moduledoc "A developer team. https://docs.discord.com/developers/topics/teams" + use Dexcord.Struct + + discord_struct do + field :icon, :string + field :id, :snowflake + field :members, {:list, {:struct, Dexcord.TeamMember}}, default: [] + field :name, :string + field :owner_user_id, :snowflake + end +end diff --git a/lib/dexcord/model/user.ex b/lib/dexcord/model/user.ex new file mode 100644 index 0000000..6ed188a --- /dev/null +++ b/lib/dexcord/model/user.ex @@ -0,0 +1,60 @@ +defmodule Dexcord.User do + @moduledoc "A Discord user. https://docs.discord.com/developers/resources/user" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :username, :string + field :discriminator, :string + field :global_name, :string + field :avatar, :string + field :bot, :boolean, default: false + field :system, :boolean, default: false + field :mfa_enabled, :boolean + field :banner, :string + field :accent_color, :integer + field :locale, :string + field :verified, :boolean + field :email, :string + field :flags, {:flags, Dexcord.UserFlags} + field :premium_type, {:enum, Dexcord.PremiumType} + field :public_flags, {:flags, Dexcord.UserFlags} + field :avatar_decoration_data, {:struct, Dexcord.AvatarDecorationData} + field :collectibles, :raw + field :primary_guild, {:struct, Dexcord.PrimaryGuild} + end + + @doc "The mention string for this user (`<@id>`)." + @spec mention(t()) :: String.t() + def mention(%{id: id}), do: "<@#{id}>" + + @doc "The creation `DateTime` encoded in this user's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end + +defimpl String.Chars, for: Dexcord.User do + def to_string(user), do: Dexcord.User.mention(user) +end + +defmodule Dexcord.AvatarDecorationData do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :asset, :string + field :sku_id, :snowflake + end +end + +defmodule Dexcord.PrimaryGuild do + @moduledoc "A user's primary-guild (server tag) identity." + use Dexcord.Struct + + discord_struct do + field :identity_guild_id, :snowflake + field :identity_enabled, :boolean + field :tag, :string + field :badge, :string + end +end diff --git a/lib/dexcord/model/voice_state.ex b/lib/dexcord/model/voice_state.ex new file mode 100644 index 0000000..10203e1 --- /dev/null +++ b/lib/dexcord/model/voice_state.ex @@ -0,0 +1,20 @@ +defmodule Dexcord.VoiceState do + @moduledoc "A user's voice connection state. https://docs.discord.com/developers/resources/voice" + use Dexcord.Struct + + discord_struct do + field :guild_id, :snowflake + field :channel_id, :snowflake + field :user_id, :snowflake + field :member, {:struct, Dexcord.Member} + field :session_id, :string + field :deaf, :boolean + field :mute, :boolean + field :self_deaf, :boolean + field :self_mute, :boolean + field :self_stream, :boolean, default: false + field :self_video, :boolean + field :suppress, :boolean + field :request_to_speak_timestamp, :datetime + end +end diff --git a/lib/dexcord/model/webhook.ex b/lib/dexcord/model/webhook.ex new file mode 100644 index 0000000..984dbe7 --- /dev/null +++ b/lib/dexcord/model/webhook.ex @@ -0,0 +1,23 @@ +defmodule Dexcord.Webhook do + @moduledoc "A webhook. https://docs.discord.com/developers/resources/webhook" + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :type, {:enum, Dexcord.WebhookType} + field :guild_id, :snowflake + field :channel_id, :snowflake + field :user, {:struct, Dexcord.User} + field :name, :string + field :avatar, :string + field :token, :string + field :application_id, :snowflake + field :source_guild, {:struct, Dexcord.PartialGuild} + field :source_channel, :raw + field :url, :string + end + + @doc "The creation `DateTime` encoded in this webhook's id, or `:error`." + @spec created_at(t()) :: {:ok, DateTime.t()} | :error + def created_at(%{id: id}), do: Dexcord.Snowflake.to_datetime(id) +end diff --git a/lib/dexcord/prefix.ex b/lib/dexcord/prefix.ex index 2136053..c28aa8c 100644 --- a/lib/dexcord/prefix.ex +++ b/lib/dexcord/prefix.ex @@ -4,7 +4,7 @@ defmodule Dexcord.Prefix do `parse/2` is a pure function that recognises a prefixed command and splits it into a command word plus its argument string / argument list. `dispatch/2` - wires a raw `MESSAGE_CREATE` payload through `parse/2` into a + wires a decoded `%Dexcord.Message{}` (`MESSAGE_CREATE`) through `parse/2` into a `Dexcord.Prefix.Router` module, skipping messages authored by bots (including the bot itself). A router `use`s `Dexcord.Prefix.Router` and defines `handle_command/3` clauses for the commands it cares about; an `:ignore` @@ -14,11 +14,11 @@ defmodule Dexcord.Prefix do use Dexcord.Prefix.Router def handle_command("ping", _args, msg), - do: Dexcord.Api.create_message(msg["channel_id"], "pong") + do: Dexcord.Api.create_message(msg.channel_id, "pong") end # in the handler: - def handle_event({:MESSAGE_CREATE, msg}), + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}), do: Dexcord.Prefix.dispatch(msg, prefix: "!", to: MyBot.Commands) """ @@ -77,7 +77,8 @@ defmodule Dexcord.Prefix do end @doc """ - Routes a raw `MESSAGE_CREATE` payload to a `Dexcord.Prefix.Router`. + Routes a decoded `%Dexcord.Message{}` (`MESSAGE_CREATE`) to a + `Dexcord.Prefix.Router`. Options: @@ -93,29 +94,30 @@ defmodule Dexcord.Prefix do On a match the router's `handle_command(command, args, msg)` is called and its result returned; a non-match (or a skipped bot message) returns `:ignore`. """ - @spec dispatch(map(), keyword()) :: any() - def dispatch(msg, opts) when is_map(msg) do + @spec dispatch(Dexcord.Message.t(), keyword()) :: any() + def dispatch(%Dexcord.Message{} = msg, opts) do prefix = Keyword.fetch!(opts, :prefix) router = Keyword.fetch!(opts, :to) if bot_author?(msg) do :ignore else - case parse(msg["content"] || "", prefix) do + case parse(msg.content || "", prefix) do {:ok, command, _arg_string, args} -> router.handle_command(command, args, msg) :nomatch -> :ignore end end end - defp bot_author?(msg) do - author = msg["author"] || %{} - author["bot"] == true or own_message?(author) + defp bot_author?(%Dexcord.Message{author: nil}), do: true + + defp bot_author?(%Dexcord.Message{author: author}) do + author.bot == true or own_message?(author) end - defp own_message?(author) do + defp own_message?(%Dexcord.User{id: id}) do case Dexcord.Cache.me() do - {:ok, %{"id" => id}} -> is_binary(id) and author["id"] == id + {:ok, %Dexcord.User{id: me_id}} -> id == me_id _ -> false end end @@ -128,10 +130,14 @@ defmodule Dexcord.Prefix.Router do `use Dexcord.Prefix.Router` declares the behaviour and injects an `:ignore` catch-all `handle_command/3`, so a router only writes the command clauses it wants. `handle_command/3` receives the command word, the whitespace-split - argument list, and the raw `MESSAGE_CREATE` map. + argument list, and the decoded `%Dexcord.Message{}`. """ - @callback handle_command(command :: String.t(), args :: [String.t()], msg :: map()) :: any() + @callback handle_command( + command :: String.t(), + args :: [String.t()], + msg :: Dexcord.Message.t() + ) :: any() defmacro __using__(_opts) do quote do diff --git a/lib/dexcord/slash.ex b/lib/dexcord/slash.ex index c10c435..7aafd26 100644 --- a/lib/dexcord/slash.ex +++ b/lib/dexcord/slash.ex @@ -31,13 +31,13 @@ defmodule Dexcord.Slash do def handle_modal("feedback_form", itx), do: Dexcord.Slash.respond(itx, "thanks!") end - `dispatch/2` routes a raw `INTERACTION_CREATE` payload on its **top-level** - `interaction["type"]`: type 2 (application command) → `handle_interaction/2` - keyed on `interaction["data"]["name"]`; type 3 (message component) → - `handle_component/2` keyed on `interaction["data"]["custom_id"]`; type 5 (modal - submit) → `handle_modal/2` keyed on `interaction["data"]["custom_id"]`. The + `dispatch/2` routes a decoded `%Dexcord.Interaction{}` on its `type` atom: + `:application_command` → `handle_interaction/2` keyed on the data's `name`; + `:message_component` → `handle_component/2` keyed on the data's `custom_id`; + `:modal_submit` → `handle_modal/2` keyed on the data's `custom_id`. The `Dexcord.Dispatcher` calls it automatically for those types when a `slash:` - module is configured (the raw event still reaches the handler). + module is configured (the event still reaches the handler too). Every callback + receives the full `%Dexcord.Interaction{}` as its second argument. ## Response helpers @@ -56,13 +56,22 @@ defmodule Dexcord.Slash do @callback commands() :: [map()] @doc "Handles a routed application-command interaction (type 2) for command `name`." - @callback handle_interaction(name :: String.t(), interaction :: map()) :: any() + @callback handle_interaction( + name :: String.t() | nil, + interaction :: Dexcord.Interaction.t() + ) :: any() @doc "Handles a routed message-component interaction (type 3) for `custom_id`." - @callback handle_component(custom_id :: String.t() | nil, interaction :: map()) :: any() + @callback handle_component( + custom_id :: String.t() | nil, + interaction :: Dexcord.Interaction.t() + ) :: any() @doc "Handles a routed modal-submit interaction (type 5) for `custom_id`." - @callback handle_modal(custom_id :: String.t() | nil, interaction :: map()) :: any() + @callback handle_modal( + custom_id :: String.t() | nil, + interaction :: Dexcord.Interaction.t() + ) :: any() # Only `commands/0` and `handle_interaction/2` are required; a module that never # uses components or modals need not define those callbacks (the injected @@ -112,21 +121,42 @@ defmodule Dexcord.Slash do # --- routing ------------------------------------------------------------ @doc """ - Routes an `INTERACTION_CREATE` payload to `mod` on its top-level `"type"`. + Routes a decoded `%Dexcord.Interaction{}` to `mod` on its `type` atom. - * type 2 (application command) → `mod.handle_interaction/2`, keyed on - `interaction["data"]["name"]` - * type 3 (message component) → `mod.handle_component/2`, keyed on - `interaction["data"]["custom_id"]` - * type 5 (modal submit) → `mod.handle_modal/2`, keyed on - `interaction["data"]["custom_id"]` + * `:application_command` → `mod.handle_interaction/2`, keyed on the data's `name` + * `:message_component` → `mod.handle_component/2`, keyed on the data's `custom_id` + * `:modal_submit` → `mod.handle_modal/2`, keyed on the data's `custom_id` - Any other (or missing) type is ignored - the `Dexcord.Dispatcher` only routes - types 2/3/5 here, so this is defensive. + Any other (or nil) type is ignored - the `Dexcord.Dispatcher` only routes those + three types here, so this is defensive. + + A malformed `INTERACTION_CREATE` that fails to decode into a struct is routed by + the dispatcher's integer-type gate to the **degraded raw-map head** below, which + reproduces the pre-typed routing on the top-level integer `"type"` so a + not-quite-decodable interaction still reaches the handler with the raw map. """ - @spec dispatch(map(), module()) :: any() - def dispatch(interaction, mod) when is_map(interaction) and is_atom(mod) do - case interaction["type"] do + @spec dispatch(Dexcord.Interaction.t() | map(), module()) :: any() + def dispatch(%Dexcord.Interaction{} = interaction, mod) when is_atom(mod) do + case interaction.type do + :application_command -> + mod.handle_interaction(data_field(interaction, :name), interaction) + + :message_component -> + mod.handle_component(data_field(interaction, :custom_id), interaction) + + :modal_submit -> + mod.handle_modal(data_field(interaction, :custom_id), interaction) + + _ -> + :ignore + end + end + + # Degraded raw-map head: a raw INTERACTION_CREATE the decoder could not turn into + # a %Dexcord.Interaction{}. Routes exactly as the pre-typed dispatcher did, on the + # top-level integer "type", handing the raw map to the callback. + def dispatch(%{"type" => type} = interaction, mod) when is_atom(mod) do + case type do 2 -> mod.handle_interaction(get_in(interaction, ["data", "name"]), interaction) 3 -> mod.handle_component(get_in(interaction, ["data", "custom_id"]), interaction) 5 -> mod.handle_modal(get_in(interaction, ["data", "custom_id"]), interaction) @@ -134,6 +164,12 @@ defmodule Dexcord.Slash do end end + # Reads a field from an interaction's typed `data` variant struct (or nil-data). + # `data` is one of the ApplicationCommandData / MessageComponentData / + # ModalSubmitData structs - all plain maps to `Map.get/2`; nil-data yields nil. + defp data_field(%{data: %{} = data}, key), do: Map.get(data, key) + defp data_field(_interaction, _key), do: nil + # --- response helpers --------------------------------------------------- @doc """ @@ -142,15 +178,16 @@ defmodule Dexcord.Slash do `text_or_map` is either a binary (used as `content`) or a map supporting `content`, `embeds`, `components`, and `ephemeral: true`. """ - @spec respond(map(), String.t() | map()) :: {:ok, map()} | {:ok, nil} | {:error, term()} + @spec respond(Dexcord.Interaction.t(), String.t() | map()) :: + {:ok, map()} | {:ok, nil} | {:error, term()} def respond(interaction, content) when is_binary(content) do respond(interaction, %{content: content}) end - def respond(interaction, %{} = data) do + def respond(%Dexcord.Interaction{} = interaction, %{} = data) do Dexcord.Api.create_interaction_response( - interaction["id"], - interaction["token"], + interaction.id, + interaction.token, %{"type" => 4, "data" => message_data(data)} ) end @@ -159,11 +196,11 @@ defmodule Dexcord.Slash do Sends a deferred response (type 5) - shows a loading state while you prepare a followup or edit the original response. """ - @spec respond_later(map()) :: {:ok, map()} | {:ok, nil} | {:error, term()} - def respond_later(interaction) do + @spec respond_later(Dexcord.Interaction.t()) :: {:ok, map()} | {:ok, nil} | {:error, term()} + def respond_later(%Dexcord.Interaction{} = interaction) do Dexcord.Api.create_interaction_response( - interaction["id"], - interaction["token"], + interaction.id, + interaction.token, %{"type" => 5} ) end @@ -173,15 +210,15 @@ defmodule Dexcord.Slash do `text_or_map` is a binary (used as `content`) or a map as in `respond/2`. """ - @spec followup(map(), String.t() | map()) :: {:ok, map()} | {:error, term()} + @spec followup(Dexcord.Interaction.t(), String.t() | map()) :: {:ok, map()} | {:error, term()} def followup(interaction, content) when is_binary(content) do followup(interaction, %{content: content}) end - def followup(interaction, %{} = data) do + def followup(%Dexcord.Interaction{} = interaction, %{} = data) do Dexcord.Api.create_followup_message( - interaction["application_id"], - interaction["token"], + interaction.application_id, + interaction.token, message_data(data) ) end @@ -191,15 +228,16 @@ defmodule Dexcord.Slash do `text_or_map` is a binary (used as `content`) or a map as in `respond/2`. """ - @spec edit_response(map(), String.t() | map()) :: {:ok, map()} | {:error, term()} + @spec edit_response(Dexcord.Interaction.t(), String.t() | map()) :: + {:ok, map()} | {:error, term()} def edit_response(interaction, content) when is_binary(content) do edit_response(interaction, %{content: content}) end - def edit_response(interaction, %{} = data) do + def edit_response(%Dexcord.Interaction{} = interaction, %{} = data) do Dexcord.Api.edit_original_interaction_response( - interaction["application_id"], - interaction["token"], + interaction.application_id, + interaction.token, message_data(data) ) end @@ -213,7 +251,7 @@ defmodule Dexcord.Slash do base = Enum.reduce([:content, :embeds, :components], %{}, fn field, acc -> case fetch_any(data, field) do - {:ok, value} -> Map.put(acc, Atom.to_string(field), value) + {:ok, value} -> Map.put(acc, Atom.to_string(field), encode_values(value)) :error -> acc end end) @@ -224,6 +262,14 @@ defmodule Dexcord.Slash do end end + # Normalizes recognised field values for the wire: Dexcord structs (e.g. + # `%Dexcord.Embed{}`) go through their own `to_map/1`; lists recurse; anything + # else (a binary, a plain atom/string-keyed map) passes through unchanged and is + # serialized directly by the JSON encoder downstream. + defp encode_values(%_{} = struct), do: struct.__struct__.to_map(struct) + defp encode_values(list) when is_list(list), do: Enum.map(list, &encode_values/1) + defp encode_values(value), do: value + # Combines a caller-supplied integer `flags` with the ephemeral bit (64). # Returns nil when neither is present so the key is omitted entirely. defp flags(data) do diff --git a/lib/dexcord/slash/registrar.ex b/lib/dexcord/slash/registrar.ex index b9088ae..a3fc80e 100644 --- a/lib/dexcord/slash/registrar.ex +++ b/lib/dexcord/slash/registrar.ex @@ -9,7 +9,7 @@ defmodule Dexcord.Slash.Registrar do On start it: - 1. calls `GET /oauth2/applications/@me` to learn the application id and caches + 1. calls `GET /applications/@me` to learn the application id and caches it via `Dexcord.Config.put_application_id/1` (a dedicated `:persistent_term` key, so any process can read it back with `Dexcord.Config.application_id/0`); 2. bulk-overwrites the module's `commands/0`: @@ -99,7 +99,15 @@ defmodule Dexcord.Slash.Registrar do # A single end-to-end registration: resolve the app id, then overwrite commands. # Returns :ok | {:error, message} - never exits. defp try_register(config) do - commands = config.slash.commands() + # Command defs may be plain maps or Dexcord command-builder structs; normalize + # struct defs to their wire maps before the (untouched) downstream register/3. + # `[]` normalizes to `[]`, so the empty-list global guard below still fires. + commands = + config.slash.commands() + |> Enum.map(fn + %_{} = struct -> struct.__struct__.to_map(struct) + map when is_map(map) -> map + end) with {:ok, app_id} <- resolve_app_id() do Dexcord.Config.put_application_id(app_id) @@ -109,12 +117,12 @@ defmodule Dexcord.Slash.Registrar do defp resolve_app_id do case Dexcord.Api.get_current_application() do - {:ok, %{"id" => id}} when is_binary(id) -> + {:ok, %Dexcord.ApplicationInfo{id: id}} when not is_nil(id) -> {:ok, id} other -> {:error, - "could not resolve the application id (GET /oauth2/applications/@me): " <> + "could not resolve the application id (GET /applications/@me): " <> inspect(other)} end end diff --git a/lib/dexcord/snowflake.ex b/lib/dexcord/snowflake.ex index e8c6b45..69687bc 100644 --- a/lib/dexcord/snowflake.ex +++ b/lib/dexcord/snowflake.ex @@ -2,18 +2,53 @@ defmodule Dexcord.Snowflake do @moduledoc """ Discord snowflake id helpers, replacing `Nostrum.Snowflake`. - A snowflake is a 64-bit id whose top bits encode the creation time as - milliseconds since the Discord epoch (2015-01-01). Ids are accepted either as - integers or as the raw decimal strings Discord sends over the wire. + A snowflake is a 64-bit unsigned integer encoding a creation timestamp. + Dexcord represents snowflakes as plain integers everywhere; wire strings + are normalized to integers once, at decode time, via `cast/1`. """ import Bitwise @discord_epoch_ms 1_420_070_400_000 @timestamp_shift 22 + @max 0xFFFF_FFFF_FFFF_FFFF - @typedoc "A snowflake id, as an integer or a Discord decimal string." - @type t :: integer() | String.t() + @type t :: 0..0xFFFF_FFFF_FFFF_FFFF + + @doc "Guard: `term` is an integer in the valid snowflake range." + defguard is_snowflake(term) when is_integer(term) and term >= 0 and term <= @max + + @doc """ + Casts a snowflake-ish value to an integer snowflake. + + Accepts an integer in range, a decimal string, or a struct/map with an + `:id` field holding an in-range integer. + """ + @spec cast(term()) :: {:ok, t()} | :error + def cast(int) when is_snowflake(int), do: {:ok, int} + + def cast(string) when is_binary(string) do + case Integer.parse(string) do + {int, ""} when is_snowflake(int) -> {:ok, int} + _ -> :error + end + end + + def cast(%{id: id}) when is_snowflake(id), do: {:ok, id} + def cast(_), do: :error + + @doc "Like `cast/1` but raises `ArgumentError` on invalid input." + @spec cast!(term()) :: t() + def cast!(value) do + case cast(value) do + {:ok, int} -> int + :error -> raise ArgumentError, "not a snowflake: #{inspect(value)}" + end + end + + @doc "Dumps a snowflake to its wire (string) representation." + @spec dump(t()) :: String.t() + def dump(snowflake) when is_snowflake(snowflake), do: Integer.to_string(snowflake) @doc """ Builds the smallest snowflake for a `DateTime` (timestamp bits only). @@ -30,30 +65,15 @@ defmodule Dexcord.Snowflake do def from_datetime(_), do: :error - @doc "The creation `DateTime` encoded in a snowflake, or `:error` if unparseable." - @spec to_datetime(t()) :: {:ok, DateTime.t()} | :error - def to_datetime(snowflake) do - case to_integer(snowflake) do - {:ok, int} -> DateTime.from_unix(to_unix(int), :millisecond) - :error -> :error - end - end + @doc "The creation `DateTime` encoded in a snowflake, or `:error` for non-snowflakes." + @spec to_datetime(term()) :: {:ok, DateTime.t()} | :error + def to_datetime(snowflake) when is_snowflake(snowflake), + do: DateTime.from_unix(to_unix(snowflake), :millisecond) + + def to_datetime(_), do: :error @doc "The creation time of a snowflake as Unix milliseconds." @spec to_unix(t()) :: integer() - def to_unix(snowflake) do - {:ok, int} = to_integer(snowflake) - (int >>> @timestamp_shift) + @discord_epoch_ms - end - - defp to_integer(int) when is_integer(int), do: {:ok, int} - - defp to_integer(str) when is_binary(str) do - case Integer.parse(str) do - {int, ""} -> {:ok, int} - _ -> :error - end - end - - defp to_integer(_), do: :error + def to_unix(snowflake) when is_snowflake(snowflake), + do: (snowflake >>> @timestamp_shift) + @discord_epoch_ms end diff --git a/lib/dexcord/struct.ex b/lib/dexcord/struct.ex new file mode 100644 index 0000000..0f08bda --- /dev/null +++ b/lib/dexcord/struct.ex @@ -0,0 +1,247 @@ +defmodule Dexcord.Struct do + @moduledoc """ + A DSL for declaring Discord object shapes once. + + One `field` declaration per field generates, at compile time, the + `defstruct`, `@type t`, `from_map/1` (string-keyed wire map -> struct, + minting zero atoms), `to_map/1` (struct -> JSON-ready map), and + `merge_map/2` (partial-update semantics for the cache). + + defmodule Dexcord.Example do + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :roles, {:list, :snowflake}, default: [] + end + end + + Field types: `:snowflake`, `:string`, `:integer`, `:number` (int or float), + `:boolean`, `:datetime`, `{:struct, Module}`, `{:list, type}`, + `{:enum, Module}`, `{:flags, Module}`, `:raw` (kept verbatim). Modifiers: + `default:`, `tristate: true` (`:absent | nil | value`), `presence: true` + (key-presence boolean), `wire_string: true` (re-encode an integer value as a + decimal string, for bitfields that exceed 2^53 like permissions). + + `{:flags, Module}` decodes both integers and decimal strings (Discord sends + permission bitfields as strings); pair it with `wire_string: true` to + round-trip back to a string. + + `include_fields Provider` pulls a shared field group from a plain-data module + exporting `__included_fields__/0` (a list of `{name, type, opts}`); the fields + land in declaration position, indistinguishable from hand-written `field`s. + + `hydrate slot, from: :id_field, type: Module` declares a cache-fillable + slot (always `nil` after decode; populated only by `Dexcord.Cache.fill/1`). + """ + + @scalar_types [:snowflake, :string, :integer, :number, :boolean, :datetime, :raw] + + defmacro __using__(_opts) do + quote do + import Dexcord.Struct, only: [discord_struct: 1] + Module.register_attribute(__MODULE__, :dexcord_fields, accumulate: true) + Module.register_attribute(__MODULE__, :dexcord_hydrates, accumulate: true) + @before_compile Dexcord.Struct + + # The generated decode/encode/merge entrypoints are defined here — before + # the module body — and marked `defoverridable`, so a model may define its + # own clause in the body and post-process the generated result via `super/1` + # (e.g. `Dexcord.Interaction`'s polymorphic `data` decode). They take plain + # arguments and delegate to the Codec, which does the struct matching at + # runtime; the `defstruct` they'd otherwise pattern-match is only injected + # later, by `@before_compile`. + @doc "Decodes a string-keyed wire map into `t()`. Non-maps decode to `nil`." + @spec from_map(term()) :: t() | nil + def from_map(map), do: Dexcord.Struct.Codec.from_map(__MODULE__, map) + + @doc "Encodes to a JSON-ready string-keyed map. `nil`/`:absent` fields are omitted." + @spec to_map(t()) :: %{optional(String.t()) => term()} + def to_map(struct), do: Dexcord.Struct.Codec.to_map(struct) + + @doc "Applies a partial wire map, updating only keys present in it." + @spec merge_map(t(), map()) :: t() + def merge_map(struct, map), do: Dexcord.Struct.Codec.merge_map(struct, map) + + defoverridable from_map: 1, to_map: 1, merge_map: 2 + end + end + + defmacro discord_struct(do: block) do + quote do + try do + import Dexcord.Struct, only: [field: 2, field: 3, hydrate: 2, include_fields: 1] + unquote(block) + after + :ok + end + end + end + + @doc false + defmacro field(name, type, opts \\ []) do + quote do + @dexcord_fields {unquote(name), unquote(Macro.escape(type)), unquote(opts)} + end + end + + @doc false + defmacro include_fields(mod) do + quote do + Enum.each(unquote(mod).__included_fields__(), fn {name, type, opts} -> + Module.put_attribute(__MODULE__, :dexcord_fields, {name, type, opts}) + end) + end + end + + @doc false + defmacro hydrate(name, opts) do + quote do + @dexcord_hydrates {unquote(name), unquote(Macro.escape(opts))} + end + end + + @doc false + defmacro __before_compile__(env) do + module = env.module + + raw_fields = Module.get_attribute(module, :dexcord_fields) || [] + raw_hydrates = Module.get_attribute(module, :dexcord_hydrates) || [] + + if raw_fields == [] and raw_hydrates == [] do + raise ArgumentError, + "#{inspect(module)}: `use Dexcord.Struct` requires a `discord_struct do ... end` " <> + "block declaring at least one `field` or `hydrate`" + end + + fields = + raw_fields + |> Enum.reverse() + |> Enum.map(fn {name, type_ast, opts} -> build_field(name, type_ast, opts, env) end) + + hydrates = + raw_hydrates + |> Enum.reverse() + |> Enum.map(fn {name, opts} -> build_hydrate(name, opts, env) end) + + struct_defaults = + Enum.map(fields, fn f -> {f.name, f.default} end) ++ + Enum.map(hydrates, fn h -> {h.name, nil} end) + + field_type_pairs = + Enum.map(fields, fn f -> {f.name, field_typespec(f, module)} end) ++ + Enum.map(hydrates, fn h -> {h.name, hydrate_typespec(h, module)} end) + + type_ast = {:%, [], [{:__MODULE__, [], Elixir}, {:%{}, [], field_type_pairs}]} + + quote do + defstruct unquote(Macro.escape(struct_defaults)) + + @type t :: unquote(type_ast) + + @doc false + def __fields__, do: unquote(Macro.escape(fields)) + + @doc false + def __hydrations__, do: unquote(Macro.escape(hydrates)) + end + end + + # -- compile-time helpers (run in the macro, not in generated code) -- + + @doc false + def build_field(name, type_ast, opts, env) when type_ast == :presence, + do: build_field(name, :boolean, Keyword.put(opts, :presence, true), env) + + def build_field(name, type_ast, opts, env) do + type = resolve_type(type_ast, env) + validate_type!(type, name, env.module) + tristate = Keyword.get(opts, :tristate, false) + presence = Keyword.get(opts, :presence, false) + + if tristate and presence do + raise ArgumentError, + "#{inspect(env.module)}.#{name}: tristate and presence are mutually exclusive" + end + + default = + cond do + Keyword.has_key?(opts, :default) -> Keyword.fetch!(opts, :default) + tristate -> :absent + presence -> false + true -> nil + end + + %{ + name: name, + key: Atom.to_string(name), + type: type, + default: default, + tristate: tristate, + presence: presence, + wire_string: Keyword.get(opts, :wire_string, false) + } + end + + @doc false + def build_hydrate(name, opts, env) do + %{ + name: name, + from: Keyword.fetch!(opts, :from), + type: resolve_type(Keyword.fetch!(opts, :type), env) + } + end + + defp resolve_type({:struct, mod_ast}, env), do: {:struct, resolve_type(mod_ast, env)} + defp resolve_type({:list, inner}, env), do: {:list, resolve_type(inner, env)} + defp resolve_type({:enum, mod_ast}, env), do: {:enum, resolve_type(mod_ast, env)} + defp resolve_type({:flags, mod_ast}, env), do: {:flags, resolve_type(mod_ast, env)} + defp resolve_type({:id_map, inner}, env), do: {:id_map, resolve_type(inner, env)} + defp resolve_type(atom, _env) when is_atom(atom), do: atom + defp resolve_type(ast, env), do: Macro.expand(ast, env) + + defp validate_type!(type, _name, _module) when type in @scalar_types, do: :ok + defp validate_type!({:struct, mod}, _name, _module) when is_atom(mod), do: :ok + defp validate_type!({:enum, mod}, _name, _module) when is_atom(mod), do: :ok + defp validate_type!({:flags, mod}, _name, _module) when is_atom(mod), do: :ok + defp validate_type!({:list, inner}, name, module), do: validate_type!(inner, name, module) + defp validate_type!({:id_map, inner}, name, module), do: validate_type!(inner, name, module) + + defp validate_type!(other, name, module) do + raise ArgumentError, + "#{inspect(module)}.#{name}: unknown field type #{inspect(other)}" + end + + # -- typespec construction -- + + defp field_typespec(%{presence: true}, _module), do: quote(do: boolean()) + + defp field_typespec(%{tristate: true} = f, module), + do: quote(do: :absent | nil | unquote(base_typespec(f.type, module))) + + defp field_typespec(f, module), + do: quote(do: unquote(base_typespec(f.type, module)) | nil) + + defp hydrate_typespec(%{type: :channel}, _module), do: quote(do: struct() | nil) + + defp hydrate_typespec(%{type: mod}, module), + do: quote(do: unquote(base_typespec({:struct, mod}, module)) | nil) + + defp base_typespec(:snowflake, _m), do: quote(do: Dexcord.Snowflake.t()) + defp base_typespec(:string, _m), do: quote(do: String.t()) + defp base_typespec(:integer, _m), do: quote(do: integer()) + defp base_typespec(:number, _m), do: quote(do: number()) + defp base_typespec(:boolean, _m), do: quote(do: boolean()) + defp base_typespec(:datetime, _m), do: quote(do: DateTime.t()) + defp base_typespec(:raw, _m), do: quote(do: term()) + defp base_typespec({:struct, mod}, mod), do: quote(do: t()) + defp base_typespec({:struct, mod}, _m), do: quote(do: unquote(mod).t()) + defp base_typespec({:list, inner}, m), do: quote(do: [unquote(base_typespec(inner, m))]) + + defp base_typespec({:id_map, inner}, m), + do: quote(do: %{optional(Dexcord.Snowflake.t()) => unquote(base_typespec(inner, m))}) + + defp base_typespec({:enum, mod}, _m), do: quote(do: unquote(mod).t()) + defp base_typespec({:flags, _mod}, _m), do: quote(do: non_neg_integer()) +end diff --git a/lib/dexcord/struct/codec.ex b/lib/dexcord/struct/codec.ex new file mode 100644 index 0000000..cb146c8 --- /dev/null +++ b/lib/dexcord/struct/codec.ex @@ -0,0 +1,159 @@ +defmodule Dexcord.Struct.Codec do + @moduledoc false + # Runtime walker behind every generated from_map/1, to_map/1, merge_map/2. + # All decode tolerance semantics live here, in one place. + + @doc false + def from_map(module, map) when is_map(map) and not is_struct(map) do + fields = + for f <- module.__fields__() do + {f.name, decode_field(f, map)} + end + + struct!(module, fields) + end + + def from_map(_module, _non_map), do: nil + + @doc false + def decode_field(%{presence: true} = f, map), do: Map.has_key?(map, f.key) + + def decode_field(f, map) do + case Map.fetch(map, f.key) do + :error -> f.default + {:ok, nil} -> nil + {:ok, value} -> decode_value(f.type, value, f.default) + end + end + + # decode_value(type, non-nil wire value, default) — wrong container shape + # falls to the default; failed scalar casts keep the raw value. + + defp decode_value(:snowflake, v, _d) when is_integer(v), do: v + + defp decode_value(:snowflake, v, _d) when is_binary(v) do + case Dexcord.Snowflake.cast(v) do + {:ok, int} -> int + :error -> v + end + end + + defp decode_value(:datetime, v, _d) when is_binary(v) do + case DateTime.from_iso8601(v) do + {:ok, dt, _offset} -> dt + {:error, _} -> v + end + end + + defp decode_value({:struct, mod}, v, _d) when is_map(v) and not is_struct(v), + do: mod.from_map(v) + + defp decode_value({:struct, _mod}, _v, d), do: d + + defp decode_value({:list, inner}, v, _d) when is_list(v), + do: Enum.map(v, &decode_element(inner, &1)) + + defp decode_value({:list, _inner}, _v, d), do: d + + defp decode_value({:enum, mod}, v, _d) when is_integer(v), do: mod.decode(v) + + defp decode_value({:flags, _mod}, v, _d) when is_integer(v), do: v + + # Permission-style bitfields arrive as decimal strings (they exceed 2^53). + # Parse a clean decimal string to an integer; anything else stays raw. + defp decode_value({:flags, _mod}, v, _d) when is_binary(v) do + case Integer.parse(v) do + {int, ""} -> int + _ -> v + end + end + + # Discord's `%{"snowflake_string" => object}` dictionaries (interaction + # resolved data): snowflake-castable keys become integers, everything else + # keeps its string key; values decode leniently through the inner type. + defp decode_value({:id_map, inner}, v, _d) when is_map(v) and not is_struct(v) do + Map.new(v, fn {k, val} -> + key = + case Dexcord.Snowflake.cast(k) do + {:ok, int} -> int + :error -> k + end + + {key, decode_element(inner, val)} + end) + end + + defp decode_value({:id_map, _inner}, _v, d), do: d + + # :string, :integer, :boolean, :raw, plus every failed scalar cast: + # the raw wire value passes through untouched. + defp decode_value(_type, v, _d), do: v + + defp decode_element(_inner, nil), do: nil + defp decode_element(inner, v), do: decode_value(inner, v, nil) + + @doc false + def to_map(%module{} = struct) do + Enum.reduce(module.__fields__(), %{}, fn f, acc -> + case encode_field(f, Map.fetch!(struct, f.name)) do + :skip -> acc + {:ok, encoded} -> Map.put(acc, f.key, encoded) + end + end) + end + + defp encode_field(%{tristate: true}, :absent), do: :skip + defp encode_field(%{presence: true}, true), do: {:ok, nil} + defp encode_field(%{presence: true}, false), do: :skip + defp encode_field(_f, nil), do: :skip + + defp encode_field(f, value) do + encoded = encode_value(f.type, value) + + if f.wire_string and is_integer(encoded) do + {:ok, Integer.to_string(encoded)} + else + {:ok, encoded} + end + end + + defp encode_value(:snowflake, v) when is_integer(v), do: Integer.to_string(v) + defp encode_value(:datetime, %DateTime{} = v), do: DateTime.to_iso8601(v) + # Route through the struct's own to_map/1 (not Codec.to_map directly): generated + # structs delegate straight back here, while hand-written fallback structs + # (UnknownChannel, Component.Unknown — no __fields__/0) provide their own. + defp encode_value({:struct, _mod}, %_{} = v), do: v.__struct__.to_map(v) + + defp encode_value({:list, inner}, v) when is_list(v), + do: Enum.map(v, &encode_element(inner, &1)) + + # id_map: re-stringify integer snowflake keys, encode values through the inner + # type; non-integer keys (junk we kept verbatim on decode) pass through. + defp encode_value({:id_map, inner}, v) when is_map(v) and not is_struct(v) do + Map.new(v, fn {k, val} -> + {if(is_integer(k), do: Integer.to_string(k), else: k), encode_element(inner, val)} + end) + end + + defp encode_value({:enum, mod}, v) when is_atom(v) and not is_nil(v), do: mod.encode(v) + defp encode_value({:enum, _mod}, {:unknown, n}) when is_integer(n), do: n + # raw passthrough: junk scalars that decode kept raw re-encode raw; a raw + # int in an enum/flags field is already wire format. + defp encode_value(_type, v), do: v + + defp encode_element(_inner, nil), do: nil + defp encode_element(inner, v), do: encode_value(inner, v) + + @doc false + def merge_map(%module{} = struct, map) when is_map(map) and not is_struct(map) do + Enum.reduce(module.__fields__(), struct, fn f, acc -> + if Map.has_key?(map, f.key) do + Map.put(acc, f.name, decode_field(f, map)) + else + acc + end + end) + end + + def merge_map(struct, _non_map), do: struct +end diff --git a/lib/dexcord/util.ex b/lib/dexcord/util.ex new file mode 100644 index 0000000..ccc0d6d --- /dev/null +++ b/lib/dexcord/util.ex @@ -0,0 +1,22 @@ +defmodule Dexcord.Util do + @moduledoc "Small formatting helpers for the ergonomics layer." + + # The nine documented Discord timestamp styles (Message Formatting reference): + # t short time, T long time, d short date, D long date, f short date/time, + # F long date/time, s short relative-ish (seconds), S, R relative. + @styles ~w(t T d D f F s S R) + + @doc """ + Renders a Discord timestamp markdown token ``. + + Accepts a `DateTime` or a unix-seconds integer. `style` is one of + `#{Enum.join(@styles, " ")}` and defaults to `"f"` (per Discord's docs). + """ + @spec format_dt(DateTime.t() | integer(), String.t()) :: String.t() + def format_dt(dt_or_unix, style \\ "f") + + def format_dt(%DateTime{} = dt, style), do: format_dt(DateTime.to_unix(dt), style) + + def format_dt(unix, style) when is_integer(unix) and style in @styles, + do: "" +end diff --git a/lib/mix/tasks/dexcord.coverage.ex b/lib/mix/tasks/dexcord.coverage.ex new file mode 100644 index 0000000..65b8c1a --- /dev/null +++ b/lib/mix/tasks/dexcord.coverage.ex @@ -0,0 +1,22 @@ +defmodule Mix.Tasks.Dexcord.Coverage do + @shortdoc "Verifies declared REST routes cover the pinned Discord OpenAPI spec" + @moduledoc "See Dexcord.Api.Coverage. Exits non-zero when in-scope endpoints are missing." + use Mix.Task + + @impl Mix.Task + def run(_argv) do + Mix.Task.run("compile") + + case Dexcord.Api.Coverage.check() do + {:ok, stats} -> + Mix.shell().info( + "coverage OK — #{stats.declared} declared, #{stats.spec} in spec, #{stats.allowlisted} allowlisted" + ) + + {:missing, missing} -> + Mix.shell().error("MISSING #{length(missing)} in-scope endpoint(s):") + Enum.each(missing, &Mix.shell().error(" " <> &1)) + exit({:shutdown, 1}) + end + end +end diff --git a/lib/mix/tasks/dexcord.coverage.refresh.ex b/lib/mix/tasks/dexcord.coverage.refresh.ex new file mode 100644 index 0000000..ccb960f --- /dev/null +++ b/lib/mix/tasks/dexcord.coverage.refresh.ex @@ -0,0 +1,81 @@ +defmodule Mix.Tasks.Dexcord.Coverage.Refresh do + @shortdoc "Downloads and pins the latest Discord OpenAPI spec into priv/" + @moduledoc """ + Refreshes `priv/discord_openapi.json` from the upstream + `discord/discord-api-spec` repo. + + This is a DELIBERATE, human-initiated action — it is never run in CI. Pinning + means upstream churn can only affect coverage when an operator explicitly runs + this task, reviews the `git diff`, reconciles the allowlist, and reruns + `mix dexcord.coverage`. + """ + use Mix.Task + + @url "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json" + + @impl Mix.Task + def run(_argv) do + # Mix prunes OTP apps it doesn't depend on from the code path, so inets/ssl + # (needed only by this deliberate, dev-only task) may not be loadable. Add + # their ebin dirs from the Erlang root before starting them. + ensure_otp_app_on_path(:ssl) + ensure_otp_app_on_path(:inets) + {:ok, _} = Application.ensure_all_started(:ssl) + {:ok, _} = Application.ensure_all_started(:inets) + + dest = Path.join(:code.priv_dir(:dexcord), "discord_openapi.json") + File.mkdir_p!(Path.dirname(dest)) + + Mix.shell().info("fetching #{@url} ...") + + body = fetch!(@url) + File.write!(dest, body) + + Mix.shell().info("wrote #{dest} (#{byte_size(body)} bytes)") + + Mix.shell().info( + "review `git diff priv/discord_openapi.json`, reconcile priv/coverage_allowlist.exs, then rerun `mix dexcord.coverage`" + ) + end + + # Adds an OTP application's ebin dir (under the Erlang root) to the code path + # if its modules aren't already loadable. + defp ensure_otp_app_on_path(app) do + if :code.lib_dir(app) == {:error, :bad_name} do + root = to_string(:code.root_dir()) + + case Path.wildcard(Path.join([root, "lib", "#{app}-*", "ebin"])) do + [ebin | _] -> :code.add_patha(String.to_charlist(ebin)) + [] -> Mix.raise("cannot locate OTP application #{app} under #{root}/lib") + end + end + end + + # Verifies the peer with castore's CA bundle so the pin can't be MITM'd. + defp fetch!(url) do + cacerts = CAStore.file_path() |> to_charlist() + + http_opts = [ + ssl: [ + verify: :verify_peer, + cacertfile: cacerts, + depth: 3, + customize_hostname_check: [ + match_fun: :public_key.pkix_verify_hostname_match_fun(:https) + ] + ], + autoredirect: true + ] + + case :httpc.request(:get, {to_charlist(url), []}, http_opts, body_format: :binary) do + {:ok, {{_v, 200, _r}, _headers, body}} -> + body + + {:ok, {{_v, status, _r}, _headers, _body}} -> + Mix.raise("failed to fetch spec: HTTP #{status}") + + {:error, reason} -> + Mix.raise("failed to fetch spec: #{inspect(reason)}") + end + end +end diff --git a/priv/coverage_allowlist.exs b/priv/coverage_allowlist.exs new file mode 100644 index 0000000..4cabbee --- /dev/null +++ b/priv/coverage_allowlist.exs @@ -0,0 +1,95 @@ +# Routes present in the pinned OpenAPI spec but deliberately NOT declared. +# Everything here remains reachable via Dexcord.Api.request/4. +[ + # -- monetization (out of scope) -- + "GET /applications/*/skus", + "GET /applications/*/entitlements*", + "POST /applications/*/entitlements*", + "DELETE /applications/*/entitlements*", + "GET /applications/*/subscriptions*", + "GET /skus/*/subscriptions*", + # -- soundboard (out of scope) -- + "GET /soundboard-default-sounds", + "GET /guilds/*/soundboard-sounds*", + "POST /guilds/*/soundboard-sounds", + "PATCH /guilds/*/soundboard-sounds/*", + "DELETE /guilds/*/soundboard-sounds/*", + "POST /channels/*/send-soundboard-sound", + # -- stage instances (out of scope) -- + "GET /stage-instances/*", + "POST /stage-instances", + "PATCH /stage-instances/*", + "DELETE /stage-instances/*", + # -- guild templates (out of scope) -- + "GET /guilds/templates/*", + "POST /guilds/templates/*", + "GET /guilds/*/templates", + "POST /guilds/*/templates", + "PUT /guilds/*/templates/*", + "PATCH /guilds/*/templates/*", + "DELETE /guilds/*/templates/*", + # -- Bearer-only / not bot-usable -- + "GET /oauth2/@me", + "GET /users/@me/applications/*/role-connection", + "PUT /users/@me/applications/*/role-connection", + "DELETE /users/@me/applications/*/role-connection", + "GET /applications/*/role-connections/metadata", + "PUT /applications/*/role-connections/metadata", + "PUT /applications/*/guilds/*/commands/*/permissions", + "PUT /applications/*/guilds/*/commands/permissions", + # -- deprecated routes kept out of the typed surface -- + "GET /channels/*/pins", + "PUT /channels/*/pins/*", + "DELETE /channels/*/pins/*", + "PATCH /guilds/*/members/@me/nick", + # -- removed from official docs (2026-07); spec may still carry them -- + "POST /guilds", + "DELETE /guilds/*", + "POST /guilds/*/mfa", + # -- special CSV multipart (guest invites); escape hatch -- + "GET /invites/*/target-users*", + "PUT /invites/*/target-users", + # -- activities -- + "GET /applications/*/activity-instances/*", + # -- OAuth2 token machinery (not a bot-library concern) -- + "POST /oauth2/token*", + "POST /oauth2/token/revoke", + # -- OpenID Connect / key-discovery (bearer/OIDC, not bot-token) -- + "GET /oauth2/keys", + "GET /oauth2/userinfo", + # --------------------------------------------------------------------------- + # Reconciliation against the pinned spec (2026-07-05). Everything below is + # present upstream but NOT in the plan's authoritative declared surface: all + # are Social-SDK-era or post-inventory additions, none documented as part of + # the bot-token REST surface the design scoped. Reachable via request/4. + # --------------------------------------------------------------------------- + # + # -- Social SDK: Lobbies (whole resource family, out of scope) -- + "PUT /lobbies*", + "POST /lobbies*", + "GET /lobbies*", + "PATCH /lobbies*", + "DELETE /lobbies*", + # -- Social SDK: Partner SDK token + provisional accounts + DM moderation -- + "POST /partner-sdk*", + "PUT /partner-sdk*", + # -- Application-by-id (Social SDK / OAuth variants of the @me routes we + # declare) + application attachment upload -- + "GET /applications/*", + "PATCH /applications/*", + "POST /applications/*/attachment", + # -- Guild scheduled-event exceptions & user counts (recurring-event feature; + # the base scheduled-events family IS declared — only these extras aren't) -- + "POST /guilds/*/scheduled-events/*/exceptions", + "PATCH /guilds/*/scheduled-events/*/exceptions/*", + "GET /guilds/*/scheduled-events/*/users/counts", + "GET /guilds/*/scheduled-events/*/*/users", + # -- Guild join requests / membership application flow (newer feature) -- + "GET /guilds/*/new-member-welcome", + "GET /guilds/*/requests", + "PATCH /guilds/*/requests/*", + # -- Thread search (not in the documented channel bot surface) -- + "GET /channels/*/threads/search", + # -- monetization: per-user application entitlements (monetization excluded) -- + "GET /users/@me/applications/*/entitlements" +] diff --git a/priv/discord_openapi.json b/priv/discord_openapi.json new file mode 100644 index 0000000..71801e0 --- /dev/null +++ b/priv/discord_openapi.json @@ -0,0 +1,43111 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Discord HTTP API (Preview)", + "description": "Preview of the Discord v10 HTTP API specification. See https://discord.com/developers/docs for more details.", + "termsOfService": "https://discord.com/developers/docs/policies-and-agreements/developer-terms-of-service", + "license": { + "name": "MIT", + "identifier": "MIT" + }, + "version": "10" + }, + "externalDocs": { + "url": "https://discord.com/developers/docs", + "description": "Discord Developer Documentation" + }, + "servers": [ + { + "url": "https://discord.com/api/v10" + } + ], + "paths": { + "/applications/@me": { + "get": { + "operationId": "get_my_application", + "responses": { + "200": { + "description": "200 response for get_my_application", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateApplicationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_my_application", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationFormPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_my_application", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateApplicationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_application", + "responses": { + "200": { + "description": "200 response for get_application", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateApplicationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_application", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationFormPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_application", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateApplicationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}/activity-instances/{instance_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "instance_id", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "applications_get_activity_instance", + "responses": { + "200": { + "description": "200 response for applications_get_activity_instance", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmbeddedActivityInstance" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}/attachment": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "upload_application_attachment", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "contentEncoding": "binary" + } + }, + "required": [ + "file" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for upload_application_attachment", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ActivitiesAttachmentResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "activities.invites.write", + "activities.read", + "activities.write", + "applications.builds.read", + "applications.builds.upload", + "applications.commands", + "applications.commands.permissions.update", + "applications.commands.update", + "applications.entitlements", + "applications.store.update", + "bot", + "connections", + "dm_channels.read", + "email", + "gdm.join", + "guilds", + "guilds.join", + "guilds.members.read", + "identify", + "messages.read", + "openid", + "relationships.read", + "role_connections.write", + "rpc", + "rpc.activities.write", + "rpc.notifications.read", + "rpc.screenshare.read", + "rpc.screenshare.write", + "rpc.video.read", + "rpc.video.write", + "rpc.voice.read", + "rpc.voice.write", + "voice", + "webhook.incoming" + ] + } + ] + } + }, + "/applications/{application_id}/commands": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_application_commands", + "parameters": [ + { + "name": "with_localizations", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_application_commands", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "put": { + "operationId": "bulk_set_application_commands", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandUpdateRequest" + }, + "maxItems": 130 + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bulk_set_application_commands", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "post": { + "operationId": "create_application_command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "201": { + "description": "201 response for create_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + } + }, + "/applications/{application_id}/commands/{command_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "command_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_application_command", + "responses": { + "200": { + "description": "200 response for get_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "delete": { + "operationId": "delete_application_command", + "responses": { + "204": { + "description": "204 response for delete_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "patch": { + "operationId": "update_application_command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + } + }, + "/applications/{application_id}/emojis": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_application_emojis", + "responses": { + "200": { + "description": "200 response for list_application_emojis", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListApplicationEmojisResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_application_emoji", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "image": { + "type": "string", + "contentEncoding": "base64" + } + }, + "required": [ + "name", + "image" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_application_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}/emojis/{emoji_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "emoji_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_application_emoji", + "responses": { + "200": { + "description": "200 response for get_application_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_application_emoji", + "responses": { + "204": { + "description": "204 response for delete_application_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_application_emoji", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_application_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}/entitlements": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_entitlements", + "parameters": [ + { + "name": "user_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "sku_ids", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 100, + "uniqueItems": true + } + ] + } + }, + { + "name": "guild_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "exclude_ended", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "exclude_deleted", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "only_active", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_entitlements", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntitlementResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.entitlements" + ] + } + ] + }, + "post": { + "operationId": "create_entitlement", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateEntitlementRequestData" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_entitlement", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntitlementResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/applications/{application_id}/entitlements/{entitlement_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "entitlement_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_entitlement", + "responses": { + "200": { + "description": "200 response for get_entitlement", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EntitlementResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.entitlements" + ] + } + ] + }, + "delete": { + "operationId": "delete_entitlement", + "responses": { + "204": { + "description": "204 response for delete_entitlement", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.entitlements" + ] + } + ] + } + }, + "/applications/{application_id}/entitlements/{entitlement_id}/consume": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "entitlement_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "consume_entitlement", + "responses": { + "204": { + "description": "204 response for consume_entitlement", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.entitlements" + ] + } + ] + } + }, + "/applications/{application_id}/guilds/{guild_id}/commands": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_application_commands", + "parameters": [ + { + "name": "with_localizations", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_application_commands", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "put": { + "operationId": "bulk_set_guild_application_commands", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandUpdateRequest" + }, + "maxItems": 130 + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bulk_set_guild_application_commands", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "post": { + "operationId": "create_guild_application_command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_guild_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "201": { + "description": "201 response for create_guild_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + } + }, + "/applications/{application_id}/guilds/{guild_id}/commands/permissions": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_application_command_permissions", + "responses": { + "200": { + "description": "200 response for list_guild_application_command_permissions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandPermissionsResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.permissions.update" + ] + } + ] + } + }, + "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "command_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_application_command", + "responses": { + "200": { + "description": "200 response for get_guild_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "delete": { + "operationId": "delete_guild_application_command", + "responses": { + "204": { + "description": "204 response for delete_guild_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + }, + "patch": { + "operationId": "update_guild_application_command", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_application_command", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.update" + ] + } + ] + } + }, + "/applications/{application_id}/guilds/{guild_id}/commands/{command_id}/permissions": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "command_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_application_command_permissions", + "responses": { + "200": { + "description": "200 response for get_guild_application_command_permissions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandPermissionsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.permissions.update" + ] + } + ] + }, + "put": { + "operationId": "set_guild_application_command_permissions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "permissions": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandPermission" + }, + "maxItems": 100 + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for set_guild_application_command_permissions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandPermissionsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "applications.commands.permissions.update" + ] + } + ] + } + }, + "/applications/{application_id}/role-connections/metadata": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_application_role_connections_metadata", + "responses": { + "200": { + "description": "200 response for get_application_role_connections_metadata", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationRoleConnectionsMetadataItemResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "operationId": "update_application_role_connections_metadata", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationRoleConnectionsMetadataItemRequest" + }, + "maxItems": 5 + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_application_role_connections_metadata", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationRoleConnectionsMetadataItemResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_channel", + "responses": { + "200": { + "description": "200 response for get_channel", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + }, + { + "$ref": "#/components/schemas/ThreadResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_channel", + "responses": { + "200": { + "description": "200 response for delete_channel", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + }, + { + "$ref": "#/components/schemas/ThreadResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_channel", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/UpdateDMRequestPartial" + }, + { + "$ref": "#/components/schemas/UpdateGroupDMRequestPartial" + }, + { + "$ref": "#/components/schemas/UpdateGuildChannelRequestPartial" + }, + { + "$ref": "#/components/schemas/UpdateThreadRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_channel", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + }, + { + "$ref": "#/components/schemas/ThreadResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/followers": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "follow_channel", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "webhook_channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "webhook_channel_id" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for follow_channel", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ChannelFollowerResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/invites": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_channel_invites", + "responses": { + "200": { + "description": "200 response for list_channel_invites", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendInviteResponse" + }, + { + "$ref": "#/components/schemas/GroupDMInviteResponse" + }, + { + "$ref": "#/components/schemas/GuildInviteResponse" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_channel_invite", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateGroupDMInviteRequest" + }, + { + "$ref": "#/components/schemas/CreateGuildInviteRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateGroupDMInviteRequest" + }, + { + "$ref": "#/components/schemas/CreateGuildInviteRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "multipart/form-data": { + "schema": { + "allOf": [ + { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateGroupDMInviteRequest" + }, + { + "$ref": "#/components/schemas/CreateGuildInviteRequest" + } + ], + "x-discord-union": "oneOf" + }, + { + "type": "object", + "properties": { + "target_users_file": { + "type": "string", + "contentEncoding": "binary" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_channel_invite", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendInviteResponse" + }, + { + "$ref": "#/components/schemas/GroupDMInviteResponse" + }, + { + "$ref": "#/components/schemas/GuildInviteResponse" + } + ] + } + } + } + }, + "204": { + "description": "204 response for create_channel_invite", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_messages", + "parameters": [ + { + "name": "around", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_messages", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageCreateRequest" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/MessageCreateRequest" + } + }, + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MessageCreateRequest" + }, + { + "type": "object", + "properties": { + "files[0]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[1]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[2]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[3]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[4]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[5]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[6]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[7]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[8]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[9]": { + "type": "string", + "contentEncoding": "binary" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/bulk-delete": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "bulk_delete_messages", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "minItems": 2, + "maxItems": 100, + "uniqueItems": true + } + }, + "required": [ + "messages" + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for bulk_delete_messages", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/pins": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_pins", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 50 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_pins", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PinnedMessagesResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/pins/{message_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "create_pin", + "responses": { + "204": { + "description": "204 response for create_pin", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_pin", + "responses": { + "204": { + "description": "204 response for delete_pin", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_message", + "responses": { + "200": { + "description": "200 response for get_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_message", + "responses": { + "204": { + "description": "204 response for delete_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageEditRequestPartial" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/MessageEditRequestPartial" + } + }, + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/MessageEditRequestPartial" + }, + { + "type": "object", + "properties": { + "files[0]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[1]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[2]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[3]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[4]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[5]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[6]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[7]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[8]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[9]": { + "type": "string", + "contentEncoding": "binary" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/crosspost": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "crosspost_message", + "responses": { + "200": { + "description": "200 response for crosspost_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/reactions": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "operationId": "delete_all_message_reactions", + "responses": { + "204": { + "description": "204 response for delete_all_message_reactions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "emoji_name", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "list_message_reactions_by_emoji", + "parameters": [ + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "type", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ReactionTypes" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_message_reactions_by_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_all_message_reactions_by_emoji", + "responses": { + "204": { + "description": "204 response for delete_all_message_reactions_by_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}/@me": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "emoji_name", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "put": { + "operationId": "add_my_message_reaction", + "responses": { + "204": { + "description": "204 response for add_my_message_reaction", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_my_message_reaction", + "responses": { + "204": { + "description": "204 response for delete_my_message_reaction", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/reactions/{emoji_name}/{user_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "emoji_name", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "operationId": "delete_user_message_reaction", + "responses": { + "204": { + "description": "204 response for delete_user_message_reaction", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/messages/{message_id}/threads": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "create_thread_from_message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTextThreadWithMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_thread_from_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/permissions/{overwrite_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "overwrite_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "set_channel_permission_overwrite", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ChannelPermissionOverwrites" + } + ] + }, + "allow": { + "type": [ + "integer", + "null" + ] + }, + "deny": { + "type": [ + "integer", + "null" + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for set_channel_permission_overwrite", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_channel_permission_overwrite", + "responses": { + "204": { + "description": "204 response for delete_channel_permission_overwrite", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/pins": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "deprecated_list_pins", + "responses": { + "200": { + "description": "200 response for deprecated_list_pins", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/pins/{message_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "deprecated_create_pin", + "responses": { + "204": { + "description": "204 response for deprecated_create_pin", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "deprecated_delete_pin", + "responses": { + "204": { + "description": "204 response for deprecated_delete_pin", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/polls/{message_id}/answers/{answer_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "answer_id", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 10, + "format": "int32" + }, + "required": true + } + ], + "get": { + "operationId": "get_answer_voters", + "parameters": [ + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for get_answer_voters", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PollAnswerDetailsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/polls/{message_id}/expire": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "poll_expire", + "responses": { + "200": { + "description": "200 response for poll_expire", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/recipients/{user_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "add_group_dm_user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "access_token": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "nick": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + } + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for add_group_dm_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + } + ] + } + } + } + }, + "204": { + "description": "204 response for add_group_dm_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_group_dm_user", + "responses": { + "204": { + "description": "204 response for delete_group_dm_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/send-soundboard-sound": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "send_soundboard_sound", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardSoundSendRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for send_soundboard_sound", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/thread-members": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_thread_members", + "parameters": [ + { + "name": "with_member", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_thread_members", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/thread-members/@me": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "join_thread", + "responses": { + "204": { + "description": "204 response for join_thread", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "leave_thread", + "responses": { + "204": { + "description": "204 response for leave_thread", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/thread-members/{user_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_thread_member", + "parameters": [ + { + "name": "with_member", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_thread_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "operationId": "add_thread_member", + "responses": { + "204": { + "description": "204 response for add_thread_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_thread_member", + "responses": { + "204": { + "description": "204 response for delete_thread_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/threads": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "create_thread", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateForumThreadRequest" + }, + { + "$ref": "#/components/schemas/CreateTextThreadWithoutMessageRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateForumThreadRequest" + }, + { + "$ref": "#/components/schemas/CreateTextThreadWithoutMessageRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "multipart/form-data": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CreateForumThreadRequest" + }, + { + "$ref": "#/components/schemas/CreateTextThreadWithoutMessageRequest" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_thread", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatedThreadResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/threads/archived/private": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_private_archived_threads", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 2, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_private_archived_threads", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/threads/archived/public": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_public_archived_threads", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 2, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_public_archived_threads", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/threads/search": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "thread_search", + "parameters": [ + { + "name": "name", + "in": "query", + "schema": { + "type": "string", + "maxLength": 100 + } + }, + { + "name": "slop", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "min_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "max_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "tag", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + } + ] + } + }, + { + "name": "tag_setting", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ThreadSearchTagSetting" + } + }, + { + "name": "archived", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "sort_by", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ThreadSortingMode" + } + }, + { + "name": "sort_order", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SortingOrder" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 25 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 9975 + } + } + ], + "responses": { + "200": { + "description": "200 response for thread_search", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadSearchResponse" + } + } + } + }, + "202": { + "description": "202 response for thread_search", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchIndexNotReadyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/typing": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "trigger_typing_indicator", + "responses": { + "200": { + "description": "200 response for trigger_typing_indicator", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TypingIndicatorResponse" + } + } + } + }, + "204": { + "description": "204 response for trigger_typing_indicator", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/users/@me/threads/archived/private": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_my_private_archived_threads", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 2, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_my_private_archived_threads", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/voice-status": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "description": "Set a voice channel's status.", + "operationId": "update_voice_channel_status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": [ + "string", + "null" + ], + "description": "The new voice channel status", + "maxLength": 500 + } + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_voice_channel_status", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/channels/{channel_id}/webhooks": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_channel_webhooks", + "responses": { + "200": { + "description": "200 response for list_channel_webhooks", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_webhook", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "avatar": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + } + }, + "required": [ + "name" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/gateway": { + "get": { + "operationId": "get_gateway", + "responses": { + "200": { + "description": "200 response for get_gateway", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/gateway/bot": { + "get": { + "operationId": "get_bot_gateway", + "responses": { + "200": { + "description": "200 response for get_bot_gateway", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GatewayBotResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/templates/{code}": { + "parameters": [ + { + "name": "code", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_template", + "responses": { + "200": { + "description": "200 response for get_guild_template", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild", + "parameters": [ + { + "name": "with_counts", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildWithCountsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/audit-logs": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_audit_log_entries", + "parameters": [ + { + "name": "user_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "target_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "action_type", + "in": "query", + "schema": { + "$ref": "#/components/schemas/AuditLogActionTypes" + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_audit_log_entries", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildAuditLogResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/auto-moderation/rules": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_auto_moderation_rules", + "responses": { + "200": { + "description": "200 response for list_auto_moderation_rules", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/KeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/MLSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/MentionSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/UserProfileRuleResponse" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_auto_moderation_rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordListUpsertRequest" + }, + { + "$ref": "#/components/schemas/KeywordUpsertRequest" + }, + { + "$ref": "#/components/schemas/MLSpamUpsertRequest" + }, + { + "$ref": "#/components/schemas/MentionSpamUpsertRequest" + }, + { + "$ref": "#/components/schemas/UserProfileUpsertRequest" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_auto_moderation_rule", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/KeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/MLSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/MentionSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/UserProfileRuleResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/auto-moderation/rules/{rule_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "rule_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_auto_moderation_rule", + "responses": { + "200": { + "description": "200 response for get_auto_moderation_rule", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/KeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/MLSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/MentionSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/UserProfileRuleResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_auto_moderation_rule", + "responses": { + "204": { + "description": "204 response for delete_auto_moderation_rule", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_auto_moderation_rule", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordListUpsertRequestPartial" + }, + { + "$ref": "#/components/schemas/KeywordUpsertRequestPartial" + }, + { + "$ref": "#/components/schemas/MLSpamUpsertRequestPartial" + }, + { + "$ref": "#/components/schemas/MentionSpamUpsertRequestPartial" + }, + { + "$ref": "#/components/schemas/UserProfileUpsertRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_auto_moderation_rule", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/KeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/MLSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/MentionSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/UserProfileRuleResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/bans": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_bans", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_bans", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GuildBanResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/bans/{user_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_ban", + "responses": { + "200": { + "description": "200 response for get_guild_ban", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildBanResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "operationId": "ban_user_from_guild", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BanUserFromGuildRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for ban_user_from_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "unban_user_from_guild", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UnbanUserFromGuildRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for unban_user_from_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/bulk-ban": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "bulk_ban_users_from_guild", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkBanUsersRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bulk_ban_users_from_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkBanUsersResponse" + } + } + } + }, + "204": { + "description": "204 response for bulk_ban_users_from_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/channels": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_channels", + "responses": { + "200": { + "description": "200 response for list_guild_channels", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + }, + { + "$ref": "#/components/schemas/ThreadResponse" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + }, + "post": { + "operationId": "create_guild_channel", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGuildChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_guild_channel", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildChannelResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "bulk_update_guild_channels", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "position": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "lock_permissions": { + "type": [ + "boolean", + "null" + ] + } + } + } + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for bulk_update_guild_channels", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/emojis": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_emojis", + "responses": { + "200": { + "description": "200 response for list_guild_emojis", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_emoji", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "image": { + "type": "string", + "contentEncoding": "base64" + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 1521, + "uniqueItems": true + } + }, + "required": [ + "name", + "image" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_guild_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/emojis/{emoji_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "emoji_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_emoji", + "responses": { + "200": { + "description": "200 response for get_guild_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_emoji", + "responses": { + "204": { + "description": "204 response for delete_guild_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_emoji", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 1521, + "uniqueItems": true + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_emoji", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/incident-actions": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "description": "Modifies the incident actions of the guild", + "operationId": "update_guild_incident_actions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildIncidentActionsRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_incident_actions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildIncidentsDataResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/integrations": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_integrations", + "responses": { + "200": { + "description": "200 response for list_guild_integrations", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DiscordIntegrationResponse" + }, + { + "$ref": "#/components/schemas/ExternalConnectionIntegrationResponse" + }, + { + "$ref": "#/components/schemas/GuildSubscriptionIntegrationResponse" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/integrations/{integration_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "integration_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "operationId": "delete_guild_integration", + "responses": { + "204": { + "description": "204 response for delete_guild_integration", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/invites": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_invites", + "responses": { + "200": { + "description": "200 response for list_guild_invites", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendInviteResponse" + }, + { + "$ref": "#/components/schemas/GroupDMInviteResponse" + }, + { + "$ref": "#/components/schemas/GuildInviteResponse" + }, + { + "type": "null" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/members": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_members", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + { + "name": "after", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0 + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_members", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/members/@me": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "patch": { + "operationId": "update_my_guild_member", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nick": { + "type": [ + "string", + "null" + ], + "maxLength": 32 + }, + "avatar": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "bio": { + "type": [ + "string", + "null" + ], + "maxLength": 190 + }, + "banner": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_my_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateGuildMemberResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/members/search": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "search_guild_members", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + } + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "required": true + } + ], + "responses": { + "200": { + "description": "200 response for search_guild_members", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/members/{user_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_member", + "responses": { + "200": { + "description": "200 response for get_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "operationId": "add_guild_member", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotAddGuildMemberRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for add_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + } + } + }, + "204": { + "description": "204 response for add_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_member", + "responses": { + "204": { + "description": "204 response for delete_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_member", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nick": { + "type": [ + "string", + "null" + ], + "maxLength": 32 + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 350, + "uniqueItems": true + }, + "mute": { + "type": [ + "boolean", + "null" + ] + }, + "deaf": { + "type": [ + "boolean", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "communication_disabled_until": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "flags": { + "type": [ + "integer", + "null" + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + } + } + }, + "204": { + "description": "204 response for update_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/members/{user_id}/roles/{role_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "role_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "add_guild_member_role", + "responses": { + "204": { + "description": "204 response for add_guild_member_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_member_role", + "responses": { + "204": { + "description": "204 response for delete_guild_member_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/messages/search": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "guild_search", + "parameters": [ + { + "name": "sort_by", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SortingMode" + } + }, + { + "name": "sort_order", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SortingOrder" + } + }, + { + "name": "content", + "in": "query", + "schema": { + "type": "string", + "maxLength": 1024 + } + }, + { + "name": "slop", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 100 + } + }, + { + "name": "author_id", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "author_type", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuthorType" + }, + "maxItems": 6, + "uniqueItems": true + } + }, + { + "name": "mentions", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "mentions_role_id", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "replied_to_user_id", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "replied_to_message_id", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "mention_everyone", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "min_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "max_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 25 + } + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "minimum": 0, + "maximum": 9975 + } + }, + { + "name": "has", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/HasOption" + }, + "maxItems": 18, + "uniqueItems": true + } + }, + { + "name": "link_hostname", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "embed_provider", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "embed_type", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchableEmbedType" + }, + "maxItems": 5, + "uniqueItems": true + } + }, + { + "name": "attachment_extension", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "maxLength": 256 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "attachment_filename", + "in": "query", + "schema": { + "type": "array", + "items": { + "type": "string", + "maxLength": 1024 + }, + "maxItems": 100, + "uniqueItems": true + } + }, + { + "name": "pinned", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "include_nsfw", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "channel_id", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 500, + "uniqueItems": true + } + } + ], + "responses": { + "200": { + "description": "200 response for guild_search", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildSearchResponse" + } + } + } + }, + "202": { + "description": "202 response for guild_search", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchIndexNotReadyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/new-member-welcome": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_new_member_welcome", + "responses": { + "200": { + "description": "200 response for get_guild_new_member_welcome", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildHomeSettingsResponse" + } + } + } + }, + "204": { + "description": "204 response for get_guild_new_member_welcome", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/onboarding": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guilds_onboarding", + "responses": { + "200": { + "description": "200 response for get_guilds_onboarding", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGuildOnboardingResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "operationId": "put_guilds_onboarding", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGuildOnboardingRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for put_guilds_onboarding", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildOnboardingResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/preview": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_preview", + "responses": { + "200": { + "description": "200 response for get_guild_preview", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildPreviewResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/prune": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "preview_prune_guild", + "parameters": [ + { + "name": "days", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 30 + } + }, + { + "name": "include_roles", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 100, + "uniqueItems": true + } + ] + } + } + ], + "responses": { + "200": { + "description": "200 response for preview_prune_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildPruneResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "prune_guild", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PruneGuildRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for prune_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildPruneResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/regions": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_voice_regions", + "responses": { + "200": { + "description": "200 response for list_guild_voice_regions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/VoiceRegionResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/requests": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "description": "List join requests for guild, optionally filtered by application status", + "operationId": "get_guild_join_requests", + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": [], + "allOf": [ + { + "$ref": "#/components/schemas/GuildJoinRequestApplicationStatus" + } + ] + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_guild_join_requests", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildJoinRequestsListResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/requests/{request_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "request_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "patch": { + "description": "Approve or reject guild join request", + "operationId": "action_guild_join_request", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Whether to approve or reject the join request", + "enum": [], + "allOf": [ + { + "$ref": "#/components/schemas/GuildJoinRequestApplicationStatus" + } + ] + }, + "rejection_reason": { + "type": [ + "string", + "null" + ], + "description": "Reason for rejection. Only used when action is REJECTED", + "maxLength": 160 + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for action_guild_join_request", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildJoinRequestResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/roles": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_roles", + "responses": { + "200": { + "description": "200 response for list_guild_roles", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_role", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_guild_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "bulk_update_guild_roles", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UpdateRolePositionsRequest" + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bulk_update_guild_roles", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/roles/member-counts": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "guild_role_member_counts", + "responses": { + "200": { + "description": "200 response for guild_role_member_counts", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/roles/{role_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "role_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_role", + "responses": { + "200": { + "description": "200 response for get_guild_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_role", + "responses": { + "204": { + "description": "204 response for delete_guild_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_role", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRoleRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_role", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_scheduled_events", + "parameters": [ + { + "name": "with_user_count", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_scheduled_events", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/StageScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventResponse" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_scheduled_event", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventCreateRequest" + }, + { + "$ref": "#/components/schemas/StageScheduledEventCreateRequest" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventCreateRequest" + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_guild_scheduled_event", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/StageScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_scheduled_event", + "parameters": [ + { + "name": "with_user_count", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_guild_scheduled_event", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/StageScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_scheduled_event", + "responses": { + "204": { + "description": "204 response for delete_guild_scheduled_event", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_scheduled_event", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventPatchRequestPartial" + }, + { + "$ref": "#/components/schemas/StageScheduledEventPatchRequestPartial" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventPatchRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_scheduled_event", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/StageScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/exceptions": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "description": "Create an exception to a recurring guild scheduled event", + "operationId": "create_guild_scheduled_event_exception", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_guild_scheduled_event_exception", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/exceptions/{exception_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "exception_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "description": "Delete an exception to a recurring guild scheduled event", + "operationId": "delete_guild_scheduled_event_exception", + "responses": { + "204": { + "description": "204 response for delete_guild_scheduled_event_exception", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "description": "Modify an exception to a recurring guild scheduled event", + "operationId": "update_guild_scheduled_event_exception", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_scheduled_event_exception", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/users": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_scheduled_event_users", + "parameters": [ + { + "name": "with_member", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_scheduled_event_users", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/users/counts": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "description": "Get the count of users subscribed to a guild scheduled event", + "operationId": "count_guild_scheduled_event_users", + "parameters": [ + { + "name": "guild_scheduled_event_exception_ids", + "in": "query", + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 10 + } + } + ], + "responses": { + "200": { + "description": "200 response for count_guild_scheduled_event_users", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ScheduledEventUserCountResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/scheduled-events/{guild_scheduled_event_id}/{guild_scheduled_event_exception_id}/users": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "guild_scheduled_event_exception_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "description": "Get a list of users subscribed to a guild scheduled event exception", + "operationId": "list_guild_scheduled_event_exception_users", + "parameters": [ + { + "name": "with_member", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_guild_scheduled_event_exception_users", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/soundboard-sounds": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_soundboard_sounds", + "responses": { + "200": { + "description": "200 response for list_guild_soundboard_sounds", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListGuildSoundboardSoundsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_soundboard_sound", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardCreateRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_guild_soundboard_sound", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardSoundResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/soundboard-sounds/{sound_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "sound_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_soundboard_sound", + "responses": { + "200": { + "description": "200 response for get_guild_soundboard_sound", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardSoundResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_soundboard_sound", + "responses": { + "204": { + "description": "204 response for delete_guild_soundboard_sound", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_soundboard_sound", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_soundboard_sound", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SoundboardSoundResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/stickers": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_stickers", + "responses": { + "200": { + "description": "200 response for list_guild_stickers", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_sticker", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 30 + }, + "tags": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "file": { + "type": "string", + "contentEncoding": "binary" + } + }, + "required": [ + "name", + "tags", + "file" + ] + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_guild_sticker", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/stickers/{sticker_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "sticker_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_sticker", + "responses": { + "200": { + "description": "200 response for get_guild_sticker", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_sticker", + "responses": { + "204": { + "description": "204 response for delete_guild_sticker", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_sticker", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 30 + }, + "tags": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_sticker", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/templates": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "list_guild_templates", + "responses": { + "200": { + "description": "200 response for list_guild_templates", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "create_guild_template", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 120 + } + }, + "required": [ + "name" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_guild_template", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/templates/{code}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "code", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "put": { + "operationId": "sync_guild_template", + "responses": { + "200": { + "description": "200 response for sync_guild_template", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_guild_template", + "responses": { + "200": { + "description": "200 response for delete_guild_template", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_template", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 120 + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_template", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildTemplateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/threads/active": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_active_guild_threads", + "responses": { + "200": { + "description": "200 response for get_active_guild_threads", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThreadsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/vanity-url": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_vanity_url", + "responses": { + "200": { + "description": "200 response for get_guild_vanity_url", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VanityURLResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/voice-states/@me": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_self_voice_state", + "responses": { + "200": { + "description": "200 response for get_self_voice_state", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceStateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_self_voice_state", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateSelfVoiceStateRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_self_voice_state", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/voice-states/{user_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_voice_state", + "responses": { + "200": { + "description": "200 response for get_voice_state", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VoiceStateResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_voice_state", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateVoiceStateRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_voice_state", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/webhooks": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_webhooks", + "responses": { + "200": { + "description": "200 response for get_guild_webhooks", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/welcome-screen": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_welcome_screen", + "responses": { + "200": { + "description": "200 response for get_guild_welcome_screen", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildWelcomeScreenResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_welcome_screen", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WelcomeScreenPatchRequestPartial" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_welcome_screen", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GuildWelcomeScreenResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/widget": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_widget_settings", + "responses": { + "200": { + "description": "200 response for get_guild_widget_settings", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetSettingsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_guild_widget_settings", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_guild_widget_settings", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetSettingsResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/widget.json": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_widget", + "responses": { + "200": { + "description": "200 response for get_guild_widget", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WidgetResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/guilds/{guild_id}/widget.png": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_guild_widget_png", + "parameters": [ + { + "name": "style", + "in": "query", + "schema": { + "$ref": "#/components/schemas/WidgetImageStyles" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_guild_widget_png", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "image/png": { + "schema": { + "type": "string", + "contentEncoding": "binary" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/interactions/{interaction_id}/{interaction_token}/callback": { + "parameters": [ + { + "name": "interaction_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "interaction_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "post": { + "operationId": "create_interaction_response", + "parameters": [ + { + "name": "with_response", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAutocompleteCallbackRequest" + }, + { + "$ref": "#/components/schemas/CreateMessageInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/LaunchActivityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/ModalInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/PongInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/SocialLayerSKUPurchaseEligibilityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/UpdateMessageInteractionCallbackRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAutocompleteCallbackRequest" + }, + { + "$ref": "#/components/schemas/CreateMessageInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/LaunchActivityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/ModalInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/PongInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/SocialLayerSKUPurchaseEligibilityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/UpdateMessageInteractionCallbackRequest" + } + ], + "x-discord-union": "oneOf" + } + }, + "multipart/form-data": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAutocompleteCallbackRequest" + }, + { + "$ref": "#/components/schemas/CreateMessageInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/LaunchActivityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/ModalInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/PongInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/SocialLayerSKUPurchaseEligibilityInteractionCallbackRequest" + }, + { + "$ref": "#/components/schemas/UpdateMessageInteractionCallbackRequest" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_interaction_response", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InteractionCallbackResponse" + } + } + } + }, + "204": { + "description": "204 response for create_interaction_response", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/invites/{code}": { + "parameters": [ + { + "name": "code", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "invite_resolve", + "parameters": [ + { + "name": "with_counts", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "guild_scheduled_event_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "target_channel_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "target_message_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for invite_resolve", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendInviteResponse" + }, + { + "$ref": "#/components/schemas/GroupDMInviteResponse" + }, + { + "$ref": "#/components/schemas/GuildInviteResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "invite_revoke", + "responses": { + "200": { + "description": "200 response for invite_revoke", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/FriendInviteResponse" + }, + { + "$ref": "#/components/schemas/GroupDMInviteResponse" + }, + { + "$ref": "#/components/schemas/GuildInviteResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/invites/{code}/target-users": { + "parameters": [ + { + "name": "code", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "description": "Get the target users for an invite.", + "operationId": "get_invite_target_users", + "responses": { + "200": { + "description": "200 response for get_invite_target_users", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "text/csv": { + "schema": { + "type": "string", + "description": "CSV file containing target users for the invite.", + "contentEncoding": "binary" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "put": { + "description": "Update the target users for an existing invite.", + "operationId": "update_invite_target_users", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "target_users_file": { + "type": "string", + "contentEncoding": "binary" + } + }, + "required": [ + "target_users_file" + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_invite_target_users", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/invites/{code}/target-users/job-status": { + "parameters": [ + { + "name": "code", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "description": "Get the target users job status for an invite.", + "operationId": "get_invite_target_users_job_status", + "responses": { + "200": { + "description": "200 response for get_invite_target_users_job_status", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TargetUsersJobStatusResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies": { + "put": { + "operationId": "create_or_join_lobby", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "idle_timeout_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 5, + "maximum": 604800, + "format": "int32" + }, + "lobby_metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "member_metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "secret": { + "type": "string", + "maxLength": 250 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + } + }, + "required": [ + "secret" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_or_join_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + }, + "post": { + "operationId": "create_lobby", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "idle_timeout_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 5, + "maximum": 604800, + "format": "int32" + }, + "members": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/LobbyMemberRequest" + }, + "maxItems": 25 + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + }, + "override_event_webhooks_url": { + "type": [ + "string", + "null" + ], + "maxLength": 512, + "format": "uri" + } + } + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies/{lobby_id}": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_lobby", + "responses": { + "200": { + "description": "200 response for get_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "description": "Deletes the specified lobby if it exists. It is safe to call even if the lobby is already deleted.", + "operationId": "delete_lobby", + "responses": { + "204": { + "description": "204 response for delete_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "edit_lobby", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "idle_timeout_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 5, + "maximum": 604800, + "format": "int32" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "members": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/LobbyMemberRequest" + }, + "maxItems": 25 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + }, + "override_event_webhooks_url": { + "type": [ + "string", + "null" + ], + "maxLength": 512, + "format": "uri" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for edit_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies/{lobby_id}/channel-linking": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "patch": { + "operationId": "edit_lobby_channel_link", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for edit_lobby_channel_link", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + } + }, + "/lobbies/{lobby_id}/members/@me": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "operationId": "leave_lobby", + "responses": { + "204": { + "description": "204 response for leave_lobby", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + } + }, + "/lobbies/{lobby_id}/members/@me/invites": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "create_linked_lobby_guild_invite_for_self", + "responses": { + "200": { + "description": "200 response for create_linked_lobby_guild_invite_for_self", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyGuildInviteResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + } + }, + "/lobbies/{lobby_id}/members/bulk": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "bulk_update_lobby_members", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/BulkLobbyMemberRequest" + }, + "minItems": 1, + "maxItems": 25 + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bulk_update_lobby_members", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/LobbyMemberResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies/{lobby_id}/members/{user_id}": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "operationId": "add_lobby_member", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for add_lobby_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyMemberResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_lobby_member", + "responses": { + "204": { + "description": "204 response for delete_lobby_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies/{lobby_id}/members/{user_id}/invites": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "post": { + "operationId": "create_linked_lobby_guild_invite_for_user", + "responses": { + "200": { + "description": "200 response for create_linked_lobby_guild_invite_for_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyGuildInviteResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/lobbies/{lobby_id}/messages": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_lobby_messages", + "parameters": [ + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200 + } + } + ], + "responses": { + "200": { + "description": "200 response for get_lobby_messages", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/LobbyMessageResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + }, + "post": { + "operationId": "create_lobby_message", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SDKMessageRequest" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/SDKMessageRequest" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/SDKMessageRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "201 response for create_lobby_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LobbyMessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [] + } + ] + } + }, + "/lobbies/{lobby_id}/messages/{message_id}/moderation-metadata": { + "parameters": [ + { + "name": "lobby_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "description": "Update the external moderation metadata for a lobby message.", + "operationId": "update_lobby_message_external_moderation_metadata", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_lobby_message_external_moderation_metadata", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/oauth2/@me": { + "get": { + "operationId": "get_my_oauth2_authorization", + "responses": { + "200": { + "description": "200 response for get_my_oauth2_authorization", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuth2GetAuthorizationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "activities.invites.write", + "activities.read", + "activities.write", + "applications.builds.read", + "applications.builds.upload", + "applications.commands", + "applications.commands.permissions.update", + "applications.commands.update", + "applications.entitlements", + "applications.store.update", + "bot", + "connections", + "dm_channels.read", + "email", + "gdm.join", + "guilds", + "guilds.join", + "guilds.members.read", + "identify", + "messages.read", + "openid", + "relationships.read", + "role_connections.write", + "rpc", + "rpc.activities.write", + "rpc.notifications.read", + "rpc.screenshare.read", + "rpc.screenshare.write", + "rpc.video.read", + "rpc.video.write", + "rpc.voice.read", + "rpc.voice.write", + "voice", + "webhook.incoming" + ] + } + ] + } + }, + "/oauth2/applications/@me": { + "get": { + "operationId": "get_my_oauth2_application", + "responses": { + "200": { + "description": "200 response for get_my_oauth2_application", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateApplicationResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/oauth2/keys": { + "get": { + "operationId": "get_public_keys", + "responses": { + "200": { + "description": "200 response for get_public_keys", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuth2GetKeys" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/oauth2/userinfo": { + "get": { + "operationId": "get_openid_connect_userinfo", + "responses": { + "200": { + "description": "200 response for get_openid_connect_userinfo", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OAuth2GetOpenIDConnectUserInfoResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "openid" + ] + } + ] + } + }, + "/partner-sdk/dms/{user_id_1}/{user_id_2}/messages/{message_id}/moderation-metadata": { + "parameters": [ + { + "name": "user_id_1", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "user_id_2", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "put": { + "description": "Update the external moderation metadata for a user message (DM).", + "operationId": "update_user_message_external_moderation_metadata", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + }, + "multipart/form-data": { + "schema": { + "type": "object", + "description": "The moderation metadata attached to the message", + "additionalProperties": { + "type": "string", + "maxLength": 2000 + }, + "maxProperties": 5 + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for update_user_message_external_moderation_metadata", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/partner-sdk/provisional-accounts/unmerge": { + "post": { + "operationId": "partner_sdk_unmerge_provisional_account", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "client_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "client_secret": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "external_auth_token": { + "type": "string", + "maxLength": 10240 + }, + "external_auth_type": { + "$ref": "#/components/schemas/ApplicationIdentityProviderAuthType" + } + }, + "required": [ + "client_id", + "external_auth_token", + "external_auth_type" + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for partner_sdk_unmerge_provisional_account", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/partner-sdk/provisional-accounts/unmerge/bot": { + "post": { + "operationId": "bot_partner_sdk_unmerge_provisional_account", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "external_user_id": { + "type": "string", + "maxLength": 1024 + } + }, + "required": [ + "external_user_id" + ] + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for bot_partner_sdk_unmerge_provisional_account", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/partner-sdk/token": { + "post": { + "operationId": "partner_sdk_token", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "client_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "client_secret": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "external_auth_token": { + "type": "string", + "maxLength": 10240 + }, + "external_auth_type": { + "$ref": "#/components/schemas/ApplicationIdentityProviderAuthType" + } + }, + "required": [ + "client_id", + "external_auth_token", + "external_auth_type" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for partner_sdk_token", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProvisionalTokenResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/partner-sdk/token/bot": { + "post": { + "operationId": "bot_partner_sdk_token", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "provisional_user_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "external_user_id": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + }, + "preferred_global_name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 32 + } + }, + "required": [ + "external_user_id" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for bot_partner_sdk_token", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProvisionalTokenResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/skus/{sku_id}/subscriptions": { + "parameters": [ + { + "name": "sku_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "description": "Returns all subscriptions containing the SKU, filtered by user.", + "operationId": "get_sku_subscriptions", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + { + "name": "user_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_sku_subscriptions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SubscriptionResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "activities.invites.write", + "activities.read", + "activities.write", + "applications.builds.read", + "applications.builds.upload", + "applications.commands", + "applications.commands.permissions.update", + "applications.commands.update", + "applications.entitlements", + "applications.store.update", + "bot", + "connections", + "dm_channels.read", + "email", + "gdm.join", + "guilds", + "guilds.join", + "guilds.members.read", + "identify", + "messages.read", + "openid", + "relationships.read", + "role_connections.write", + "rpc", + "rpc.activities.write", + "rpc.notifications.read", + "rpc.screenshare.read", + "rpc.screenshare.write", + "rpc.video.read", + "rpc.video.write", + "rpc.voice.read", + "rpc.voice.write", + "voice", + "webhook.incoming" + ] + } + ] + } + }, + "/skus/{sku_id}/subscriptions/{subscription_id}": { + "parameters": [ + { + "name": "sku_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "subscription_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "description": "Get a subscription by its ID.", + "operationId": "get_sku_subscription", + "parameters": [ + { + "name": "user_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_sku_subscription", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubscriptionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "activities.invites.write", + "activities.read", + "activities.write", + "applications.builds.read", + "applications.builds.upload", + "applications.commands", + "applications.commands.permissions.update", + "applications.commands.update", + "applications.entitlements", + "applications.store.update", + "bot", + "connections", + "dm_channels.read", + "email", + "gdm.join", + "guilds", + "guilds.join", + "guilds.members.read", + "identify", + "messages.read", + "openid", + "relationships.read", + "role_connections.write", + "rpc", + "rpc.activities.write", + "rpc.notifications.read", + "rpc.screenshare.read", + "rpc.screenshare.write", + "rpc.video.read", + "rpc.video.write", + "rpc.voice.read", + "rpc.voice.write", + "voice", + "webhook.incoming" + ] + } + ] + } + }, + "/soundboard-default-sounds": { + "get": { + "operationId": "get_soundboard_default_sounds", + "responses": { + "200": { + "description": "200 response for get_soundboard_default_sounds", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SoundboardSoundResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/stage-instances": { + "post": { + "operationId": "create_stage_instance", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "privacy_level": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StageInstancesPrivacyLevels" + } + ] + }, + "guild_scheduled_event_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "send_start_notification": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "topic", + "channel_id" + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_stage_instance", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StageInstanceResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/stage-instances/{channel_id}": { + "parameters": [ + { + "name": "channel_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_stage_instance", + "responses": { + "200": { + "description": "200 response for get_stage_instance", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StageInstanceResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_stage_instance", + "responses": { + "204": { + "description": "204 response for delete_stage_instance", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_stage_instance", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "topic": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "privacy_level": { + "$ref": "#/components/schemas/StageInstancesPrivacyLevels" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_stage_instance", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StageInstanceResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/sticker-packs": { + "get": { + "operationId": "list_sticker_packs", + "responses": { + "200": { + "description": "200 response for list_sticker_packs", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickerPackCollectionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/sticker-packs/{pack_id}": { + "parameters": [ + { + "name": "pack_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_sticker_pack", + "responses": { + "200": { + "description": "200 response for get_sticker_pack", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StickerPackResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/stickers/{sticker_id}": { + "parameters": [ + { + "name": "sticker_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_sticker", + "responses": { + "200": { + "description": "200 response for get_sticker", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildStickerResponse" + }, + { + "$ref": "#/components/schemas/StandardStickerResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/users/@me": { + "get": { + "operationId": "get_my_user", + "responses": { + "200": { + "description": "200 response for get_my_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPIIResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "identify" + ] + } + ] + }, + "patch": { + "operationId": "update_my_user", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BotAccountPatchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_my_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPIIResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/users/@me/applications/{application_id}/entitlements": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_current_user_application_entitlements", + "parameters": [ + { + "name": "sku_ids", + "in": "query", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 100, + "uniqueItems": true + } + ] + } + }, + { + "name": "exclude_consumed", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_current_user_application_entitlements", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntitlementResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "OAuth2": [ + "activities.invites.write", + "activities.read", + "activities.write", + "applications.builds.read", + "applications.builds.upload", + "applications.commands", + "applications.commands.permissions.update", + "applications.commands.update", + "applications.entitlements", + "applications.store.update", + "bot", + "connections", + "dm_channels.read", + "email", + "gdm.join", + "guilds", + "guilds.join", + "guilds.members.read", + "identify", + "messages.read", + "openid", + "relationships.read", + "role_connections.write", + "rpc", + "rpc.activities.write", + "rpc.notifications.read", + "rpc.screenshare.read", + "rpc.screenshare.write", + "rpc.video.read", + "rpc.video.write", + "rpc.voice.read", + "rpc.voice.write", + "voice", + "webhook.incoming" + ] + } + ] + } + }, + "/users/@me/applications/{application_id}/role-connection": { + "parameters": [ + { + "name": "application_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_application_user_role_connection", + "responses": { + "200": { + "description": "200 response for get_application_user_role_connection", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserRoleConnectionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "OAuth2": [ + "role_connections.write" + ] + } + ] + }, + "put": { + "operationId": "update_application_user_role_connection", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateApplicationUserRoleConnectionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_application_user_role_connection", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationUserRoleConnectionResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "OAuth2": [ + "role_connections.write" + ] + } + ] + }, + "delete": { + "operationId": "delete_application_user_role_connection", + "responses": { + "204": { + "description": "204 response for delete_application_user_role_connection", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "OAuth2": [ + "role_connections.write" + ] + } + ] + } + }, + "/users/@me/channels": { + "post": { + "operationId": "create_dm", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePrivateChannelRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for create_dm", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/users/@me/connections": { + "get": { + "operationId": "list_my_connections", + "responses": { + "200": { + "description": "200 response for list_my_connections", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ConnectedAccountResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "connections" + ] + } + ] + } + }, + "/users/@me/guilds": { + "get": { + "operationId": "list_my_guilds", + "parameters": [ + { + "name": "before", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "after", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200 + } + }, + { + "name": "with_counts", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "200 response for list_my_guilds", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MyGuildResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + }, + { + "OAuth2": [ + "guilds" + ] + } + ] + } + }, + "/users/@me/guilds/{guild_id}": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "delete": { + "operationId": "leave_guild", + "responses": { + "204": { + "description": "204 response for leave_guild", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/users/@me/guilds/{guild_id}/member": { + "parameters": [ + { + "name": "guild_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_my_guild_member", + "responses": { + "200": { + "description": "200 response for get_my_guild_member", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PrivateGuildMemberResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "OAuth2": [ + "guilds.members.read" + ] + } + ] + } + }, + "/users/{user_id}": { + "parameters": [ + { + "name": "user_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_user", + "responses": { + "200": { + "description": "200 response for get_user", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/voice/regions": { + "get": { + "operationId": "list_voice_regions", + "responses": { + "200": { + "description": "200 response for list_voice_regions", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/VoiceRegionResponse" + } + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_webhook", + "responses": { + "200": { + "description": "200 response for get_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_webhook", + "responses": { + "204": { + "description": "204 response for delete_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_webhook", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "avatar": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}/{webhook_token}": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "webhook_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "get_webhook_by_token", + "responses": { + "200": { + "description": "200 response for get_webhook_by_token", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "post": { + "operationId": "execute_webhook", + "parameters": [ + { + "name": "wait", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "with_components", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IncomingWebhookRequestPartial" + }, + { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IncomingWebhookRequestPartial" + }, + { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + }, + "multipart/form-data": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/IncomingWebhookRequestPartial" + }, + { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + ], + "x-discord-union": "oneOf" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for execute_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "204": { + "description": "204 response for execute_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_webhook_by_token", + "responses": { + "204": { + "description": "204 response for delete_webhook_by_token", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_webhook_by_token", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "avatar": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + } + } + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_webhook_by_token", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}/{webhook_token}/github": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "webhook_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "post": { + "operationId": "execute_github_compatible_webhook", + "parameters": [ + { + "name": "wait", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GithubWebhook" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "204 response for execute_github_compatible_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}/{webhook_token}/messages/@original": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "webhook_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "get": { + "operationId": "get_original_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_original_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_original_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "204": { + "description": "204 response for delete_original_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_original_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "with_components", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + }, + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + }, + { + "type": "object", + "properties": { + "files[0]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[1]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[2]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[3]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[4]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[5]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[6]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[7]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[8]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[9]": { + "type": "string", + "contentEncoding": "binary" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_original_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}/{webhook_token}/messages/{message_id}": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "webhook_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + }, + { + "name": "message_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + } + ], + "get": { + "operationId": "get_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "200": { + "description": "200 response for get_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "delete": { + "operationId": "delete_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "responses": { + "204": { + "description": "204 response for delete_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + }, + "patch": { + "operationId": "update_webhook_message", + "parameters": [ + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + { + "name": "with_components", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + } + }, + "multipart/form-data": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/IncomingWebhookUpdateRequestPartial" + }, + { + "type": "object", + "properties": { + "files[0]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[1]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[2]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[3]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[4]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[5]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[6]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[7]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[8]": { + "type": "string", + "contentEncoding": "binary" + }, + "files[9]": { + "type": "string", + "contentEncoding": "binary" + } + } + } + ] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for update_webhook_message", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponse" + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + }, + "/webhooks/{webhook_id}/{webhook_token}/slack": { + "parameters": [ + { + "name": "webhook_id", + "in": "path", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "required": true + }, + { + "name": "webhook_token", + "in": "path", + "schema": { + "type": "string", + "maxLength": 152133 + }, + "required": true + } + ], + "post": { + "operationId": "execute_slack_compatible_webhook", + "parameters": [ + { + "name": "wait", + "in": "query", + "schema": { + "type": "boolean" + } + }, + { + "name": "thread_id", + "in": "query", + "schema": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SlackWebhook" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/SlackWebhook" + } + }, + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/SlackWebhook" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response for execute_slack_compatible_webhook", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "429": { + "$ref": "#/components/responses/ClientRatelimitedResponse" + }, + "4XX": { + "$ref": "#/components/responses/ClientErrorResponse" + } + }, + "security": [ + {}, + { + "BotToken": [] + } + ] + } + } + }, + "components": { + "schemas": { + "AccountResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name" + ] + }, + "ActionRowComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ButtonComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ChannelSelectComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MentionableSelectComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/RoleSelectComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/StringSelectComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/UserSelectComponentForMessageRequest" + } + ] + }, + "minItems": 1, + "maxItems": 5 + } + }, + "required": [ + "type", + "components" + ] + }, + "ActionRowComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TextInputComponentForModalRequest" + }, + "minItems": 1, + "maxItems": 5 + } + }, + "required": [ + "type", + "components" + ] + }, + "ActionRowComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ButtonComponentResponse" + }, + { + "$ref": "#/components/schemas/ChannelSelectComponentResponse" + }, + { + "$ref": "#/components/schemas/MentionableSelectComponentResponse" + }, + { + "$ref": "#/components/schemas/RoleSelectComponentResponse" + }, + { + "$ref": "#/components/schemas/StringSelectComponentResponse" + }, + { + "$ref": "#/components/schemas/TextInputComponentResponse" + }, + { + "$ref": "#/components/schemas/UserSelectComponentResponse" + } + ] + } + } + }, + "required": [ + "type", + "id", + "components" + ] + }, + "ActionTypes": { + "type": "string", + "oneOf": [ + { + "title": "TYPING_START", + "description": "User started typing in a channel", + "const": "TYPING_START" + }, + { + "title": "INVITE_CREATE", + "description": "Invite to a channel was created", + "const": "INVITE_CREATE" + }, + { + "title": "INVITE_DELETE", + "description": "Invite to a channel was deleted", + "const": "INVITE_DELETE" + }, + { + "title": "WEBHOOKS_UPDATE", + "description": "Guild channel webhook was created, updated, or deleted", + "const": "WEBHOOKS_UPDATE" + }, + { + "title": "CHANNEL_CREATE", + "description": "New guild channel created", + "const": "CHANNEL_CREATE" + }, + { + "title": "VOICE_CHANNEL_STATUS_UPDATE", + "description": "Voice channel status was updated", + "const": "VOICE_CHANNEL_STATUS_UPDATE" + }, + { + "title": "CHANNEL_UPDATE", + "description": "Channel was updated", + "const": "CHANNEL_UPDATE" + }, + { + "title": "CHANNEL_DELETE", + "description": "Channel was deleted", + "const": "CHANNEL_DELETE" + }, + { + "title": "CHANNEL_PINS_UPDATE", + "description": "Message was pinned or unpinned", + "const": "CHANNEL_PINS_UPDATE" + }, + { + "title": "THREAD_CREATE", + "description": "Thread created, also sent when being added to a private thread", + "const": "THREAD_CREATE" + }, + { + "title": "THREAD_UPDATE", + "description": "Thread was updated", + "const": "THREAD_UPDATE" + }, + { + "title": "THREAD_DELETE", + "description": "Thread was deleted", + "const": "THREAD_DELETE" + }, + { + "title": "THREAD_LIST_SYNC", + "description": "Sent when gaining access to a channel, contains all active threads in that channel", + "const": "THREAD_LIST_SYNC" + }, + { + "title": "THREAD_MEMBER_UPDATE", + "description": "Thread member for the current user was updated", + "const": "THREAD_MEMBER_UPDATE" + }, + { + "title": "THREAD_MEMBERS_UPDATE", + "description": "Some user(s) were added to or removed from a thread", + "const": "THREAD_MEMBERS_UPDATE" + }, + { + "title": "GUILD_CREATE", + "description": "Lazy-load for unavailable guild, guild became available, or user joined a new guild", + "const": "GUILD_CREATE" + }, + { + "title": "GUILD_UPDATE", + "description": "Guild was updated", + "const": "GUILD_UPDATE" + }, + { + "title": "GUILD_DELETE", + "description": "Guild became unavailable, or user left/was removed from a guild", + "const": "GUILD_DELETE" + }, + { + "title": "GUILD_EMOJIS_UPDATE", + "description": "Guild emojis were updated", + "const": "GUILD_EMOJIS_UPDATE" + }, + { + "title": "GUILD_STICKERS_UPDATE", + "description": "Guild stickers were updated", + "const": "GUILD_STICKERS_UPDATE" + }, + { + "title": "GUILD_INTEGRATIONS_UPDATE", + "description": "Guild integration was updated", + "const": "GUILD_INTEGRATIONS_UPDATE" + }, + { + "title": "GUILD_MEMBER_ADD", + "description": "New user joined a guild", + "const": "GUILD_MEMBER_ADD" + }, + { + "title": "GUILD_MEMBER_UPDATE", + "description": "Guild member was updated", + "const": "GUILD_MEMBER_UPDATE" + }, + { + "title": "GUILD_MEMBER_REMOVE", + "description": "User was removed from a guild", + "const": "GUILD_MEMBER_REMOVE" + }, + { + "title": "GUILD_BAN_ADD", + "description": "User was banned from a guild", + "const": "GUILD_BAN_ADD" + }, + { + "title": "GUILD_BAN_REMOVE", + "description": "User was unbanned from a guild", + "const": "GUILD_BAN_REMOVE" + }, + { + "title": "GUILD_ROLE_CREATE", + "description": "Guild role was created", + "const": "GUILD_ROLE_CREATE" + }, + { + "title": "GUILD_ROLE_UPDATE", + "description": "Guild role was updated", + "const": "GUILD_ROLE_UPDATE" + }, + { + "title": "GUILD_ROLE_DELETE", + "description": "Guild role was deleted", + "const": "GUILD_ROLE_DELETE" + }, + { + "title": "GUILD_MEMBERS_CHUNK", + "description": "Response to Request Guild Members", + "const": "GUILD_MEMBERS_CHUNK" + }, + { + "title": "MESSAGE_CREATE", + "description": "Message was created", + "const": "MESSAGE_CREATE" + }, + { + "title": "MESSAGE_UPDATE", + "description": "Message was edited", + "const": "MESSAGE_UPDATE" + }, + { + "title": "MESSAGE_DELETE", + "description": "Message was deleted", + "const": "MESSAGE_DELETE" + }, + { + "title": "MESSAGE_DELETE_BULK", + "description": "Multiple messages were deleted at once", + "const": "MESSAGE_DELETE_BULK" + }, + { + "title": "MESSAGE_REACTION_ADD", + "description": "User reacted to a message", + "const": "MESSAGE_REACTION_ADD" + }, + { + "title": "MESSAGE_REACTION_REMOVE", + "description": "User removed a reaction from a message", + "const": "MESSAGE_REACTION_REMOVE" + }, + { + "title": "MESSAGE_REACTION_REMOVE_ALL", + "description": "All reactions were explicitly removed from a message", + "const": "MESSAGE_REACTION_REMOVE_ALL" + }, + { + "title": "MESSAGE_REACTION_REMOVE_EMOJI", + "description": "All reactions for a given emoji were explicitly removed from a message", + "const": "MESSAGE_REACTION_REMOVE_EMOJI" + }, + { + "title": "USER_UPDATE", + "description": "Properties about the user changed", + "const": "USER_UPDATE" + }, + { + "title": "ENTITLEMENT_CREATE", + "description": "Entitlement was created", + "const": "ENTITLEMENT_CREATE" + }, + { + "title": "ENTITLEMENT_UPDATE", + "description": "Entitlement was updated", + "const": "ENTITLEMENT_UPDATE" + }, + { + "title": "ENTITLEMENT_DELETE", + "description": "Entitlement was deleted", + "const": "ENTITLEMENT_DELETE" + }, + { + "title": "READY", + "description": "Contains the initial state information", + "const": "READY" + }, + { + "title": "RESUMED", + "description": "Response to Resume", + "const": "RESUMED" + }, + { + "title": "PRESENCE_UPDATE", + "description": "User was updated", + "const": "PRESENCE_UPDATE" + }, + { + "title": "VOICE_STATE_UPDATE", + "description": "Someone joined, left, or moved a voice channel", + "const": "VOICE_STATE_UPDATE" + }, + { + "title": "VOICE_SERVER_UPDATE", + "description": "Guild's voice server was updated", + "const": "VOICE_SERVER_UPDATE" + }, + { + "title": "LOBBY_MESSAGE_CREATE", + "description": "Sent when a message is created in a lobby", + "const": "LOBBY_MESSAGE_CREATE" + }, + { + "title": "LOBBY_MESSAGE_UPDATE", + "description": "Sent when a message is updated in a lobby", + "const": "LOBBY_MESSAGE_UPDATE" + }, + { + "title": "LOBBY_MESSAGE_DELETE", + "description": "Sent when a message is deleted from a lobby", + "const": "LOBBY_MESSAGE_DELETE" + }, + { + "title": "GAME_DIRECT_MESSAGE_CREATE", + "description": "Sent when a direct message is created during an active Social SDK session", + "const": "GAME_DIRECT_MESSAGE_CREATE" + }, + { + "title": "GAME_DIRECT_MESSAGE_DELETE", + "description": "Sent when a direct message is deleted during an active Social SDK session", + "const": "GAME_DIRECT_MESSAGE_DELETE" + }, + { + "title": "GAME_DIRECT_MESSAGE_UPDATE", + "description": "Sent when a direct message is updated during an active Social SDK session", + "const": "GAME_DIRECT_MESSAGE_UPDATE" + }, + { + "title": "INTERACTION_CREATE", + "description": "User used an interaction, such as an Application Command", + "const": "INTERACTION_CREATE" + }, + { + "title": "INTEGRATION_CREATE", + "description": "Guild integration was created", + "const": "INTEGRATION_CREATE" + }, + { + "title": "INTEGRATION_UPDATE", + "description": "Guild integration was updated", + "const": "INTEGRATION_UPDATE" + }, + { + "title": "INTEGRATION_DELETE", + "description": "Guild integration was deleted", + "const": "INTEGRATION_DELETE" + }, + { + "title": "APPLICATION_COMMAND_PERMISSIONS_UPDATE", + "description": "Application command permission was updated", + "const": "APPLICATION_COMMAND_PERMISSIONS_UPDATE" + }, + { + "title": "APPLICATION_AUTHORIZED", + "description": "Sent when an app was authorized by a user to a server or their account", + "const": "APPLICATION_AUTHORIZED" + }, + { + "title": "APPLICATION_DEAUTHORIZED", + "description": "Sent when an app was deauthorized by a user", + "const": "APPLICATION_DEAUTHORIZED" + }, + { + "title": "STAGE_INSTANCE_CREATE", + "description": "Stage instance was created", + "const": "STAGE_INSTANCE_CREATE" + }, + { + "title": "STAGE_INSTANCE_UPDATE", + "description": "Stage instance was updated", + "const": "STAGE_INSTANCE_UPDATE" + }, + { + "title": "STAGE_INSTANCE_DELETE", + "description": "Stage instance was deleted or closed", + "const": "STAGE_INSTANCE_DELETE" + }, + { + "title": "GUILD_AUDIT_LOG_ENTRY_CREATE", + "description": "A guild audit log entry was created", + "const": "GUILD_AUDIT_LOG_ENTRY_CREATE" + }, + { + "title": "GUILD_SCHEDULED_EVENT_CREATE", + "description": "Guild scheduled event was created", + "const": "GUILD_SCHEDULED_EVENT_CREATE" + }, + { + "title": "GUILD_SCHEDULED_EVENT_UPDATE", + "description": "Guild scheduled event was updated", + "const": "GUILD_SCHEDULED_EVENT_UPDATE" + }, + { + "title": "GUILD_SCHEDULED_EVENT_DELETE", + "description": "Guild scheduled event was deleted", + "const": "GUILD_SCHEDULED_EVENT_DELETE" + }, + { + "title": "GUILD_SCHEDULED_EVENT_USER_ADD", + "description": "User subscribed to a guild scheduled event", + "const": "GUILD_SCHEDULED_EVENT_USER_ADD" + }, + { + "title": "GUILD_SCHEDULED_EVENT_USER_REMOVE", + "description": "User unsubscribed from a guild scheduled event", + "const": "GUILD_SCHEDULED_EVENT_USER_REMOVE" + }, + { + "title": "AUTO_MODERATION_RULE_CREATE", + "description": "Auto Moderation rule was created", + "const": "AUTO_MODERATION_RULE_CREATE" + }, + { + "title": "AUTO_MODERATION_RULE_UPDATE", + "description": "Auto Moderation rule was updated", + "const": "AUTO_MODERATION_RULE_UPDATE" + }, + { + "title": "AUTO_MODERATION_RULE_DELETE", + "description": "Auto Moderation rule was deleted", + "const": "AUTO_MODERATION_RULE_DELETE" + }, + { + "title": "AUTO_MODERATION_ACTION_EXECUTION", + "description": "Auto Moderation rule was triggered and an action was executed (.e.g. a message was blocked)", + "const": "AUTO_MODERATION_ACTION_EXECUTION" + }, + { + "title": "GUILD_SOUNDBOARD_SOUNDS_UPDATE", + "const": "GUILD_SOUNDBOARD_SOUNDS_UPDATE" + }, + { + "title": "GUILD_SOUNDBOARD_SOUND_CREATE", + "const": "GUILD_SOUNDBOARD_SOUND_CREATE" + }, + { + "title": "GUILD_SOUNDBOARD_SOUND_UPDATE", + "const": "GUILD_SOUNDBOARD_SOUND_UPDATE" + }, + { + "title": "GUILD_SOUNDBOARD_SOUND_DELETE", + "const": "GUILD_SOUNDBOARD_SOUND_DELETE" + }, + { + "title": "QUEST_USER_ENROLLMENT", + "description": "User was added to a Quest (currently unavailable)", + "const": "QUEST_USER_ENROLLMENT" + }, + { + "title": "RATE_LIMITED", + "const": "RATE_LIMITED" + } + ] + }, + "ActivitiesAttachmentResponse": { + "type": "object", + "properties": { + "attachment": { + "$ref": "#/components/schemas/AttachmentResponse" + } + }, + "required": [ + "attachment" + ] + }, + "ActivityActionTypes": { + "type": "integer", + "oneOf": [ + { + "title": "JOIN", + "const": 1 + }, + { + "title": "SPECTATE", + "const": 2 + }, + { + "title": "LISTEN", + "const": 3 + }, + { + "title": "JOIN_REQUEST", + "const": 5 + }, + { + "title": "STREAM_REQUEST", + "const": 6 + } + ], + "format": "int32" + }, + "ActivityInstanceCallbackResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "AfkTimeouts": { + "type": "integer", + "oneOf": [ + { + "title": "ONE_MINUTE", + "const": 60 + }, + { + "title": "FIVE_MINUTES", + "const": 300 + }, + { + "title": "FIFTEEN_MINUTES", + "const": 900 + }, + { + "title": "THIRTY_MINUTES", + "const": 1800 + }, + { + "title": "ONE_HOUR", + "const": 3600 + } + ], + "format": "int32" + }, + "AllowedMentionTypes": { + "type": "string", + "oneOf": [ + { + "title": "USERS", + "description": "Controls role mentions", + "const": "users" + }, + { + "title": "ROLES", + "description": "Controls user mentions", + "const": "roles" + }, + { + "title": "EVERYONE", + "description": "Controls @everyone and @here mentions", + "const": "everyone" + } + ] + }, + "ApplicationCommandAttachmentOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 11 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandAttachmentOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 11 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandAutocompleteCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "data": { + "anyOf": [ + { + "$ref": "#/components/schemas/InteractionApplicationCommandAutocompleteCallbackIntegerData" + }, + { + "$ref": "#/components/schemas/InteractionApplicationCommandAutocompleteCallbackNumberData" + }, + { + "$ref": "#/components/schemas/InteractionApplicationCommandAutocompleteCallbackStringData" + } + ], + "x-discord-union": "oneOf" + } + }, + "required": [ + "type", + "data" + ] + }, + "ApplicationCommandBooleanOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandBooleanOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandChannelOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "channel_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "uniqueItems": true + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandChannelOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "channel_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "uniqueItems": true + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "options": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandGroupOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOption" + } + ] + }, + "maxItems": 25 + }, + "default_member_permissions": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 18014398509481983 + }, + "dm_permission": { + "type": [ + "boolean", + "null" + ] + }, + "contexts": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InteractionContextType" + }, + "minItems": 1, + "uniqueItems": true + }, + "integration_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationIntegrationType" + }, + "minItems": 1, + "uniqueItems": true + }, + "handler": { + "description": "Determines whether the interaction is handled by the app's interactions handler or by Discord", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandHandler" + } + ] + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandType" + } + ] + } + }, + "required": [ + "name" + ] + }, + "ApplicationCommandHandler": { + "type": "integer", + "oneOf": [ + { + "title": "APP_HANDLER", + "description": "The app handles the interaction using an interaction token", + "const": 1 + }, + { + "title": "DISCORD_LAUNCH_ACTIVITY", + "description": "Discord handles the interaction by launching an Activity and sending a follow-up message without coordinating with the app", + "const": 2 + } + ], + "format": "int32" + }, + "ApplicationCommandIntegerOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "autocomplete": { + "type": [ + "boolean", + "null" + ] + }, + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionIntegerChoice" + }, + "maxItems": 25 + }, + "min_value": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Int53Type" + } + ] + }, + "max_value": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Int53Type" + } + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandIntegerOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "autocomplete": { + "type": "boolean" + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionIntegerChoiceResponse" + } + }, + "min_value": { + "$ref": "#/components/schemas/Int53Type" + }, + "max_value": { + "$ref": "#/components/schemas/Int53Type" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandInteractionMetadataResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "authorizing_integration_owners": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "original_response_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "target_user": { + "$ref": "#/components/schemas/UserResponse" + }, + "target_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type", + "authorizing_integration_owners" + ] + }, + "ApplicationCommandMentionableOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 9 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandMentionableOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 9 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandNumberOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 10 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "autocomplete": { + "type": [ + "boolean", + "null" + ] + }, + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionNumberChoice" + }, + "maxItems": 25 + }, + "min_value": { + "type": [ + "number", + "null" + ], + "format": "double" + }, + "max_value": { + "type": [ + "number", + "null" + ], + "format": "double" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandNumberOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 10 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "autocomplete": { + "type": "boolean" + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionNumberChoiceResponse" + } + }, + "min_value": { + "type": "number", + "format": "double" + }, + "max_value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandOptionIntegerChoice": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "value": { + "$ref": "#/components/schemas/Int53Type" + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionIntegerChoiceResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "value": { + "$ref": "#/components/schemas/Int53Type" + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionNumberChoice": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionNumberChoiceResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionStringChoice": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "value": { + "type": "string", + "maxLength": 6000 + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionStringChoiceResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "value": { + "type": "string" + } + }, + "required": [ + "name", + "value" + ] + }, + "ApplicationCommandOptionType": { + "type": "integer", + "oneOf": [ + { + "title": "SUB_COMMAND", + "description": "A sub-action within a command or group", + "const": 1 + }, + { + "title": "SUB_COMMAND_GROUP", + "description": "A group of subcommands", + "const": 2 + }, + { + "title": "STRING", + "description": "A string option", + "const": 3 + }, + { + "title": "INTEGER", + "description": "An integer option. Any integer between -2^53 and 2^53 is a valid value", + "const": 4 + }, + { + "title": "BOOLEAN", + "description": "A boolean option", + "const": 5 + }, + { + "title": "USER", + "description": "A snowflake option that represents a User", + "const": 6 + }, + { + "title": "CHANNEL", + "description": "A snowflake option that represents a Channel. Includes all channel types and categories", + "const": 7 + }, + { + "title": "ROLE", + "description": "A snowflake option that represents a Role", + "const": 8 + }, + { + "title": "MENTIONABLE", + "description": "A snowflake option that represents anything you can mention", + "const": 9 + }, + { + "title": "NUMBER", + "description": "A number option. Any double between -2^53 and 2^53 is a valid value", + "const": 10 + }, + { + "title": "ATTACHMENT", + "description": "An attachment option", + "const": 11 + } + ], + "format": "int32" + }, + "ApplicationCommandPatchRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "options": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandGroupOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOption" + } + ] + }, + "maxItems": 25 + }, + "default_member_permissions": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 18014398509481983 + }, + "dm_permission": { + "type": [ + "boolean", + "null" + ] + }, + "contexts": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InteractionContextType" + }, + "minItems": 1, + "uniqueItems": true + }, + "integration_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationIntegrationType" + }, + "minItems": 1, + "uniqueItems": true + }, + "handler": { + "description": "Determines whether the interaction is handled by the app's interactions handler or by Discord", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandHandler" + } + ] + } + } + }, + "ApplicationCommandPermission": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/ApplicationCommandPermissionType" + }, + "permission": { + "type": "boolean" + } + }, + "required": [ + "id", + "type", + "permission" + ] + }, + "ApplicationCommandPermissionType": { + "type": "integer", + "oneOf": [ + { + "title": "ROLE", + "description": "This permission is for a role.", + "const": 1 + }, + { + "title": "USER", + "description": "This permission is for a user.", + "const": 2 + }, + { + "title": "CHANNEL", + "description": "This permission is for a channel.", + "const": 3 + } + ], + "format": "int32" + }, + "ApplicationCommandResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "version": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "default_member_permissions": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/components/schemas/ApplicationCommandType" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "dm_permission": { + "type": "boolean" + }, + "contexts": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InteractionContextType" + }, + "uniqueItems": true + }, + "integration_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationIntegrationType" + }, + "uniqueItems": true + }, + "options": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandGroupOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOptionResponse" + } + ] + } + }, + "nsfw": { + "type": "boolean" + }, + "handler": { + "$ref": "#/components/schemas/ApplicationCommandHandler", + "description": "Determines whether the interaction is handled by the app's interactions handler or by Discord" + } + }, + "required": [ + "id", + "application_id", + "version", + "default_member_permissions", + "type", + "name", + "description" + ] + }, + "ApplicationCommandRoleOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandRoleOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandStringOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "autocomplete": { + "type": [ + "boolean", + "null" + ] + }, + "min_length": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 6000 + }, + "max_length": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 6000 + }, + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionStringChoice" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandStringOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "autocomplete": { + "type": "boolean" + }, + "choices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommandOptionStringChoiceResponse" + } + }, + "min_length": { + "type": "integer", + "format": "int32" + }, + "max_length": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandSubcommandGroupOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOption" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandSubcommandGroupOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOptionResponse" + } + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandSubcommandOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOption" + } + ] + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandSubcommandOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOptionResponse" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOptionResponse" + } + ] + } + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandType": { + "type": "integer", + "oneOf": [ + { + "title": "CHAT", + "description": "Slash commands; a text-based command that shows up when a user types /", + "const": 1 + }, + { + "title": "USER", + "description": "A UI-based command that shows up when you right click or tap on a user", + "const": 2 + }, + { + "title": "MESSAGE", + "description": "A UI-based command that shows up when you right click or tap on a message", + "const": 3 + }, + { + "title": "PRIMARY_ENTRY_POINT", + "description": "A command that represents the primary way to use an application (e.g. launching an Activity)", + "const": 4 + } + ], + "format": "int32" + }, + "ApplicationCommandUpdateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "options": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandAttachmentOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandBooleanOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandChannelOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandIntegerOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandMentionableOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandNumberOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandRoleOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandStringOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandGroupOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandSubcommandOption" + }, + { + "$ref": "#/components/schemas/ApplicationCommandUserOption" + } + ] + }, + "maxItems": 25 + }, + "default_member_permissions": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 18014398509481983 + }, + "dm_permission": { + "type": [ + "boolean", + "null" + ] + }, + "contexts": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InteractionContextType" + }, + "minItems": 1, + "uniqueItems": true + }, + "integration_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ApplicationIntegrationType" + }, + "minItems": 1, + "uniqueItems": true + }, + "handler": { + "description": "Determines whether the interaction is handled by the app's interactions handler or by Discord", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandHandler" + } + ] + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandType" + } + ] + }, + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "name" + ] + }, + "ApplicationCommandUserOption": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 32 + }, + "maxProperties": 34 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 34 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationCommandUserOptionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandOptionType" + } + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "name_localized": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localized": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "required": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "description" + ] + }, + "ApplicationEventWebhooksStatus": { + "type": "integer", + "oneOf": [ + { + "title": "DISABLED", + "description": "Webhook events are disabled by developer", + "const": 1 + }, + { + "title": "ENABLED", + "description": "Webhook events are enabled by developer", + "const": 2 + }, + { + "title": "DISABLED_BY_DISCORD", + "description": "Webhook events are disabled by Discord, usually due to inactivity", + "const": 3 + } + ], + "format": "int32" + }, + "ApplicationExplicitContentFilterTypes": { + "type": "integer", + "oneOf": [ + { + "title": "INHERIT", + "description": "inherit guild content filter setting", + "const": 0 + }, + { + "title": "ALWAYS", + "description": "interactions will always be scanned", + "const": 1 + } + ], + "format": "int32" + }, + "ApplicationFormPartial": { + "type": "object", + "properties": { + "description": { + "type": [ + "object", + "null" + ], + "properties": { + "default": { + "type": "string", + "maxLength": 400 + }, + "localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 400 + } + } + }, + "required": [ + "default" + ] + }, + "icon": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "cover_image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "team_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "interactions_endpoint_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "explicit_content_filter": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationExplicitContentFilterTypes" + } + ] + }, + "max_participants": { + "type": [ + "integer", + "null" + ], + "minimum": -1, + "format": "int32" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "tags": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "maxLength": 20 + }, + "maxItems": 5, + "uniqueItems": true + }, + "custom_install_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "install_params": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParams" + } + ] + }, + "role_connections_verification_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "integration_types_config": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationIntegrationTypeConfiguration" + } + ] + }, + "minProperties": 1, + "maxProperties": 2 + }, + "event_webhooks_status": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1, + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ApplicationEventWebhooksStatus" + } + ], + "format": "int32" + } + ] + }, + "event_webhooks_url": { + "type": [ + "string", + "null" + ], + "description": "Event webhooks URL for the app to receive webhook events", + "maxLength": 2048, + "format": "uri" + }, + "event_webhooks_types": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "APPLICATION_AUTHORIZED", + "APPLICATION_DEAUTHORIZED", + "ENTITLEMENT_CREATE", + "ENTITLEMENT_DELETE", + "ENTITLEMENT_UPDATE", + "GAME_DIRECT_MESSAGE_CREATE", + "GAME_DIRECT_MESSAGE_DELETE", + "GAME_DIRECT_MESSAGE_UPDATE", + "LOBBY_MESSAGE_CREATE", + "LOBBY_MESSAGE_DELETE", + "LOBBY_MESSAGE_UPDATE", + "QUEST_USER_ENROLLMENT" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ActionTypes" + } + ] + }, + "uniqueItems": true + } + } + }, + "ApplicationIdentityProviderAuthType": { + "type": "string", + "oneOf": [ + { + "title": "OIDC", + "const": "OIDC" + }, + { + "title": "EPIC_ONLINE_SERVICES_ACCESS_TOKEN", + "const": "EPIC_ONLINE_SERVICES_ACCESS_TOKEN" + }, + { + "title": "EPIC_ONLINE_SERVICES_ID_TOKEN", + "const": "EPIC_ONLINE_SERVICES_ID_TOKEN" + }, + { + "title": "STEAM_SESSION_TICKET", + "const": "STEAM_SESSION_TICKET" + }, + { + "title": "UNITY_SERVICES_ID_TOKEN", + "const": "UNITY_SERVICES_ID_TOKEN" + }, + { + "title": "DISCORD_BOT_ISSUED_ACCESS_TOKEN", + "const": "DISCORD_BOT_ISSUED_ACCESS_TOKEN" + }, + { + "title": "APPLE_ID_TOKEN", + "const": "APPLE_ID_TOKEN" + }, + { + "title": "PLAYSTATION_NETWORK_ID_TOKEN", + "const": "PLAYSTATION_NETWORK_ID_TOKEN" + } + ] + }, + "ApplicationIncomingWebhookResponse": { + "type": "object", + "properties": { + "application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/WebhookTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "application_id", + "avatar", + "channel_id", + "id", + "name", + "type" + ] + }, + "ApplicationIntegrationType": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_INSTALL", + "description": "For Guild install.", + "const": 0 + }, + { + "title": "USER_INSTALL", + "description": "For User install.", + "const": 1 + } + ], + "format": "int32" + }, + "ApplicationIntegrationTypeConfiguration": { + "type": "object", + "properties": { + "oauth2_install_params": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParams" + } + ] + } + } + }, + "ApplicationIntegrationTypeConfigurationResponse": { + "type": "object", + "properties": { + "oauth2_install_params": { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParamsResponse" + } + } + }, + "ApplicationOAuth2InstallParams": { + "type": "object", + "properties": { + "scopes": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "enum": [ + "applications.commands", + "bot" + ], + "allOf": [ + { + "$ref": "#/components/schemas/OAuth2Scopes" + } + ] + }, + "minItems": 1, + "uniqueItems": true + }, + "permissions": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 18014398509481983 + } + } + }, + "ApplicationOAuth2InstallParamsResponse": { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "applications.commands", + "bot" + ], + "allOf": [ + { + "$ref": "#/components/schemas/OAuth2Scopes" + } + ] + }, + "uniqueItems": true + }, + "permissions": { + "type": "string" + } + }, + "required": [ + "scopes", + "permissions" + ] + }, + "ApplicationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "cover_image": { + "type": "string" + }, + "primary_sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "bot": { + "$ref": "#/components/schemas/UserResponse" + }, + "slug": { + "type": "string" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "rpc_origins": { + "type": "array", + "items": { + "type": "string" + } + }, + "bot_public": { + "type": "boolean" + }, + "bot_require_code_grant": { + "type": "boolean" + }, + "terms_of_service_url": { + "type": "string", + "format": "uri" + }, + "privacy_policy_url": { + "type": "string", + "format": "uri" + }, + "custom_install_url": { + "type": "string", + "format": "uri" + }, + "install_params": { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParamsResponse" + }, + "integration_types_config": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ApplicationIntegrationTypeConfigurationResponse" + } + }, + "verify_key": { + "type": "string" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "flags_new": { + "type": "string" + }, + "max_participants": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "type", + "verify_key", + "flags", + "flags_new" + ] + }, + "ApplicationRoleConnectionsMetadataItemRequest": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MetadataItemTypes" + }, + "key": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 1521 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 200 + }, + "maxProperties": 1521 + } + }, + "required": [ + "type", + "key", + "name", + "description" + ] + }, + "ApplicationRoleConnectionsMetadataItemResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MetadataItemTypes" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "name_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "description_localizations": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "type", + "key", + "name", + "description" + ] + }, + "ApplicationTypes": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_ROLE_SUBSCRIPTIONS", + "const": 4 + } + ], + "format": "int32" + }, + "ApplicationUserRoleConnectionResponse": { + "type": "object", + "properties": { + "platform_name": { + "type": "string" + }, + "platform_username": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "AttachmentResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "filename": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "url": { + "type": "string", + "format": "uri" + }, + "proxy_url": { + "type": "string", + "format": "uri" + }, + "width": { + "type": "integer", + "format": "int32" + }, + "height": { + "type": "integer", + "format": "int32" + }, + "duration_secs": { + "type": "number", + "format": "double" + }, + "waveform": { + "type": "string" + }, + "description": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "ephemeral": { + "type": "boolean" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "placeholder": { + "type": "string" + }, + "placeholder_version": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "application": { + "$ref": "#/components/schemas/ApplicationResponse" + }, + "clip_created_at": { + "type": "string", + "format": "date-time" + }, + "clip_participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + } + }, + "required": [ + "id", + "filename", + "size", + "url", + "proxy_url" + ] + }, + "AuditLogActionTypes": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_UPDATE", + "const": 1 + }, + { + "title": "CHANNEL_CREATE", + "const": 10 + }, + { + "title": "CHANNEL_UPDATE", + "const": 11 + }, + { + "title": "CHANNEL_DELETE", + "const": 12 + }, + { + "title": "CHANNEL_OVERWRITE_CREATE", + "const": 13 + }, + { + "title": "CHANNEL_OVERWRITE_UPDATE", + "const": 14 + }, + { + "title": "CHANNEL_OVERWRITE_DELETE", + "const": 15 + }, + { + "title": "MEMBER_KICK", + "const": 20 + }, + { + "title": "MEMBER_PRUNE", + "const": 21 + }, + { + "title": "MEMBER_BAN_ADD", + "const": 22 + }, + { + "title": "MEMBER_BAN_REMOVE", + "const": 23 + }, + { + "title": "MEMBER_UPDATE", + "const": 24 + }, + { + "title": "MEMBER_ROLE_UPDATE", + "const": 25 + }, + { + "title": "MEMBER_MOVE", + "const": 26 + }, + { + "title": "MEMBER_DISCONNECT", + "const": 27 + }, + { + "title": "BOT_ADD", + "const": 28 + }, + { + "title": "ROLE_CREATE", + "const": 30 + }, + { + "title": "ROLE_UPDATE", + "const": 31 + }, + { + "title": "ROLE_DELETE", + "const": 32 + }, + { + "title": "INVITE_CREATE", + "const": 40 + }, + { + "title": "INVITE_UPDATE", + "const": 41 + }, + { + "title": "INVITE_DELETE", + "const": 42 + }, + { + "title": "WEBHOOK_CREATE", + "const": 50 + }, + { + "title": "WEBHOOK_UPDATE", + "const": 51 + }, + { + "title": "WEBHOOK_DELETE", + "const": 52 + }, + { + "title": "EMOJI_CREATE", + "const": 60 + }, + { + "title": "EMOJI_UPDATE", + "const": 61 + }, + { + "title": "EMOJI_DELETE", + "const": 62 + }, + { + "title": "MESSAGE_DELETE", + "const": 72 + }, + { + "title": "MESSAGE_BULK_DELETE", + "const": 73 + }, + { + "title": "MESSAGE_PIN", + "const": 74 + }, + { + "title": "MESSAGE_UNPIN", + "const": 75 + }, + { + "title": "INTEGRATION_CREATE", + "const": 80 + }, + { + "title": "INTEGRATION_UPDATE", + "const": 81 + }, + { + "title": "INTEGRATION_DELETE", + "const": 82 + }, + { + "title": "STAGE_INSTANCE_CREATE", + "const": 83 + }, + { + "title": "STAGE_INSTANCE_UPDATE", + "const": 84 + }, + { + "title": "STAGE_INSTANCE_DELETE", + "const": 85 + }, + { + "title": "STICKER_CREATE", + "const": 90 + }, + { + "title": "STICKER_UPDATE", + "const": 91 + }, + { + "title": "STICKER_DELETE", + "const": 92 + }, + { + "title": "GUILD_SCHEDULED_EVENT_CREATE", + "const": 100 + }, + { + "title": "GUILD_SCHEDULED_EVENT_UPDATE", + "const": 101 + }, + { + "title": "GUILD_SCHEDULED_EVENT_DELETE", + "const": 102 + }, + { + "title": "THREAD_CREATE", + "const": 110 + }, + { + "title": "THREAD_UPDATE", + "const": 111 + }, + { + "title": "THREAD_DELETE", + "const": 112 + }, + { + "title": "APPLICATION_COMMAND_PERMISSION_UPDATE", + "const": 121 + }, + { + "title": "SOUNDBOARD_SOUND_CREATE", + "const": 130 + }, + { + "title": "SOUNDBOARD_SOUND_UPDATE", + "const": 131 + }, + { + "title": "SOUNDBOARD_SOUND_DELETE", + "const": 132 + }, + { + "title": "AUTO_MODERATION_RULE_CREATE", + "const": 140 + }, + { + "title": "AUTO_MODERATION_RULE_UPDATE", + "const": 141 + }, + { + "title": "AUTO_MODERATION_RULE_DELETE", + "const": 142 + }, + { + "title": "AUTO_MODERATION_BLOCK_MESSAGE", + "const": 143 + }, + { + "title": "AUTO_MODERATION_FLAG_TO_CHANNEL", + "const": 144 + }, + { + "title": "AUTO_MODERATION_USER_COMM_DISABLED", + "const": 145 + }, + { + "title": "AUTO_MODERATION_QUARANTINE_USER", + "const": 146 + }, + { + "title": "CREATOR_MONETIZATION_REQUEST_CREATED", + "const": 150 + }, + { + "title": "CREATOR_MONETIZATION_TERMS_ACCEPTED", + "const": 151 + }, + { + "title": "ONBOARDING_PROMPT_CREATE", + "const": 163 + }, + { + "title": "ONBOARDING_PROMPT_UPDATE", + "const": 164 + }, + { + "title": "ONBOARDING_PROMPT_DELETE", + "const": 165 + }, + { + "title": "ONBOARDING_CREATE", + "const": 166 + }, + { + "title": "ONBOARDING_UPDATE", + "const": 167 + }, + { + "title": "GUILD_HOME_FEATURE_ITEM", + "const": 171 + }, + { + "title": "GUILD_HOME_REMOVE_ITEM", + "const": 172 + }, + { + "title": "HARMFUL_LINKS_BLOCKED_MESSAGE", + "const": 180 + }, + { + "title": "HOME_SETTINGS_CREATE", + "const": 190 + }, + { + "title": "HOME_SETTINGS_UPDATE", + "const": 191 + }, + { + "title": "VOICE_CHANNEL_STATUS_CREATE", + "const": 192 + }, + { + "title": "VOICE_CHANNEL_STATUS_DELETE", + "const": 193 + }, + { + "title": "GUILD_SCHEDULED_EVENT_EXCEPTION_CREATE", + "description": "Scheduled event exception was created", + "const": 200 + }, + { + "title": "GUILD_SCHEDULED_EVENT_EXCEPTION_UPDATE", + "description": "Scheduled event exception was updated", + "const": 201 + }, + { + "title": "GUILD_SCHEDULED_EVENT_EXCEPTION_DELETE", + "description": "Scheduled event exception was deleted", + "const": 202 + }, + { + "title": "GUILD_PROFILE_UPDATE", + "const": 211 + } + ], + "format": "int32" + }, + "AuditLogEntryResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "action_type": { + "$ref": "#/components/schemas/AuditLogActionTypes" + }, + "user_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "target_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "changes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogObjectChangeResponse" + } + }, + "options": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "reason": { + "type": "string" + } + }, + "required": [ + "id", + "action_type", + "user_id", + "target_id" + ] + }, + "AuditLogObjectChangeResponse": { + "type": "object", + "properties": { + "key": { + "type": [ + "string", + "null" + ] + }, + "new_value": {}, + "old_value": {} + }, + "required": [ + "key" + ] + }, + "AuthorType": { + "type": "string", + "oneOf": [ + { + "title": "USER", + "const": "user" + }, + { + "title": "BOT", + "const": "bot" + }, + { + "title": "WEBHOOK", + "const": "webhook" + }, + { + "title": "NO_USER", + "const": "-user" + }, + { + "title": "NO_BOT", + "const": "-bot" + }, + { + "title": "NO_WEBHOOK", + "const": "-webhook" + } + ] + }, + "AutomodActionType": { + "type": "integer", + "oneOf": [ + { + "title": "BLOCK_MESSAGE", + "description": "Block a user's message and prevent it from being posted. A custom explanation can be specified and shown to members whenever their message is blocked", + "const": 1 + }, + { + "title": "FLAG_TO_CHANNEL", + "description": "Send a system message to a channel in order to log the user message that triggered the rule", + "const": 2 + }, + { + "title": "USER_COMMUNICATION_DISABLED", + "description": "Temporarily disable a user's ability to communicate in the server (timeout)", + "const": 3 + }, + { + "title": "QUARANTINE_USER", + "description": "Prevent a user from interacting in the server", + "const": 4 + } + ], + "format": "int32" + }, + "AutomodEventType": { + "type": "integer", + "oneOf": [ + { + "title": "MESSAGE_SEND", + "description": "A user submitted a message to a channel", + "const": 1 + }, + { + "title": "GUILD_MEMBER_JOIN_OR_UPDATE", + "description": "A user is attempting to join the server or a member's properties were updated.", + "const": 2 + } + ], + "format": "int32" + }, + "AutomodKeywordPresetType": { + "type": "integer", + "oneOf": [ + { + "title": "PROFANITY", + "description": "Words and phrases that may be considered profanity", + "const": 1 + }, + { + "title": "SEXUAL_CONTENT", + "description": "Words and phrases that may be considered as sexual content", + "const": 2 + }, + { + "title": "SLURS", + "description": "Words and phrases that may be considered slurs and hate speech", + "const": 3 + } + ], + "format": "int32" + }, + "AutomodTriggerType": { + "type": "integer", + "oneOf": [ + { + "title": "KEYWORD", + "description": "Check if content contains words from a list of keywords or matches regex", + "const": 1 + }, + { + "title": "ML_SPAM", + "description": "Check if content represents generic spam", + "const": 3 + }, + { + "title": "DEFAULT_KEYWORD_LIST", + "description": "Check if content contains words from internal pre-defined wordsets", + "const": 4 + }, + { + "title": "MENTION_SPAM", + "description": "Check if content contains more unique mentions than allowed", + "const": 5 + }, + { + "title": "USER_PROFILE", + "description": "Check if user profile fields contains words from a list of keywords or matches regex", + "const": 6 + } + ], + "format": "int32" + }, + "AvailableLocalesEnum": { + "type": "string", + "oneOf": [ + { + "title": "ar", + "description": "The ar locale", + "const": "ar" + }, + { + "title": "bg", + "description": "The bg locale", + "const": "bg" + }, + { + "title": "cs", + "description": "The cs locale", + "const": "cs" + }, + { + "title": "da", + "description": "The da locale", + "const": "da" + }, + { + "title": "de", + "description": "The de locale", + "const": "de" + }, + { + "title": "el", + "description": "The el locale", + "const": "el" + }, + { + "title": "en-GB", + "description": "The en-GB locale", + "const": "en-GB" + }, + { + "title": "en-US", + "description": "The en-US locale", + "const": "en-US" + }, + { + "title": "es-419", + "description": "The es-419 locale", + "const": "es-419" + }, + { + "title": "es-ES", + "description": "The es-ES locale", + "const": "es-ES" + }, + { + "title": "fi", + "description": "The fi locale", + "const": "fi" + }, + { + "title": "fr", + "description": "The fr locale", + "const": "fr" + }, + { + "title": "he", + "description": "The he locale", + "const": "he" + }, + { + "title": "hi", + "description": "The hi locale", + "const": "hi" + }, + { + "title": "hr", + "description": "The hr locale", + "const": "hr" + }, + { + "title": "hu", + "description": "The hu locale", + "const": "hu" + }, + { + "title": "id", + "description": "The id locale", + "const": "id" + }, + { + "title": "it", + "description": "The it locale", + "const": "it" + }, + { + "title": "ja", + "description": "The ja locale", + "const": "ja" + }, + { + "title": "ko", + "description": "The ko locale", + "const": "ko" + }, + { + "title": "lt", + "description": "The lt locale", + "const": "lt" + }, + { + "title": "nl", + "description": "The nl locale", + "const": "nl" + }, + { + "title": "no", + "description": "The no locale", + "const": "no" + }, + { + "title": "pl", + "description": "The pl locale", + "const": "pl" + }, + { + "title": "pt-BR", + "description": "The pt-BR locale", + "const": "pt-BR" + }, + { + "title": "ro", + "description": "The ro locale", + "const": "ro" + }, + { + "title": "ru", + "description": "The ru locale", + "const": "ru" + }, + { + "title": "sv-SE", + "description": "The sv-SE locale", + "const": "sv-SE" + }, + { + "title": "th", + "description": "The th locale", + "const": "th" + }, + { + "title": "tr", + "description": "The tr locale", + "const": "tr" + }, + { + "title": "uk", + "description": "The uk locale", + "const": "uk" + }, + { + "title": "vi", + "description": "The vi locale", + "const": "vi" + }, + { + "title": "zh-CN", + "description": "The zh-CN locale", + "const": "zh-CN" + }, + { + "title": "zh-TW", + "description": "The zh-TW locale", + "const": "zh-TW" + } + ] + }, + "BanUserFromGuildRequest": { + "type": "object", + "properties": { + "delete_message_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 604800 + }, + "delete_message_days": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 7 + } + } + }, + "BaseCreateMessageCreateRequest": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "sticker_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 3 + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "shared_client_theme": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CustomClientThemeShareRequest" + } + ] + } + } + }, + "BasicApplicationResponseWithBot": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "cover_image": { + "type": "string" + }, + "primary_sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "bot": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "type" + ] + }, + "BasicGuildMemberResponse": { + "type": "object", + "properties": { + "avatar": { + "type": [ + "string", + "null" + ], + "description": "the member's guild avatar hash" + }, + "avatar_decoration_data": { + "description": "data for the member's guild avatar decoration", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserAvatarDecorationResponse" + } + ] + }, + "banner": { + "type": [ + "string", + "null" + ], + "description": "the member's guild banner hash" + }, + "communication_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out", + "format": "date-time" + }, + "flags": { + "type": "integer", + "description": "guild member flags represented as a bit set, defaults to 0", + "format": "int32" + }, + "joined_at": { + "type": "string", + "description": "when the user joined the guild", + "format": "date-time" + }, + "nick": { + "type": [ + "string", + "null" + ], + "description": "this user's guild nickname" + }, + "pending": { + "type": "boolean", + "description": "whether the user has not yet passed the guild's Membership Screening requirements" + }, + "premium_since": { + "type": [ + "string", + "null" + ], + "description": "when the user started boosting the guild", + "format": "date-time" + }, + "roles": { + "type": "array", + "description": "array of role object ids", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "collectibles": { + "description": "data for the member's collectibles", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserCollectiblesResponse" + } + ] + } + }, + "required": [ + "avatar", + "banner", + "communication_disabled_until", + "flags", + "joined_at", + "nick", + "pending", + "premium_since", + "roles" + ] + }, + "BasicMessageResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MessageType" + }, + "content": { + "type": "string" + }, + "mentions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "mention_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageAttachmentResponse" + } + }, + "embeds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageEmbedResponse" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "edited_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentResponse" + }, + { + "$ref": "#/components/schemas/ContainerComponentResponse" + }, + { + "$ref": "#/components/schemas/FileComponentResponse" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentResponse" + }, + { + "$ref": "#/components/schemas/SectionComponentResponse" + }, + { + "$ref": "#/components/schemas/SeparatorComponentResponse" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + ] + } + }, + "stickers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildStickerResponse" + }, + { + "$ref": "#/components/schemas/StandardStickerResponse" + } + ] + } + }, + "sticker_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageStickerItemResponse" + } + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "author": { + "$ref": "#/components/schemas/UserResponse" + }, + "pinned": { + "type": "boolean" + }, + "mention_everyone": { + "type": "boolean" + }, + "tts": { + "type": "boolean" + }, + "call": { + "$ref": "#/components/schemas/MessageCallResponse" + }, + "activity": { + "$ref": "#/components/schemas/MessageActivityResponse" + }, + "application": { + "$ref": "#/components/schemas/BasicApplicationResponseWithBot" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "interaction": { + "$ref": "#/components/schemas/MessageInteractionResponse" + }, + "nonce": { + "oneOf": [ + { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "format": "int64" + }, + { + "type": "string", + "maxLength": 25, + "format": "nonce" + } + ] + }, + "webhook_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "message_reference": { + "$ref": "#/components/schemas/MessageReferenceResponse" + }, + "thread": { + "$ref": "#/components/schemas/ThreadResponse" + }, + "mention_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageMentionChannelResponse" + } + }, + "role_subscription_data": { + "$ref": "#/components/schemas/MessageRoleSubscriptionDataResponse" + }, + "purchase_notification": { + "$ref": "#/components/schemas/PurchaseNotificationResponse" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "resolved": { + "$ref": "#/components/schemas/ResolvedObjectsResponse" + }, + "poll": { + "$ref": "#/components/schemas/PollResponse" + }, + "shared_client_theme": { + "$ref": "#/components/schemas/CustomClientThemeResponse" + }, + "interaction_metadata": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/MessageComponentInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/ModalSubmitInteractionMetadataResponse" + } + ] + }, + "message_snapshots": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageSnapshotResponse" + } + } + }, + "required": [ + "type", + "content", + "mentions", + "mention_roles", + "attachments", + "embeds", + "timestamp", + "edited_timestamp", + "flags", + "components", + "id", + "channel_id", + "author", + "pinned", + "mention_everyone", + "tts" + ] + }, + "BlockMessageAction": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/BlockMessageActionMetadata" + } + ] + } + }, + "required": [ + "type" + ] + }, + "BlockMessageActionMetadata": { + "type": "object", + "properties": { + "custom_message": { + "type": [ + "string", + "null" + ], + "maxLength": 300 + } + } + }, + "BlockMessageActionMetadataResponse": { + "type": "object", + "properties": { + "custom_message": { + "type": "string" + } + } + }, + "BlockMessageActionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/BlockMessageActionMetadataResponse" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "BotAccountPatchRequest": { + "type": "object", + "properties": { + "username": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "avatar": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "banner": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + } + }, + "required": [ + "username" + ] + }, + "BotAddGuildMemberRequest": { + "type": "object", + "properties": { + "nick": { + "type": [ + "string", + "null" + ], + "maxLength": 32 + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 250, + "uniqueItems": true + }, + "mute": { + "type": [ + "boolean", + "null" + ] + }, + "deaf": { + "type": [ + "boolean", + "null" + ] + }, + "access_token": { + "type": "string", + "maxLength": 10240 + }, + "flags": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "access_token" + ] + }, + "BulkBanUsersRequest": { + "type": "object", + "properties": { + "user_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 200, + "uniqueItems": true + }, + "delete_message_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 604800 + } + }, + "required": [ + "user_ids" + ] + }, + "BulkBanUsersResponse": { + "type": "object", + "properties": { + "banned_users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "failed_users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + }, + "required": [ + "banned_users", + "failed_users" + ] + }, + "BulkLobbyMemberRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + }, + "remove_member": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id" + ] + }, + "ButtonComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 100 + }, + "style": { + "$ref": "#/components/schemas/ButtonStyleTypes" + }, + "label": { + "type": [ + "string", + "null" + ], + "maxLength": 80 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 512, + "format": "uri" + }, + "sku_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComponentEmojiForRequest" + } + ] + } + }, + "required": [ + "type", + "style" + ] + }, + "ButtonComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "style": { + "$ref": "#/components/schemas/ButtonStyleTypes" + }, + "label": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "emoji": { + "$ref": "#/components/schemas/ComponentEmojiResponse" + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id", + "style" + ] + }, + "ButtonStyleTypes": { + "type": "integer", + "oneOf": [ + { + "title": "PRIMARY", + "const": 1 + }, + { + "title": "SECONDARY", + "const": 2 + }, + { + "title": "SUCCESS", + "const": 3 + }, + { + "title": "DANGER", + "const": 4 + }, + { + "title": "LINK", + "const": 5 + }, + { + "title": "PREMIUM", + "const": 6 + } + ], + "format": "int32" + }, + "ByNWeekday": { + "type": "object", + "properties": { + "n": { + "type": "integer", + "description": "The week to reoccur on (1-5, where 5 represents the last week)", + "minimum": 1, + "maximum": 5, + "format": "int32" + }, + "day": { + "$ref": "#/components/schemas/RecurrenceRuleWeekdays", + "description": "The day within the week to reoccur on" + } + }, + "required": [ + "n", + "day" + ] + }, + "ByNWeekdayResponse": { + "type": "object", + "properties": { + "n": { + "type": "integer", + "description": "The week to reoccur on (1-5, where 5 represents the last week)", + "format": "int32" + }, + "day": { + "$ref": "#/components/schemas/RecurrenceRuleWeekdays", + "description": "The day within the week to reoccur on" + } + }, + "required": [ + "n", + "day" + ] + }, + "ChannelFollowerResponse": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "webhook_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "channel_id", + "webhook_id" + ] + }, + "ChannelFollowerWebhookResponse": { + "type": "object", + "properties": { + "application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/WebhookTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "source_guild": { + "$ref": "#/components/schemas/WebhookSourceGuildResponse" + }, + "source_channel": { + "$ref": "#/components/schemas/WebhookSourceChannelResponse" + } + }, + "required": [ + "application_id", + "avatar", + "channel_id", + "id", + "name", + "type" + ] + }, + "ChannelPermissionOverwriteRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ChannelPermissionOverwrites" + } + ] + }, + "allow": { + "type": [ + "integer", + "null" + ] + }, + "deny": { + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "id" + ] + }, + "ChannelPermissionOverwriteResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/ChannelPermissionOverwrites" + }, + "allow": { + "type": "string" + }, + "deny": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "allow", + "deny" + ] + }, + "ChannelPermissionOverwrites": { + "type": "integer", + "oneOf": [ + { + "title": "ROLE", + "const": 0 + }, + { + "title": "MEMBER", + "const": 1 + } + ], + "format": "int32" + }, + "ChannelSelectComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelSelectDefaultValue" + }, + "maxItems": 25 + }, + "channel_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "uniqueItems": true + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "ChannelSelectComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelSelectDefaultValue" + }, + "maxItems": 25 + }, + "channel_types": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "uniqueItems": true + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "ChannelSelectComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 8 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "min_values": { + "type": "integer", + "format": "int32" + }, + "max_values": { + "type": "integer", + "format": "int32" + }, + "disabled": { + "type": "boolean" + }, + "channel_types": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "uniqueItems": true + }, + "default_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelSelectDefaultValueResponse" + } + } + }, + "required": [ + "type", + "id", + "custom_id", + "min_values", + "max_values" + ] + }, + "ChannelSelectDefaultValue": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "channel" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "ChannelSelectDefaultValueResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "channel" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "ChannelTypes": { + "type": "integer", + "oneOf": [ + { + "title": "DM", + "description": "A direct message between users", + "const": 1 + }, + { + "title": "GROUP_DM", + "description": "A direct message between multiple users", + "const": 3 + }, + { + "title": "GUILD_TEXT", + "description": "A text channel within a server", + "const": 0 + }, + { + "title": "GUILD_VOICE", + "description": "A voice channel within a server", + "const": 2 + }, + { + "title": "GUILD_CATEGORY", + "description": "An organizational category that contains up to 50 channels", + "const": 4 + }, + { + "title": "GUILD_ANNOUNCEMENT", + "description": "A channel that users can follow and crosspost into their own server (formerly news channels)", + "const": 5 + }, + { + "title": "ANNOUNCEMENT_THREAD", + "description": "A temporary sub-channel within a GUILD_ANNOUNCEMENT channel", + "const": 10 + }, + { + "title": "PUBLIC_THREAD", + "description": "A temporary sub-channel within a GUILD_TEXT or GUILD_THREADS_ONLY channel type set", + "const": 11 + }, + { + "title": "PRIVATE_THREAD", + "description": "A temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission", + "const": 12 + }, + { + "title": "GUILD_STAGE_VOICE", + "description": "A voice channel for hosting events with an audience", + "const": 13 + }, + { + "title": "GUILD_DIRECTORY", + "description": "The channel in a hub containing the listed servers", + "const": 14 + }, + { + "title": "GUILD_FORUM", + "description": "Channel that can only contain threads", + "const": 15 + } + ], + "format": "int32" + }, + "CheckboxComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 23 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "default": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "CheckboxGroupComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 22 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 10 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 10 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckboxGroupOptionForRequest" + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": [ + "type", + "custom_id", + "options" + ] + }, + "CheckboxGroupOptionForRequest": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "default": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "label", + "value" + ] + }, + "CommandPermissionResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/ApplicationCommandPermissionType" + }, + "permission": { + "type": "boolean" + } + }, + "required": [ + "id", + "type", + "permission" + ] + }, + "CommandPermissionsResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "permissions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CommandPermissionResponse" + } + } + }, + "required": [ + "id", + "application_id", + "guild_id", + "permissions" + ] + }, + "ComponentEmojiForRequest": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "name": { + "type": "string", + "maxLength": 32 + } + }, + "required": [ + "name" + ] + }, + "ComponentEmojiResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "animated": { + "type": "boolean" + } + }, + "required": [ + "name" + ] + }, + "ConnectedAccountGuildResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name", + "icon" + ] + }, + "ConnectedAccountIntegrationResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/IntegrationTypes" + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + }, + "guild": { + "$ref": "#/components/schemas/ConnectedAccountGuildResponse" + } + }, + "required": [ + "id", + "type", + "account", + "guild" + ] + }, + "ConnectedAccountProviders": { + "type": "string", + "oneOf": [ + { + "title": "BATTLENET", + "const": "battlenet" + }, + { + "title": "BLUESKY", + "const": "bluesky" + }, + { + "title": "BUNGIE", + "const": "bungie" + }, + { + "title": "EBAY", + "const": "ebay" + }, + { + "title": "EPIC_GAMES", + "const": "epicgames" + }, + { + "title": "FACEBOOK", + "const": "facebook" + }, + { + "title": "GITHUB", + "const": "github" + }, + { + "title": "INSTAGRAM", + "const": "instagram" + }, + { + "title": "MASTODON", + "const": "mastodon" + }, + { + "title": "LEAGUE_OF_LEGENDS", + "const": "leagueoflegends" + }, + { + "title": "PAYPAL", + "const": "paypal" + }, + { + "title": "PLAYSTATION", + "const": "playstation" + }, + { + "title": "REDDIT", + "const": "reddit" + }, + { + "title": "RIOT_GAMES", + "const": "riotgames" + }, + { + "title": "ROBLOX", + "const": "roblox" + }, + { + "title": "SKYPE", + "const": "skype" + }, + { + "title": "SPOTIFY", + "const": "spotify" + }, + { + "title": "STEAM", + "const": "steam" + }, + { + "title": "TIKTOK", + "const": "tiktok" + }, + { + "title": "TWITCH", + "const": "twitch" + }, + { + "title": "TWITTER", + "const": "twitter" + }, + { + "title": "XBOX", + "const": "xbox" + }, + { + "title": "YOUTUBE", + "const": "youtube" + }, + { + "title": "DOMAIN", + "const": "domain" + } + ] + }, + "ConnectedAccountResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "type": { + "$ref": "#/components/schemas/ConnectedAccountProviders" + }, + "friend_sync": { + "type": "boolean" + }, + "integrations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ConnectedAccountIntegrationResponse" + } + }, + "show_activity": { + "type": "boolean" + }, + "two_way_link": { + "type": "boolean" + }, + "verified": { + "type": "boolean" + }, + "visibility": { + "$ref": "#/components/schemas/ConnectedAccountVisibility" + }, + "revoked": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "type", + "friend_sync", + "show_activity", + "two_way_link", + "verified", + "visibility" + ] + }, + "ConnectedAccountVisibility": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "const": 0 + }, + { + "title": "EVERYONE", + "const": 1 + } + ], + "format": "int32" + }, + "ContainerComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 17 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "accent_color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "minItems": 1, + "maxItems": 40 + }, + "spoiler": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "components" + ] + }, + "ContainerComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 17 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "accent_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentResponse" + }, + { + "$ref": "#/components/schemas/FileComponentResponse" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentResponse" + }, + { + "$ref": "#/components/schemas/SectionComponentResponse" + }, + { + "$ref": "#/components/schemas/SeparatorComponentResponse" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + ] + } + }, + "spoiler": { + "type": "boolean" + } + }, + "required": [ + "type", + "id", + "accent_color", + "components", + "spoiler" + ] + }, + "CreateEntitlementRequestData": { + "type": "object", + "properties": { + "sku_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the SKU to grant the entitlement to" + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the guild or user to grant the entitlement to" + }, + "owner_type": { + "$ref": "#/components/schemas/EntitlementOwnerTypes", + "description": "1 for a guild subscription, 2 for a user subscription" + } + }, + "required": [ + "sku_id", + "owner_id", + "owner_type" + ] + }, + "CreateForumThreadRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600, + "format": "int32" + }, + "applied_tags": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 5 + }, + "message": { + "$ref": "#/components/schemas/BaseCreateMessageCreateRequest" + } + }, + "required": [ + "name", + "message" + ] + }, + "CreateGroupDMInviteRequest": { + "type": "object", + "properties": { + "max_age": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 604800 + } + } + }, + "CreateGuildChannelRequest": { + "type": "object", + "properties": { + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 0, + 2, + 4, + 5, + 13, + 14, + 15 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + } + ] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "position": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "topic": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 4096 + }, + "bitrate": { + "type": [ + "integer", + "null" + ], + "minimum": 8000, + "format": "int32" + }, + "user_limit": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "nsfw": { + "type": [ + "boolean", + "null" + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600 + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "permission_overwrites": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelPermissionOverwriteRequest" + }, + "maxItems": 100 + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VideoQualityModes" + } + ] + }, + "default_auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "default_reaction_emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UpdateDefaultReactionEmojiRequest" + } + ] + }, + "default_thread_rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600 + }, + "default_sort_order": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSortOrder" + } + ] + }, + "default_forum_layout": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ForumLayout" + } + ] + }, + "default_tag_setting": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSearchTagSetting" + } + ] + }, + "available_tags": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CreateOrUpdateThreadTagRequest" + } + ] + }, + "maxItems": 20 + } + }, + "required": [ + "name" + ] + }, + "CreateGuildInviteRequest": { + "type": "object", + "properties": { + "max_age": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 5184000 + }, + "temporary": { + "type": [ + "boolean", + "null" + ] + }, + "max_uses": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 100 + }, + "unique": { + "type": [ + "boolean", + "null" + ] + }, + "target_user_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "target_application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "target_type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1, + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InviteTargetTypes" + } + ], + "format": "int32" + } + ] + }, + "role_ids": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 250, + "uniqueItems": true + }, + { + "type": "null" + } + ] + } + } + }, + "CreateMessageInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4, + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/IncomingWebhookInteractionRequest" + } + ] + } + }, + "required": [ + "type" + ] + }, + "CreateMessageInteractionCallbackResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "message": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "type", + "message" + ] + }, + "CreateOrUpdateThreadTagRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 0, + "maxLength": 50 + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "moderated": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "name" + ] + }, + "CreatePrivateChannelRequest": { + "type": "object", + "properties": { + "recipient_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "access_tokens": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "maxLength": 152133 + }, + "maxItems": 1521, + "uniqueItems": true + }, + "nicks": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "maxProperties": 1521 + } + } + }, + "CreateRoleRequest": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 100 + }, + "permissions": { + "type": [ + "integer", + "null" + ] + }, + "color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "colors": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RoleColors" + } + ] + }, + "hoist": { + "type": [ + "boolean", + "null" + ] + }, + "mentionable": { + "type": [ + "boolean", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "unicode_emoji": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + } + }, + "CreateTextThreadWithMessageRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600, + "format": "int32" + } + }, + "required": [ + "name" + ] + }, + "CreateTextThreadWithoutMessageRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600, + "format": "int32" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 10, + 11, + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + } + ] + }, + "invitable": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "name" + ] + }, + "CreatedThreadResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 10, + 11, + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "last_message_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "last_pin_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "rate_limit_per_user": { + "type": "integer", + "format": "int32" + }, + "bitrate": { + "type": "integer", + "format": "int32" + }, + "user_limit": { + "type": "integer", + "format": "int32" + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "$ref": "#/components/schemas/VideoQualityModes" + }, + "permissions": { + "type": "string" + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "thread_metadata": { + "$ref": "#/components/schemas/ThreadMetadataResponse" + }, + "message_count": { + "type": "integer", + "format": "int32" + }, + "member_count": { + "type": "integer", + "format": "int32" + }, + "total_message_sent": { + "type": "integer", + "format": "int32" + }, + "applied_tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "member": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + }, + "required": [ + "id", + "type", + "flags", + "guild_id", + "name", + "owner_id", + "thread_metadata", + "message_count", + "member_count", + "total_message_sent" + ] + }, + "CustomClientThemeResponse": { + "type": "object", + "properties": { + "colors": { + "type": "array", + "items": { + "type": "string" + } + }, + "gradient_angle": { + "type": "integer", + "format": "int32" + }, + "base_mix": { + "type": "integer", + "format": "int32" + }, + "base_theme": { + "$ref": "#/components/schemas/MessageShareCustomUserThemeBaseTheme" + } + }, + "required": [ + "colors", + "gradient_angle", + "base_mix", + "base_theme" + ] + }, + "CustomClientThemeShareRequest": { + "type": "object", + "properties": { + "colors": { + "type": "array", + "items": { + "type": "string", + "minLength": 6, + "maxLength": 6 + }, + "minItems": 1, + "maxItems": 5 + }, + "gradient_angle": { + "type": "integer", + "minimum": 0, + "maximum": 360, + "format": "int32" + }, + "base_mix": { + "type": "integer", + "minimum": 0, + "maximum": 100, + "format": "int32" + }, + "base_theme": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageShareCustomUserThemeBaseTheme" + } + ] + } + }, + "required": [ + "colors", + "gradient_angle", + "base_mix" + ] + }, + "DefaultKeywordListTriggerMetadata": { + "type": "object", + "properties": { + "allow_list": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "maxItems": 1000 + }, + "presets": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/AutomodKeywordPresetType" + }, + "uniqueItems": true + } + } + }, + "DefaultKeywordListTriggerMetadataResponse": { + "type": "object", + "properties": { + "allow_list": { + "type": "array", + "items": { + "type": "string" + } + }, + "presets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AutomodKeywordPresetType" + }, + "uniqueItems": true + } + }, + "required": [ + "allow_list", + "presets" + ] + }, + "DefaultKeywordListUpsertRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "$ref": "#/components/schemas/DefaultKeywordListTriggerMetadata" + } + }, + "required": [ + "name", + "event_type", + "trigger_type", + "trigger_metadata" + ] + }, + "DefaultKeywordListUpsertRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "$ref": "#/components/schemas/DefaultKeywordListTriggerMetadata" + } + } + }, + "DefaultKeywordRuleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageActionResponse" + }, + { + "$ref": "#/components/schemas/FlagToChannelActionResponse" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionResponse" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledActionResponse" + } + ] + } + }, + "trigger_type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "exempt_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "exempt_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "trigger_metadata": { + "$ref": "#/components/schemas/DefaultKeywordListTriggerMetadataResponse" + } + }, + "required": [ + "id", + "guild_id", + "creator_id", + "name", + "event_type", + "actions", + "trigger_type", + "enabled", + "exempt_roles", + "exempt_channels", + "trigger_metadata" + ] + }, + "DefaultReactionEmojiResponse": { + "type": "object", + "properties": { + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "emoji_id", + "emoji_name" + ] + }, + "DiscordIntegrationResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "discord" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application": { + "$ref": "#/components/schemas/IntegrationApplicationResponse" + }, + "scopes": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "applications.commands", + "bot", + "webhook.incoming" + ], + "allOf": [ + { + "$ref": "#/components/schemas/OAuth2Scopes" + } + ] + }, + "uniqueItems": true + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "type", + "name", + "account", + "enabled", + "id", + "application", + "scopes" + ] + }, + "EmbeddedActivityInstance": { + "type": "object", + "properties": { + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "instance_id": { + "type": "string" + }, + "launch_id": { + "type": "string" + }, + "location": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelLocation" + }, + { + "$ref": "#/components/schemas/PrivateChannelLocation" + } + ] + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + } + }, + "required": [ + "application_id", + "instance_id", + "launch_id", + "location", + "users" + ] + }, + "EmbeddedActivityLocationKind": { + "type": "string", + "oneOf": [ + { + "title": "GUILD_CHANNEL", + "description": "guild channel", + "const": "gc" + }, + { + "title": "PRIVATE_CHANNEL", + "description": "private channel", + "const": "pc" + }, + { + "title": "PARTY", + "description": "party", + "const": "party" + } + ] + }, + "EmojiResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "require_colons": { + "type": "boolean" + }, + "managed": { + "type": "boolean" + }, + "animated": { + "type": "boolean" + }, + "available": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "roles", + "require_colons", + "managed", + "animated", + "available" + ] + }, + "EntitlementOwnerTypes": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD", + "description": "A guild subscription", + "const": 1 + }, + { + "title": "USER", + "description": "A user subscription", + "const": 2 + } + ], + "format": "int32" + }, + "EntitlementResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "deleted": { + "type": "boolean" + }, + "starts_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "ends_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "type": { + "$ref": "#/components/schemas/EntitlementTypes" + }, + "fulfilled_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "fulfillment_status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntitlementTenantFulfillmentStatusResponse" + } + ] + }, + "consumed": { + "type": "boolean" + }, + "gifter_user_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "id", + "sku_id", + "application_id", + "user_id", + "deleted", + "starts_at", + "ends_at", + "type" + ] + }, + "EntitlementTenantFulfillmentStatusResponse": { + "type": "integer", + "oneOf": [ + { + "title": "UNKNOWN", + "const": 0 + }, + { + "title": "FULFILLMENT_NOT_NEEDED", + "const": 1 + }, + { + "title": "FULFILLMENT_NEEDED", + "const": 2 + }, + { + "title": "FULFILLED", + "const": 3 + }, + { + "title": "FULFILLMENT_FAILED", + "const": 4 + }, + { + "title": "UNFULFILLMENT_NEEDED", + "const": 5 + }, + { + "title": "UNFULFILLED", + "const": 6 + }, + { + "title": "UNFULFILLMENT_FAILED", + "const": 7 + } + ], + "format": "int32" + }, + "EntitlementTypes": { + "type": "integer", + "oneOf": [ + { + "title": "APPLICATION_SUBSCRIPTION", + "const": 8 + }, + { + "title": "QUEST_REWARD", + "const": 10 + } + ], + "format": "int32" + }, + "EntityMetadataExternal": { + "type": "object", + "properties": { + "location": { + "type": "string", + "maxLength": 100 + } + }, + "required": [ + "location" + ] + }, + "EntityMetadataExternalResponse": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "Location of the external event" + } + }, + "required": [ + "location" + ] + }, + "EntityMetadataStageInstance": { + "type": "object", + "properties": {} + }, + "EntityMetadataStageInstanceResponse": { + "type": "object", + "properties": {} + }, + "EntityMetadataVoice": { + "type": "object", + "properties": {} + }, + "EntityMetadataVoiceResponse": { + "type": "object", + "properties": {} + }, + "ExternalConnectionIntegrationResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "twitch", + "youtube" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "revoked": { + "type": "boolean" + }, + "expire_behavior": { + "$ref": "#/components/schemas/IntegrationExpireBehaviorTypes" + }, + "expire_grace_period": { + "$ref": "#/components/schemas/IntegrationExpireGracePeriodTypes" + }, + "subscriber_count": { + "type": "integer", + "format": "int32" + }, + "synced_at": { + "type": "string", + "format": "date-time" + }, + "role_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "syncing": { + "type": "boolean" + }, + "enable_emoticons": { + "type": "boolean" + } + }, + "required": [ + "type", + "name", + "account", + "enabled", + "id", + "user" + ] + }, + "ExternalScheduledEventCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "entity_type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "$ref": "#/components/schemas/EntityMetadataExternal" + } + }, + "required": [ + "name", + "scheduled_start_time", + "privacy_level", + "entity_type", + "entity_metadata" + ] + }, + "ExternalScheduledEventPatchRequestPartial": { + "type": "object", + "properties": { + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildScheduledEventStatuses" + } + ] + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "entity_type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + } + ] + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "$ref": "#/components/schemas/EntityMetadataExternal" + } + } + }, + "ExternalScheduledEventResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the guild the scheduled event belongs to" + }, + "name": { + "type": "string", + "description": "Name of the scheduled event" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the scheduled event" + }, + "channel_id": { + "description": "Channel ID in which the scheduled event will be hosted, or null if entity type is EXTERNAL", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator_id": { + "description": "ID of the user that created the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator": { + "$ref": "#/components/schemas/UserResponse", + "description": "User that created the scheduled event" + }, + "image": { + "type": [ + "string", + "null" + ], + "description": "Cover image hash of the scheduled event" + }, + "scheduled_start_time": { + "type": "string", + "description": "When the scheduled event will start", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "When the scheduled event will end, or null if no end time", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/GuildScheduledEventStatuses", + "description": "Status of the scheduled event" + }, + "entity_type": { + "type": "integer", + "description": "Type of hosting entity associated with the scheduled event", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "entity_id": { + "description": "ID of the hosting entity associated with the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event, or null if not recurring", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRuleResponse" + } + ] + }, + "user_count": { + "type": "integer", + "description": "Number of users subscribed to the scheduled event", + "format": "int32" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels", + "description": "Privacy level of the scheduled event" + }, + "user_rsvp": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + ] + }, + "guild_scheduled_event_exceptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + }, + "entity_metadata": { + "$ref": "#/components/schemas/EntityMetadataExternalResponse" + } + }, + "required": [ + "id", + "guild_id", + "name", + "description", + "channel_id", + "creator_id", + "image", + "scheduled_start_time", + "scheduled_end_time", + "status", + "entity_type", + "entity_id", + "recurrence_rule", + "privacy_level", + "guild_scheduled_event_exceptions", + "entity_metadata" + ] + }, + "FileComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 13 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "spoiler": { + "type": [ + "boolean", + "null" + ] + }, + "file": { + "$ref": "#/components/schemas/UnfurledMediaRequestWithAttachmentReferenceRequired" + } + }, + "required": [ + "type", + "file" + ] + }, + "FileComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 13 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "file": { + "$ref": "#/components/schemas/UnfurledMediaResponse" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "size": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "spoiler": { + "type": "boolean" + } + }, + "required": [ + "type", + "id", + "file", + "name", + "size", + "spoiler" + ] + }, + "FileUploadComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 19 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 10 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 10 + }, + "required": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "FlagToChannelAction": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/FlagToChannelActionMetadata" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "FlagToChannelActionMetadata": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "channel_id" + ] + }, + "FlagToChannelActionMetadataResponse": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "channel_id" + ] + }, + "FlagToChannelActionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/FlagToChannelActionMetadataResponse" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "ForumLayout": { + "type": "integer", + "oneOf": [ + { + "title": "DEFAULT", + "description": "No default has been set for forum channel", + "const": 0 + }, + { + "title": "LIST", + "description": "Display posts as a list", + "const": 1 + }, + { + "title": "GRID", + "description": "Display posts as a collection of tiles", + "const": 2 + } + ], + "format": "int32" + }, + "ForumTagResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "moderated": { + "type": "boolean" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name", + "moderated", + "emoji_id", + "emoji_name" + ] + }, + "FriendInviteResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InviteTypes" + } + ], + "format": "int32" + }, + "code": { + "type": "string" + }, + "inviter": { + "$ref": "#/components/schemas/UserResponse" + }, + "max_age": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "friends_count": { + "type": "integer", + "format": "int32" + }, + "channel": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/InviteChannelResponse" + } + ] + }, + "is_contact": { + "type": "boolean" + }, + "uses": { + "type": "integer", + "format": "int32" + }, + "max_uses": { + "type": "integer", + "format": "int32" + }, + "flags": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "type", + "code", + "expires_at", + "channel" + ] + }, + "GatewayBotResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "session_start_limit": { + "$ref": "#/components/schemas/GatewayBotSessionStartLimitResponse" + }, + "shards": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "url", + "session_start_limit", + "shards" + ] + }, + "GatewayBotSessionStartLimitResponse": { + "type": "object", + "properties": { + "max_concurrency": { + "type": "integer", + "format": "int32" + }, + "remaining": { + "type": "integer", + "format": "int32" + }, + "reset_after": { + "type": "integer", + "format": "int32" + }, + "total": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "max_concurrency", + "remaining", + "reset_after", + "total" + ] + }, + "GatewayResponse": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "url" + ] + }, + "GithubAuthor": { + "type": "object", + "properties": { + "username": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "name": { + "type": "string", + "maxLength": 152133 + } + }, + "required": [ + "name" + ] + }, + "GithubCheckApp": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 152133 + } + }, + "required": [ + "name" + ] + }, + "GithubCheckPullRequest": { + "type": "object", + "properties": { + "number": { + "type": "integer" + } + }, + "required": [ + "number" + ] + }, + "GithubCheckRun": { + "type": "object", + "properties": { + "conclusion": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "name": { + "type": "string", + "maxLength": 152133 + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "check_suite": { + "$ref": "#/components/schemas/GithubCheckSuite" + }, + "details_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "output": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubCheckRunOutput" + } + ] + }, + "pull_requests": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GithubCheckPullRequest" + }, + "maxItems": 1521 + } + }, + "required": [ + "name", + "html_url", + "check_suite" + ] + }, + "GithubCheckRunOutput": { + "type": "object", + "properties": { + "title": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "summary": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + } + } + }, + "GithubCheckSuite": { + "type": "object", + "properties": { + "conclusion": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "head_branch": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "head_sha": { + "type": "string", + "maxLength": 152133 + }, + "pull_requests": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GithubCheckPullRequest" + }, + "maxItems": 1521 + }, + "app": { + "$ref": "#/components/schemas/GithubCheckApp" + } + }, + "required": [ + "head_sha", + "app" + ] + }, + "GithubComment": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "user": { + "$ref": "#/components/schemas/GithubUser" + }, + "commit_id": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "body": { + "type": "string", + "maxLength": 152133 + } + }, + "required": [ + "id", + "html_url", + "user", + "body" + ] + }, + "GithubCommit": { + "type": "object", + "properties": { + "id": { + "type": "string", + "maxLength": 152133 + }, + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "message": { + "type": "string", + "maxLength": 152133 + }, + "author": { + "$ref": "#/components/schemas/GithubAuthor" + } + }, + "required": [ + "id", + "url", + "message", + "author" + ] + }, + "GithubDiscussion": { + "type": "object", + "properties": { + "title": { + "type": "string", + "maxLength": 152133 + }, + "number": { + "type": "integer" + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "answer_html_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "body": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "user": { + "$ref": "#/components/schemas/GithubUser" + } + }, + "required": [ + "title", + "number", + "html_url", + "user" + ] + }, + "GithubIssue": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "number": { + "type": "integer" + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "user": { + "$ref": "#/components/schemas/GithubUser" + }, + "title": { + "type": "string", + "maxLength": 152133 + }, + "body": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "pull_request": {} + }, + "required": [ + "id", + "number", + "html_url", + "user", + "title" + ] + }, + "GithubRelease": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "tag_name": { + "type": "string", + "maxLength": 152133 + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "author": { + "$ref": "#/components/schemas/GithubUser" + } + }, + "required": [ + "id", + "tag_name", + "html_url", + "author" + ] + }, + "GithubRepository": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "name": { + "type": "string", + "maxLength": 152133 + }, + "full_name": { + "type": "string", + "maxLength": 152133 + } + }, + "required": [ + "id", + "html_url", + "name", + "full_name" + ] + }, + "GithubReview": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/GithubUser" + }, + "body": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "state": { + "type": "string", + "maxLength": 152133 + } + }, + "required": [ + "user", + "html_url", + "state" + ] + }, + "GithubUser": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "login": { + "type": "string", + "maxLength": 152133 + }, + "html_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + }, + "avatar_url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": [ + "id", + "login", + "html_url", + "avatar_url" + ] + }, + "GithubWebhook": { + "type": "object", + "properties": { + "action": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "ref": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "ref_type": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "comment": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubComment" + } + ] + }, + "issue": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubIssue" + } + ] + }, + "pull_request": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubIssue" + } + ] + }, + "repository": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubRepository" + } + ] + }, + "forkee": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubRepository" + } + ] + }, + "sender": { + "$ref": "#/components/schemas/GithubUser" + }, + "member": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubUser" + } + ] + }, + "release": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubRelease" + } + ] + }, + "head_commit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubCommit" + } + ] + }, + "commits": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GithubCommit" + }, + "maxItems": 1521 + }, + "forced": { + "type": [ + "boolean", + "null" + ] + }, + "compare": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "review": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubReview" + } + ] + }, + "check_run": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubCheckRun" + } + ] + }, + "check_suite": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubCheckSuite" + } + ] + }, + "discussion": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubDiscussion" + } + ] + }, + "answer": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GithubComment" + } + ] + } + }, + "required": [ + "sender" + ] + }, + "GroupDMInviteResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InviteTypes" + } + ], + "format": "int32" + }, + "code": { + "type": "string" + }, + "inviter": { + "$ref": "#/components/schemas/UserResponse" + }, + "max_age": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "channel": { + "$ref": "#/components/schemas/InviteChannelResponse" + }, + "approximate_member_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "type", + "code", + "expires_at", + "channel" + ] + }, + "GuildAuditLogResponse": { + "type": "object", + "properties": { + "audit_log_entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AuditLogEntryResponse" + } + }, + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "integrations": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/PartialDiscordIntegrationResponse" + }, + { + "$ref": "#/components/schemas/PartialExternalConnectionIntegrationResponse" + }, + { + "$ref": "#/components/schemas/PartialGuildSubscriptionIntegrationResponse" + } + ] + } + }, + "webhooks": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationIncomingWebhookResponse" + }, + { + "$ref": "#/components/schemas/ChannelFollowerWebhookResponse" + }, + { + "$ref": "#/components/schemas/GuildIncomingWebhookResponse" + } + ] + } + }, + "guild_scheduled_events": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ExternalScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/StageScheduledEventResponse" + }, + { + "$ref": "#/components/schemas/VoiceScheduledEventResponse" + } + ] + } + }, + "threads": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadResponse" + } + }, + "application_commands": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationCommandResponse" + } + }, + "auto_moderation_rules": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DefaultKeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/KeywordRuleResponse" + }, + { + "$ref": "#/components/schemas/MLSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/MentionSpamRuleResponse" + }, + { + "$ref": "#/components/schemas/UserProfileRuleResponse" + }, + { + "type": "null" + } + ] + } + } + }, + "required": [ + "audit_log_entries", + "users", + "integrations", + "webhooks", + "guild_scheduled_events", + "threads", + "application_commands", + "auto_moderation_rules" + ] + }, + "GuildBanResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "reason": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "user", + "reason" + ] + }, + "GuildChannelLocation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "gc" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EmbeddedActivityLocationKind" + } + ] + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "kind", + "channel_id", + "guild_id" + ] + }, + "GuildChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 0, + 2, + 4, + 5, + 13, + 14, + 15 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "last_message_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "last_pin_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "rate_limit_per_user": { + "type": "integer", + "format": "int32" + }, + "bitrate": { + "type": "integer", + "format": "int32" + }, + "user_limit": { + "type": "integer", + "format": "int32" + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "$ref": "#/components/schemas/VideoQualityModes" + }, + "permissions": { + "type": "string" + }, + "topic": { + "type": [ + "string", + "null" + ] + }, + "default_auto_archive_duration": { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + }, + "default_thread_rate_limit_per_user": { + "type": "integer", + "format": "int32" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "permission_overwrites": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelPermissionOverwriteResponse" + } + }, + "nsfw": { + "type": "boolean" + }, + "available_tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ForumTagResponse" + } + }, + "default_reaction_emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DefaultReactionEmojiResponse" + } + ] + }, + "default_sort_order": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSortOrder" + } + ] + }, + "default_forum_layout": { + "$ref": "#/components/schemas/ForumLayout" + }, + "default_tag_setting": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSearchTagSetting" + } + ] + } + }, + "required": [ + "id", + "type", + "flags", + "guild_id", + "name", + "position" + ] + }, + "GuildExplicitContentFilterTypes": { + "type": "integer", + "oneOf": [ + { + "title": "DISABLED", + "description": "media content will not be scanned", + "const": 0 + }, + { + "title": "MEMBERS_WITHOUT_ROLES", + "description": "media content sent by members without roles will be scanned", + "const": 1 + }, + { + "title": "ALL_MEMBERS", + "description": "media content sent by all members will be scanned", + "const": 2 + } + ], + "format": "int32" + }, + "GuildFeatures": { + "type": "string", + "oneOf": [ + { + "title": "ANIMATED_BANNER", + "description": "guild has access to set an animated guild banner image", + "const": "ANIMATED_BANNER" + }, + { + "title": "ANIMATED_ICON", + "description": "guild has access to set an animated guild icon", + "const": "ANIMATED_ICON" + }, + { + "title": "APPLICATION_COMMAND_PERMISSIONS_V2", + "description": "guild is using the old permissions configuration behavior", + "const": "APPLICATION_COMMAND_PERMISSIONS_V2" + }, + { + "title": "AUTO_MODERATION", + "description": "guild has set up auto moderation rules", + "const": "AUTO_MODERATION" + }, + { + "title": "BANNER", + "description": "guild has access to set a guild banner image", + "const": "BANNER" + }, + { + "title": "COMMUNITY", + "description": "guild can enable welcome screen, Membership Screening, stage channels and discovery, and receives community updates", + "const": "COMMUNITY" + }, + { + "title": "CREATOR_MONETIZABLE_PROVISIONAL", + "description": "guild has enabled monetization", + "const": "CREATOR_MONETIZABLE_PROVISIONAL" + }, + { + "title": "CREATOR_STORE_PAGE", + "description": "guild has enabled the role subscription promo page", + "const": "CREATOR_STORE_PAGE" + }, + { + "title": "DEVELOPER_SUPPORT_SERVER", + "description": "guild has been set as a support server on the App Directory", + "const": "DEVELOPER_SUPPORT_SERVER" + }, + { + "title": "DISCOVERABLE", + "description": "guild is able to be discovered in the directory", + "const": "DISCOVERABLE" + }, + { + "title": "FEATURABLE", + "description": "guild is able to be featured in the directory", + "const": "FEATURABLE" + }, + { + "title": "INVITES_DISABLED", + "description": "guild has paused invites, preventing new users from joining", + "const": "INVITES_DISABLED" + }, + { + "title": "INVITE_SPLASH", + "description": "guild has access to set an invite splash background", + "const": "INVITE_SPLASH" + }, + { + "title": "MEMBER_VERIFICATION_GATE_ENABLED", + "description": "guild has enabled Membership Screening", + "const": "MEMBER_VERIFICATION_GATE_ENABLED" + }, + { + "title": "MORE_STICKERS", + "description": "guild has increased custom sticker slots", + "const": "MORE_STICKERS" + }, + { + "title": "NEWS", + "description": "guild has access to create announcement channels", + "const": "NEWS" + }, + { + "title": "PARTNERED", + "description": "guild is partnered", + "const": "PARTNERED" + }, + { + "title": "PREVIEW_ENABLED", + "description": "guild can be previewed before joining via Membership Screening or the directory", + "const": "PREVIEW_ENABLED" + }, + { + "title": "RAID_ALERTS_DISABLED", + "description": "guild has disabled activity alerts in the configured safety alerts channel", + "const": "RAID_ALERTS_DISABLED" + }, + { + "title": "PRUNE_REQUIRES_ADMIN", + "description": "guild has restricted member prune to administrators and the guild owner", + "const": "PRUNE_REQUIRES_ADMIN" + }, + { + "title": "ROLE_ICONS", + "description": "guild is able to set role icons", + "const": "ROLE_ICONS" + }, + { + "title": "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE", + "description": "guild has role subscriptions that can be purchased", + "const": "ROLE_SUBSCRIPTIONS_AVAILABLE_FOR_PURCHASE" + }, + { + "title": "ROLE_SUBSCRIPTIONS_ENABLED", + "description": "guild has enabled role subscriptions", + "const": "ROLE_SUBSCRIPTIONS_ENABLED" + }, + { + "title": "TICKETED_EVENTS_ENABLED", + "description": "guild has enabled ticketed events", + "const": "TICKETED_EVENTS_ENABLED" + }, + { + "title": "VANITY_URL", + "description": "guild has access to set a vanity URL", + "const": "VANITY_URL" + }, + { + "title": "VERIFIED", + "description": "guild is verified", + "const": "VERIFIED" + }, + { + "title": "VIP_REGIONS", + "description": "guild has access to set 384kbps bitrate in voice (previously VIP voice servers)", + "const": "VIP_REGIONS" + }, + { + "title": "WELCOME_SCREEN_ENABLED", + "description": "guild has enabled the welcome screen", + "const": "WELCOME_SCREEN_ENABLED" + }, + { + "title": "OFFICIAL_GAME_GUILD", + "description": "guild is an official guild for one or more games", + "const": "OFFICIAL_GAME_GUILD" + } + ] + }, + "GuildHomeSettingsResponse": { + "type": "object", + "properties": { + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "enabled": { + "type": "boolean" + }, + "welcome_message": { + "$ref": "#/components/schemas/WelcomeMessageResponse" + }, + "new_member_actions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NewMemberActionResponse" + } + }, + "resource_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceChannelResponse" + } + } + }, + "required": [ + "guild_id", + "enabled", + "new_member_actions", + "resource_channels" + ] + }, + "GuildIncidentActionsRequest": { + "type": "object", + "properties": { + "invites_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "When invites will be enabled again", + "format": "date-time" + }, + "dms_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "When direct messages will be enabled again", + "format": "date-time" + } + } + }, + "GuildIncidentsDataResponse": { + "type": "object", + "properties": { + "invites_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "When invites get enabled again", + "format": "date-time" + }, + "dms_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "When direct messages get enabled again", + "format": "date-time" + } + }, + "required": [ + "invites_disabled_until", + "dms_disabled_until" + ] + }, + "GuildIncomingWebhookResponse": { + "type": "object", + "properties": { + "application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/WebhookTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "token": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "application_id", + "avatar", + "channel_id", + "id", + "name", + "type" + ] + }, + "GuildInviteResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 0 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InviteTypes" + } + ], + "format": "int32" + }, + "code": { + "type": "string" + }, + "inviter": { + "$ref": "#/components/schemas/UserResponse" + }, + "max_age": { + "type": "integer", + "format": "int32" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "expires_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "is_contact": { + "type": "boolean" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "guild": { + "$ref": "#/components/schemas/InviteGuildResponse" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel": { + "$ref": "#/components/schemas/InviteChannelResponse" + }, + "target_type": { + "$ref": "#/components/schemas/InviteTargetTypes" + }, + "target_user": { + "$ref": "#/components/schemas/UserResponse" + }, + "target_application": { + "$ref": "#/components/schemas/InviteApplicationResponse" + }, + "guild_scheduled_event": { + "$ref": "#/components/schemas/ScheduledEventResponse" + }, + "target_channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "target_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uses": { + "type": "integer", + "format": "int32" + }, + "max_uses": { + "type": "integer", + "format": "int32" + }, + "temporary": { + "type": "boolean" + }, + "approximate_member_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "approximate_presence_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "is_nickname_changeable": { + "type": "boolean" + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/InviteGuildRoleResponse" + } + } + }, + "required": [ + "type", + "code", + "expires_at", + "guild", + "guild_id", + "channel" + ] + }, + "GuildJoinRequestApplicationStatus": { + "type": "string", + "oneOf": [ + { + "title": "STARTED", + "description": "Applicant started but not yet submitted join request", + "const": "STARTED" + }, + { + "title": "SUBMITTED", + "description": "Applicant submitted join request that is awaiting review", + "const": "SUBMITTED" + }, + { + "title": "REJECTED", + "description": "Join request rejected", + "const": "REJECTED" + }, + { + "title": "APPROVED", + "description": "Join request approved", + "const": "APPROVED" + } + ] + }, + "GuildJoinRequestResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "reviewed_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "application_status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildJoinRequestApplicationStatus" + } + ] + }, + "rejection_reason": { + "type": [ + "string", + "null" + ], + "description": "Reason request was rejected. Only set when application_status is REJECTED" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "user": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserResponse" + } + ] + }, + "form_responses": { + "type": [ + "array", + "null" + ], + "description": "Applicant's responses on join request form", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/MultipleChoiceFormFieldResponse" + }, + { + "$ref": "#/components/schemas/ParagraphFormFieldResponse" + }, + { + "$ref": "#/components/schemas/TermsFormFieldResponse" + }, + { + "$ref": "#/components/schemas/TextInputFormFieldResponse" + } + ] + } + }, + "actioned_by_user": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserResponse" + } + ] + } + }, + "required": [ + "id", + "created_at", + "reviewed_at", + "application_status", + "rejection_reason", + "guild_id", + "user_id" + ] + }, + "GuildJoinRequestsListResponse": { + "type": "object", + "properties": { + "total": { + "type": "integer", + "format": "int32" + }, + "guild_join_requests": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildJoinRequestResponse" + } + } + } + }, + "GuildMFALevel": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "description": "Guild has no MFA/2FA requirement for moderation actions", + "const": 0 + }, + { + "title": "ELEVATED", + "description": "Guild has a 2FA requirement for moderation actions", + "const": 1 + } + ], + "format": "int32" + }, + "GuildMemberResponse": { + "type": "object", + "properties": { + "avatar": { + "type": [ + "string", + "null" + ], + "description": "the member's guild avatar hash" + }, + "avatar_decoration_data": { + "description": "data for the member's guild avatar decoration", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserAvatarDecorationResponse" + } + ] + }, + "banner": { + "type": [ + "string", + "null" + ], + "description": "the member's guild banner hash" + }, + "communication_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out", + "format": "date-time" + }, + "flags": { + "type": "integer", + "description": "guild member flags represented as a bit set, defaults to 0", + "format": "int32" + }, + "joined_at": { + "type": "string", + "description": "when the user joined the guild", + "format": "date-time" + }, + "nick": { + "type": [ + "string", + "null" + ], + "description": "this user's guild nickname" + }, + "pending": { + "type": "boolean", + "description": "whether the user has not yet passed the guild's Membership Screening requirements" + }, + "premium_since": { + "type": [ + "string", + "null" + ], + "description": "when the user started boosting the guild", + "format": "date-time" + }, + "roles": { + "type": "array", + "description": "array of role object ids", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "collectibles": { + "description": "data for the member's collectibles", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserCollectiblesResponse" + } + ] + }, + "user": { + "$ref": "#/components/schemas/UserResponse", + "description": "the user this guild member represents" + }, + "mute": { + "type": "boolean", + "description": "whether the user is muted in voice channels" + }, + "deaf": { + "type": "boolean", + "description": "whether the user is deafened in voice channels" + } + }, + "required": [ + "avatar", + "banner", + "communication_disabled_until", + "flags", + "joined_at", + "nick", + "pending", + "premium_since", + "roles", + "user", + "mute", + "deaf" + ] + }, + "GuildMemberVerificationFormFieldType": { + "type": "string", + "oneOf": [ + { + "title": "TERMS", + "description": "Field requiring applicant to acknowledge list of terms", + "const": "TERMS" + }, + { + "title": "TEXT_INPUT", + "description": "Short text input field", + "const": "TEXT_INPUT" + }, + { + "title": "PARAGRAPH", + "description": "Long-form text input field", + "const": "PARAGRAPH" + }, + { + "title": "MULTIPLE_CHOICE", + "description": "Field where applicant selects one of many options", + "const": "MULTIPLE_CHOICE" + } + ] + }, + "GuildNSFWContentLevel": { + "type": "integer", + "oneOf": [ + { + "title": "DEFAULT", + "const": 0 + }, + { + "title": "EXPLICIT", + "const": 1 + }, + { + "title": "SAFE", + "const": 2 + }, + { + "title": "AGE_RESTRICTED", + "const": 3 + } + ], + "format": "int32" + }, + "GuildOnboardingMode": { + "type": "integer", + "oneOf": [ + { + "title": "ONBOARDING_DEFAULT", + "description": "Only Default Channels considered in constraints", + "const": 0 + }, + { + "title": "ONBOARDING_ADVANCED", + "description": "Default Channels and Onboarding Prompts considered in constraints", + "const": 1 + } + ], + "format": "int32" + }, + "GuildOnboardingResponse": { + "type": "object", + "properties": { + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "prompts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OnboardingPromptResponse" + } + }, + "default_channel_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "enabled": { + "type": "boolean" + }, + "mode": { + "$ref": "#/components/schemas/GuildOnboardingMode" + } + }, + "required": [ + "guild_id", + "prompts", + "default_channel_ids", + "enabled", + "mode" + ] + }, + "GuildPatchRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 300 + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "verification_level": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VerificationLevels" + } + ] + }, + "default_message_notifications": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserNotificationSettings" + } + ] + }, + "explicit_content_filter": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildExplicitContentFilterTypes" + } + ] + }, + "preferred_locale": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/AvailableLocalesEnum" + } + ] + }, + "afk_timeout": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/AfkTimeouts" + } + ] + }, + "afk_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "system_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "splash": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "banner": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "system_channel_flags": { + "type": [ + "integer", + "null" + ] + }, + "features": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "maxItems": 1521, + "uniqueItems": true + }, + "discovery_splash": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "home_header": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "rules_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "safety_alerts_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "public_updates_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "premium_progress_bar_enabled": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "GuildPreviewResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "home_header": { + "type": [ + "string", + "null" + ] + }, + "splash": { + "type": [ + "string", + "null" + ] + }, + "discovery_splash": { + "type": [ + "string", + "null" + ] + }, + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildFeatures" + }, + "uniqueItems": true + }, + "approximate_member_count": { + "type": "integer", + "format": "int32" + }, + "approximate_presence_count": { + "type": "integer", + "format": "int32" + }, + "emojis": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "home_header", + "splash", + "discovery_splash", + "features", + "approximate_member_count", + "approximate_presence_count", + "emojis", + "stickers" + ] + }, + "GuildProductPurchaseResponse": { + "type": "object", + "properties": { + "listing_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "product_name": { + "type": "string" + } + }, + "required": [ + "listing_id", + "product_name" + ] + }, + "GuildPruneResponse": { + "type": "object", + "properties": { + "pruned": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "pruned" + ] + }, + "GuildResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "home_header": { + "type": [ + "string", + "null" + ] + }, + "splash": { + "type": [ + "string", + "null" + ] + }, + "discovery_splash": { + "type": [ + "string", + "null" + ] + }, + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildFeatures" + }, + "uniqueItems": true + }, + "banner": { + "type": [ + "string", + "null" + ] + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "region": { + "type": "string" + }, + "afk_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "afk_timeout": { + "$ref": "#/components/schemas/AfkTimeouts" + }, + "system_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "system_channel_flags": { + "type": "integer", + "format": "int32" + }, + "widget_enabled": { + "type": "boolean" + }, + "widget_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "verification_level": { + "$ref": "#/components/schemas/VerificationLevels" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + }, + "default_message_notifications": { + "$ref": "#/components/schemas/UserNotificationSettings" + }, + "mfa_level": { + "$ref": "#/components/schemas/GuildMFALevel" + }, + "explicit_content_filter": { + "$ref": "#/components/schemas/GuildExplicitContentFilterTypes" + }, + "max_presences": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "max_members": { + "type": "integer", + "format": "int32" + }, + "max_stage_video_channel_users": { + "type": "integer", + "format": "int32" + }, + "max_video_channel_users": { + "type": "integer", + "format": "int32" + }, + "vanity_url_code": { + "type": [ + "string", + "null" + ] + }, + "premium_tier": { + "$ref": "#/components/schemas/PremiumGuildTiers" + }, + "premium_subscription_count": { + "type": "integer", + "format": "int32" + }, + "preferred_locale": { + "$ref": "#/components/schemas/AvailableLocalesEnum" + }, + "rules_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "safety_alerts_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "public_updates_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "premium_progress_bar_enabled": { + "type": "boolean" + }, + "premium_progress_bar_enabled_user_updated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "nsfw": { + "type": "boolean" + }, + "nsfw_level": { + "$ref": "#/components/schemas/GuildNSFWContentLevel" + }, + "emojis": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + }, + "incidents_data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildIncidentsDataResponse" + } + ] + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "home_header", + "splash", + "discovery_splash", + "features", + "banner", + "owner_id", + "application_id", + "region", + "afk_channel_id", + "afk_timeout", + "system_channel_id", + "system_channel_flags", + "widget_enabled", + "widget_channel_id", + "verification_level", + "roles", + "default_message_notifications", + "mfa_level", + "explicit_content_filter", + "max_presences", + "max_members", + "max_stage_video_channel_users", + "max_video_channel_users", + "vanity_url_code", + "premium_tier", + "premium_subscription_count", + "preferred_locale", + "rules_channel_id", + "safety_alerts_channel_id", + "public_updates_channel_id", + "premium_progress_bar_enabled", + "nsfw", + "nsfw_level", + "emojis", + "stickers", + "incidents_data" + ] + }, + "GuildRoleColorsResponse": { + "type": "object", + "properties": { + "primary_color": { + "type": "integer", + "format": "int32" + }, + "secondary_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "tertiary_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "primary_color", + "secondary_color", + "tertiary_color" + ] + }, + "GuildRoleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "color": { + "type": "integer", + "format": "int32" + }, + "colors": { + "$ref": "#/components/schemas/GuildRoleColorsResponse" + }, + "hoist": { + "type": "boolean" + }, + "managed": { + "type": "boolean" + }, + "mentionable": { + "type": "boolean" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "unicode_emoji": { + "type": [ + "string", + "null" + ] + }, + "tags": { + "$ref": "#/components/schemas/GuildRoleTagsResponse" + }, + "flags": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "permissions", + "position", + "color", + "colors", + "hoist", + "managed", + "mentionable", + "icon", + "unicode_emoji", + "flags" + ] + }, + "GuildRoleTagsResponse": { + "type": "object", + "properties": { + "premium_subscriber": { + "type": "null" + }, + "bot_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "integration_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "subscription_listing_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "available_for_purchase": { + "type": "null" + }, + "guild_connections": { + "type": "null" + } + } + }, + "GuildScheduledEventEntityTypes": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "const": 0 + }, + { + "title": "STAGE_INSTANCE", + "const": 1 + }, + { + "title": "VOICE", + "const": 2 + }, + { + "title": "EXTERNAL", + "const": 3 + } + ], + "format": "int32" + }, + "GuildScheduledEventExceptionCreateRequest": { + "type": "object", + "properties": { + "scheduled_start_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden start time of this occurrence", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden end time of this occurrence", + "format": "date-time" + }, + "original_scheduled_start_time": { + "type": "string", + "description": "The original start time of the occurrence to create an exception for", + "format": "date-time" + }, + "is_canceled": { + "type": [ + "boolean", + "null" + ], + "description": "Whether this occurrence is canceled" + } + }, + "required": [ + "original_scheduled_start_time" + ] + }, + "GuildScheduledEventExceptionPatchRequestPartial": { + "type": "object", + "properties": { + "scheduled_start_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden start time of this occurrence", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden end time of this occurrence", + "format": "date-time" + }, + "is_canceled": { + "type": [ + "boolean", + "null" + ], + "description": "Whether this occurrence is canceled" + } + } + }, + "GuildScheduledEventExceptionResponse": { + "type": "object", + "properties": { + "event_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event this exception belongs to" + }, + "event_exception_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the event exception" + }, + "scheduled_start_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden start time of this occurrence", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "Overridden end time of this occurrence", + "format": "date-time" + }, + "is_canceled": { + "type": "boolean", + "description": "Whether this occurrence is canceled" + } + }, + "required": [ + "event_id", + "event_exception_id", + "scheduled_start_time", + "scheduled_end_time", + "is_canceled" + ] + }, + "GuildScheduledEventPrivacyLevels": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_ONLY", + "description": "the scheduled event is only accessible to guild members", + "const": 2 + } + ], + "format": "int32" + }, + "GuildScheduledEventStatuses": { + "type": "integer", + "oneOf": [ + { + "title": "SCHEDULED", + "const": 1 + }, + { + "title": "ACTIVE", + "const": 2 + }, + { + "title": "COMPLETED", + "const": 3 + }, + { + "title": "CANCELED", + "const": 4 + } + ], + "format": "int32" + }, + "GuildScheduledEventUserResponses": { + "type": "integer", + "oneOf": [ + { + "title": "UNINTERESTED", + "description": "User is not interested in the event", + "const": 0 + }, + { + "title": "INTERESTED", + "description": "User is interested in the event", + "const": 1 + } + ], + "format": "int32" + }, + "GuildSearchResponse": { + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchMessageResponse" + } + } + }, + "doing_deep_historical_index": { + "type": "boolean" + }, + "total_results": { + "type": "integer", + "format": "int32" + }, + "threads": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ThreadResponse" + } + }, + "members": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + }, + "documents_indexed": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "messages", + "doing_deep_historical_index", + "total_results" + ] + }, + "GuildStickerResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/StickerTypes" + } + ], + "format": "int32" + }, + "format_type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StickerFormatTypes" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "available": { + "type": "boolean" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "id", + "name", + "tags", + "type", + "format_type", + "description", + "available", + "guild_id" + ] + }, + "GuildSubscriptionIntegrationResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "guild_subscription" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "name", + "account", + "enabled", + "id" + ] + }, + "GuildTemplateChannelResponse": { + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "type": { + "type": "integer", + "enum": [ + 0, + 2, + 4, + 15 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "position": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "topic": { + "type": [ + "string", + "null" + ] + }, + "bitrate": { + "type": "integer", + "format": "int32" + }, + "user_limit": { + "type": "integer", + "format": "int32" + }, + "nsfw": { + "type": "boolean" + }, + "rate_limit_per_user": { + "type": "integer", + "format": "int32" + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "default_auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "permission_overwrites": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChannelPermissionOverwriteResponse" + } + }, + "available_tags": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GuildTemplateChannelTags" + } + }, + "template": { + "type": "string" + }, + "default_reaction_emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DefaultReactionEmojiResponse" + } + ] + }, + "default_thread_rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "default_sort_order": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSortOrder" + } + ] + }, + "default_forum_layout": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ForumLayout" + } + ] + }, + "default_tag_setting": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSearchTagSetting" + } + ] + }, + "theme_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id", + "type", + "name", + "position", + "topic", + "bitrate", + "user_limit", + "nsfw", + "rate_limit_per_user", + "parent_id", + "default_auto_archive_duration", + "permission_overwrites", + "available_tags", + "template", + "default_reaction_emoji", + "default_thread_rate_limit_per_user", + "default_sort_order", + "default_forum_layout", + "default_tag_setting", + "theme_color" + ] + }, + "GuildTemplateChannelTags": { + "type": "object", + "properties": { + "id": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "name": { + "type": "string" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ] + }, + "moderated": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id", + "name", + "emoji_id", + "emoji_name", + "moderated" + ] + }, + "GuildTemplateResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "usage_count": { + "type": "integer", + "format": "int32" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserResponse" + } + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "source_guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "serialized_source_guild": { + "$ref": "#/components/schemas/GuildTemplateSnapshotResponse" + }, + "is_dirty": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "code", + "name", + "description", + "usage_count", + "creator_id", + "creator", + "created_at", + "updated_at", + "source_guild_id", + "serialized_source_guild", + "is_dirty" + ] + }, + "GuildTemplateRoleColorsResponse": { + "type": "object", + "properties": { + "primary_color": { + "type": "integer", + "format": "int32" + }, + "secondary_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "tertiary_color": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "primary_color", + "secondary_color", + "tertiary_color" + ] + }, + "GuildTemplateRoleResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + }, + "name": { + "type": "string" + }, + "permissions": { + "type": "string" + }, + "color": { + "type": "integer", + "format": "int32" + }, + "colors": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildTemplateRoleColorsResponse" + } + ] + }, + "hoist": { + "type": "boolean" + }, + "mentionable": { + "type": "boolean" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "unicode_emoji": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "name", + "permissions", + "color", + "colors", + "hoist", + "mentionable", + "icon", + "unicode_emoji" + ] + }, + "GuildTemplateSnapshotResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "region": { + "type": [ + "string", + "null" + ] + }, + "verification_level": { + "$ref": "#/components/schemas/VerificationLevels" + }, + "default_message_notifications": { + "$ref": "#/components/schemas/UserNotificationSettings" + }, + "explicit_content_filter": { + "$ref": "#/components/schemas/GuildExplicitContentFilterTypes" + }, + "preferred_locale": { + "$ref": "#/components/schemas/AvailableLocalesEnum" + }, + "afk_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "afk_timeout": { + "$ref": "#/components/schemas/AfkTimeouts" + }, + "system_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "system_channel_flags": { + "type": "integer", + "format": "int32" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildTemplateRoleResponse" + } + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildTemplateChannelResponse" + } + } + }, + "required": [ + "name", + "description", + "region", + "verification_level", + "default_message_notifications", + "explicit_content_filter", + "preferred_locale", + "afk_channel_id", + "afk_timeout", + "system_channel_id", + "system_channel_flags", + "roles", + "channels" + ] + }, + "GuildWelcomeChannel": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + } + }, + "required": [ + "channel_id", + "description" + ] + }, + "GuildWelcomeScreenChannelResponse": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "description": { + "type": "string" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "channel_id", + "description", + "emoji_id", + "emoji_name" + ] + }, + "GuildWelcomeScreenResponse": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ] + }, + "welcome_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildWelcomeScreenChannelResponse" + } + } + }, + "required": [ + "description", + "welcome_channels" + ] + }, + "GuildWithCountsResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "home_header": { + "type": [ + "string", + "null" + ] + }, + "splash": { + "type": [ + "string", + "null" + ] + }, + "discovery_splash": { + "type": [ + "string", + "null" + ] + }, + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildFeatures" + }, + "uniqueItems": true + }, + "banner": { + "type": [ + "string", + "null" + ] + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "region": { + "type": "string" + }, + "afk_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "afk_timeout": { + "$ref": "#/components/schemas/AfkTimeouts" + }, + "system_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "system_channel_flags": { + "type": "integer", + "format": "int32" + }, + "widget_enabled": { + "type": "boolean" + }, + "widget_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "verification_level": { + "$ref": "#/components/schemas/VerificationLevels" + }, + "roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + }, + "default_message_notifications": { + "$ref": "#/components/schemas/UserNotificationSettings" + }, + "mfa_level": { + "$ref": "#/components/schemas/GuildMFALevel" + }, + "explicit_content_filter": { + "$ref": "#/components/schemas/GuildExplicitContentFilterTypes" + }, + "max_presences": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "max_members": { + "type": "integer", + "format": "int32" + }, + "max_stage_video_channel_users": { + "type": "integer", + "format": "int32" + }, + "max_video_channel_users": { + "type": "integer", + "format": "int32" + }, + "vanity_url_code": { + "type": [ + "string", + "null" + ] + }, + "premium_tier": { + "$ref": "#/components/schemas/PremiumGuildTiers" + }, + "premium_subscription_count": { + "type": "integer", + "format": "int32" + }, + "preferred_locale": { + "$ref": "#/components/schemas/AvailableLocalesEnum" + }, + "rules_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "safety_alerts_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "public_updates_channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "premium_progress_bar_enabled": { + "type": "boolean" + }, + "premium_progress_bar_enabled_user_updated_at": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "nsfw": { + "type": "boolean" + }, + "nsfw_level": { + "$ref": "#/components/schemas/GuildNSFWContentLevel" + }, + "emojis": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildStickerResponse" + } + }, + "incidents_data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildIncidentsDataResponse" + } + ] + }, + "approximate_member_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "approximate_presence_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "home_header", + "splash", + "discovery_splash", + "features", + "banner", + "owner_id", + "application_id", + "region", + "afk_channel_id", + "afk_timeout", + "system_channel_id", + "system_channel_flags", + "widget_enabled", + "widget_channel_id", + "verification_level", + "roles", + "default_message_notifications", + "mfa_level", + "explicit_content_filter", + "max_presences", + "max_members", + "max_stage_video_channel_users", + "max_video_channel_users", + "vanity_url_code", + "premium_tier", + "premium_subscription_count", + "preferred_locale", + "rules_channel_id", + "safety_alerts_channel_id", + "public_updates_channel_id", + "premium_progress_bar_enabled", + "nsfw", + "nsfw_level", + "emojis", + "stickers", + "incidents_data" + ] + }, + "HasOption": { + "type": "string", + "oneOf": [ + { + "title": "LINK", + "const": "link" + }, + { + "title": "EMBED", + "const": "embed" + }, + { + "title": "FILE", + "const": "file" + }, + { + "title": "IMAGE", + "const": "image" + }, + { + "title": "VIDEO", + "const": "video" + }, + { + "title": "SOUND", + "const": "sound" + }, + { + "title": "STICKER", + "const": "sticker" + }, + { + "title": "POLL", + "const": "poll" + }, + { + "title": "SNAPSHOT", + "const": "snapshot" + }, + { + "title": "NO_LINK", + "const": "-link" + }, + { + "title": "NO_EMBED", + "const": "-embed" + }, + { + "title": "NO_FILE", + "const": "-file" + }, + { + "title": "NO_IMAGE", + "const": "-image" + }, + { + "title": "NO_VIDEO", + "const": "-video" + }, + { + "title": "NO_SOUND", + "const": "-sound" + }, + { + "title": "NO_STICKER", + "const": "-sticker" + }, + { + "title": "NO_POLL", + "const": "-poll" + }, + { + "title": "NO_SNAPSHOT", + "const": "-snapshot" + } + ] + }, + "IncomingWebhookInteractionRequest": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "tts": { + "type": [ + "boolean", + "null" + ] + }, + "flags": { + "type": [ + "integer", + "null" + ] + } + } + }, + "IncomingWebhookRequestPartial": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "tts": { + "type": [ + "boolean", + "null" + ] + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "username": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 80 + }, + "avatar_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "thread_name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 100 + }, + "applied_tags": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 5 + } + } + }, + "IncomingWebhookUpdateForInteractionCallbackRequestPartial": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "flags": { + "type": [ + "integer", + "null" + ] + } + } + }, + "IncomingWebhookUpdateRequestPartial": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "flags": { + "type": [ + "integer", + "null" + ] + } + } + }, + "Int53Type": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991, + "format": "int64" + }, + "IntegrationApplicationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "cover_image": { + "type": "string" + }, + "primary_sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "bot": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "type" + ] + }, + "IntegrationExpireBehaviorTypes": { + "type": "integer", + "oneOf": [ + { + "title": "REMOVE_ROLE", + "description": "Remove role", + "const": 0 + }, + { + "title": "KICK", + "description": "Kick", + "const": 1 + } + ], + "format": "int32" + }, + "IntegrationExpireGracePeriodTypes": { + "type": "integer", + "oneOf": [ + { + "title": "ONE_DAY", + "description": "1 day", + "const": 1 + }, + { + "title": "THREE_DAYS", + "description": "3 days", + "const": 3 + }, + { + "title": "SEVEN_DAYS", + "description": "7 days", + "const": 7 + }, + { + "title": "FOURTEEN_DAYS", + "description": "14 days", + "const": 14 + }, + { + "title": "THIRTY_DAYS", + "description": "30 days", + "const": 30 + } + ], + "format": "int32" + }, + "IntegrationTypes": { + "type": "string", + "oneOf": [ + { + "title": "DISCORD", + "const": "discord" + }, + { + "title": "TWITCH", + "const": "twitch" + }, + { + "title": "YOUTUBE", + "const": "youtube" + }, + { + "title": "GUILD_SUBSCRIPTION", + "const": "guild_subscription" + } + ] + }, + "InteractionApplicationCommandAutocompleteCallbackIntegerData": { + "type": "object", + "properties": { + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandOptionIntegerChoice" + } + ] + }, + "maxItems": 25 + } + } + }, + "InteractionApplicationCommandAutocompleteCallbackNumberData": { + "type": "object", + "properties": { + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandOptionNumberChoice" + } + ] + }, + "maxItems": 25 + } + } + }, + "InteractionApplicationCommandAutocompleteCallbackStringData": { + "type": "object", + "properties": { + "choices": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationCommandOptionStringChoice" + } + ] + }, + "maxItems": 25 + } + } + }, + "InteractionCallbackResponse": { + "type": "object", + "properties": { + "interaction": { + "$ref": "#/components/schemas/InteractionResponse" + }, + "resource": { + "oneOf": [ + { + "$ref": "#/components/schemas/CreateMessageInteractionCallbackResponse" + }, + { + "$ref": "#/components/schemas/LaunchActivityInteractionCallbackResponse" + }, + { + "$ref": "#/components/schemas/UpdateMessageInteractionCallbackResponse" + } + ] + } + }, + "required": [ + "interaction" + ] + }, + "InteractionCallbackTypes": { + "type": "integer", + "oneOf": [ + { + "title": "PONG", + "const": 1 + }, + { + "title": "CHANNEL_MESSAGE_WITH_SOURCE", + "const": 4 + }, + { + "title": "DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE", + "const": 5 + }, + { + "title": "DEFERRED_UPDATE_MESSAGE", + "const": 6 + }, + { + "title": "UPDATE_MESSAGE", + "const": 7 + }, + { + "title": "APPLICATION_COMMAND_AUTOCOMPLETE_RESULT", + "const": 8 + }, + { + "title": "MODAL", + "const": 9 + }, + { + "title": "LAUNCH_ACTIVITY", + "const": 12 + }, + { + "title": "SOCIAL_LAYER_SKU_PURCHASE_ELIGIBILITY", + "const": 13 + } + ], + "format": "int32" + }, + "InteractionContextType": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD", + "description": "This command can be used within a Guild.", + "const": 0 + }, + { + "title": "BOT_DM", + "description": "This command can be used within a DM with this application's bot.", + "const": 1 + }, + { + "title": "PRIVATE_CHANNEL", + "description": "This command can be used within DMs and Group DMs with users.", + "const": 2 + } + ], + "format": "int32" + }, + "InteractionResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/InteractionTypes" + }, + "activity_instance_id": { + "type": [ + "string", + "null" + ] + }, + "response_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "response_message_loading": { + "type": "boolean" + }, + "response_message_ephemeral": { + "type": "boolean" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type" + ] + }, + "InteractionTypes": { + "type": "integer", + "oneOf": [ + { + "title": "PING", + "description": "Sent by Discord to validate your application's interaction handler", + "const": 1 + }, + { + "title": "APPLICATION_COMMAND", + "description": "Sent when a user uses an application command", + "const": 2 + }, + { + "title": "MESSAGE_COMPONENT", + "description": "Sent when a user interacts with a message component previously sent by your application", + "const": 3 + }, + { + "title": "APPLICATION_COMMAND_AUTOCOMPLETE", + "description": "Sent when a user is filling in an autocomplete option in a chat command", + "const": 4 + }, + { + "title": "MODAL_SUBMIT", + "description": "Sent when a user submits a modal previously sent by your application", + "const": 5 + }, + { + "title": "SOCIAL_LAYER_SKU_PURCHASE_ELIGIBILITY", + "description": "Sent when Discord is checking if a user can purchase a Social Layer SKU", + "const": 6 + } + ], + "format": "int32" + }, + "InviteApplicationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "cover_image": { + "type": "string" + }, + "primary_sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "bot": { + "$ref": "#/components/schemas/UserResponse" + }, + "slug": { + "type": "string" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "rpc_origins": { + "type": "array", + "items": { + "type": "string" + } + }, + "bot_public": { + "type": "boolean" + }, + "bot_require_code_grant": { + "type": "boolean" + }, + "terms_of_service_url": { + "type": "string", + "format": "uri" + }, + "privacy_policy_url": { + "type": "string", + "format": "uri" + }, + "custom_install_url": { + "type": "string", + "format": "uri" + }, + "install_params": { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParamsResponse" + }, + "integration_types_config": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ApplicationIntegrationTypeConfigurationResponse" + } + }, + "verify_key": { + "type": "string" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "flags_new": { + "type": "string" + }, + "max_participants": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "type", + "verify_key", + "flags", + "flags_new" + ] + }, + "InviteChannelRecipientResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "username": { + "type": "string" + }, + "avatar": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "username", + "avatar" + ] + }, + "InviteChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InviteChannelRecipientResponse" + } + } + }, + "required": [ + "id", + "type", + "name" + ] + }, + "InviteGuildResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "splash": { + "type": [ + "string", + "null" + ] + }, + "banner": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildFeatures" + }, + "uniqueItems": true + }, + "verification_level": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VerificationLevels" + } + ] + }, + "vanity_url_code": { + "type": [ + "string", + "null" + ] + }, + "nsfw_level": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildNSFWContentLevel" + } + ] + }, + "nsfw": { + "type": [ + "boolean", + "null" + ] + }, + "premium_subscription_count": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "splash", + "banner", + "description", + "icon", + "features", + "verification_level", + "vanity_url_code", + "nsfw_level", + "nsfw", + "premium_subscription_count" + ] + }, + "InviteGuildRoleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "color": { + "type": "integer", + "format": "int32" + }, + "colors": { + "$ref": "#/components/schemas/GuildRoleColorsResponse" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "unicode_emoji": { + "type": [ + "string", + "null" + ] + }, + "permissions": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "position", + "color", + "colors", + "icon", + "unicode_emoji" + ] + }, + "InviteTargetTypes": { + "type": "integer", + "oneOf": [ + { + "title": "STREAM", + "const": 1 + }, + { + "title": "EMBEDDED_APPLICATION", + "const": 2 + }, + { + "title": "ROLE_SUBSCRIPTIONS_PURCHASE", + "const": 3 + } + ], + "format": "int32" + }, + "InviteTypes": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD", + "const": 0 + }, + { + "title": "GROUP_DM", + "const": 1 + }, + { + "title": "FRIEND", + "const": 2 + } + ], + "format": "int32" + }, + "KeywordRuleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageActionResponse" + }, + { + "$ref": "#/components/schemas/FlagToChannelActionResponse" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionResponse" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledActionResponse" + } + ] + } + }, + "trigger_type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "exempt_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "exempt_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "trigger_metadata": { + "$ref": "#/components/schemas/KeywordTriggerMetadataResponse" + } + }, + "required": [ + "id", + "guild_id", + "creator_id", + "name", + "event_type", + "actions", + "trigger_type", + "enabled", + "exempt_roles", + "exempt_channels", + "trigger_metadata" + ] + }, + "KeywordTriggerMetadata": { + "type": "object", + "properties": { + "keyword_filter": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "maxItems": 1000 + }, + "regex_patterns": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 260 + }, + "maxItems": 10 + }, + "allow_list": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "maxItems": 100 + } + } + }, + "KeywordTriggerMetadataResponse": { + "type": "object", + "properties": { + "keyword_filter": { + "type": "array", + "items": { + "type": "string" + } + }, + "regex_patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "allow_list": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "keyword_filter", + "regex_patterns", + "allow_list" + ] + }, + "KeywordUpsertRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/KeywordTriggerMetadata" + } + ] + } + }, + "required": [ + "name", + "event_type", + "trigger_type" + ] + }, + "KeywordUpsertRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/KeywordTriggerMetadata" + } + ] + } + } + }, + "LabelComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 18 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 45 + }, + "description": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 100 + }, + "component": { + "oneOf": [ + { + "$ref": "#/components/schemas/ChannelSelectComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/CheckboxComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/CheckboxGroupComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/FileUploadComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/MentionableSelectComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/RadioGroupComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/RoleSelectComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/StringSelectComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/TextInputComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/UserSelectComponentForModalRequest" + } + ] + } + }, + "required": [ + "type", + "label", + "component" + ] + }, + "LaunchActivityInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + } + }, + "required": [ + "type" + ] + }, + "LaunchActivityInteractionCallbackResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "activity_instance": { + "$ref": "#/components/schemas/ActivityInstanceCallbackResponse" + } + }, + "required": [ + "type", + "activity_instance" + ] + }, + "ListApplicationEmojisResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EmojiResponse" + } + } + }, + "required": [ + "items" + ] + }, + "ListGuildSoundboardSoundsResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SoundboardSoundResponse" + } + } + }, + "required": [ + "items" + ] + }, + "LobbyGuildInviteResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + }, + "required": [ + "code" + ] + }, + "LobbyMemberRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "maxLength": 1024 + }, + "maxProperties": 25 + }, + "flags": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ] + } + ] + } + }, + "required": [ + "id" + ] + }, + "LobbyMemberResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "flags": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "metadata", + "flags" + ] + }, + "LobbyMessageResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/MessageType" + }, + "content": { + "type": "string" + }, + "lobby_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "author": { + "$ref": "#/components/schemas/UserResponse" + }, + "metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "moderation_metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type", + "content", + "lobby_id", + "channel_id", + "author", + "flags" + ] + }, + "LobbyResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LobbyMemberResponse" + } + }, + "linked_channel": { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + "flags": { + "$ref": "#/components/schemas/UInt32Type" + }, + "override_event_webhooks_url": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "id", + "application_id", + "metadata", + "members", + "flags" + ] + }, + "MLSpamRuleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageActionResponse" + }, + { + "$ref": "#/components/schemas/FlagToChannelActionResponse" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionResponse" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledActionResponse" + } + ] + } + }, + "trigger_type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "exempt_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "exempt_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "trigger_metadata": { + "$ref": "#/components/schemas/MLSpamTriggerMetadataResponse" + } + }, + "required": [ + "id", + "guild_id", + "creator_id", + "name", + "event_type", + "actions", + "trigger_type", + "enabled", + "exempt_roles", + "exempt_channels", + "trigger_metadata" + ] + }, + "MLSpamTriggerMetadata": { + "type": "object", + "properties": {} + }, + "MLSpamTriggerMetadataResponse": { + "type": "object", + "properties": {} + }, + "MLSpamUpsertRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MLSpamTriggerMetadata" + } + ] + } + }, + "required": [ + "name", + "event_type", + "trigger_type" + ] + }, + "MLSpamUpsertRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MLSpamTriggerMetadata" + } + ] + } + } + }, + "MediaGalleryComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaGalleryItemRequest" + }, + "minItems": 1, + "maxItems": 10 + } + }, + "required": [ + "type", + "items" + ] + }, + "MediaGalleryComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MediaGalleryItemResponse" + } + } + }, + "required": [ + "type", + "id", + "items" + ] + }, + "MediaGalleryItemRequest": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 1024 + }, + "spoiler": { + "type": [ + "boolean", + "null" + ] + }, + "media": { + "$ref": "#/components/schemas/UnfurledMediaRequest" + } + }, + "required": [ + "media" + ] + }, + "MediaGalleryItemResponse": { + "type": "object", + "properties": { + "media": { + "$ref": "#/components/schemas/UnfurledMediaResponse" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "spoiler": { + "type": "boolean" + } + }, + "required": [ + "media", + "description", + "spoiler" + ] + }, + "MentionSpamRuleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageActionResponse" + }, + { + "$ref": "#/components/schemas/FlagToChannelActionResponse" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionResponse" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledActionResponse" + } + ] + } + }, + "trigger_type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "exempt_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "exempt_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "trigger_metadata": { + "$ref": "#/components/schemas/MentionSpamTriggerMetadataResponse" + } + }, + "required": [ + "id", + "guild_id", + "creator_id", + "name", + "event_type", + "actions", + "trigger_type", + "enabled", + "exempt_roles", + "exempt_channels", + "trigger_metadata" + ] + }, + "MentionSpamTriggerMetadata": { + "type": "object", + "properties": { + "mention_total_limit": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 50 + }, + "mention_raid_protection_enabled": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "MentionSpamTriggerMetadataResponse": { + "type": "object", + "properties": { + "mention_total_limit": { + "type": "integer", + "format": "int32" + }, + "mention_raid_protection_enabled": { + "type": "boolean" + } + }, + "required": [ + "mention_total_limit", + "mention_raid_protection_enabled" + ] + }, + "MentionSpamUpsertRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MentionSpamTriggerMetadata" + } + ] + } + }, + "required": [ + "name", + "event_type", + "trigger_type" + ] + }, + "MentionSpamUpsertRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MentionSpamTriggerMetadata" + } + ] + } + } + }, + "MentionableSelectComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoleSelectDefaultValue" + }, + { + "$ref": "#/components/schemas/UserSelectDefaultValue" + } + ] + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "MentionableSelectComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoleSelectDefaultValue" + }, + { + "$ref": "#/components/schemas/UserSelectDefaultValue" + } + ] + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "MentionableSelectComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "min_values": { + "type": "integer", + "format": "int32" + }, + "max_values": { + "type": "integer", + "format": "int32" + }, + "disabled": { + "type": "boolean" + }, + "default_values": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RoleSelectDefaultValueResponse" + }, + { + "$ref": "#/components/schemas/UserSelectDefaultValueResponse" + } + ] + } + } + }, + "required": [ + "type", + "id", + "custom_id", + "min_values", + "max_values" + ] + }, + "MessageActivityResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/ActivityActionTypes" + }, + "party_id": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "MessageAllowedMentionsRequest": { + "type": "object", + "properties": { + "parse": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/AllowedMentionTypes" + } + ] + }, + "maxItems": 1521, + "uniqueItems": true + }, + "users": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 100, + "uniqueItems": true + }, + "roles": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "maxItems": 100, + "uniqueItems": true + }, + "replied_user": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "MessageAttachmentRequest": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "filename": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "duration_secs": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 2147483647, + "format": "double" + }, + "waveform": { + "type": [ + "string", + "null" + ], + "maxLength": 400 + }, + "title": { + "type": [ + "string", + "null" + ], + "maxLength": 1024 + }, + "is_spoiler": { + "type": [ + "boolean", + "null" + ] + }, + "is_remix": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "id" + ] + }, + "MessageAttachmentResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "filename": { + "type": "string" + }, + "size": { + "type": "integer", + "format": "int32" + }, + "url": { + "type": "string", + "format": "uri" + }, + "proxy_url": { + "type": "string", + "format": "uri" + }, + "width": { + "type": "integer", + "format": "int32" + }, + "height": { + "type": "integer", + "format": "int32" + }, + "duration_secs": { + "type": "number", + "format": "double" + }, + "waveform": { + "type": "string" + }, + "description": { + "type": "string" + }, + "content_type": { + "type": "string" + }, + "ephemeral": { + "type": "boolean" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "placeholder": { + "type": "string" + }, + "placeholder_version": { + "type": "integer", + "format": "int32" + }, + "title": { + "type": [ + "string", + "null" + ] + }, + "application": { + "$ref": "#/components/schemas/ApplicationResponse" + }, + "clip_created_at": { + "type": "string", + "format": "date-time" + }, + "clip_participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + } + }, + "required": [ + "id", + "filename", + "size", + "url", + "proxy_url" + ] + }, + "MessageCallResponse": { + "type": "object", + "properties": { + "ended_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "participants": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + } + }, + "required": [ + "participants" + ] + }, + "MessageComponentInteractionMetadataResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "authorizing_integration_owners": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "original_response_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "interacted_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type", + "authorizing_integration_owners", + "interacted_message_id" + ] + }, + "MessageComponentSeparatorSpacingSize": { + "type": "integer", + "oneOf": [ + { + "title": "SMALL", + "description": "Small spacing", + "const": 1 + }, + { + "title": "LARGE", + "description": "Large spacing", + "const": 2 + } + ], + "format": "int32" + }, + "MessageComponentTypes": { + "type": "integer", + "oneOf": [ + { + "title": "ACTION_ROW", + "description": "Container for other components", + "const": 1 + }, + { + "title": "BUTTON", + "description": "Button object", + "const": 2 + }, + { + "title": "STRING_SELECT", + "description": "Select menu for picking from defined text options", + "const": 3 + }, + { + "title": "TEXT_INPUT", + "description": "Text input object", + "const": 4 + }, + { + "title": "USER_SELECT", + "description": "Select menu for users", + "const": 5 + }, + { + "title": "ROLE_SELECT", + "description": "Select menu for roles", + "const": 6 + }, + { + "title": "MENTIONABLE_SELECT", + "description": "Select menu for mentionables (users and roles)", + "const": 7 + }, + { + "title": "CHANNEL_SELECT", + "description": "Select menu for channels", + "const": 8 + }, + { + "title": "SECTION", + "description": "Section component", + "const": 9 + }, + { + "title": "TEXT_DISPLAY", + "description": "Text component", + "const": 10 + }, + { + "title": "THUMBNAIL", + "description": "Thumbnail component", + "const": 11 + }, + { + "title": "MEDIA_GALLERY", + "description": "Media gallery component", + "const": 12 + }, + { + "title": "FILE", + "description": "File component", + "const": 13 + }, + { + "title": "SEPARATOR", + "description": "Separator component", + "const": 14 + }, + { + "title": "CONTAINER", + "description": "Container component", + "const": 17 + }, + { + "title": "LABEL", + "description": "Label component", + "const": 18 + }, + { + "title": "FILE_UPLOAD", + "description": "File upload component", + "const": 19 + }, + { + "title": "RADIO_GROUP", + "description": "Radio group component", + "const": 21 + }, + { + "title": "CHECKBOX_GROUP", + "description": "Checkbox group component", + "const": 22 + }, + { + "title": "CHECKBOX", + "description": "Checkbox component", + "const": 23 + } + ], + "format": "int32" + }, + "MessageCreateRequest": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "sticker_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 3 + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "shared_client_theme": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CustomClientThemeShareRequest" + } + ] + }, + "message_reference": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageReferenceRequest" + } + ] + }, + "nonce": { + "oneOf": [ + { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "format": "int64" + }, + { + "type": "string", + "maxLength": 25, + "format": "nonce" + }, + { + "type": "null" + } + ] + }, + "enforce_nonce": { + "type": [ + "boolean", + "null" + ] + }, + "tts": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "MessageEditRequestPartial": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "sticker_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 1521 + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + } + } + }, + "MessageEmbedAuthorResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ] + }, + "MessageEmbedFieldResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "string" + }, + "inline": { + "type": "boolean" + } + }, + "required": [ + "name", + "value", + "inline" + ] + }, + "MessageEmbedFooterResponse": { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "icon_url": { + "type": "string" + }, + "proxy_icon_url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "text" + ] + }, + "MessageEmbedImageResponse": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string", + "format": "uri" + }, + "width": { + "$ref": "#/components/schemas/UInt32Type" + }, + "height": { + "$ref": "#/components/schemas/UInt32Type" + }, + "content_type": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "placeholder_version": { + "$ref": "#/components/schemas/UInt32Type" + }, + "description": { + "type": "string" + }, + "flags": { + "$ref": "#/components/schemas/UInt32Type" + } + } + }, + "MessageEmbedProviderResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + } + }, + "required": [ + "name" + ] + }, + "MessageEmbedResponse": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "color": { + "type": "integer", + "format": "int32" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageEmbedFieldResponse" + } + }, + "author": { + "$ref": "#/components/schemas/MessageEmbedAuthorResponse" + }, + "provider": { + "$ref": "#/components/schemas/MessageEmbedProviderResponse" + }, + "image": { + "$ref": "#/components/schemas/MessageEmbedImageResponse" + }, + "thumbnail": { + "$ref": "#/components/schemas/MessageEmbedImageResponse" + }, + "video": { + "$ref": "#/components/schemas/MessageEmbedVideoResponse" + }, + "footer": { + "$ref": "#/components/schemas/MessageEmbedFooterResponse" + }, + "flags": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContainerComponentResponse" + } + } + }, + "required": [ + "type" + ] + }, + "MessageEmbedVideoResponse": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string", + "format": "uri" + }, + "width": { + "$ref": "#/components/schemas/UInt32Type" + }, + "height": { + "$ref": "#/components/schemas/UInt32Type" + }, + "content_type": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "placeholder_version": { + "$ref": "#/components/schemas/UInt32Type" + }, + "description": { + "type": "string" + }, + "flags": { + "$ref": "#/components/schemas/UInt32Type" + } + } + }, + "MessageInteractionResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "$ref": "#/components/schemas/InteractionTypes" + }, + "name": { + "type": "string" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "name_localized": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "name" + ] + }, + "MessageMentionChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/ChannelTypes" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "name", + "type", + "guild_id" + ] + }, + "MessageReactionCountDetailsResponse": { + "type": "object", + "properties": { + "burst": { + "type": "integer", + "format": "int32" + }, + "normal": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "burst", + "normal" + ] + }, + "MessageReactionEmojiResponse": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "animated": { + "type": "boolean" + } + }, + "required": [ + "id", + "name" + ] + }, + "MessageReactionResponse": { + "type": "object", + "properties": { + "emoji": { + "$ref": "#/components/schemas/MessageReactionEmojiResponse" + }, + "count": { + "type": "integer", + "format": "int32" + }, + "count_details": { + "$ref": "#/components/schemas/MessageReactionCountDetailsResponse" + }, + "burst_colors": { + "type": "array", + "items": { + "type": "string" + } + }, + "me_burst": { + "type": "boolean" + }, + "me": { + "type": "boolean" + } + }, + "required": [ + "emoji", + "count", + "count_details", + "burst_colors", + "me_burst", + "me" + ] + }, + "MessageReferenceRequest": { + "type": "object", + "properties": { + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "fail_if_not_exists": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageReferenceType" + } + ] + } + }, + "required": [ + "message_id" + ] + }, + "MessageReferenceResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MessageReferenceType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "channel_id" + ] + }, + "MessageReferenceType": { + "type": "integer", + "oneOf": [ + { + "title": "DEFAULT", + "description": "Reference to a message", + "const": 0 + } + ], + "format": "int32" + }, + "MessageResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MessageType" + }, + "content": { + "type": "string" + }, + "mentions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "mention_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageAttachmentResponse" + } + }, + "embeds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageEmbedResponse" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "edited_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentResponse" + }, + { + "$ref": "#/components/schemas/ContainerComponentResponse" + }, + { + "$ref": "#/components/schemas/FileComponentResponse" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentResponse" + }, + { + "$ref": "#/components/schemas/SectionComponentResponse" + }, + { + "$ref": "#/components/schemas/SeparatorComponentResponse" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + ] + } + }, + "stickers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildStickerResponse" + }, + { + "$ref": "#/components/schemas/StandardStickerResponse" + } + ] + } + }, + "sticker_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageStickerItemResponse" + } + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "author": { + "$ref": "#/components/schemas/UserResponse" + }, + "pinned": { + "type": "boolean" + }, + "mention_everyone": { + "type": "boolean" + }, + "tts": { + "type": "boolean" + }, + "call": { + "$ref": "#/components/schemas/MessageCallResponse" + }, + "activity": { + "$ref": "#/components/schemas/MessageActivityResponse" + }, + "application": { + "$ref": "#/components/schemas/BasicApplicationResponseWithBot" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "interaction": { + "$ref": "#/components/schemas/MessageInteractionResponse" + }, + "nonce": { + "oneOf": [ + { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "format": "int64" + }, + { + "type": "string", + "maxLength": 25, + "format": "nonce" + } + ] + }, + "webhook_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "message_reference": { + "$ref": "#/components/schemas/MessageReferenceResponse" + }, + "thread": { + "$ref": "#/components/schemas/ThreadResponse" + }, + "mention_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageMentionChannelResponse" + } + }, + "role_subscription_data": { + "$ref": "#/components/schemas/MessageRoleSubscriptionDataResponse" + }, + "purchase_notification": { + "$ref": "#/components/schemas/PurchaseNotificationResponse" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "resolved": { + "$ref": "#/components/schemas/ResolvedObjectsResponse" + }, + "poll": { + "$ref": "#/components/schemas/PollResponse" + }, + "shared_client_theme": { + "$ref": "#/components/schemas/CustomClientThemeResponse" + }, + "interaction_metadata": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/MessageComponentInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/ModalSubmitInteractionMetadataResponse" + } + ] + }, + "message_snapshots": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageSnapshotResponse" + } + }, + "reactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageReactionResponse" + } + }, + "referenced_message": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/BasicMessageResponse" + } + ] + } + }, + "required": [ + "type", + "content", + "mentions", + "mention_roles", + "attachments", + "embeds", + "timestamp", + "edited_timestamp", + "flags", + "components", + "id", + "channel_id", + "author", + "pinned", + "mention_everyone", + "tts" + ] + }, + "MessageRoleSubscriptionDataResponse": { + "type": "object", + "properties": { + "role_subscription_listing_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "tier_name": { + "type": "string" + }, + "total_months_subscribed": { + "type": "integer", + "format": "int32" + }, + "is_renewal": { + "type": "boolean" + } + }, + "required": [ + "role_subscription_listing_id", + "tier_name", + "total_months_subscribed", + "is_renewal" + ] + }, + "MessageShareCustomUserThemeBaseTheme": { + "type": "integer", + "oneOf": [ + { + "title": "UNSET", + "description": "No base theme", + "const": 0 + }, + { + "title": "DARK", + "description": "Dark base theme", + "const": 1 + }, + { + "title": "LIGHT", + "description": "Light base theme", + "const": 2 + }, + { + "title": "DARKER", + "description": "Darker base theme", + "const": 3 + }, + { + "title": "MIDNIGHT", + "description": "Midnight base theme", + "const": 4 + } + ], + "format": "int32" + }, + "MessageSnapshotResponse": { + "type": "object", + "properties": { + "message": { + "$ref": "#/components/schemas/MinimalContentMessageResponse" + } + }, + "required": [ + "message" + ] + }, + "MessageStickerItemResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "format_type": { + "$ref": "#/components/schemas/StickerFormatTypes" + } + }, + "required": [ + "id", + "name", + "format_type" + ] + }, + "MessageType": { + "type": "integer", + "oneOf": [ + { + "title": "DEFAULT", + "description": "", + "const": 0 + }, + { + "title": "RECIPIENT_ADD", + "description": "", + "const": 1 + }, + { + "title": "RECIPIENT_REMOVE", + "description": "", + "const": 2 + }, + { + "title": "CALL", + "description": "", + "const": 3 + }, + { + "title": "CHANNEL_NAME_CHANGE", + "description": "", + "const": 4 + }, + { + "title": "CHANNEL_ICON_CHANGE", + "description": "", + "const": 5 + }, + { + "title": "CHANNEL_PINNED_MESSAGE", + "description": "", + "const": 6 + }, + { + "title": "USER_JOIN", + "description": "", + "const": 7 + }, + { + "title": "GUILD_BOOST", + "description": "", + "const": 8 + }, + { + "title": "GUILD_BOOST_TIER_1", + "description": "", + "const": 9 + }, + { + "title": "GUILD_BOOST_TIER_2", + "description": "", + "const": 10 + }, + { + "title": "GUILD_BOOST_TIER_3", + "description": "", + "const": 11 + }, + { + "title": "CHANNEL_FOLLOW_ADD", + "description": "", + "const": 12 + }, + { + "title": "GUILD_DISCOVERY_DISQUALIFIED", + "description": "", + "const": 14 + }, + { + "title": "GUILD_DISCOVERY_REQUALIFIED", + "description": "", + "const": 15 + }, + { + "title": "GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING", + "description": "", + "const": 16 + }, + { + "title": "GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING", + "description": "", + "const": 17 + }, + { + "title": "THREAD_CREATED", + "description": "", + "const": 18 + }, + { + "title": "REPLY", + "description": "", + "const": 19 + }, + { + "title": "CHAT_INPUT_COMMAND", + "description": "", + "const": 20 + }, + { + "title": "THREAD_STARTER_MESSAGE", + "description": "", + "const": 21 + }, + { + "title": "GUILD_INVITE_REMINDER", + "description": "", + "const": 22 + }, + { + "title": "CONTEXT_MENU_COMMAND", + "description": "", + "const": 23 + }, + { + "title": "AUTO_MODERATION_ACTION", + "description": "", + "const": 24 + }, + { + "title": "ROLE_SUBSCRIPTION_PURCHASE", + "description": "", + "const": 25 + }, + { + "title": "INTERACTION_PREMIUM_UPSELL", + "description": "", + "const": 26 + }, + { + "title": "STAGE_START", + "description": "", + "const": 27 + }, + { + "title": "STAGE_END", + "description": "", + "const": 28 + }, + { + "title": "STAGE_SPEAKER", + "description": "", + "const": 29 + }, + { + "title": "STAGE_TOPIC", + "description": "", + "const": 31 + }, + { + "title": "GUILD_APPLICATION_PREMIUM_SUBSCRIPTION", + "description": "", + "const": 32 + }, + { + "title": "GUILD_INCIDENT_ALERT_MODE_ENABLED", + "description": "", + "const": 36 + }, + { + "title": "GUILD_INCIDENT_ALERT_MODE_DISABLED", + "description": "", + "const": 37 + }, + { + "title": "GUILD_INCIDENT_REPORT_RAID", + "description": "", + "const": 38 + }, + { + "title": "GUILD_INCIDENT_REPORT_FALSE_ALARM", + "description": "", + "const": 39 + }, + { + "title": "POLL_RESULT", + "description": "", + "const": 46 + }, + { + "title": "HD_STREAMING_UPGRADED", + "description": "", + "const": 55 + } + ], + "format": "int32" + }, + "MetadataItemTypes": { + "type": "integer", + "oneOf": [ + { + "title": "INTEGER_LESS_THAN_EQUAL", + "description": "the metadata value (integer) is less than or equal to the guild's configured value (integer)", + "const": 1 + }, + { + "title": "INTEGER_GREATER_THAN_EQUAL", + "description": "the metadata value (integer) is greater than or equal to the guild's configured value (integer)", + "const": 2 + }, + { + "title": "INTEGER_EQUAL", + "description": "the metadata value (integer) is equal to the guild's configured value (integer)", + "const": 3 + }, + { + "title": "INTEGER_NOT_EQUAL", + "description": "the metadata value (integer) is not equal to the guild's configured value (integer)", + "const": 4 + }, + { + "title": "DATETIME_LESS_THAN_EQUAL", + "description": "the metadata value (ISO8601 string) is less than or equal to the guild's configured value (integer; days before current date)", + "const": 5 + }, + { + "title": "DATETIME_GREATER_THAN_EQUAL", + "description": "the metadata value (ISO8601 string) is greater than or equal to the guild's configured value (integer; days before current date)", + "const": 6 + }, + { + "title": "BOOLEAN_EQUAL", + "description": "the metadata value (integer) is equal to the guild's configured value (integer; 1)", + "const": 7 + }, + { + "title": "BOOLEAN_NOT_EQUAL", + "description": "the metadata value (integer) is not equal to the guild's configured value (integer; 1)", + "const": 8 + } + ], + "format": "int32" + }, + "MinimalContentMessageResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MessageType" + }, + "content": { + "type": "string" + }, + "mentions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "mention_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageAttachmentResponse" + } + }, + "embeds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageEmbedResponse" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "edited_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentResponse" + }, + { + "$ref": "#/components/schemas/ContainerComponentResponse" + }, + { + "$ref": "#/components/schemas/FileComponentResponse" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentResponse" + }, + { + "$ref": "#/components/schemas/SectionComponentResponse" + }, + { + "$ref": "#/components/schemas/SeparatorComponentResponse" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + ] + } + }, + "stickers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildStickerResponse" + }, + { + "$ref": "#/components/schemas/StandardStickerResponse" + } + ] + } + }, + "sticker_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageStickerItemResponse" + } + } + }, + "required": [ + "type", + "content", + "mentions", + "mention_roles", + "attachments", + "embeds", + "timestamp", + "edited_timestamp", + "flags", + "components" + ] + }, + "ModalInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 9 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "data": { + "$ref": "#/components/schemas/ModalInteractionCallbackRequestData" + } + }, + "required": [ + "type", + "data" + ] + }, + "ModalInteractionCallbackRequestData": { + "type": "object", + "properties": { + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 45 + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/LabelComponentForModalRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForModalRequest" + } + ] + }, + "minItems": 1, + "maxItems": 40 + } + }, + "required": [ + "custom_id", + "title", + "components" + ] + }, + "ModalSubmitInteractionMetadataResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionTypes" + } + ], + "format": "int32" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "authorizing_integration_owners": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "original_response_message_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "triggering_interaction_metadata": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/MessageComponentInteractionMetadataResponse" + } + ] + } + }, + "required": [ + "id", + "type", + "authorizing_integration_owners", + "triggering_interaction_metadata" + ] + }, + "MultipleChoiceFormFieldResponse": { + "type": "object", + "properties": { + "field_type": { + "type": "string", + "description": "Type of form field", + "enum": [ + "MULTIPLE_CHOICE" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildMemberVerificationFormFieldType" + } + ] + }, + "label": { + "type": "string", + "description": "Label shown above field" + }, + "description": { + "type": "string", + "description": "Optional helper text shown below label" + }, + "required": { + "type": "boolean", + "description": "Whether applicant must fill in field" + }, + "choices": { + "type": "array", + "description": "Choices applicant can select from", + "items": { + "type": "string" + } + }, + "response": { + "type": "integer", + "description": "Index of choice selected by applicant", + "format": "int32" + } + }, + "required": [ + "field_type", + "choices" + ] + }, + "MyGuildResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "banner": { + "type": [ + "string", + "null" + ] + }, + "owner": { + "type": "boolean" + }, + "permissions": { + "type": "string" + }, + "features": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildFeatures" + }, + "uniqueItems": true + }, + "approximate_member_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "approximate_presence_count": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "id", + "name", + "icon", + "banner", + "owner", + "permissions", + "features" + ] + }, + "NameplatePalette": { + "type": "string", + "oneOf": [ + { + "title": "CRIMSON", + "description": "Crimson color palette", + "const": "crimson" + }, + { + "title": "BERRY", + "description": "Berry color palette", + "const": "berry" + }, + { + "title": "SKY", + "description": "Sky color palette", + "const": "sky" + }, + { + "title": "TEAL", + "description": "Teal color palette", + "const": "teal" + }, + { + "title": "FOREST", + "description": "Forest color palette", + "const": "forest" + }, + { + "title": "BUBBLE_GUM", + "description": "Bubble gum color palette", + "const": "bubble_gum" + }, + { + "title": "VIOLET", + "description": "Violet color palette", + "const": "violet" + }, + { + "title": "COBALT", + "description": "Cobalt color palette", + "const": "cobalt" + }, + { + "title": "CLOVER", + "description": "Clover color palette", + "const": "clover" + }, + { + "title": "LEMON", + "description": "Lemon color palette", + "const": "lemon" + }, + { + "title": "WHITE", + "description": "White color palette", + "const": "white" + }, + { + "title": "BLACK", + "description": "Black color palette", + "const": "black" + } + ] + }, + "NewMemberActionResponse": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "action_type": { + "$ref": "#/components/schemas/NewMemberActionType" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "emoji": { + "$ref": "#/components/schemas/SettingsEmojiResponse" + }, + "icon": { + "type": "string" + } + }, + "required": [ + "channel_id", + "action_type", + "title", + "description" + ] + }, + "NewMemberActionType": { + "type": "integer", + "oneOf": [ + { + "title": "VIEW", + "const": 0 + }, + { + "title": "TALK", + "const": 1 + } + ], + "format": "int32" + }, + "OAuth2GetAuthorizationResponse": { + "type": "object", + "properties": { + "application": { + "$ref": "#/components/schemas/ApplicationResponse" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuth2Scopes" + }, + "uniqueItems": true + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "application", + "expires", + "scopes" + ] + }, + "OAuth2GetKeys": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OAuth2Key" + } + } + }, + "required": [ + "keys" + ] + }, + "OAuth2GetOpenIDConnectUserInfoResponse": { + "type": "object", + "properties": { + "sub": { + "type": "string" + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "email_verified": { + "type": "boolean" + }, + "preferred_username": { + "type": "string" + }, + "nickname": { + "type": [ + "string", + "null" + ] + }, + "picture": { + "type": "string" + }, + "locale": { + "type": "string" + } + }, + "required": [ + "sub" + ] + }, + "OAuth2Key": { + "type": "object", + "properties": { + "kty": { + "type": "string" + }, + "use": { + "type": "string" + }, + "kid": { + "type": "string" + }, + "n": { + "type": "string" + }, + "e": { + "type": "string" + }, + "alg": { + "type": "string" + } + }, + "required": [ + "kty", + "use", + "kid", + "n", + "e", + "alg" + ] + }, + "OAuth2Scopes": { + "type": "string", + "oneOf": [ + { + "title": "IDENTIFY", + "description": "allows /users/@me without email", + "const": "identify" + }, + { + "title": "EMAIL", + "description": "enables /users/@me to return an email", + "const": "email" + }, + { + "title": "CONNECTIONS", + "description": "allows /users/@me/connections to return linked third-party accounts", + "const": "connections" + }, + { + "title": "GUILDS", + "description": "allows /users/@me/guilds to return basic information about all of a user's guilds", + "const": "guilds" + }, + { + "title": "GUILDS_JOIN", + "description": "allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild", + "const": "guilds.join" + }, + { + "title": "GUILDS_MEMBERS_READ", + "description": "allows /users/@me/guilds/{guild.id}/member to return a user's member information in a guild", + "const": "guilds.members.read" + }, + { + "title": "GDM_JOIN", + "description": "allows your app to join users to a group dm", + "const": "gdm.join" + }, + { + "title": "BOT", + "description": "for oauth2 bots, this puts the bot in the user's selected guild by default", + "const": "bot" + }, + { + "title": "RPC", + "description": "for local rpc server access, this allows you to control a user's local Discord client - requires Discord approval", + "const": "rpc" + }, + { + "title": "RPC_NOTIFICATIONS_READ", + "description": "for local rpc server access, this allows you to receive notifications pushed out to the user - requires Discord approval", + "const": "rpc.notifications.read" + }, + { + "title": "RPC_VOICE_READ", + "description": "for local rpc server access, this allows you to read a user's voice settings and listen for voice events - requires Discord approval", + "const": "rpc.voice.read" + }, + { + "title": "RPC_VOICE_WRITE", + "description": "for local rpc server access, this allows you to update a user's voice settings - requires Discord approval", + "const": "rpc.voice.write" + }, + { + "title": "RPC_VIDEO_READ", + "description": "for local rpc server access, this allows you to read a user's video status - requires Discord approval", + "const": "rpc.video.read" + }, + { + "title": "RPC_VIDEO_WRITE", + "description": "for local rpc server access, this allows you to update a user's video settings - requires Discord approval", + "const": "rpc.video.write" + }, + { + "title": "RPC_SCREENSHARE_READ", + "description": "for local rpc server access, this allows you to read a user's screenshare status- requires Discord approval", + "const": "rpc.screenshare.read" + }, + { + "title": "RPC_SCREENSHARE_WRITE", + "description": "for local rpc server access, this allows you to update a user's screenshare settings- requires Discord approval", + "const": "rpc.screenshare.write" + }, + { + "title": "RPC_ACTIVITIES_WRITE", + "description": "for local rpc server access, this allows you to update a user's activity - requires Discord approval", + "const": "rpc.activities.write" + }, + { + "title": "WEBHOOK_INCOMING", + "description": "this generates a webhook that is returned in the oauth token response for authorization code grants", + "const": "webhook.incoming" + }, + { + "title": "MESSAGES_READ", + "description": "for local rpc server api access, this allows you to read messages from all client channels (otherwise restricted to channels/guilds your app creates)", + "const": "messages.read" + }, + { + "title": "APPLICATIONS_BUILDS_UPLOAD", + "description": "allows your app to upload/update builds for a user's applications - requires Discord approval", + "const": "applications.builds.upload" + }, + { + "title": "APPLICATIONS_BUILDS_READ", + "description": "allows your app to read build data for a user's applications", + "const": "applications.builds.read" + }, + { + "title": "APPLICATIONS_COMMANDS", + "description": "allows your app to use commands in a guild", + "const": "applications.commands" + }, + { + "title": "APPLICATIONS_COMMANDS_PERMISSIONS_UPDATE", + "description": "allows your app to update permissions for its commands in a guild a user has permissions to", + "const": "applications.commands.permissions.update" + }, + { + "title": "APPLICATIONS_COMMANDS_UPDATE", + "description": "allows your app to update its commands using a Bearer token - client credentials grant only", + "const": "applications.commands.update" + }, + { + "title": "APPLICATIONS_STORE_UPDATE", + "description": "allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications", + "const": "applications.store.update" + }, + { + "title": "APPLICATIONS_ENTITLEMENTS", + "description": "allows your app to read entitlements for a user's applications", + "const": "applications.entitlements" + }, + { + "title": "ACTIVITIES_READ", + "description": "allows your app to fetch data from a user's \"Now Playing/Recently Played\" list - requires Discord approval", + "const": "activities.read" + }, + { + "title": "ACTIVITIES_WRITE", + "description": "allows your app to update a user's activity - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "const": "activities.write" + }, + { + "title": "ACTIVITIES_INVITES_WRITE", + "description": "allows your app to send activity invites - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "const": "activities.invites.write" + }, + { + "title": "RELATIONSHIPS_READ", + "description": "allows your app to know a user's friends and implicit relationships - requires Discord approval", + "const": "relationships.read" + }, + { + "title": "VOICE", + "description": "allows your app to connect to voice on user's behalf and see all the voice members - requires Discord approval", + "const": "voice" + }, + { + "title": "DM_CHANNELS_READ", + "description": "allows your app to see information about the user's DMs and group DMs - requires Discord approval", + "const": "dm_channels.read" + }, + { + "title": "ROLE_CONNECTIONS_WRITE", + "description": "allows your app to update a user's connection and metadata for the app", + "const": "role_connections.write" + }, + { + "title": "OPENID", + "description": "for OpenID Connect, this allows your app to receive user id and basic profile information", + "const": "openid" + } + ] + }, + "OnboardingPromptOptionRequest": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "emoji_animated": { + "type": [ + "boolean", + "null" + ] + }, + "role_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "channel_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + } + }, + "required": [ + "title" + ] + }, + "OnboardingPromptOptionResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "emoji": { + "$ref": "#/components/schemas/SettingsEmojiResponse" + }, + "role_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "channel_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + } + }, + "required": [ + "id", + "title", + "description", + "emoji", + "role_ids", + "channel_ids" + ] + }, + "OnboardingPromptResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "title": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OnboardingPromptOptionResponse" + } + }, + "single_select": { + "type": "boolean" + }, + "required": { + "type": "boolean" + }, + "in_onboarding": { + "type": "boolean" + }, + "type": { + "$ref": "#/components/schemas/OnboardingPromptType" + } + }, + "required": [ + "id", + "title", + "options", + "single_select", + "required", + "in_onboarding", + "type" + ] + }, + "OnboardingPromptType": { + "type": "integer", + "oneOf": [ + { + "title": "MULTIPLE_CHOICE", + "description": "Multiple choice options", + "const": 0 + }, + { + "title": "DROPDOWN", + "description": "Many options shown as a dropdown", + "const": 1 + } + ], + "format": "int32" + }, + "ParagraphFormFieldResponse": { + "type": "object", + "properties": { + "field_type": { + "type": "string", + "description": "Type of form field", + "enum": [ + "PARAGRAPH" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildMemberVerificationFormFieldType" + } + ] + }, + "label": { + "type": "string", + "description": "Label shown above field" + }, + "description": { + "type": "string", + "description": "Optional helper text shown below label" + }, + "required": { + "type": "boolean", + "description": "Whether applicant must fill in field" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text shown in empty input" + }, + "response": { + "type": "string", + "description": "Applicant's text response" + } + }, + "required": [ + "field_type" + ] + }, + "PartialDiscordIntegrationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "string", + "enum": [ + "discord" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type", + "name", + "account", + "application_id" + ] + }, + "PartialExternalConnectionIntegrationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "string", + "enum": [ + "twitch", + "youtube" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + } + }, + "required": [ + "id", + "type", + "name", + "account" + ] + }, + "PartialGuildSubscriptionIntegrationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "string", + "enum": [ + "guild_subscription" + ], + "allOf": [ + { + "$ref": "#/components/schemas/IntegrationTypes" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "account": { + "$ref": "#/components/schemas/AccountResponse" + } + }, + "required": [ + "id", + "type", + "name", + "account" + ] + }, + "PinnedMessageResponse": { + "type": "object", + "properties": { + "pinned_at": { + "type": "string", + "format": "date-time" + }, + "message": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "pinned_at", + "message" + ] + }, + "PinnedMessagesResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PinnedMessageResponse" + } + }, + "has_more": { + "type": "boolean" + } + }, + "required": [ + "items", + "has_more" + ] + }, + "PollAnswerCreateRequest": { + "type": "object", + "properties": { + "poll_media": { + "$ref": "#/components/schemas/PollMediaCreateRequest", + "description": "The data of the answer" + } + }, + "required": [ + "poll_media" + ] + }, + "PollAnswerDetailsResponse": { + "type": "object", + "properties": { + "users": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + } + }, + "required": [ + "users" + ] + }, + "PollAnswerResponse": { + "type": "object", + "properties": { + "answer_id": { + "type": "integer", + "description": "The ID of the answer", + "format": "int32" + }, + "poll_media": { + "$ref": "#/components/schemas/PollMediaResponse", + "description": "The data of the answer" + } + }, + "required": [ + "answer_id", + "poll_media" + ] + }, + "PollCreateRequest": { + "type": "object", + "properties": { + "question": { + "$ref": "#/components/schemas/PollMedia", + "description": "The question of the poll. Only `text` is supported." + }, + "answers": { + "type": "array", + "description": "Each of the answers available in the poll, up to 10", + "items": { + "$ref": "#/components/schemas/PollAnswerCreateRequest" + }, + "minItems": 1, + "maxItems": 10 + }, + "allow_multiselect": { + "type": [ + "boolean", + "null" + ], + "description": "Whether a user can select multiple answers" + }, + "layout_type": { + "description": "The layout type of the poll. Defaults to... DEFAULT!", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollLayoutTypes" + } + ] + }, + "duration": { + "type": [ + "integer", + "null" + ], + "description": "Number of hours the poll should be open for, up to 32 days. Defaults to 24", + "minimum": 1, + "maximum": 768, + "format": "int32" + } + }, + "required": [ + "question", + "answers" + ] + }, + "PollEmoji": { + "type": "object", + "properties": { + "id": { + "description": "The ID of the custom emoji", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "The name of the emoji, or the unicode emoji character", + "maxLength": 32 + }, + "animated": { + "type": [ + "boolean", + "null" + ], + "description": "Whether the emoji is animated" + } + } + }, + "PollEmojiCreateRequest": { + "type": "object", + "properties": { + "id": { + "description": "The ID of the custom emoji", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ], + "description": "The name of the emoji, or the unicode emoji character", + "maxLength": 32 + }, + "animated": { + "type": [ + "boolean", + "null" + ], + "description": "Whether the emoji is animated" + } + } + }, + "PollLayoutTypes": { + "type": "integer", + "oneOf": [ + { + "title": "DEFAULT", + "description": "The, uhm, default layout type.", + "const": 1 + } + ], + "format": "int32" + }, + "PollMedia": { + "type": "object", + "properties": { + "text": { + "type": [ + "string", + "null" + ], + "description": "The text of the field", + "minLength": 1, + "maxLength": 300 + }, + "emoji": { + "description": "The emoji of the field", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollEmoji" + } + ] + } + } + }, + "PollMediaCreateRequest": { + "type": "object", + "properties": { + "text": { + "type": [ + "string", + "null" + ], + "description": "The text of the field", + "minLength": 1, + "maxLength": 300 + }, + "emoji": { + "description": "The emoji of the field", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollEmojiCreateRequest" + } + ] + } + } + }, + "PollMediaResponse": { + "type": "object", + "properties": { + "text": { + "type": "string", + "description": "The text of the field" + }, + "emoji": { + "$ref": "#/components/schemas/MessageReactionEmojiResponse", + "description": "The emoji of the field" + } + } + }, + "PollResponse": { + "type": "object", + "properties": { + "question": { + "$ref": "#/components/schemas/PollMediaResponse", + "description": "The question of the poll. Only `text` is supported." + }, + "answers": { + "type": "array", + "description": "Each of the answers available in the poll", + "items": { + "$ref": "#/components/schemas/PollAnswerResponse" + } + }, + "expiry": { + "type": "string", + "description": "The time when the poll ends", + "format": "date-time" + }, + "allow_multiselect": { + "type": "boolean", + "description": "Whether a user can select multiple answers" + }, + "layout_type": { + "$ref": "#/components/schemas/PollLayoutTypes", + "description": "The layout type of the poll" + }, + "results": { + "$ref": "#/components/schemas/PollResultsResponse", + "description": "The results of the poll" + } + }, + "required": [ + "question", + "answers", + "expiry", + "allow_multiselect", + "layout_type", + "results" + ] + }, + "PollResultsEntryResponse": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "description": "The answer_id", + "format": "int32" + }, + "count": { + "type": "integer", + "description": "The number of votes for this answer", + "format": "int32" + }, + "me_voted": { + "type": "boolean", + "description": "Whether the current user voted for this answer" + } + }, + "required": [ + "id", + "count", + "me_voted" + ] + }, + "PollResultsResponse": { + "type": "object", + "properties": { + "answer_counts": { + "type": "array", + "description": "The counts for each answer", + "items": { + "$ref": "#/components/schemas/PollResultsEntryResponse" + } + }, + "is_finalized": { + "type": "boolean", + "description": "Whether the votes have been precisely counted" + } + }, + "required": [ + "answer_counts", + "is_finalized" + ] + }, + "PongInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + } + }, + "required": [ + "type" + ] + }, + "PremiumGuildTiers": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "description": "Guild has not unlocked any Server Boost perks", + "const": 0 + }, + { + "title": "TIER_1", + "description": "Guild has unlocked Server Boost level 1 perks", + "const": 1 + }, + { + "title": "TIER_2", + "description": "Guild has unlocked Server Boost level 2 perks", + "const": 2 + }, + { + "title": "TIER_3", + "description": "Guild has unlocked Server Boost level 3 perks", + "const": 3 + } + ], + "format": "int32" + }, + "PremiumTypes": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "description": "None", + "const": 0 + }, + { + "title": "TIER_1", + "description": "Nitro Classic", + "const": 1 + }, + { + "title": "TIER_2", + "description": "Nitro Standard", + "const": 2 + }, + { + "title": "TIER_0", + "description": "Nitro Basic", + "const": 3 + } + ], + "format": "int32" + }, + "PrivateApplicationResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "description": { + "type": "string" + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ApplicationTypes" + } + ] + }, + "cover_image": { + "type": "string" + }, + "primary_sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "bot": { + "$ref": "#/components/schemas/UserResponse" + }, + "slug": { + "type": "string" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "rpc_origins": { + "type": "array", + "items": { + "type": "string" + } + }, + "bot_public": { + "type": "boolean" + }, + "bot_require_code_grant": { + "type": "boolean" + }, + "terms_of_service_url": { + "type": "string", + "format": "uri" + }, + "privacy_policy_url": { + "type": "string", + "format": "uri" + }, + "custom_install_url": { + "type": "string", + "format": "uri" + }, + "install_params": { + "$ref": "#/components/schemas/ApplicationOAuth2InstallParamsResponse" + }, + "integration_types_config": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ApplicationIntegrationTypeConfigurationResponse" + } + }, + "verify_key": { + "type": "string" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "flags_new": { + "type": "string" + }, + "max_participants": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "redirect_uris": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + } + }, + "interactions_endpoint_url": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "role_connections_verification_url": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "owner": { + "$ref": "#/components/schemas/UserResponse" + }, + "approximate_guild_count": { + "type": "integer", + "format": "int32" + }, + "approximate_user_install_count": { + "type": "integer", + "format": "int32" + }, + "approximate_user_authorization_count": { + "type": "integer", + "format": "int32" + }, + "event_webhooks_url": { + "type": [ + "string", + "null" + ], + "format": "uri" + }, + "event_webhooks_status": { + "$ref": "#/components/schemas/ApplicationEventWebhooksStatus" + }, + "event_webhooks_types": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "APPLICATION_AUTHORIZED", + "APPLICATION_DEAUTHORIZED", + "ENTITLEMENT_CREATE", + "ENTITLEMENT_DELETE", + "ENTITLEMENT_UPDATE", + "GAME_DIRECT_MESSAGE_CREATE", + "GAME_DIRECT_MESSAGE_DELETE", + "GAME_DIRECT_MESSAGE_UPDATE", + "LOBBY_MESSAGE_CREATE", + "LOBBY_MESSAGE_DELETE", + "LOBBY_MESSAGE_UPDATE", + "QUEST_USER_ENROLLMENT" + ], + "allOf": [ + { + "$ref": "#/components/schemas/ActionTypes" + } + ] + }, + "uniqueItems": true + }, + "explicit_content_filter": { + "$ref": "#/components/schemas/ApplicationExplicitContentFilterTypes" + }, + "team": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/TeamResponse" + } + ] + } + }, + "required": [ + "id", + "name", + "icon", + "description", + "type", + "verify_key", + "flags", + "flags_new", + "redirect_uris", + "interactions_endpoint_url", + "role_connections_verification_url", + "owner", + "approximate_guild_count", + "approximate_user_install_count", + "approximate_user_authorization_count", + "explicit_content_filter", + "team" + ] + }, + "PrivateChannelLocation": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "kind": { + "type": "string", + "enum": [ + "pc" + ], + "allOf": [ + { + "$ref": "#/components/schemas/EmbeddedActivityLocationKind" + } + ] + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "kind", + "channel_id" + ] + }, + "PrivateChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "last_message_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "last_pin_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "recipients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + } + }, + "required": [ + "id", + "type", + "flags", + "recipients" + ] + }, + "PrivateGroupChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "last_message_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "last_pin_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "recipients": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "managed": { + "type": "boolean" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "type", + "flags", + "recipients", + "name", + "icon", + "owner_id" + ] + }, + "PrivateGuildMemberResponse": { + "type": "object", + "properties": { + "avatar": { + "type": [ + "string", + "null" + ], + "description": "the member's guild avatar hash" + }, + "avatar_decoration_data": { + "description": "data for the member's guild avatar decoration", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserAvatarDecorationResponse" + } + ] + }, + "banner": { + "type": [ + "string", + "null" + ], + "description": "the member's guild banner hash" + }, + "communication_disabled_until": { + "type": [ + "string", + "null" + ], + "description": "when the user's timeout will expire and the user will be able to communicate in the guild again, null or a time in the past if the user is not timed out", + "format": "date-time" + }, + "flags": { + "type": "integer", + "description": "guild member flags represented as a bit set, defaults to 0", + "format": "int32" + }, + "joined_at": { + "type": "string", + "description": "when the user joined the guild", + "format": "date-time" + }, + "nick": { + "type": [ + "string", + "null" + ], + "description": "this user's guild nickname" + }, + "pending": { + "type": "boolean", + "description": "whether the user has not yet passed the guild's Membership Screening requirements" + }, + "premium_since": { + "type": [ + "string", + "null" + ], + "description": "when the user started boosting the guild", + "format": "date-time" + }, + "roles": { + "type": "array", + "description": "array of role object ids", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "collectibles": { + "description": "data for the member's collectibles", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserCollectiblesResponse" + } + ] + }, + "user": { + "$ref": "#/components/schemas/UserResponse", + "description": "the user this guild member represents" + }, + "mute": { + "type": "boolean", + "description": "whether the user is muted in voice channels" + }, + "deaf": { + "type": "boolean", + "description": "whether the user is deafened in voice channels" + }, + "permissions": { + "type": "string" + } + }, + "required": [ + "avatar", + "banner", + "communication_disabled_until", + "flags", + "joined_at", + "nick", + "pending", + "premium_since", + "roles", + "user", + "mute", + "deaf" + ] + }, + "ProvisionalTokenResponse": { + "type": "object", + "properties": { + "token_type": { + "type": "string" + }, + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer", + "format": "int32" + }, + "scope": { + "type": "string" + }, + "id_token": { + "type": "string" + }, + "refresh_token": { + "type": [ + "string", + "null" + ] + }, + "scopes": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "expires_at_s": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "token_type", + "access_token", + "expires_in", + "scope", + "id_token" + ] + }, + "PruneGuildRequest": { + "type": "object", + "properties": { + "days": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 30 + }, + "compute_prune_count": { + "type": [ + "boolean", + "null" + ] + }, + "include_roles": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 100, + "uniqueItems": true + }, + { + "type": "null" + } + ] + } + } + }, + "PurchaseNotificationResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/PurchaseType" + }, + "guild_product_purchase": { + "$ref": "#/components/schemas/GuildProductPurchaseResponse" + } + }, + "required": [ + "type" + ] + }, + "PurchaseType": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_PRODUCT", + "const": 0 + } + ], + "format": "int32" + }, + "QuarantineUserAction": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionMetadata" + } + ] + } + }, + "required": [ + "type" + ] + }, + "QuarantineUserActionMetadata": { + "type": "object", + "properties": {} + }, + "QuarantineUserActionMetadataResponse": { + "type": "object", + "properties": {} + }, + "QuarantineUserActionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/QuarantineUserActionMetadataResponse" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "RadioGroupComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 21 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RadioGroupOptionForRequest" + }, + "minItems": 2, + "maxItems": 10 + } + }, + "required": [ + "type", + "custom_id", + "options" + ] + }, + "RadioGroupOptionForRequest": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "default": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "label", + "value" + ] + }, + "ReactionTypes": { + "type": "integer", + "oneOf": [ + { + "title": "NORMAL", + "description": "Normal reaction type", + "const": 0 + }, + { + "title": "BURST", + "description": "Burst reaction type", + "const": 1 + } + ], + "format": "int32" + }, + "RecurrenceRule": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Starting time of the recurrence interval", + "format": "date-time" + }, + "end": { + "type": [ + "string", + "null" + ], + "description": "Ending time of the recurrence interval", + "format": "date-time" + }, + "frequency": { + "$ref": "#/components/schemas/RecurrenceRuleFrequencies", + "description": "How often the event occurs" + }, + "interval": { + "type": [ + "integer", + "null" + ], + "description": "The spacing between events, defined by frequency", + "format": "int32" + }, + "by_weekday": { + "type": [ + "array", + "null" + ], + "description": "Set of specific days within a week for the event to recur on", + "items": { + "$ref": "#/components/schemas/RecurrenceRuleWeekdays" + }, + "maxItems": 7, + "uniqueItems": true + }, + "by_n_weekday": { + "type": [ + "array", + "null" + ], + "description": "List of specific days within a specific week to recur on", + "items": { + "$ref": "#/components/schemas/ByNWeekday" + }, + "maxItems": 7 + }, + "by_month": { + "type": [ + "array", + "null" + ], + "description": "Set of specific months to recur on", + "items": { + "$ref": "#/components/schemas/RecurrenceRuleMonths" + }, + "maxItems": 12, + "uniqueItems": true + }, + "by_month_day": { + "type": [ + "array", + "null" + ], + "description": "Set of specific dates within a month to recur on", + "items": { + "type": "integer", + "minimum": -1, + "maximum": 31, + "format": "int32" + }, + "maxItems": 33, + "uniqueItems": true + }, + "by_year_day": { + "type": [ + "array", + "null" + ], + "description": "Set of days within a year to recur on (1-364)", + "items": { + "type": "integer", + "minimum": -1, + "maximum": 365, + "format": "int32" + }, + "maxItems": 367, + "uniqueItems": true + }, + "count": { + "type": [ + "integer", + "null" + ], + "description": "Total number of times the event is allowed to recur", + "format": "int32" + } + }, + "required": [ + "start", + "frequency" + ] + }, + "RecurrenceRuleFrequencies": { + "type": "integer", + "oneOf": [ + { + "title": "DAILY", + "const": 3 + }, + { + "title": "WEEKLY", + "const": 2 + }, + { + "title": "MONTHLY", + "const": 1 + }, + { + "title": "YEARLY", + "const": 0 + } + ], + "format": "int32" + }, + "RecurrenceRuleMonths": { + "type": "integer", + "oneOf": [ + { + "title": "JANUARY", + "const": 1 + }, + { + "title": "FEBRUARY", + "const": 2 + }, + { + "title": "MARCH", + "const": 3 + }, + { + "title": "APRIL", + "const": 4 + }, + { + "title": "MAY", + "const": 5 + }, + { + "title": "JUNE", + "const": 6 + }, + { + "title": "JULY", + "const": 7 + }, + { + "title": "AUGUST", + "const": 8 + }, + { + "title": "SEPTEMBER", + "const": 9 + }, + { + "title": "OCTOBER", + "const": 10 + }, + { + "title": "NOVEMBER", + "const": 11 + }, + { + "title": "DECEMBER", + "const": 12 + } + ], + "format": "int32" + }, + "RecurrenceRuleResponse": { + "type": "object", + "properties": { + "start": { + "type": "string", + "description": "Starting time of the recurrence interval", + "format": "date-time" + }, + "end": { + "type": [ + "string", + "null" + ], + "description": "Ending time of the recurrence interval", + "format": "date-time" + }, + "frequency": { + "$ref": "#/components/schemas/RecurrenceRuleFrequencies", + "description": "How often the event occurs" + }, + "interval": { + "type": "integer", + "description": "The spacing between events, defined by frequency", + "format": "int32" + }, + "by_weekday": { + "type": [ + "array", + "null" + ], + "description": "Set of specific days within a week for the event to recur on", + "items": { + "$ref": "#/components/schemas/RecurrenceRuleWeekdays" + }, + "uniqueItems": true + }, + "by_n_weekday": { + "type": [ + "array", + "null" + ], + "description": "List of specific days within a specific week to recur on", + "items": { + "$ref": "#/components/schemas/ByNWeekdayResponse" + } + }, + "by_month": { + "type": [ + "array", + "null" + ], + "description": "Set of specific months to recur on", + "items": { + "$ref": "#/components/schemas/RecurrenceRuleMonths" + }, + "uniqueItems": true + }, + "by_month_day": { + "type": [ + "array", + "null" + ], + "description": "Set of specific dates within a month to recur on", + "items": { + "type": "integer", + "format": "int32" + }, + "uniqueItems": true + }, + "by_year_day": { + "type": [ + "array", + "null" + ], + "description": "Set of days within a year to recur on (1-364)", + "items": { + "type": "integer", + "format": "int32" + }, + "uniqueItems": true + }, + "count": { + "type": [ + "integer", + "null" + ], + "description": "Total number of times the event is allowed to recur", + "format": "int32" + } + }, + "required": [ + "start", + "frequency", + "interval", + "by_weekday", + "by_n_weekday", + "by_month", + "by_month_day" + ] + }, + "RecurrenceRuleWeekdays": { + "type": "integer", + "oneOf": [ + { + "title": "MONDAY", + "const": 0 + }, + { + "title": "TUESDAY", + "const": 1 + }, + { + "title": "WEDNESDAY", + "const": 2 + }, + { + "title": "THURSDAY", + "const": 3 + }, + { + "title": "FRIDAY", + "const": 4 + }, + { + "title": "SATURDAY", + "const": 5 + }, + { + "title": "SUNDAY", + "const": 6 + } + ], + "format": "int32" + }, + "ResolvedObjectsResponse": { + "type": "object", + "properties": { + "users": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "members": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/BasicGuildMemberResponse" + } + }, + "channels": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateChannelResponse" + }, + { + "$ref": "#/components/schemas/PrivateGroupChannelResponse" + }, + { + "$ref": "#/components/schemas/ThreadResponse" + } + ] + } + }, + "roles": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "$ref": "#/components/schemas/GuildRoleResponse" + } + } + } + }, + "ResourceChannelResponse": { + "type": "object", + "properties": { + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "title": { + "type": "string" + }, + "emoji": { + "$ref": "#/components/schemas/SettingsEmojiResponse" + }, + "icon": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "channel_id", + "title", + "description" + ] + }, + "RichEmbed": { + "type": "object", + "properties": { + "type": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "title": { + "type": [ + "string", + "null" + ], + "maxLength": 256 + }, + "color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 4096 + }, + "author": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedAuthor" + } + ] + }, + "image": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedImage" + } + ] + }, + "thumbnail": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedThumbnail" + } + ] + }, + "footer": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedFooter" + } + ] + }, + "fields": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbedField" + }, + "maxItems": 25 + }, + "provider": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedProvider" + } + ] + }, + "video": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RichEmbedVideo" + } + ] + } + } + }, + "RichEmbedAuthor": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "maxLength": 256 + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "icon_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + } + } + }, + "RichEmbedField": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 256 + }, + "value": { + "type": "string", + "maxLength": 1024 + }, + "inline": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "name", + "value" + ] + }, + "RichEmbedFooter": { + "type": "object", + "properties": { + "text": { + "type": [ + "string", + "null" + ], + "maxLength": 2048 + }, + "icon_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + } + } + }, + "RichEmbedImage": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "width": { + "type": [ + "integer", + "null" + ] + }, + "height": { + "type": [ + "integer", + "null" + ] + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 64 + }, + "placeholder_version": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 2147483647 + }, + "is_animated": { + "type": [ + "boolean", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 4096 + } + } + }, + "RichEmbedProvider": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "maxLength": 256 + }, + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + } + } + }, + "RichEmbedThumbnail": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "width": { + "type": [ + "integer", + "null" + ] + }, + "height": { + "type": [ + "integer", + "null" + ] + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 64 + }, + "placeholder_version": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 2147483647 + }, + "is_animated": { + "type": [ + "boolean", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 4096 + } + } + }, + "RichEmbedVideo": { + "type": "object", + "properties": { + "url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "width": { + "type": [ + "integer", + "null" + ] + }, + "height": { + "type": [ + "integer", + "null" + ] + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 64 + }, + "placeholder_version": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 2147483647 + }, + "is_animated": { + "type": [ + "boolean", + "null" + ] + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 4096 + } + } + }, + "RoleColors": { + "type": "object", + "properties": { + "primary_color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "secondary_color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "tertiary_color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + } + } + }, + "RoleSelectComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RoleSelectDefaultValue" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "RoleSelectComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RoleSelectDefaultValue" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "RoleSelectComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "min_values": { + "type": "integer", + "format": "int32" + }, + "max_values": { + "type": "integer", + "format": "int32" + }, + "disabled": { + "type": "boolean" + }, + "default_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleSelectDefaultValueResponse" + } + } + }, + "required": [ + "type", + "id", + "custom_id", + "min_values", + "max_values" + ] + }, + "RoleSelectDefaultValue": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "role" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "RoleSelectDefaultValueResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "role" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "SDKMessageRequest": { + "type": "object", + "properties": { + "content": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "embeds": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/RichEmbed" + }, + "maxItems": 10 + }, + "allowed_mentions": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageAllowedMentionsRequest" + } + ] + }, + "sticker_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 3 + }, + "components": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ContainerComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/FileComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SectionComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/SeparatorComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + } + ] + }, + "maxItems": 40 + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/MessageAttachmentRequest" + }, + "maxItems": 10 + }, + "poll": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/PollCreateRequest" + } + ] + }, + "shared_client_theme": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/CustomClientThemeShareRequest" + } + ] + }, + "message_reference": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageReferenceRequest" + } + ] + }, + "nonce": { + "oneOf": [ + { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "format": "int64" + }, + { + "type": "string", + "maxLength": 25, + "format": "nonce" + }, + { + "type": "null" + } + ] + }, + "enforce_nonce": { + "type": [ + "boolean", + "null" + ] + }, + "tts": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "SKUIneligibilityReason": { + "type": "integer", + "oneOf": [ + { + "title": "OTHER", + "description": "Other / catch-all", + "const": 0 + }, + { + "title": "OWNS_SKU_OR_BUNDLE_COMPONENT", + "description": "User already owns this SKU or one of its components", + "const": 1 + }, + { + "title": "PLATFORM_RESTRICTION", + "description": "User account is not on an eligible platform", + "const": 2 + } + ], + "format": "int32" + }, + "ScheduledEventResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the guild the scheduled event belongs to" + }, + "name": { + "type": "string", + "description": "Name of the scheduled event" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the scheduled event" + }, + "channel_id": { + "description": "Channel ID in which the scheduled event will be hosted, or null if entity type is EXTERNAL", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator_id": { + "description": "ID of the user that created the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator": { + "$ref": "#/components/schemas/UserResponse", + "description": "User that created the scheduled event" + }, + "image": { + "type": [ + "string", + "null" + ], + "description": "Cover image hash of the scheduled event" + }, + "scheduled_start_time": { + "type": "string", + "description": "When the scheduled event will start", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "When the scheduled event will end, or null if no end time", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/GuildScheduledEventStatuses", + "description": "Status of the scheduled event" + }, + "entity_type": { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes", + "description": "Type of hosting entity associated with the scheduled event" + }, + "entity_id": { + "description": "ID of the hosting entity associated with the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event, or null if not recurring", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRuleResponse" + } + ] + }, + "user_count": { + "type": "integer", + "description": "Number of users subscribed to the scheduled event", + "format": "int32" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels", + "description": "Privacy level of the scheduled event" + }, + "user_rsvp": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + ] + }, + "guild_scheduled_event_exceptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + } + }, + "required": [ + "id", + "guild_id", + "name", + "description", + "channel_id", + "creator_id", + "image", + "scheduled_start_time", + "scheduled_end_time", + "status", + "entity_type", + "entity_id", + "recurrence_rule", + "privacy_level", + "guild_scheduled_event_exceptions" + ] + }, + "ScheduledEventUserCountResponse": { + "type": "object", + "properties": { + "guild_scheduled_event_count": { + "type": "integer", + "description": "The number of users subscribed to the scheduled event", + "format": "int32" + }, + "guild_scheduled_event_exception_counts": { + "type": "object", + "description": "Map of exception IDs to user counts for each exception", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + }, + "required": [ + "guild_scheduled_event_count", + "guild_scheduled_event_exception_counts" + ] + }, + "ScheduledEventUserResponse": { + "type": "object", + "properties": { + "guild_scheduled_event_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event" + }, + "guild_scheduled_event_exception_id": { + "description": "ID of the scheduled event exception", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the user" + }, + "user": { + "$ref": "#/components/schemas/UserResponse", + "description": "User object for the RSVP user" + }, + "member": { + "$ref": "#/components/schemas/GuildMemberResponse", + "description": "Guild member object for the RSVP user" + }, + "response": { + "$ref": "#/components/schemas/GuildScheduledEventUserResponses", + "description": "User's RSVP status for the event" + } + }, + "required": [ + "guild_scheduled_event_id", + "user_id", + "response" + ] + }, + "SearchIndexNotReadyResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "integer", + "format": "int32" + }, + "documents_indexed": { + "type": "integer", + "format": "int32" + }, + "retry_after": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "message", + "code", + "documents_indexed", + "retry_after" + ] + }, + "SearchMessageResponse": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/MessageType" + }, + "content": { + "type": "string" + }, + "mentions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "mention_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageAttachmentResponse" + } + }, + "embeds": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageEmbedResponse" + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "edited_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ActionRowComponentResponse" + }, + { + "$ref": "#/components/schemas/ContainerComponentResponse" + }, + { + "$ref": "#/components/schemas/FileComponentResponse" + }, + { + "$ref": "#/components/schemas/MediaGalleryComponentResponse" + }, + { + "$ref": "#/components/schemas/SectionComponentResponse" + }, + { + "$ref": "#/components/schemas/SeparatorComponentResponse" + }, + { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + ] + } + }, + "stickers": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/GuildStickerResponse" + }, + { + "$ref": "#/components/schemas/StandardStickerResponse" + } + ] + } + }, + "sticker_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageStickerItemResponse" + } + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "author": { + "$ref": "#/components/schemas/UserResponse" + }, + "pinned": { + "type": "boolean" + }, + "mention_everyone": { + "type": "boolean" + }, + "tts": { + "type": "boolean" + }, + "call": { + "$ref": "#/components/schemas/MessageCallResponse" + }, + "activity": { + "$ref": "#/components/schemas/MessageActivityResponse" + }, + "application": { + "$ref": "#/components/schemas/BasicApplicationResponseWithBot" + }, + "application_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "interaction": { + "$ref": "#/components/schemas/MessageInteractionResponse" + }, + "nonce": { + "oneOf": [ + { + "type": "integer", + "minimum": -9223372036854775808, + "maximum": 9223372036854775807, + "format": "int64" + }, + { + "type": "string", + "maxLength": 25, + "format": "nonce" + } + ] + }, + "webhook_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "message_reference": { + "$ref": "#/components/schemas/MessageReferenceResponse" + }, + "thread": { + "$ref": "#/components/schemas/ThreadResponse" + }, + "mention_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageMentionChannelResponse" + } + }, + "role_subscription_data": { + "$ref": "#/components/schemas/MessageRoleSubscriptionDataResponse" + }, + "purchase_notification": { + "$ref": "#/components/schemas/PurchaseNotificationResponse" + }, + "position": { + "type": "integer", + "format": "int32" + }, + "resolved": { + "$ref": "#/components/schemas/ResolvedObjectsResponse" + }, + "poll": { + "$ref": "#/components/schemas/PollResponse" + }, + "shared_client_theme": { + "$ref": "#/components/schemas/CustomClientThemeResponse" + }, + "interaction_metadata": { + "oneOf": [ + { + "$ref": "#/components/schemas/ApplicationCommandInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/MessageComponentInteractionMetadataResponse" + }, + { + "$ref": "#/components/schemas/ModalSubmitInteractionMetadataResponse" + } + ] + }, + "message_snapshots": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageSnapshotResponse" + } + }, + "reactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageReactionResponse" + } + }, + "referenced_message": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/BasicMessageResponse" + } + ] + }, + "hit": { + "type": "boolean" + } + }, + "required": [ + "type", + "content", + "mentions", + "mention_roles", + "attachments", + "embeds", + "timestamp", + "edited_timestamp", + "flags", + "components", + "id", + "channel_id", + "author", + "pinned", + "mention_everyone", + "tts", + "hit" + ] + }, + "SearchableEmbedType": { + "type": "string", + "oneOf": [ + { + "title": "IMAGE", + "const": "image" + }, + { + "title": "VIDEO", + "const": "video" + }, + { + "title": "GIFV", + "const": "gif" + }, + { + "title": "SOUND", + "const": "sound" + }, + { + "title": "ARTICLE", + "const": "article" + } + ] + }, + "SectionComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 9 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TextDisplayComponentForMessageRequest" + }, + "minItems": 1, + "maxItems": 3 + }, + "accessory": { + "oneOf": [ + { + "$ref": "#/components/schemas/ButtonComponentForMessageRequest" + }, + { + "$ref": "#/components/schemas/ThumbnailComponentForMessageRequest" + } + ] + } + }, + "required": [ + "type", + "components", + "accessory" + ] + }, + "SectionComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 9 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "components": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TextDisplayComponentResponse" + } + }, + "accessory": { + "oneOf": [ + { + "$ref": "#/components/schemas/ButtonComponentResponse" + }, + { + "$ref": "#/components/schemas/ThumbnailComponentResponse" + } + ] + } + }, + "required": [ + "type", + "id", + "components", + "accessory" + ] + }, + "SeparatorComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 14 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "spacing": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/MessageComponentSeparatorSpacingSize" + } + ] + }, + "divider": { + "type": [ + "boolean", + "null" + ] + } + }, + "required": [ + "type" + ] + }, + "SeparatorComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 14 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "spacing": { + "$ref": "#/components/schemas/MessageComponentSeparatorSpacingSize" + }, + "divider": { + "type": "boolean" + } + }, + "required": [ + "type", + "id", + "spacing", + "divider" + ] + }, + "SettingsEmojiResponse": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "animated": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "animated" + ] + }, + "SlackWebhook": { + "type": "object", + "properties": { + "text": { + "type": [ + "string", + "null" + ], + "maxLength": 2000 + }, + "username": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "icon_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "attachments": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/WebhookSlackEmbed" + }, + "maxItems": 1521 + } + } + }, + "SnowflakeSelectDefaultValueTypes": { + "type": "string", + "oneOf": [ + { + "title": "USER", + "const": "user" + }, + { + "title": "ROLE", + "const": "role" + }, + { + "title": "CHANNEL", + "const": "channel" + } + ] + }, + "SnowflakeType": { + "type": "string", + "pattern": "^(0|[1-9][0-9]*)$", + "format": "snowflake" + }, + "SocialLayerSKUPurchaseEligibilityCallbackData": { + "type": "object", + "properties": { + "eligible": { + "type": "boolean" + }, + "ineligible_reason": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SKUIneligibilityReason" + } + ] + }, + "ineligible_reason_description": { + "type": [ + "string", + "null" + ], + "maxLength": 255 + } + }, + "required": [ + "eligible" + ] + }, + "SocialLayerSKUPurchaseEligibilityInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 13 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "data": { + "$ref": "#/components/schemas/SocialLayerSKUPurchaseEligibilityCallbackData" + } + }, + "required": [ + "type", + "data" + ] + }, + "SortingMode": { + "type": "string", + "oneOf": [ + { + "title": "RELEVANCE", + "const": "relevance" + }, + { + "title": "TIMESTAMP", + "const": "timestamp" + } + ] + }, + "SortingOrder": { + "type": "string", + "oneOf": [ + { + "title": "ASC", + "const": "asc" + }, + { + "title": "DESC", + "const": "desc" + } + ] + }, + "SoundboardCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "volume": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 1, + "format": "double" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 32 + }, + "sound": { + "type": "string", + "contentEncoding": "base64" + } + }, + "required": [ + "name", + "sound" + ] + }, + "SoundboardPatchRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 2, + "maxLength": 32 + }, + "volume": { + "type": [ + "number", + "null" + ], + "minimum": 0, + "maximum": 1, + "format": "double" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 32 + } + } + }, + "SoundboardSoundResponse": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sound_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "volume": { + "type": "number", + "format": "double" + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ] + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "available": { + "type": "boolean" + }, + "user": { + "$ref": "#/components/schemas/UserResponse" + } + }, + "required": [ + "name", + "sound_id", + "volume", + "emoji_id", + "emoji_name", + "available" + ] + }, + "SoundboardSoundSendRequest": { + "type": "object", + "properties": { + "sound_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "source_guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "sound_id" + ] + }, + "StageInstanceResponse": { + "type": "object", + "properties": { + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "topic": { + "type": "string" + }, + "privacy_level": { + "$ref": "#/components/schemas/StageInstancesPrivacyLevels" + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "discoverable_disabled": { + "type": "boolean" + }, + "guild_scheduled_event_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "guild_id", + "channel_id", + "topic", + "privacy_level", + "id", + "discoverable_disabled", + "guild_scheduled_event_id" + ] + }, + "StageInstancesPrivacyLevels": { + "type": "integer", + "oneOf": [ + { + "title": "PUBLIC", + "description": "The Stage instance is visible publicly. (deprecated)", + "const": 1 + }, + { + "title": "GUILD_ONLY", + "description": "The Stage instance is visible to only guild members.", + "const": 2 + } + ], + "format": "int32" + }, + "StageScheduledEventCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "entity_type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataStageInstance" + } + ] + } + }, + "required": [ + "name", + "scheduled_start_time", + "privacy_level", + "entity_type" + ] + }, + "StageScheduledEventPatchRequestPartial": { + "type": "object", + "properties": { + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildScheduledEventStatuses" + } + ] + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "entity_type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + } + ] + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataStageInstance" + } + ] + } + } + }, + "StageScheduledEventResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the guild the scheduled event belongs to" + }, + "name": { + "type": "string", + "description": "Name of the scheduled event" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the scheduled event" + }, + "channel_id": { + "description": "Channel ID in which the scheduled event will be hosted, or null if entity type is EXTERNAL", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator_id": { + "description": "ID of the user that created the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator": { + "$ref": "#/components/schemas/UserResponse", + "description": "User that created the scheduled event" + }, + "image": { + "type": [ + "string", + "null" + ], + "description": "Cover image hash of the scheduled event" + }, + "scheduled_start_time": { + "type": "string", + "description": "When the scheduled event will start", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "When the scheduled event will end, or null if no end time", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/GuildScheduledEventStatuses", + "description": "Status of the scheduled event" + }, + "entity_type": { + "type": "integer", + "description": "Type of hosting entity associated with the scheduled event", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "entity_id": { + "description": "ID of the hosting entity associated with the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event, or null if not recurring", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRuleResponse" + } + ] + }, + "user_count": { + "type": "integer", + "description": "Number of users subscribed to the scheduled event", + "format": "int32" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels", + "description": "Privacy level of the scheduled event" + }, + "user_rsvp": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + ] + }, + "guild_scheduled_event_exceptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataStageInstanceResponse" + } + ] + } + }, + "required": [ + "id", + "guild_id", + "name", + "description", + "channel_id", + "creator_id", + "image", + "scheduled_start_time", + "scheduled_end_time", + "status", + "entity_type", + "entity_id", + "recurrence_rule", + "privacy_level", + "guild_scheduled_event_exceptions", + "entity_metadata" + ] + }, + "StandardStickerResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "tags": { + "type": "string" + }, + "type": { + "type": "integer", + "enum": [ + 1 + ], + "allOf": [ + { + "$ref": "#/components/schemas/StickerTypes" + } + ], + "format": "int32" + }, + "format_type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/StickerFormatTypes" + } + ] + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "pack_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "sort_value": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "tags", + "type", + "format_type", + "description", + "pack_id", + "sort_value" + ] + }, + "StickerFormatTypes": { + "type": "integer", + "oneOf": [ + { + "title": "PNG", + "const": 1 + }, + { + "title": "APNG", + "const": 2 + }, + { + "title": "LOTTIE", + "const": 3 + }, + { + "title": "GIF", + "const": 4 + } + ], + "format": "int32" + }, + "StickerPackCollectionResponse": { + "type": "object", + "properties": { + "sticker_packs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StickerPackResponse" + } + } + }, + "required": [ + "sticker_packs" + ] + }, + "StickerPackResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "sku_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "stickers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StandardStickerResponse" + } + }, + "cover_sticker_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "banner_asset_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "sku_id", + "name", + "description", + "stickers" + ] + }, + "StickerTypes": { + "type": "integer", + "oneOf": [ + { + "title": "STANDARD", + "description": "an official sticker in a pack, part of Nitro or in a removed purchasable pack", + "const": 1 + }, + { + "title": "GUILD", + "description": "a sticker uploaded to a guild for the guild's members", + "const": 2 + } + ], + "format": "int32" + }, + "StringSelectComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StringSelectOptionForRequest" + }, + "minItems": 1, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id", + "options" + ] + }, + "StringSelectComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StringSelectOptionForRequest" + }, + "minItems": 1, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id", + "options" + ] + }, + "StringSelectComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "min_values": { + "type": "integer", + "format": "int32" + }, + "max_values": { + "type": "integer", + "format": "int32" + }, + "disabled": { + "type": "boolean" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StringSelectOptionResponse" + } + } + }, + "required": [ + "type", + "id", + "custom_id", + "min_values", + "max_values", + "options" + ] + }, + "StringSelectOptionForRequest": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "value": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "default": { + "type": [ + "boolean", + "null" + ] + }, + "emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ComponentEmojiForRequest" + } + ] + } + }, + "required": [ + "label", + "value" + ] + }, + "StringSelectOptionResponse": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + }, + "description": { + "type": "string" + }, + "emoji": { + "$ref": "#/components/schemas/ComponentEmojiResponse" + }, + "default": { + "type": "boolean" + } + }, + "required": [ + "label", + "value" + ] + }, + "SubscriptionResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the subscription" + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the user who is subscribed" + }, + "sku_ids": { + "type": "array", + "description": "List of SKUs subscribed to", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "renewal_sku_ids": { + "type": [ + "array", + "null" + ], + "description": "List of SKUs that this user will be subscribed to at renewal", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "entitlement_ids": { + "type": "array", + "description": "List of entitlements granted for this subscription", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "current_period_start": { + "type": "string", + "description": "Start of the current subscription period", + "format": "date-time" + }, + "current_period_end": { + "type": "string", + "description": "End of the current subscription period", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/SubscriptionResponseStatusType", + "description": "Current status of the subscription" + }, + "canceled_at": { + "type": [ + "string", + "null" + ], + "description": "When the subscription was canceled", + "format": "date-time" + }, + "country": { + "type": [ + "string", + "null" + ], + "description": "ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription" + } + }, + "required": [ + "id", + "user_id", + "sku_ids", + "renewal_sku_ids", + "entitlement_ids", + "current_period_start", + "current_period_end", + "status", + "canceled_at" + ] + }, + "SubscriptionResponseStatusType": { + "type": "integer", + "oneOf": [ + { + "title": "ACTIVE", + "description": "Subscription is active and scheduled to renew", + "const": 0 + }, + { + "title": "INACTIVE", + "description": "Subscription is inactive and not being charged", + "const": 1 + }, + { + "title": "ENDING", + "description": "Subscription is active but will not renew", + "const": 2 + } + ], + "format": "int32" + }, + "TargetUsersJobStatusResponse": { + "type": "object", + "properties": { + "status": { + "$ref": "#/components/schemas/TargetUsersJobStatusTypes", + "description": "The status of the job processing the target users." + }, + "total_users": { + "$ref": "#/components/schemas/UInt32Type", + "description": "The total number of users in the provided list." + }, + "processed_users": { + "$ref": "#/components/schemas/UInt32Type", + "description": "The number of users processed so far." + }, + "created_at": { + "type": [ + "string", + "null" + ], + "description": "The timestamp when the job was created.", + "format": "date-time" + }, + "completed_at": { + "type": [ + "string", + "null" + ], + "description": "The timestamp when the job was successfully completed.", + "format": "date-time" + }, + "error_message": { + "type": [ + "string", + "null" + ], + "description": "The error message if the job failed." + } + }, + "required": [ + "status", + "total_users", + "processed_users", + "created_at", + "completed_at", + "error_message" + ] + }, + "TargetUsersJobStatusTypes": { + "type": "integer", + "oneOf": [ + { + "title": "UNSPECIFIED", + "description": "The default value.", + "const": 0 + }, + { + "title": "PROCESSING", + "description": "The job is still being processed.", + "const": 1 + }, + { + "title": "COMPLETED", + "description": "The job has been completed successfully.", + "const": 2 + }, + { + "title": "FAILED", + "description": "The job has failed, see error_message field for more details.", + "const": 3 + } + ], + "format": "int32" + }, + "TeamMemberResponse": { + "type": "object", + "properties": { + "user": { + "$ref": "#/components/schemas/UserResponse" + }, + "team_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "membership_state": { + "$ref": "#/components/schemas/TeamMembershipStates" + }, + "role": { + "$ref": "#/components/schemas/TeamMemberRoles" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "user", + "team_id", + "membership_state", + "role", + "permissions" + ] + }, + "TeamMemberRoles": { + "type": "string", + "oneOf": [ + { + "title": "ADMIN", + "description": "Admins have similar access as owners, except they cannot take destructive actions on the team or team-owned apps.", + "const": "admin" + }, + { + "title": "DEVELOPER", + "description": "Developers can access information about team-owned apps, like the client secret or public key. They can also take limited actions on team-owned apps, like configuring interaction endpoints or resetting the bot token. Members with the Developer role cannot manage the team or its members, or take destructive actions on team-owned apps.", + "const": "developer" + }, + { + "title": "READ_ONLY", + "description": "Read-only members can access information about a team and any team-owned apps. Some examples include getting the IDs of applications and exporting payout records. Members can also invite bots associated with team-owned apps that are marked private.", + "const": "read_only" + } + ] + }, + "TeamMembershipStates": { + "type": "integer", + "oneOf": [ + { + "title": "INVITED", + "description": "User has been invited to the team.", + "const": 1 + }, + { + "title": "ACCEPTED", + "description": "User has accepted the team invitation.", + "const": 2 + } + ], + "format": "int32" + }, + "TeamResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + }, + "owner_user_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamMemberResponse" + } + } + }, + "required": [ + "id", + "icon", + "name", + "owner_user_id", + "members" + ] + }, + "TermsFormFieldResponse": { + "type": "object", + "properties": { + "field_type": { + "type": "string", + "description": "Type of form field", + "enum": [ + "TERMS" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildMemberVerificationFormFieldType" + } + ] + }, + "label": { + "type": "string", + "description": "Label shown above field" + }, + "description": { + "type": "string", + "description": "Optional helper text shown below label" + }, + "required": { + "type": "boolean", + "description": "Whether applicant must fill in field" + }, + "values": { + "type": "array", + "description": "Terms applicant must acknowledge", + "items": { + "type": "string" + } + }, + "response": { + "type": "boolean", + "description": "Whether applicant accepted terms" + } + }, + "required": [ + "field_type", + "values" + ] + }, + "TextDisplayComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 10 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + } + }, + "required": [ + "type", + "content" + ] + }, + "TextDisplayComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 10 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + } + }, + "required": [ + "type", + "content" + ] + }, + "TextDisplayComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 10 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "content": { + "type": "string" + } + }, + "required": [ + "type", + "id", + "content" + ] + }, + "TextInputComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "style": { + "$ref": "#/components/schemas/TextInputStyleTypes" + }, + "label": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 45 + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 4000 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "min_length": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 4000 + }, + "max_length": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 4000 + } + }, + "required": [ + "type", + "custom_id", + "style" + ] + }, + "TextInputComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 4 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "style": { + "$ref": "#/components/schemas/TextInputStyleTypes" + }, + "label": { + "type": [ + "string", + "null" + ] + }, + "value": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "min_length": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "max_length": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + }, + "required": [ + "type", + "id", + "custom_id", + "style", + "label", + "min_length", + "max_length" + ] + }, + "TextInputFormFieldResponse": { + "type": "object", + "properties": { + "field_type": { + "type": "string", + "description": "Type of form field", + "enum": [ + "TEXT_INPUT" + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildMemberVerificationFormFieldType" + } + ] + }, + "label": { + "type": "string", + "description": "Label shown above field" + }, + "description": { + "type": "string", + "description": "Optional helper text shown below label" + }, + "required": { + "type": "boolean", + "description": "Whether applicant must fill in field" + }, + "placeholder": { + "type": "string", + "description": "Placeholder text shown in empty input" + }, + "response": { + "type": "string", + "description": "Applicant's text response" + } + }, + "required": [ + "field_type" + ] + }, + "TextInputStyleTypes": { + "type": "integer", + "oneOf": [ + { + "title": "SHORT", + "description": "Single-line input", + "const": 1 + }, + { + "title": "PARAGRAPH", + "description": "Multi-line input", + "const": 2 + } + ], + "format": "int32" + }, + "ThreadAutoArchiveDuration": { + "type": "integer", + "oneOf": [ + { + "title": "ONE_HOUR", + "description": "One hour", + "const": 60 + }, + { + "title": "ONE_DAY", + "description": "One day", + "const": 1440 + }, + { + "title": "THREE_DAY", + "description": "Three days", + "const": 4320 + }, + { + "title": "SEVEN_DAY", + "description": "Seven days", + "const": 10080 + } + ], + "format": "int32" + }, + "ThreadMemberResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "join_timestamp": { + "type": "string", + "format": "date-time" + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "member": { + "$ref": "#/components/schemas/GuildMemberResponse" + } + }, + "required": [ + "id", + "user_id", + "join_timestamp", + "flags" + ] + }, + "ThreadMetadataResponse": { + "type": "object", + "properties": { + "archived": { + "type": "boolean" + }, + "archive_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "auto_archive_duration": { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + }, + "locked": { + "type": "boolean" + }, + "create_timestamp": { + "type": "string", + "format": "date-time" + }, + "invitable": { + "type": "boolean" + } + }, + "required": [ + "archived", + "archive_timestamp", + "auto_archive_duration", + "locked" + ] + }, + "ThreadResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "type": { + "type": "integer", + "enum": [ + 10, + 11, + 12 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + }, + "last_message_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "flags": { + "type": "integer", + "format": "int32" + }, + "last_pin_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "rate_limit_per_user": { + "type": "integer", + "format": "int32" + }, + "bitrate": { + "type": "integer", + "format": "int32" + }, + "user_limit": { + "type": "integer", + "format": "int32" + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "$ref": "#/components/schemas/VideoQualityModes" + }, + "permissions": { + "type": "string" + }, + "owner_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "thread_metadata": { + "$ref": "#/components/schemas/ThreadMetadataResponse" + }, + "message_count": { + "type": "integer", + "format": "int32" + }, + "member_count": { + "type": "integer", + "format": "int32" + }, + "total_message_sent": { + "type": "integer", + "format": "int32" + }, + "applied_tags": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "member": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + }, + "required": [ + "id", + "type", + "flags", + "guild_id", + "name", + "owner_id", + "thread_metadata", + "message_count", + "member_count", + "total_message_sent" + ] + }, + "ThreadSearchResponse": { + "type": "object", + "properties": { + "threads": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadResponse" + } + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + }, + "has_more": { + "type": "boolean" + }, + "first_messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "total_results": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "threads", + "members", + "has_more", + "total_results" + ] + }, + "ThreadSearchTagSetting": { + "type": "string", + "oneOf": [ + { + "title": "MATCH_ALL", + "description": "The thread tags must contain all tags in the search query", + "const": "match_all" + }, + { + "title": "MATCH_SOME", + "description": "The thread tags must contain at least one of tags in the search query", + "const": "match_some" + } + ] + }, + "ThreadSortOrder": { + "type": "integer", + "oneOf": [ + { + "title": "LATEST_ACTIVITY", + "description": "Sort forum posts by activity", + "const": 0 + }, + { + "title": "CREATION_DATE", + "description": "Sort forum posts by creation time (from most recent to oldest)", + "const": 1 + } + ], + "format": "int32" + }, + "ThreadSortingMode": { + "type": "string", + "oneOf": [ + { + "title": "RELEVANCE", + "const": "relevance" + }, + { + "title": "CREATION_TIME", + "const": "creation_time" + }, + { + "title": "LAST_MESSAGE_TIME", + "const": "last_message_time" + }, + { + "title": "ARCHIVE_TIME", + "const": "archive_time" + } + ] + }, + "ThreadsResponse": { + "type": "object", + "properties": { + "threads": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadResponse" + } + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ThreadMemberResponse" + } + }, + "has_more": { + "type": "boolean" + }, + "first_messages": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessageResponse" + } + } + }, + "required": [ + "threads", + "members", + "has_more" + ] + }, + "ThumbnailComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 11 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "description": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 1024 + }, + "spoiler": { + "type": [ + "boolean", + "null" + ] + }, + "media": { + "$ref": "#/components/schemas/UnfurledMediaRequest" + } + }, + "required": [ + "type", + "media" + ] + }, + "ThumbnailComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 11 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "media": { + "$ref": "#/components/schemas/UnfurledMediaResponse" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "spoiler": { + "type": "boolean" + } + }, + "required": [ + "type", + "id", + "media", + "description", + "spoiler" + ] + }, + "TypingIndicatorResponse": { + "type": "object", + "properties": {} + }, + "UInt32Type": { + "type": "integer", + "minimum": 0, + "maximum": 4294967295, + "format": "int64" + }, + "UnbanUserFromGuildRequest": { + "type": "object", + "properties": {} + }, + "UnfurledMediaRequest": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": [ + "url" + ] + }, + "UnfurledMediaRequestWithAttachmentReferenceRequired": { + "type": "object", + "properties": { + "url": { + "type": "string", + "maxLength": 2048, + "format": "uri" + } + }, + "required": [ + "url" + ] + }, + "UnfurledMediaResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "url": { + "type": "string" + }, + "proxy_url": { + "type": "string" + }, + "width": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "height": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "content_type": { + "type": [ + "string", + "null" + ] + }, + "attachment_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "url", + "proxy_url" + ] + }, + "UpdateApplicationUserRoleConnectionRequest": { + "type": "object", + "properties": { + "platform_name": { + "type": [ + "string", + "null" + ], + "maxLength": 50 + }, + "platform_username": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "metadata": { + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "maxProperties": 5 + } + } + }, + "UpdateDMRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 100 + } + } + }, + "UpdateDefaultReactionEmojiRequest": { + "type": "object", + "properties": { + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + } + }, + "UpdateGroupDMRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 100 + }, + "icon": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + } + } + }, + "UpdateGuildChannelRequestPartial": { + "type": "object", + "properties": { + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 0, + 2, + 4, + 5, + 13, + 14, + 15 + ], + "allOf": [ + { + "$ref": "#/components/schemas/ChannelTypes" + } + ], + "format": "int32" + } + ] + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "position": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "topic": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 4096 + }, + "bitrate": { + "type": [ + "integer", + "null" + ], + "minimum": 8000, + "format": "int32" + }, + "user_limit": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "nsfw": { + "type": [ + "boolean", + "null" + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600 + }, + "parent_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "permission_overwrites": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/ChannelPermissionOverwriteRequest" + }, + "maxItems": 100 + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VideoQualityModes" + } + ] + }, + "default_auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "default_reaction_emoji": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UpdateDefaultReactionEmojiRequest" + } + ] + }, + "default_thread_rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600 + }, + "default_sort_order": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSortOrder" + } + ] + }, + "default_forum_layout": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ForumLayout" + } + ] + }, + "default_tag_setting": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadSearchTagSetting" + } + ] + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "available_tags": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/UpdateThreadTagRequest" + }, + "maxItems": 20 + } + } + }, + "UpdateGuildOnboardingRequest": { + "type": "object", + "properties": { + "prompts": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/UpdateOnboardingPromptRequest" + }, + "maxItems": 15 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "default_channel_ids": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 500, + "uniqueItems": true + }, + "mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildOnboardingMode" + } + ] + } + } + }, + "UpdateMessageInteractionCallbackRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 6, + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "data": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/IncomingWebhookUpdateForInteractionCallbackRequestPartial" + } + ] + } + }, + "required": [ + "type" + ] + }, + "UpdateMessageInteractionCallbackResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 7 + ], + "allOf": [ + { + "$ref": "#/components/schemas/InteractionCallbackTypes" + } + ], + "format": "int32" + }, + "message": { + "$ref": "#/components/schemas/MessageResponse" + } + }, + "required": [ + "type", + "message" + ] + }, + "UpdateOnboardingPromptRequest": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "options": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OnboardingPromptOptionRequest" + }, + "minItems": 1, + "maxItems": 50 + }, + "single_select": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "in_onboarding": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/OnboardingPromptType" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "title", + "options", + "id" + ] + }, + "UpdateRolePositionsRequest": { + "type": "object", + "properties": { + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "position": { + "type": [ + "integer", + "null" + ], + "format": "int32" + } + } + }, + "UpdateRoleRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 1, + "maxLength": 100 + }, + "permissions": { + "type": [ + "integer", + "null" + ] + }, + "color": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 16777215 + }, + "colors": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RoleColors" + } + ] + }, + "hoist": { + "type": [ + "boolean", + "null" + ] + }, + "mentionable": { + "type": [ + "boolean", + "null" + ] + }, + "icon": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "unicode_emoji": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + } + } + }, + "UpdateSelfVoiceStateRequestPartial": { + "type": "object", + "properties": { + "request_to_speak_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "suppress": { + "type": [ + "boolean", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + } + }, + "UpdateThreadRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "minLength": 0, + "maxLength": 100 + }, + "archived": { + "type": [ + "boolean", + "null" + ] + }, + "locked": { + "type": [ + "boolean", + "null" + ] + }, + "invitable": { + "type": [ + "boolean", + "null" + ] + }, + "auto_archive_duration": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ThreadAutoArchiveDuration" + } + ] + }, + "rate_limit_per_user": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 21600, + "format": "int32" + }, + "flags": { + "type": [ + "integer", + "null" + ] + }, + "applied_tags": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 5 + }, + "bitrate": { + "type": [ + "integer", + "null" + ], + "minimum": 8000, + "format": "int32" + }, + "user_limit": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 99 + }, + "rtc_region": { + "type": [ + "string", + "null" + ] + }, + "video_quality_mode": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VideoQualityModes" + } + ] + } + } + }, + "UpdateThreadTagRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 0, + "maxLength": 50 + }, + "emoji_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "emoji_name": { + "type": [ + "string", + "null" + ], + "maxLength": 100 + }, + "moderated": { + "type": [ + "boolean", + "null" + ] + }, + "id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "name" + ] + }, + "UpdateVoiceStateRequestPartial": { + "type": "object", + "properties": { + "suppress": { + "type": [ + "boolean", + "null" + ] + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + } + }, + "UserAvatarDecorationResponse": { + "type": "object", + "properties": { + "asset": { + "type": "string", + "description": "the avatar decoration hash" + }, + "sku_id": { + "description": "id of the avatar decoration's SKU", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "asset", + "sku_id" + ] + }, + "UserCollectiblesResponse": { + "type": "object", + "properties": { + "nameplate": { + "description": "Object mapping of nameplate data", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserNameplateResponse" + } + ] + } + }, + "required": [ + "nameplate" + ] + }, + "UserCommunicationDisabledAction": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/UserCommunicationDisabledActionMetadata" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "UserCommunicationDisabledActionMetadata": { + "type": "object", + "properties": { + "duration_seconds": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 2419200 + } + } + }, + "UserCommunicationDisabledActionMetadataResponse": { + "type": "object", + "properties": { + "duration_seconds": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "duration_seconds" + ] + }, + "UserCommunicationDisabledActionResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 3 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodActionType" + } + ], + "format": "int32" + }, + "metadata": { + "$ref": "#/components/schemas/UserCommunicationDisabledActionMetadataResponse" + } + }, + "required": [ + "type", + "metadata" + ] + }, + "UserGuildOnboardingResponse": { + "type": "object", + "properties": { + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "prompts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/OnboardingPromptResponse" + } + }, + "default_channel_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "enabled": { + "type": "boolean" + }, + "mode": { + "$ref": "#/components/schemas/GuildOnboardingMode" + } + }, + "required": [ + "guild_id", + "prompts", + "default_channel_ids", + "enabled", + "mode" + ] + }, + "UserNameplateResponse": { + "type": "object", + "properties": { + "sku_id": { + "description": "ID of the nameplate SKU", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "asset": { + "type": "string", + "description": "Path to the nameplate asset" + }, + "label": { + "type": "string", + "description": "The label of this nameplate. Currently unused" + }, + "palette": { + "$ref": "#/components/schemas/NameplatePalette", + "description": "Background color of the nameplate" + } + }, + "required": [ + "sku_id", + "asset", + "label", + "palette" + ] + }, + "UserNotificationSettings": { + "type": "integer", + "oneOf": [ + { + "title": "ALL_MESSAGES", + "description": "members will receive notifications for all messages by default", + "const": 0 + }, + { + "title": "ONLY_MENTIONS", + "description": "members will receive notifications only for messages that @mention them by default", + "const": 1 + } + ], + "format": "int32" + }, + "UserPIIResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "the user's id" + }, + "username": { + "type": "string", + "description": "the user's username, not unique across the platform" + }, + "avatar": { + "type": [ + "string", + "null" + ], + "description": "the user's avatar hash" + }, + "discriminator": { + "type": "string", + "description": "the user's Discord-tag" + }, + "public_flags": { + "type": "integer", + "description": "the public flags on a user's account", + "format": "int32" + }, + "flags": { + "$ref": "#/components/schemas/Int53Type", + "description": "the flags on a user's account" + }, + "bot": { + "type": "boolean", + "description": "whether the user belongs to an OAuth2 application" + }, + "system": { + "type": "boolean", + "description": "whether the user is an Official Discord System user (part of the urgent message system)" + }, + "banner": { + "type": [ + "string", + "null" + ], + "description": "the user's banner hash" + }, + "accent_color": { + "type": [ + "integer", + "null" + ], + "description": "the user's banner color encoded as an integer representation of hexadecimal color code", + "format": "int32" + }, + "global_name": { + "type": [ + "string", + "null" + ], + "description": "the user's display name, if it is set" + }, + "avatar_decoration_data": { + "description": "data for the user's avatar decoration", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserAvatarDecorationResponse" + } + ] + }, + "collectibles": { + "description": "data for the user's collectibles", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserCollectiblesResponse" + } + ] + }, + "primary_guild": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserPrimaryGuildResponse" + } + ] + }, + "mfa_enabled": { + "type": "boolean" + }, + "locale": { + "$ref": "#/components/schemas/AvailableLocalesEnum" + }, + "premium_type": { + "$ref": "#/components/schemas/PremiumTypes" + }, + "email": { + "type": [ + "string", + "null" + ] + }, + "verified": { + "type": "boolean" + } + }, + "required": [ + "id", + "username", + "avatar", + "discriminator", + "public_flags", + "flags", + "global_name", + "mfa_enabled", + "locale" + ] + }, + "UserPrimaryGuildResponse": { + "type": "object", + "properties": { + "identity_guild_id": { + "description": "the id of the user's primary guild", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "identity_enabled": { + "type": [ + "boolean", + "null" + ], + "description": "whether the user is displaying the primary guild's server tag" + }, + "tag": { + "type": [ + "string", + "null" + ], + "description": "the text of the user's server tag, limited to 4 characters" + }, + "badge": { + "type": [ + "string", + "null" + ], + "description": "the server tag badge hash" + } + }, + "required": [ + "identity_guild_id", + "identity_enabled", + "tag", + "badge" + ] + }, + "UserProfileMetadata": { + "type": "object", + "properties": { + "keyword_filter": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "maxItems": 1000 + }, + "regex_patterns": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 260 + }, + "maxItems": 10 + }, + "allow_list": { + "type": [ + "array", + "null" + ], + "items": { + "type": "string", + "minLength": 1, + "maxLength": 60 + }, + "maxItems": 100 + } + } + }, + "UserProfileMetadataResponse": { + "type": "object", + "properties": { + "keyword_filter": { + "type": "array", + "items": { + "type": "string" + } + }, + "regex_patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "allow_list": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "keyword_filter", + "regex_patterns", + "allow_list" + ] + }, + "UserProfileRuleResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "creator_id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageActionResponse" + }, + { + "$ref": "#/components/schemas/FlagToChannelActionResponse" + }, + { + "$ref": "#/components/schemas/QuarantineUserActionResponse" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledActionResponse" + } + ] + } + }, + "trigger_type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "enabled": { + "type": "boolean" + }, + "exempt_roles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "exempt_channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "uniqueItems": true + }, + "trigger_metadata": { + "$ref": "#/components/schemas/UserProfileMetadataResponse" + } + }, + "required": [ + "id", + "guild_id", + "creator_id", + "name", + "event_type", + "actions", + "trigger_type", + "enabled", + "exempt_roles", + "exempt_channels", + "trigger_metadata" + ] + }, + "UserProfileUpsertRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "$ref": "#/components/schemas/UserProfileMetadata" + } + }, + "required": [ + "name", + "event_type", + "trigger_type", + "trigger_metadata" + ] + }, + "UserProfileUpsertRequestPartial": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "event_type": { + "$ref": "#/components/schemas/AutomodEventType" + }, + "actions": { + "type": [ + "array", + "null" + ], + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/BlockMessageAction" + }, + { + "$ref": "#/components/schemas/FlagToChannelAction" + }, + { + "$ref": "#/components/schemas/QuarantineUserAction" + }, + { + "$ref": "#/components/schemas/UserCommunicationDisabledAction" + } + ] + }, + "minItems": 1, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + }, + "exempt_roles": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 20, + "uniqueItems": true + }, + "exempt_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "maxItems": 50, + "uniqueItems": true + }, + "trigger_type": { + "type": "integer", + "enum": [ + 6 + ], + "allOf": [ + { + "$ref": "#/components/schemas/AutomodTriggerType" + } + ], + "format": "int32" + }, + "trigger_metadata": { + "$ref": "#/components/schemas/UserProfileMetadata" + } + } + }, + "UserResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "the user's id" + }, + "username": { + "type": "string", + "description": "the user's username, not unique across the platform" + }, + "avatar": { + "type": [ + "string", + "null" + ], + "description": "the user's avatar hash" + }, + "discriminator": { + "type": "string", + "description": "the user's Discord-tag" + }, + "public_flags": { + "type": "integer", + "description": "the public flags on a user's account", + "format": "int32" + }, + "flags": { + "$ref": "#/components/schemas/Int53Type", + "description": "the flags on a user's account" + }, + "bot": { + "type": "boolean", + "description": "whether the user belongs to an OAuth2 application" + }, + "system": { + "type": "boolean", + "description": "whether the user is an Official Discord System user (part of the urgent message system)" + }, + "banner": { + "type": [ + "string", + "null" + ], + "description": "the user's banner hash" + }, + "accent_color": { + "type": [ + "integer", + "null" + ], + "description": "the user's banner color encoded as an integer representation of hexadecimal color code", + "format": "int32" + }, + "global_name": { + "type": [ + "string", + "null" + ], + "description": "the user's display name, if it is set" + }, + "avatar_decoration_data": { + "description": "data for the user's avatar decoration", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserAvatarDecorationResponse" + } + ] + }, + "collectibles": { + "description": "data for the user's collectibles", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserCollectiblesResponse" + } + ] + }, + "primary_guild": { + "description": "the user's primary guild", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/UserPrimaryGuildResponse" + } + ] + } + }, + "required": [ + "id", + "username", + "avatar", + "discriminator", + "public_flags", + "flags", + "global_name", + "primary_guild" + ] + }, + "UserSelectComponentForMessageRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/UserSelectDefaultValue" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "UserSelectComponentForModalRequest": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "format": "int32" + }, + "custom_id": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "placeholder": { + "type": [ + "string", + "null" + ], + "maxLength": 150 + }, + "min_values": { + "type": [ + "integer", + "null" + ], + "minimum": 0, + "maximum": 25 + }, + "max_values": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "maximum": 25 + }, + "disabled": { + "type": [ + "boolean", + "null" + ] + }, + "required": { + "type": [ + "boolean", + "null" + ] + }, + "default_values": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/UserSelectDefaultValue" + }, + "maxItems": 25 + } + }, + "required": [ + "type", + "custom_id" + ] + }, + "UserSelectComponentResponse": { + "type": "object", + "properties": { + "type": { + "type": "integer", + "enum": [ + 5 + ], + "allOf": [ + { + "$ref": "#/components/schemas/MessageComponentTypes" + } + ], + "format": "int32" + }, + "id": { + "type": "integer", + "format": "int32" + }, + "custom_id": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "min_values": { + "type": "integer", + "format": "int32" + }, + "max_values": { + "type": "integer", + "format": "int32" + }, + "disabled": { + "type": "boolean" + }, + "default_values": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSelectDefaultValueResponse" + } + } + }, + "required": [ + "type", + "id", + "custom_id", + "min_values", + "max_values" + ] + }, + "UserSelectDefaultValue": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "user" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "UserSelectDefaultValueResponse": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "user" + ], + "allOf": [ + { + "$ref": "#/components/schemas/SnowflakeSelectDefaultValueTypes" + } + ] + }, + "id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "type", + "id" + ] + }, + "VanityURLErrorResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "code": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "message", + "code" + ] + }, + "VanityURLResponse": { + "type": "object", + "properties": { + "code": { + "type": [ + "string", + "null" + ] + }, + "uses": { + "type": "integer", + "format": "int32" + }, + "error": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/VanityURLErrorResponse" + } + ] + } + }, + "required": [ + "code", + "uses" + ] + }, + "VerificationLevels": { + "type": "integer", + "oneOf": [ + { + "title": "NONE", + "description": "unrestricted", + "const": 0 + }, + { + "title": "LOW", + "description": "must have verified email on account", + "const": 1 + }, + { + "title": "MEDIUM", + "description": "must be registered on Discord for longer than 5 minutes", + "const": 2 + }, + { + "title": "HIGH", + "description": "must be a member of the server for longer than 10 minutes", + "const": 3 + }, + { + "title": "VERY_HIGH", + "description": "must have a verified phone number", + "const": 4 + } + ], + "format": "int32" + }, + "VideoQualityModes": { + "type": "integer", + "oneOf": [ + { + "title": "AUTO", + "description": "Discord chooses the quality for optimal performance", + "const": 1 + }, + { + "title": "FULL", + "description": "720p", + "const": 2 + } + ], + "format": "int32" + }, + "VoiceRegionResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "custom": { + "type": "boolean" + }, + "deprecated": { + "type": "boolean" + }, + "optimal": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "custom", + "deprecated", + "optimal" + ] + }, + "VoiceScheduledEventCreateRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "entity_type": { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataVoice" + } + ] + } + }, + "required": [ + "name", + "scheduled_start_time", + "privacy_level", + "entity_type" + ] + }, + "VoiceScheduledEventPatchRequestPartial": { + "type": "object", + "properties": { + "status": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/GuildScheduledEventStatuses" + } + ] + }, + "name": { + "type": "string", + "maxLength": 100 + }, + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 1000 + }, + "image": { + "type": [ + "string", + "null" + ], + "contentEncoding": "base64" + }, + "scheduled_start_time": { + "type": "string", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "entity_type": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "integer", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + } + ] + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRule" + } + ] + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataVoice" + } + ] + } + } + }, + "VoiceScheduledEventResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the scheduled event" + }, + "guild_id": { + "$ref": "#/components/schemas/SnowflakeType", + "description": "ID of the guild the scheduled event belongs to" + }, + "name": { + "type": "string", + "description": "Name of the scheduled event" + }, + "description": { + "type": [ + "string", + "null" + ], + "description": "Description of the scheduled event" + }, + "channel_id": { + "description": "Channel ID in which the scheduled event will be hosted, or null if entity type is EXTERNAL", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator_id": { + "description": "ID of the user that created the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "creator": { + "$ref": "#/components/schemas/UserResponse", + "description": "User that created the scheduled event" + }, + "image": { + "type": [ + "string", + "null" + ], + "description": "Cover image hash of the scheduled event" + }, + "scheduled_start_time": { + "type": "string", + "description": "When the scheduled event will start", + "format": "date-time" + }, + "scheduled_end_time": { + "type": [ + "string", + "null" + ], + "description": "When the scheduled event will end, or null if no end time", + "format": "date-time" + }, + "status": { + "$ref": "#/components/schemas/GuildScheduledEventStatuses", + "description": "Status of the scheduled event" + }, + "entity_type": { + "type": "integer", + "description": "Type of hosting entity associated with the scheduled event", + "enum": [ + 2 + ], + "allOf": [ + { + "$ref": "#/components/schemas/GuildScheduledEventEntityTypes" + } + ], + "format": "int32" + }, + "entity_id": { + "description": "ID of the hosting entity associated with the scheduled event", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "recurrence_rule": { + "description": "Recurrence rule for the scheduled event, or null if not recurring", + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RecurrenceRuleResponse" + } + ] + }, + "user_count": { + "type": "integer", + "description": "Number of users subscribed to the scheduled event", + "format": "int32" + }, + "privacy_level": { + "$ref": "#/components/schemas/GuildScheduledEventPrivacyLevels", + "description": "Privacy level of the scheduled event" + }, + "user_rsvp": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/ScheduledEventUserResponse" + } + ] + }, + "guild_scheduled_event_exceptions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GuildScheduledEventExceptionResponse" + } + }, + "entity_metadata": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/EntityMetadataVoiceResponse" + } + ] + } + }, + "required": [ + "id", + "guild_id", + "name", + "description", + "channel_id", + "creator_id", + "image", + "scheduled_start_time", + "scheduled_end_time", + "status", + "entity_type", + "entity_id", + "recurrence_rule", + "privacy_level", + "guild_scheduled_event_exceptions", + "entity_metadata" + ] + }, + "VoiceStateResponse": { + "type": "object", + "properties": { + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "deaf": { + "type": "boolean" + }, + "guild_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + }, + "member": { + "$ref": "#/components/schemas/GuildMemberResponse" + }, + "mute": { + "type": "boolean" + }, + "request_to_speak_timestamp": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "suppress": { + "type": "boolean" + }, + "self_stream": { + "type": [ + "boolean", + "null" + ] + }, + "self_deaf": { + "type": "boolean" + }, + "self_mute": { + "type": "boolean" + }, + "self_video": { + "type": "boolean" + }, + "session_id": { + "type": "string" + }, + "user_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "channel_id", + "deaf", + "guild_id", + "mute", + "request_to_speak_timestamp", + "suppress", + "self_stream", + "self_deaf", + "self_mute", + "self_video", + "session_id", + "user_id" + ] + }, + "WebhookSlackEmbed": { + "type": "object", + "properties": { + "title": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "title_link": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "text": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "color": { + "type": [ + "string", + "null" + ], + "maxLength": 7, + "pattern": "^#(([0-9a-fA-F]{2}){3}|([0-9a-fA-F]){3})$" + }, + "ts": { + "type": [ + "integer", + "null" + ] + }, + "pretext": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "footer": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "footer_icon": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "author_name": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "author_link": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "author_icon": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "image_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "thumb_url": { + "type": [ + "string", + "null" + ], + "maxLength": 2048, + "format": "uri" + }, + "fields": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/WebhookSlackEmbedField" + }, + "maxItems": 1521 + } + } + }, + "WebhookSlackEmbedField": { + "type": "object", + "properties": { + "name": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "value": { + "type": [ + "string", + "null" + ], + "maxLength": 152133 + }, + "inline": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "WebhookSourceChannelResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] + }, + "WebhookSourceGuildResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "icon": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "icon", + "name" + ] + }, + "WebhookTypes": { + "type": "integer", + "oneOf": [ + { + "title": "GUILD_INCOMING", + "description": "Incoming Webhooks can post messages to channels with a generated token", + "const": 1 + }, + { + "title": "CHANNEL_FOLLOWER", + "description": "Channel Follower Webhooks are internal webhooks used with Channel Following to post new messages into channels", + "const": 2 + }, + { + "title": "APPLICATION_INCOMING", + "description": "Application webhooks are webhooks used with Interactions", + "const": 3 + } + ], + "format": "int32" + }, + "WelcomeMessageResponse": { + "type": "object", + "properties": { + "author_ids": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "message": { + "type": "string" + } + }, + "required": [ + "author_ids", + "message" + ] + }, + "WelcomeScreenPatchRequestPartial": { + "type": "object", + "properties": { + "description": { + "type": [ + "string", + "null" + ], + "maxLength": 140 + }, + "welcome_channels": { + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/components/schemas/GuildWelcomeChannel" + }, + "maxItems": 5 + }, + "enabled": { + "type": [ + "boolean", + "null" + ] + } + } + }, + "WidgetActivity": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + }, + "WidgetChannel": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "position": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "position" + ] + }, + "WidgetImageStyles": { + "type": "string", + "oneOf": [ + { + "title": "SHIELD", + "description": "shield style widget with Discord icon and guild members online count", + "const": "shield" + }, + { + "title": "BANNER1", + "description": "large image with guild icon, name and online count. \"POWERED BY DISCORD\" as the footer of the widget", + "const": "banner1" + }, + { + "title": "BANNER2", + "description": "smaller widget style with guild icon, name and online count. Split on the right with Discord logo", + "const": "banner2" + }, + { + "title": "BANNER3", + "description": "large image with guild icon, name and online count. In the footer, Discord logo on the left and \"Chat Now\" on the right", + "const": "banner3" + }, + { + "title": "BANNER4", + "description": "large Discord logo at the top of the widget. Guild icon, name and online count in the middle portion of the widget and a \"JOIN MY SERVER\" button at the bottom", + "const": "banner4" + } + ] + }, + "WidgetMember": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "discriminator": { + "$ref": "#/components/schemas/WidgetUserDiscriminator" + }, + "avatar": { + "type": "null" + }, + "status": { + "type": "string" + }, + "avatar_url": { + "type": "string", + "format": "uri" + }, + "activity": { + "$ref": "#/components/schemas/WidgetActivity" + }, + "deaf": { + "type": "boolean" + }, + "mute": { + "type": "boolean" + }, + "self_deaf": { + "type": "boolean" + }, + "self_mute": { + "type": "boolean" + }, + "suppress": { + "type": "boolean" + }, + "channel_id": { + "$ref": "#/components/schemas/SnowflakeType" + } + }, + "required": [ + "id", + "username", + "discriminator", + "avatar", + "status", + "avatar_url" + ] + }, + "WidgetResponse": { + "type": "object", + "properties": { + "id": { + "$ref": "#/components/schemas/SnowflakeType" + }, + "name": { + "type": "string" + }, + "instant_invite": { + "type": [ + "string", + "null" + ] + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WidgetChannel" + } + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WidgetMember" + } + }, + "presence_count": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "id", + "name", + "instant_invite", + "channels", + "members", + "presence_count" + ] + }, + "WidgetSettingsResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "channel_id": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/SnowflakeType" + } + ] + } + }, + "required": [ + "enabled", + "channel_id" + ] + }, + "WidgetUserDiscriminator": { + "type": "string", + "oneOf": [ + { + "title": "ZEROES", + "const": "0000" + } + ] + }, + "Error": { + "type": "object", + "description": "A single error, either for an API response or a specific field.", + "properties": { + "code": { + "type": "integer", + "description": "Discord internal error code. See error code reference" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + } + }, + "required": [ + "code", + "message" + ] + }, + "InnerErrors": { + "type": "object", + "properties": { + "_errors": { + "type": "array", + "description": "The list of errors for this field", + "items": { + "$ref": "#/components/schemas/Error" + } + } + }, + "additionalProperties": false, + "required": [ + "_errors" + ] + }, + "ErrorDetails": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/ErrorDetails" + } + }, + { + "$ref": "#/components/schemas/InnerErrors" + } + ] + }, + "ErrorResponse": { + "type": "object", + "description": "Errors object returned by the Discord API", + "allOf": [ + { + "$ref": "#/components/schemas/Error" + }, + { + "type": "object", + "properties": { + "errors": { + "$ref": "#/components/schemas/ErrorDetails" + } + } + } + ] + }, + "RatelimitedResponse": { + "type": "object", + "description": "Ratelimit error object returned by the Discord API", + "allOf": [ + { + "$ref": "#/components/schemas/Error" + }, + { + "type": "object", + "properties": { + "retry_after": { + "type": "number", + "description": "The number of seconds to wait before retrying your request" + }, + "global": { + "type": "boolean", + "description": "Whether you are being ratelimited by the global ratelimit or a per-endpoint ratelimit" + } + }, + "required": [ + "retry_after", + "global" + ] + } + ] + } + }, + "securitySchemes": { + "BotToken": { + "type": "apiKey", + "description": "Discord bot token", + "name": "Authorization", + "in": "header" + }, + "OAuth2": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://discord.com/api/oauth2/authorize", + "refreshUrl": "https://discord.com/api/oauth2/token", + "scopes": { + "activities.invites.write": "allows your app to send activity invites - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "activities.read": "allows your app to fetch data from a user's \"Now Playing/Recently Played\" list - requires Discord approval", + "activities.write": "allows your app to update a user's activity - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "applications.builds.read": "allows your app to read build data for a user's applications", + "applications.builds.upload": "allows your app to upload/update builds for a user's applications - requires Discord approval", + "applications.commands": "allows your app to use commands in a guild", + "applications.commands.permissions.update": "allows your app to update permissions for its commands in a guild a user has permissions to", + "applications.entitlements": "allows your app to read entitlements for a user's applications", + "applications.store.update": "allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications", + "bot": "for oauth2 bots, this puts the bot in the user's selected guild by default", + "connections": "allows /users/@me/connections to return linked third-party accounts", + "dm_channels.read": "allows your app to see information about the user's DMs and group DMs - requires Discord approval", + "email": "enables /users/@me to return an email", + "gdm.join": "allows your app to join users to a group dm", + "guilds": "allows /users/@me/guilds to return basic information about all of a user's guilds", + "guilds.join": "allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild", + "guilds.members.read": "allows /users/@me/guilds/{guild.id}/member to return a user's member information in a guild", + "identify": "allows /users/@me without email", + "messages.read": "for local rpc server api access, this allows you to read messages from all client channels (otherwise restricted to channels/guilds your app creates)", + "openid": "for OpenID Connect, this allows your app to receive user id and basic profile information", + "relationships.read": "allows your app to know a user's friends and implicit relationships - requires Discord approval", + "rpc": "for local rpc server access, this allows you to control a user's local Discord client - requires Discord approval", + "rpc.activities.write": "for local rpc server access, this allows you to update a user's activity - requires Discord approval", + "rpc.notifications.read": "for local rpc server access, this allows you to receive notifications pushed out to the user - requires Discord approval", + "rpc.screenshare.read": "for local rpc server access, this allows you to read a user's screenshare status- requires Discord approval", + "rpc.screenshare.write": "for local rpc server access, this allows you to update a user's screenshare settings- requires Discord approval", + "rpc.video.read": "for local rpc server access, this allows you to read a user's video status - requires Discord approval", + "rpc.video.write": "for local rpc server access, this allows you to update a user's video settings - requires Discord approval", + "rpc.voice.read": "for local rpc server access, this allows you to read a user's voice settings and listen for voice events - requires Discord approval", + "rpc.voice.write": "for local rpc server access, this allows you to update a user's voice settings - requires Discord approval", + "voice": "allows your app to connect to voice on user's behalf and see all the voice members - requires Discord approval", + "webhook.incoming": "this generates a webhook that is returned in the oauth token response for authorization code grants" + } + }, + "clientCredentials": { + "tokenUrl": "https://discord.com/api/oauth2/token", + "refreshUrl": "https://discord.com/api/oauth2/token", + "scopes": { + "activities.invites.write": "allows your app to send activity invites - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "activities.read": "allows your app to fetch data from a user's \"Now Playing/Recently Played\" list - requires Discord approval", + "activities.write": "allows your app to update a user's activity - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "applications.builds.read": "allows your app to read build data for a user's applications", + "applications.builds.upload": "allows your app to upload/update builds for a user's applications - requires Discord approval", + "applications.commands": "allows your app to use commands in a guild", + "applications.commands.permissions.update": "allows your app to update permissions for its commands in a guild a user has permissions to", + "applications.commands.update": "allows your app to update its commands using a Bearer token - client credentials grant only", + "applications.entitlements": "allows your app to read entitlements for a user's applications", + "applications.store.update": "allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications", + "bot": "for oauth2 bots, this puts the bot in the user's selected guild by default", + "connections": "allows /users/@me/connections to return linked third-party accounts", + "dm_channels.read": "allows your app to see information about the user's DMs and group DMs - requires Discord approval", + "email": "enables /users/@me to return an email", + "gdm.join": "allows your app to join users to a group dm", + "guilds": "allows /users/@me/guilds to return basic information about all of a user's guilds", + "guilds.join": "allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild", + "guilds.members.read": "allows /users/@me/guilds/{guild.id}/member to return a user's member information in a guild", + "identify": "allows /users/@me without email", + "messages.read": "for local rpc server api access, this allows you to read messages from all client channels (otherwise restricted to channels/guilds your app creates)", + "openid": "for OpenID Connect, this allows your app to receive user id and basic profile information", + "relationships.read": "allows your app to know a user's friends and implicit relationships - requires Discord approval", + "rpc": "for local rpc server access, this allows you to control a user's local Discord client - requires Discord approval", + "rpc.activities.write": "for local rpc server access, this allows you to update a user's activity - requires Discord approval", + "rpc.notifications.read": "for local rpc server access, this allows you to receive notifications pushed out to the user - requires Discord approval", + "rpc.screenshare.read": "for local rpc server access, this allows you to read a user's screenshare status- requires Discord approval", + "rpc.screenshare.write": "for local rpc server access, this allows you to update a user's screenshare settings- requires Discord approval", + "rpc.video.read": "for local rpc server access, this allows you to read a user's video status - requires Discord approval", + "rpc.video.write": "for local rpc server access, this allows you to update a user's video settings - requires Discord approval", + "rpc.voice.read": "for local rpc server access, this allows you to read a user's voice settings and listen for voice events - requires Discord approval", + "rpc.voice.write": "for local rpc server access, this allows you to update a user's voice settings - requires Discord approval", + "voice": "allows your app to connect to voice on user's behalf and see all the voice members - requires Discord approval", + "webhook.incoming": "this generates a webhook that is returned in the oauth token response for authorization code grants" + } + }, + "authorizationCode": { + "authorizationUrl": "https://discord.com/api/oauth2/authorize", + "tokenUrl": "https://discord.com/api/oauth2/token", + "refreshUrl": "https://discord.com/api/oauth2/token", + "scopes": { + "activities.invites.write": "allows your app to send activity invites - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "activities.read": "allows your app to fetch data from a user's \"Now Playing/Recently Played\" list - requires Discord approval", + "activities.write": "allows your app to update a user's activity - requires Discord approval (NOT REQUIRED FOR GAMESDK ACTIVITY MANAGER)", + "applications.builds.read": "allows your app to read build data for a user's applications", + "applications.builds.upload": "allows your app to upload/update builds for a user's applications - requires Discord approval", + "applications.commands": "allows your app to use commands in a guild", + "applications.commands.permissions.update": "allows your app to update permissions for its commands in a guild a user has permissions to", + "applications.entitlements": "allows your app to read entitlements for a user's applications", + "applications.store.update": "allows your app to read and update store data (SKUs, store listings, achievements, etc.) for a user's applications", + "bot": "for oauth2 bots, this puts the bot in the user's selected guild by default", + "connections": "allows /users/@me/connections to return linked third-party accounts", + "dm_channels.read": "allows your app to see information about the user's DMs and group DMs - requires Discord approval", + "email": "enables /users/@me to return an email", + "gdm.join": "allows your app to join users to a group dm", + "guilds": "allows /users/@me/guilds to return basic information about all of a user's guilds", + "guilds.join": "allows /guilds/{guild.id}/members/{user.id} to be used for joining users to a guild", + "guilds.members.read": "allows /users/@me/guilds/{guild.id}/member to return a user's member information in a guild", + "identify": "allows /users/@me without email", + "messages.read": "for local rpc server api access, this allows you to read messages from all client channels (otherwise restricted to channels/guilds your app creates)", + "openid": "for OpenID Connect, this allows your app to receive user id and basic profile information", + "relationships.read": "allows your app to know a user's friends and implicit relationships - requires Discord approval", + "role_connections.write": "allows your app to update a user's connection and metadata for the app", + "rpc": "for local rpc server access, this allows you to control a user's local Discord client - requires Discord approval", + "rpc.activities.write": "for local rpc server access, this allows you to update a user's activity - requires Discord approval", + "rpc.notifications.read": "for local rpc server access, this allows you to receive notifications pushed out to the user - requires Discord approval", + "rpc.screenshare.read": "for local rpc server access, this allows you to read a user's screenshare status- requires Discord approval", + "rpc.screenshare.write": "for local rpc server access, this allows you to update a user's screenshare settings- requires Discord approval", + "rpc.video.read": "for local rpc server access, this allows you to read a user's video status - requires Discord approval", + "rpc.video.write": "for local rpc server access, this allows you to update a user's video settings - requires Discord approval", + "rpc.voice.read": "for local rpc server access, this allows you to read a user's voice settings and listen for voice events - requires Discord approval", + "rpc.voice.write": "for local rpc server access, this allows you to update a user's voice settings - requires Discord approval", + "voice": "allows your app to connect to voice on user's behalf and see all the voice members - requires Discord approval", + "webhook.incoming": "this generates a webhook that is returned in the oauth token response for authorization code grants" + } + } + } + } + }, + "responses": { + "ClientErrorResponse": { + "description": "Client error response", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "ClientRatelimitedResponse": { + "description": "Client ratelimited response", + "headers": { + "X-RateLimit-Limit": { + "$ref": "#/components/headers/X-RateLimit-Limit" + }, + "X-RateLimit-Remaining": { + "$ref": "#/components/headers/X-RateLimit-Remaining" + }, + "X-RateLimit-Reset": { + "$ref": "#/components/headers/X-RateLimit-Reset" + }, + "X-RateLimit-Reset-After": { + "$ref": "#/components/headers/X-RateLimit-Reset-After" + }, + "X-RateLimit-Bucket": { + "$ref": "#/components/headers/X-RateLimit-Bucket" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RatelimitedResponse" + } + } + } + } + }, + "headers": { + "X-RateLimit-Limit": { + "schema": { + "type": "integer" + }, + "description": "The maximum number of requests that can be made in the current ratelimit window" + }, + "X-RateLimit-Remaining": { + "schema": { + "type": "integer" + }, + "description": "The number of requests remaining in the current ratelimit window" + }, + "X-RateLimit-Reset": { + "schema": { + "type": "number" + }, + "description": "A unix timestamp in seconds at which the current ratelimit window resets" + }, + "X-RateLimit-Reset-After": { + "schema": { + "type": "number" + }, + "description": "The duration in seconds until the current ratelimit window resets" + }, + "X-RateLimit-Bucket": { + "schema": { + "type": "string" + }, + "description": "The bucket that the request belongs to" + } + } + } +} \ No newline at end of file diff --git a/test/dexcord/api_channels_test.exs b/test/dexcord/api_channels_test.exs new file mode 100644 index 0000000..027a233 --- /dev/null +++ b/test/dexcord/api_channels_test.exs @@ -0,0 +1,178 @@ +defmodule Dexcord.ApiChannelsTest do + @moduledoc """ + Representative wire tests for the `Dexcord.Api.Channels` group, driven through + the real `Dexcord.Api` + `Dexcord.Api.Ratelimit` against `Dexcord.FakeRest`. + One round-trip per shape family plus the specials called out in Phase 5 Task 2. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + test "edit_channel PATCHes a body and decodes the concrete channel struct" do + FakeRest.stub( + :patch, + "/channels/1", + FakeRest.resp(200, body: ~s({"id":"1","type":0,"name":"renamed"})) + ) + + assert {:ok, %Dexcord.TextChannel{id: 1, name: "renamed"}} = + Api.edit_channel(1, %{"name" => "renamed"}, reason: "tidy up") + + assert_receive {:rest_hit, info} + assert info.method == "PATCH" + assert info.path == "/channels/1" + assert JSON.decode!(info.body) == %{"name" => "renamed"} + headers = Map.new(info.headers) + assert headers["x-audit-log-reason"] == "tidy%20up" + end + + test "delete_channel decodes the deleted channel and carries a reason" do + FakeRest.stub(:delete, "/channels/5", FakeRest.resp(200, body: ~s({"id":"5","type":0}))) + + assert {:ok, %Dexcord.TextChannel{id: 5}} = Api.delete_channel(5, reason: "gone") + + assert_receive {:rest_hit, info} + assert info.method == "DELETE" + assert Map.new(info.headers)["x-audit-log-reason"] == "gone" + end + + test "get_channel_invites decodes a list of invites" do + FakeRest.stub( + :get, + "/channels/1/invites", + FakeRest.resp(200, body: ~s([{"code":"abc"},{"code":"def"}])) + ) + + assert {:ok, [%Dexcord.Invite{code: "abc"}, %Dexcord.Invite{code: "def"}]} = + Api.get_channel_invites(1) + end + + test "create_channel_invite posts a body and decodes an invite" do + FakeRest.stub( + :post, + "/channels/1/invites", + FakeRest.resp(200, body: ~s({"code":"xyz"})) + ) + + assert {:ok, %Dexcord.Invite{code: "xyz"}} = + Api.create_channel_invite(1, %{"max_age" => 3600}, reason: "share") + + assert_receive {:rest_hit, info} + assert JSON.decode!(info.body) == %{"max_age" => 3600} + end + + test "follow_announcement_channel decodes a FollowedChannel" do + FakeRest.stub( + :post, + "/channels/1/followers", + FakeRest.resp(200, body: ~s({"channel_id":"9","webhook_id":"10"})) + ) + + assert {:ok, %Dexcord.FollowedChannel{channel_id: 9, webhook_id: 10}} = + Api.follow_announcement_channel(1, %{"webhook_channel_id" => "9"}) + end + + test "trigger_typing POSTs with no body and returns {:ok, nil}" do + FakeRest.stub(:post, "/channels/1/typing", FakeRest.resp(204)) + + assert {:ok, nil} = Api.trigger_typing(1) + + assert_receive {:rest_hit, info} + assert info.method == "POST" + assert info.body == "" + end + + test "edit_channel_permissions PUTs a body, returns nil, carries a reason" do + FakeRest.stub(:put, "/channels/1/permissions/2", FakeRest.resp(204)) + + assert {:ok, nil} = + Api.edit_channel_permissions(1, 2, %{"allow" => "8", "type" => 0}, reason: "perm") + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/permissions/2" + assert Map.new(info.headers)["x-audit-log-reason"] == "perm" + end + + test "get_thread_member forwards the with_member query and decodes a ThreadMember" do + FakeRest.stub( + :get, + "/channels/1/thread-members/2", + FakeRest.resp(200, body: ~s({"id":"1","user_id":"2"})) + ) + + assert {:ok, %Dexcord.ThreadMember{user_id: 2}} = + Api.get_thread_member(1, 2, with_member: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_member=true" + end + + test "list_thread_members decodes a list and forwards the query keys" do + FakeRest.stub( + :get, + "/channels/1/thread-members", + FakeRest.resp(200, body: ~s([{"user_id":"2"},{"user_id":"3"}])) + ) + + assert {:ok, [%Dexcord.ThreadMember{user_id: 2}, %Dexcord.ThreadMember{user_id: 3}]} = + Api.list_thread_members(1, with_member: true, limit: 50) + + assert_receive {:rest_hit, info} + assert URI.decode_query(info.query_string) == %{"with_member" => "true", "limit" => "50"} + end + + test "list_public_archived_threads returns the raw envelope untouched" do + FakeRest.stub( + :get, + "/channels/1/threads/archived/public", + FakeRest.resp(200, body: ~s({"threads":[],"members":[],"has_more":false})) + ) + + assert {:ok, %{"threads" => [], "members" => [], "has_more" => false}} = + Api.list_public_archived_threads(1, limit: 10) + end + + test "start_thread wraps a binary as name, carries a reason, decodes a Thread" do + FakeRest.stub( + :post, + "/channels/1/threads", + FakeRest.resp(201, body: ~s({"id":"77","type":11,"name":"chat"})) + ) + + assert {:ok, %Dexcord.Thread{id: 77, name: "chat"}} = + Api.start_thread(1, "chat", reason: "spin up") + + assert_receive {:rest_hit, info} + assert JSON.decode!(info.body) == %{"name" => "chat"} + assert Map.new(info.headers)["x-audit-log-reason"] == "spin%20up" + end + + test "group_dm_add_recipient PUTs a body and returns nil" do + FakeRest.stub(:put, "/channels/1/recipients/2", FakeRest.resp(204)) + + assert {:ok, nil} = + Api.group_dm_add_recipient(1, 2, %{"access_token" => "tok", "nick" => "friend"}) + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/recipients/2" + assert JSON.decode!(info.body) == %{"access_token" => "tok", "nick" => "friend"} + end +end diff --git a/test/dexcord/api_commands_test.exs b/test/dexcord/api_commands_test.exs new file mode 100644 index 0000000..97a5dad --- /dev/null +++ b/test/dexcord/api_commands_test.exs @@ -0,0 +1,140 @@ +defmodule Dexcord.ApiCommandsTest do + @moduledoc """ + Representative wire tests for the extended `Dexcord.Api.Commands` group + (Phase 5 Task 5), including a struct body encoded via `to_map/1` with a + recursive (sub-command-group) options tree. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + test "get_global_commands forwards with_localizations and decodes a list" do + FakeRest.stub( + :get, + "/applications/1/commands", + FakeRest.resp(200, body: ~s([{"id":"2","name":"ping"}])) + ) + + assert {:ok, [%Dexcord.ApplicationCommand{id: 2, name: "ping"}]} = + Api.get_global_commands(1, with_localizations: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_localizations=true" + end + + test "create_global_command encodes a recursive ApplicationCommand struct body" do + FakeRest.stub( + :post, + "/applications/1/commands", + FakeRest.resp(201, body: ~s({"id":"7","name":"config"})) + ) + + command = %Dexcord.ApplicationCommand{ + name: "config", + description: "configure the bot", + options: [ + %Dexcord.ApplicationCommand.Option{ + type: :sub_command_group, + name: "group", + description: "a group", + options: [ + %Dexcord.ApplicationCommand.Option{ + type: :sub_command, + name: "sub", + description: "a sub-command" + } + ] + } + ] + } + + assert {:ok, %Dexcord.ApplicationCommand{id: 7, name: "config"}} = + Api.create_global_command(1, command) + + assert_receive {:rest_hit, info} + body = JSON.decode!(info.body) + assert body["name"] == "config" + assert [group] = body["options"] + # sub_command_group encodes to 2, the nested sub_command to 1. + assert group["type"] == 2 + assert group["name"] == "group" + assert [sub] = group["options"] + assert sub["type"] == 1 + assert sub["name"] == "sub" + end + + test "edit_global_command PATCHes and decodes an ApplicationCommand" do + FakeRest.stub( + :patch, + "/applications/1/commands/2", + FakeRest.resp(200, body: ~s({"id":"2","name":"renamed"})) + ) + + assert {:ok, %Dexcord.ApplicationCommand{id: 2, name: "renamed"}} = + Api.edit_global_command(1, 2, %{"name" => "renamed"}) + end + + test "delete_global_command returns nil" do + FakeRest.stub(:delete, "/applications/1/commands/2", FakeRest.resp(204)) + + assert {:ok, nil} = Api.delete_global_command(1, 2) + end + + test "create_guild_command decodes an ApplicationCommand" do + FakeRest.stub( + :post, + "/applications/1/guilds/9/commands", + FakeRest.resp(201, body: ~s({"id":"3","name":"g"})) + ) + + assert {:ok, %Dexcord.ApplicationCommand{id: 3, name: "g"}} = + Api.create_guild_command(1, 9, %{"name" => "g", "description" => "d"}) + end + + test "get_guild_command_permissions decodes a list of permission structs" do + FakeRest.stub( + :get, + "/applications/1/guilds/9/commands/permissions", + FakeRest.resp(200, + body: ~s([{"id":"2","permissions":[{"id":"5","type":1,"permission":true}]}]) + ) + ) + + assert {:ok, + [ + %Dexcord.GuildApplicationCommandPermissions{ + id: 2, + permissions: [%Dexcord.ApplicationCommandPermission{id: 5, permission: true}] + } + ]} = Api.get_guild_command_permissions(1, 9) + end + + test "get_command_permissions decodes a single permissions struct" do + FakeRest.stub( + :get, + "/applications/1/guilds/9/commands/2/permissions", + FakeRest.resp(200, body: ~s({"id":"2","permissions":[]})) + ) + + assert {:ok, %Dexcord.GuildApplicationCommandPermissions{id: 2, permissions: []}} = + Api.get_command_permissions(1, 9, 2) + end +end diff --git a/test/dexcord/api_facade_test.exs b/test/dexcord/api_facade_test.exs new file mode 100644 index 0000000..940a518 --- /dev/null +++ b/test/dexcord/api_facade_test.exs @@ -0,0 +1,114 @@ +defmodule Dexcord.ApiFacadeTest do + @moduledoc """ + Guards the generated `Dexcord.Api` facade: every endpoint head derived from the + group modules' static `__endpoints__/0` route tables (plus explicit sugar) must + be re-exported from `Dexcord.Api` at the same arity, so the delegation can never + silently drop an endpoint. + + This guard is deterministic: it derives the expected `{name, arity}` set from + the SAME static source of truth the facade generation uses (`__endpoints__/0`), + never from `module_info(:exports)` (whose export table races under + parallel/incremental compilation). + """ + use ExUnit.Case, async: true + + # `function_exported?/3` does NOT load a module — it only inspects an + # already-loaded one. Under `async: true` a probe-only test can otherwise run + # before anything forces `Dexcord.Api` into the VM, yielding a false negative + # that has nothing to do with the facade. Load every relevant module up front + # so the export probes below reflect the compiled beam, deterministically. + setup_all do + Code.ensure_loaded!(Dexcord.Api) + Enum.each(Dexcord.Api.__endpoint_groups__(), &Code.ensure_loaded!/1) + :ok + end + + # Hand-written sugar arities not reproduced by the `__endpoints__/0` derivation + # (mirrors `Dexcord.Api`'s own `@facade_sugar`). + @sugar [get_channel_messages: 3] + + # Independently re-derive the expected delegate set from the groups' static + # route tables, using the identical rule the facade generation applies: each + # generated head ends in `opts \\ []`, so it exports at `base` and `base + 1` + # where `base = path_param_count + (body? 1 : 0)`. + defp expected_exports do + endpoint_exports = + for group <- Dexcord.Api.__endpoint_groups__(), + spec <- group.__endpoints__(), + params = Enum.count(spec.segments, &match?({:param, _}, &1)), + base = params + if(spec.body, do: 1, else: 0), + arity <- [base, base + 1] do + {spec.name, arity} + end + + MapSet.new(endpoint_exports ++ @sugar) + end + + test "the facade's export set exactly equals the statically-derived expected set" do + # Same source of truth as the generator: if the two ever diverge, either the + # generator dropped/added a delegate or the derivation rule drifted. + assert Dexcord.Api.__facade_exports__() == expected_exports() + end + + test "every statically-derived export is actually defined on Dexcord.Api" do + # Guards the generation step itself: the derived set must correspond to real + # exported functions. This is what fails deterministically if a delegate is + # silently dropped by a racy compile. + exports = expected_exports() + assert MapSet.size(exports) > 0 + + for {name, arity} <- exports do + assert function_exported?(Dexcord.Api, name, arity), + "Dexcord.Api is missing a delegate for #{name}/#{arity}" + end + end + + test "__endpoint_groups__/0 lists all declared groups" do + assert Dexcord.Api.__endpoint_groups__() == [ + Dexcord.Api.Channels, + Dexcord.Api.Messages, + Dexcord.Api.Guilds, + Dexcord.Api.Members, + Dexcord.Api.Roles, + Dexcord.Api.Webhooks, + Dexcord.Api.Invites, + Dexcord.Api.Users, + Dexcord.Api.Interactions, + Dexcord.Api.Commands, + Dexcord.Api.EmojisStickers, + Dexcord.Api.Moderation, + Dexcord.Api.Misc + ] + end + + test "the facade covers historically-load-bearing endpoint arities" do + # These are the arities depended on by the internal call sites (gateway, + # prefix, registrar, slash) and by external Nostrum-compatible callers. + for {name, arity} <- [ + get_gateway_bot: 0, + get_current_user: 0, + get_current_application: 0, + get_user: 1, + get_channel: 1, + get_guild: 1, + create_dm: 1, + get_channel_messages: 1, + get_channel_messages: 2, + get_channel_messages: 3, + create_message: 2, + create_message: 3, + edit_message: 3, + delete_message: 2, + create_reaction: 3, + start_thread_with_message: 3, + create_interaction_response: 3, + edit_original_interaction_response: 3, + create_followup_message: 3, + bulk_overwrite_global_commands: 2, + bulk_overwrite_guild_commands: 3 + ] do + assert function_exported?(Dexcord.Api, name, arity), + "expected Dexcord.Api.#{name}/#{arity} to exist" + end + end +end diff --git a/test/dexcord/api_guilds_test.exs b/test/dexcord/api_guilds_test.exs new file mode 100644 index 0000000..ee8be5a --- /dev/null +++ b/test/dexcord/api_guilds_test.exs @@ -0,0 +1,233 @@ +defmodule Dexcord.ApiGuildsTest do + @moduledoc """ + Representative wire tests for the `Dexcord.Api.Guilds`, `Dexcord.Api.Members`, + and `Dexcord.Api.Roles` groups (Phase 5 Task 3): top-level-array bodies, the + binary widget.png, audit-log decode, and the moved `get_guild`. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + # --- Guilds ------------------------------------------------------------- + + test "get_guild still works after moving out of Misc, honoring with_counts" do + FakeRest.stub(:get, "/guilds/1", FakeRest.resp(200, body: ~s({"id":"1","name":"g"}))) + + assert {:ok, %Dexcord.Guild{id: 1, name: "g"}} = Api.get_guild(1, with_counts: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_counts=true" + end + + test "edit_guild PATCHes a body and decodes the Guild with a reason" do + FakeRest.stub(:patch, "/guilds/1", FakeRest.resp(200, body: ~s({"id":"1","name":"new"}))) + + assert {:ok, %Dexcord.Guild{name: "new"}} = + Api.edit_guild(1, %{"name" => "new"}, reason: "rebrand") + + assert_receive {:rest_hit, info} + assert Map.new(info.headers)["x-audit-log-reason"] == "rebrand" + end + + test "edit_guild_channel_positions sends a TOP-LEVEL array body and returns nil" do + FakeRest.stub(:patch, "/guilds/1/channels", FakeRest.resp(204)) + + assert {:ok, nil} = + Api.edit_guild_channel_positions(1, [ + %{"id" => "2", "position" => 1}, + %{"id" => "3", "position" => 2} + ]) + + assert_receive {:rest_hit, info} + + assert JSON.decode!(info.body) == [ + %{"id" => "2", "position" => 1}, + %{"id" => "3", "position" => 2} + ] + end + + test "get_guild_channels decodes a list of concrete channel structs" do + FakeRest.stub( + :get, + "/guilds/1/channels", + FakeRest.resp(200, body: ~s([{"id":"2","type":0},{"id":"3","type":2}])) + ) + + assert {:ok, [%Dexcord.TextChannel{id: 2}, %Dexcord.VoiceChannel{id: 3}]} = + Api.get_guild_channels(1) + end + + test "get_guild_widget_settings decodes the settings struct" do + FakeRest.stub( + :get, + "/guilds/1/widget", + FakeRest.resp(200, body: ~s({"enabled":true,"channel_id":"9"})) + ) + + assert {:ok, %Dexcord.GuildWidgetSettings{enabled: true, channel_id: 9}} = + Api.get_guild_widget_settings(1) + end + + test "get_guild_widget_image returns a non-JSON PNG body raw" do + png = <<137, 80, 78, 71, 13, 10, 26, 10>> + + FakeRest.stub( + :get, + "/guilds/1/widget.png", + FakeRest.resp(200, headers: [{"content-type", "image/png"}], body: png) + ) + + assert {:ok, {:raw, ^png}} = Api.get_guild_widget_image(1, style: "banner1") + + assert_receive {:rest_hit, info} + assert info.query_string == "style=banner1" + end + + test "get_guild_audit_log encodes its query and decodes nested entries" do + body = ~s({"audit_log_entries":[{"id":"5","action_type":1}],"users":[{"id":"7"}]}) + FakeRest.stub(:get, "/guilds/1/audit-logs", FakeRest.resp(200, body: body)) + + assert {:ok, + %Dexcord.AuditLog{ + audit_log_entries: [%Dexcord.AuditLogEntry{id: 5}], + users: [%Dexcord.User{id: 7}] + }} = Api.get_guild_audit_log(1, user_id: "7", action_type: 1, limit: 50) + + assert_receive {:rest_hit, info} + + assert URI.decode_query(info.query_string) == %{ + "user_id" => "7", + "action_type" => "1", + "limit" => "50" + } + end + + # --- Members ------------------------------------------------------------ + + test "get_guild_member decodes a Member" do + FakeRest.stub( + :get, + "/guilds/1/members/2", + FakeRest.resp(200, body: ~s({"nick":"bob","user":{"id":"2"}})) + ) + + assert {:ok, %Dexcord.Member{nick: "bob", user: %Dexcord.User{id: 2}}} = + Api.get_guild_member(1, 2) + end + + test "list_guild_members decodes a list and forwards paging query" do + FakeRest.stub( + :get, + "/guilds/1/members", + FakeRest.resp(200, body: ~s([{"nick":"a"},{"nick":"b"}])) + ) + + assert {:ok, [%Dexcord.Member{nick: "a"}, %Dexcord.Member{nick: "b"}]} = + Api.list_guild_members(1, limit: 100, after: "0") + + assert_receive {:rest_hit, info} + assert URI.decode_query(info.query_string) == %{"limit" => "100", "after" => "0"} + end + + test "create_guild_ban PUTs a body, returns nil, carries a reason" do + FakeRest.stub(:put, "/guilds/1/bans/2", FakeRest.resp(204)) + + assert {:ok, nil} = + Api.create_guild_ban(1, 2, %{"delete_message_seconds" => 3600}, reason: "spam") + + assert_receive {:rest_hit, info} + assert info.method == "PUT" + assert info.path == "/guilds/1/bans/2" + assert Map.new(info.headers)["x-audit-log-reason"] == "spam" + end + + test "get_guild_ban decodes a Ban" do + FakeRest.stub( + :get, + "/guilds/1/bans/2", + FakeRest.resp(200, body: ~s({"reason":"bad","user":{"id":"2"}})) + ) + + assert {:ok, %Dexcord.Ban{reason: "bad", user: %Dexcord.User{id: 2}}} = + Api.get_guild_ban(1, 2) + end + + test "get_current_user_voice_state decodes a VoiceState" do + FakeRest.stub( + :get, + "/guilds/1/voice-states/@me", + FakeRest.resp(200, body: ~s({"channel_id":"9","session_id":"s"})) + ) + + assert {:ok, %Dexcord.VoiceState{channel_id: 9, session_id: "s"}} = + Api.get_current_user_voice_state(1) + end + + # --- Roles -------------------------------------------------------------- + + test "get_guild_roles decodes a list of Roles" do + FakeRest.stub( + :get, + "/guilds/1/roles", + FakeRest.resp(200, body: ~s([{"id":"2","name":"mod"},{"id":"3","name":"admin"}])) + ) + + assert {:ok, [%Dexcord.Role{id: 2, name: "mod"}, %Dexcord.Role{id: 3, name: "admin"}]} = + Api.get_guild_roles(1) + end + + test "create_guild_role wraps a binary as name and decodes a Role with a reason" do + FakeRest.stub(:post, "/guilds/1/roles", FakeRest.resp(200, body: ~s({"id":"9","name":"vip"}))) + + assert {:ok, %Dexcord.Role{id: 9, name: "vip"}} = + Api.create_guild_role(1, "vip", reason: "perk") + + assert_receive {:rest_hit, info} + assert JSON.decode!(info.body) == %{"name" => "vip"} + assert Map.new(info.headers)["x-audit-log-reason"] == "perk" + end + + test "edit_guild_role_positions sends a list body and decodes a list of Roles" do + FakeRest.stub( + :patch, + "/guilds/1/roles", + FakeRest.resp(200, body: ~s([{"id":"2","position":1},{"id":"3","position":2}])) + ) + + assert {:ok, [%Dexcord.Role{id: 2, position: 1}, %Dexcord.Role{id: 3, position: 2}]} = + Api.edit_guild_role_positions(1, [ + %{"id" => "2", "position" => 1}, + %{"id" => "3", "position" => 2} + ]) + + assert_receive {:rest_hit, info} + assert is_list(JSON.decode!(info.body)) + end + + test "delete_guild_role returns nil with a reason" do + FakeRest.stub(:delete, "/guilds/1/roles/2", FakeRest.resp(204)) + + assert {:ok, nil} = Api.delete_guild_role(1, 2, reason: "obsolete") + + assert_receive {:rest_hit, info} + assert Map.new(info.headers)["x-audit-log-reason"] == "obsolete" + end +end diff --git a/test/dexcord/api_integration_test.exs b/test/dexcord/api_integration_test.exs index 16af95f..3934804 100644 --- a/test/dexcord/api_integration_test.exs +++ b/test/dexcord/api_integration_test.exs @@ -46,7 +46,7 @@ defmodule Dexcord.ApiIntegrationTest do test "auth + user-agent headers are present on every request" do FakeRest.stub(:get, "/users/@me", FakeRest.resp(200, body: ~s({"id":"42"}))) - assert {:ok, %{"id" => "42"}} = Api.get_current_user() + assert {:ok, %Dexcord.User{id: 42}} = Api.get_current_user() assert_receive {:rest_hit, info} headers = Map.new(info.headers) @@ -135,7 +135,7 @@ defmodule Dexcord.ApiIntegrationTest do {elapsed_us, result} = :timer.tc(fn -> Api.create_message(5, "hi") end) - assert {:ok, %{"id" => "ok"}} = result + assert {:ok, %Dexcord.Message{id: "ok"}} = result # Exactly two hits: the 429 and the successful retry - and no more. assert_receive {:rest_hit, _} assert_receive {:rest_hit, _} @@ -173,7 +173,7 @@ defmodule Dexcord.ApiIntegrationTest do {elapsed_us, {:ok, _}} = :timer.tc(fn -> Api.get_guild(8) end) assert elapsed_us >= 150_000, "unrelated route was not blocked by the global lock" - assert {:ok, %{"id" => "a"}} = Task.await(task, 5_000) + assert {:ok, %Dexcord.Message{id: "a"}} = Task.await(task, 5_000) end test "a wait that would exceed the request deadline returns an error tuple" do @@ -232,13 +232,13 @@ defmodule Dexcord.ApiIntegrationTest do assert catch_error(Api.create_message(999, %{"content" => self()})) # A normal follow-up to the same route completes. - assert {:ok, %{"id" => "ok"}} = Api.create_message(999, "recovered") + assert {:ok, %Dexcord.Message{id: "ok"}} = Api.create_message(999, "recovered") end test "get_channel_messages/2 with a limit only builds a plain query" do FakeRest.stub(:get, "/channels/1/messages", FakeRest.resp(200, body: ~s([{"id":"m1"}]))) - assert {:ok, [%{"id" => "m1"}]} = Api.get_channel_messages(1, limit: 50) + assert {:ok, [%Dexcord.Message{id: "m1"}]} = Api.get_channel_messages(1, limit: 50) assert_receive {:rest_hit, info} assert info.method == "GET" @@ -265,14 +265,24 @@ defmodule Dexcord.ApiIntegrationTest do assert info.query_string == "" end - test "get_channel_messages/2 emits at most one anchor (before > after > around)" do + test "get_channel_messages/2 now emits every anchor it is given (no precedence collapse)" do FakeRest.stub(:get, "/channels/1/messages", FakeRest.resp(200, body: "[]")) assert {:ok, []} = Api.get_channel_messages(1, before: "b", after: "a", around: "r", limit: 5) assert_receive {:rest_hit, info} - assert query(info) == %{"limit" => "5", "before" => "b"} + + # Behavior change from the pre-DSL surface: the old `message_query/1` kept + # only the highest-precedence anchor (before > after > around). The DSL query + # builder forwards every declared query key present in opts, so the caller is + # now responsible for passing exactly one anchor (the /3 locator arity does). + assert query(info) == %{ + "limit" => "5", + "before" => "b", + "after" => "a", + "around" => "r" + } end test "get_channel_messages/3 normalizes the Nostrum-style locator" do @@ -287,15 +297,18 @@ defmodule Dexcord.ApiIntegrationTest do assert query(info2) == %{"limit" => "25"} end - test "start_thread_with_message posts a string-keyed body with the name" do + test "start_thread_with_message posts a string-keyed body and decodes the thread" do FakeRest.stub( :post, "/channels/1/messages/2/threads", - FakeRest.resp(201, body: ~s({"id":"t1"})) + FakeRest.resp(201, body: ~s({"id":"123","type":11})) ) - assert {:ok, %{"id" => "t1"}} = - Api.start_thread_with_message(1, 2, "chat", auto_archive_duration: 1440) + # The extra body fields now travel in the keyword/map body argument (the old + # surface merged them from a trailing opts list; the DSL's opts are reserved + # for query/reason/files). + assert {:ok, %Dexcord.Thread{id: 123}} = + Api.start_thread_with_message(1, 2, name: "chat", auto_archive_duration: 1440) assert_receive {:rest_hit, info} assert info.method == "POST" @@ -307,8 +320,151 @@ defmodule Dexcord.ApiIntegrationTest do refute Map.has_key?(body, "rate_limit_per_user") end + describe "multipart bodies" do + test "a single file becomes a payload_json part plus a files[0] part" do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + + payload = %{"content" => "hi", "attachments" => [%{"id" => 0, "filename" => "a.png"}]} + files = [%{filename: "a.png", data: <<137, 80, 78, 71>>, content_type: "image/png"}] + + assert {:ok, %{"id" => "1"}} = + Api.request(:post, "/channels/1/messages", {:multipart, payload, files}) + + assert_receive {:rest_hit, info} + headers = Map.new(info.headers) + assert "multipart/form-data; boundary=" <> boundary = headers["content-type"] + assert boundary != "" + + parts = parse_multipart(info.body, boundary) + assert %{"payload_json" => payload_part} = by_name(parts) + assert payload_part.content_type == "application/json" + assert JSON.decode!(payload_part.data) == payload + + assert %{"files[0]" => file_part} = by_name(parts) + assert file_part.filename == "a.png" + assert file_part.content_type == "image/png" + assert file_part.data == <<137, 80, 78, 71>> + end + + test "two files become files[0] and files[1] parts in order" do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + + files = [ + %{filename: "a.png", data: <<1>>, content_type: "image/png"}, + %{filename: "b.txt", data: <<2>>, content_type: "text/plain"} + ] + + assert {:ok, _} = + Api.request(:post, "/channels/1/messages", {:multipart, %{"content" => ""}, files}) + + assert_receive {:rest_hit, info} + "multipart/form-data; boundary=" <> boundary = Map.new(info.headers)["content-type"] + + parts = parse_multipart(info.body, boundary) + names = Enum.map(parts, & &1.name) + assert names == ["payload_json", "files[0]", "files[1]"] + + named = by_name(parts) + assert named["files[0]"].filename == "a.png" + assert named["files[0]"].data == <<1>> + assert named["files[1]"].filename == "b.txt" + assert named["files[1]"].data == <<2>> + end + + test "a filename containing a quote arrives sanitized" do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + + files = [%{filename: ~s(ev"il.png), data: <<0>>, content_type: "image/png"}] + + assert {:ok, _} = + Api.request(:post, "/channels/1/messages", {:multipart, %{}, files}) + + assert_receive {:rest_hit, info} + "multipart/form-data; boundary=" <> boundary = Map.new(info.headers)["content-type"] + + parts = parse_multipart(info.body, boundary) + assert %{"files[0]" => file_part} = by_name(parts) + refute file_part.filename =~ "\"" + assert file_part.filename == "ev_il.png" + end + + test "a form_multipart body emits each field as its own part plus a file part" do + FakeRest.stub(:post, "/guilds/1/stickers", FakeRest.resp(201, body: ~s({"id":"1"}))) + + fields = %{"name" => "s", "tags" => "t"} + file = %{filename: "s.png", data: <<1>>, content_type: "image/png"} + + assert {:ok, %{"id" => "1"}} = + Api.request(:post, "/guilds/1/stickers", {:form_multipart, fields, file}) + + assert_receive {:rest_hit, info} + headers = Map.new(info.headers) + assert "multipart/form-data; boundary=" <> boundary = headers["content-type"] + assert boundary != "" + + parts = parse_multipart(info.body, boundary) + named = by_name(parts) + + # Each field is its own plain form part (no filename, no content-type). + assert named["name"].data == "s" + assert named["name"].filename == nil + assert named["tags"].data == "t" + assert named["tags"].filename == nil + + # The file rides in a part literally named "file" (not files[n]). + assert %{"file" => file_part} = named + assert file_part.filename == "s.png" + assert file_part.content_type == "image/png" + assert file_part.data == <<1>> + end + end + # --- helpers ------------------------------------------------------------ + # Splits a multipart/form-data body into parsed parts, preserving raw part + # bytes (no trimming, so binary payloads survive intact). + defp parse_multipart(body, boundary) do + body + |> String.split("--" <> boundary) + |> Enum.drop(1) + |> Enum.reject(fn seg -> seg == "--\r\n" or seg == "--" end) + |> Enum.map(fn seg -> + seg = String.replace_prefix(seg, "\r\n", "") + [raw_headers, rest] = String.split(seg, "\r\n\r\n", parts: 2) + data = String.replace_suffix(rest, "\r\n", "") + disposition = header_value(raw_headers, "content-disposition") + + %{ + name: extract(disposition, ~r/name="([^"]*)"/), + filename: extract(disposition, ~r/filename="([^"]*)"/), + content_type: header_value(raw_headers, "content-type"), + data: data + } + end) + end + + defp by_name(parts), do: Map.new(parts, fn part -> {part.name, part} end) + + defp header_value(raw_headers, name) do + raw_headers + |> String.split("\r\n") + |> Enum.find_value(fn line -> + case String.split(line, ": ", parts: 2) do + [key, value] -> if String.downcase(key) == name, do: value + _ -> nil + end + end) + end + + defp extract(nil, _re), do: nil + + defp extract(string, re) do + case Regex.run(re, string) do + [_, captured] -> captured + _ -> nil + end + end + defp query(info) do case info.query_string do qs when is_binary(qs) and qs != "" -> URI.decode_query(qs) diff --git a/test/dexcord/api_messages_test.exs b/test/dexcord/api_messages_test.exs new file mode 100644 index 0000000..a31fff9 --- /dev/null +++ b/test/dexcord/api_messages_test.exs @@ -0,0 +1,180 @@ +defmodule Dexcord.ApiMessagesTest do + @moduledoc """ + Representative wire tests for the `Dexcord.Api.Messages` group (Phase 5 Task 2): + reactions, bulk delete, the new pins route, polls, and guild message search. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + test "get_channel_message decodes a single Message" do + FakeRest.stub( + :get, + "/channels/1/messages/2", + FakeRest.resp(200, body: ~s({"id":"2","content":"hi"})) + ) + + assert {:ok, %Dexcord.Message{id: 2, content: "hi"}} = Api.get_channel_message(1, 2) + end + + test "crosspost_message decodes a Message" do + FakeRest.stub( + :post, + "/channels/1/messages/2/crosspost", + FakeRest.resp(200, body: ~s({"id":"2"})) + ) + + assert {:ok, %Dexcord.Message{id: 2}} = Api.crosspost_message(1, 2) + end + + test "delete_own_reaction percent-encodes the emoji in the path" do + FakeRest.stub( + :delete, + "/channels/1/messages/2/reactions/%F0%9F%94%A5/@me", + FakeRest.resp(204) + ) + + assert {:ok, nil} = Api.delete_own_reaction(1, 2, "🔥") + + assert_receive {:rest_hit, info} + assert info.method == "DELETE" + assert info.path == "/channels/1/messages/2/reactions/%F0%9F%94%A5/@me" + end + + test "delete_user_reaction targets a specific user's reaction" do + FakeRest.stub( + :delete, + "/channels/1/messages/2/reactions/%F0%9F%94%A5/3", + FakeRest.resp(204) + ) + + assert {:ok, nil} = Api.delete_user_reaction(1, 2, "🔥", 3) + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/messages/2/reactions/%F0%9F%94%A5/3" + end + + test "get_reactions decodes a list of users and forwards the query" do + FakeRest.stub( + :get, + "/channels/1/messages/2/reactions/%F0%9F%94%A5", + FakeRest.resp(200, body: ~s([{"id":"10"},{"id":"11"}])) + ) + + assert {:ok, [%Dexcord.User{id: 10}, %Dexcord.User{id: 11}]} = + Api.get_reactions(1, 2, "🔥", limit: 25) + + assert_receive {:rest_hit, info} + assert info.query_string == "limit=25" + end + + test "delete_all_reactions returns nil" do + FakeRest.stub(:delete, "/channels/1/messages/2/reactions", FakeRest.resp(204)) + + assert {:ok, nil} = Api.delete_all_reactions(1, 2) + end + + test "bulk_delete_messages posts a messages array body with a reason" do + FakeRest.stub(:post, "/channels/1/messages/bulk-delete", FakeRest.resp(204)) + + assert {:ok, nil} = + Api.bulk_delete_messages(1, %{"messages" => ["2", "3", "4"]}, reason: "cleanup") + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/messages/bulk-delete" + assert JSON.decode!(info.body) == %{"messages" => ["2", "3", "4"]} + assert Map.new(info.headers)["x-audit-log-reason"] == "cleanup" + end + + test "get_channel_pins hits the new pins route and returns the raw envelope" do + FakeRest.stub( + :get, + "/channels/1/messages/pins", + FakeRest.resp(200, body: ~s({"items":[],"has_more":false})) + ) + + assert {:ok, %{"items" => [], "has_more" => false}} = + Api.get_channel_pins(1, limit: 5) + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/messages/pins" + end + + test "pin_message PUTs the new pins route with a reason header" do + FakeRest.stub(:put, "/channels/1/messages/pins/2", FakeRest.resp(204)) + + assert {:ok, nil} = Api.pin_message(1, 2, reason: "important") + + assert_receive {:rest_hit, info} + assert info.method == "PUT" + assert info.path == "/channels/1/messages/pins/2" + assert Map.new(info.headers)["x-audit-log-reason"] == "important" + end + + test "unpin_message DELETEs the new pins route with a reason header" do + FakeRest.stub(:delete, "/channels/1/messages/pins/2", FakeRest.resp(204)) + + assert {:ok, nil} = Api.unpin_message(1, 2, reason: "stale") + + assert_receive {:rest_hit, info} + assert info.path == "/channels/1/messages/pins/2" + assert Map.new(info.headers)["x-audit-log-reason"] == "stale" + end + + test "search_guild_messages forwards its query keys and returns raw" do + FakeRest.stub( + :get, + "/guilds/1/messages/search", + FakeRest.resp(200, body: ~s({"messages":[],"total_results":0})) + ) + + assert {:ok, %{"messages" => [], "total_results" => 0}} = + Api.search_guild_messages(1, content: "hello", limit: 10, author_id: "42") + + assert_receive {:rest_hit, info} + + assert URI.decode_query(info.query_string) == %{ + "content" => "hello", + "limit" => "10", + "author_id" => "42" + } + end + + test "get_answer_voters returns the raw voters envelope" do + FakeRest.stub( + :get, + "/channels/1/polls/2/answers/3", + FakeRest.resp(200, body: ~s({"users":[]})) + ) + + assert {:ok, %{"users" => []}} = Api.get_answer_voters(1, 2, 3, limit: 5) + end + + test "end_poll returns the finalized Message" do + FakeRest.stub( + :post, + "/channels/1/polls/2/expire", + FakeRest.resp(200, body: ~s({"id":"2","content":"poll over"})) + ) + + assert {:ok, %Dexcord.Message{id: 2, content: "poll over"}} = Api.end_poll(1, 2) + end +end diff --git a/test/dexcord/api_moderation_test.exs b/test/dexcord/api_moderation_test.exs new file mode 100644 index 0000000..b64dee9 --- /dev/null +++ b/test/dexcord/api_moderation_test.exs @@ -0,0 +1,248 @@ +defmodule Dexcord.ApiModerationTest do + @moduledoc """ + Representative wire tests for the Phase 5 Task 5 groups: `Dexcord.Api.Moderation` + (auto-moderation + scheduled events), `Dexcord.Api.EmojisStickers` (base64 JSON + emoji bodies vs. plain-form sticker multipart), the `Dexcord.Api.Users` + additions, and the `Dexcord.Api.Misc` `get_current_application` re-point. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + # --- Moderation: auto-moderation --------------------------------------- + + test "create_auto_moderation_rule posts a body and decodes the rule with a reason" do + FakeRest.stub( + :post, + "/guilds/1/auto-moderation/rules", + FakeRest.resp(200, body: ~s({"id":"5","name":"no-spam","trigger_type":1})) + ) + + assert {:ok, %Dexcord.AutoModerationRule{id: 5, name: "no-spam"}} = + Api.create_auto_moderation_rule(1, %{"name" => "no-spam"}, reason: "policy") + + assert_receive {:rest_hit, info} + assert Map.new(info.headers)["x-audit-log-reason"] == "policy" + end + + test "list_auto_moderation_rules decodes a list of rules" do + FakeRest.stub( + :get, + "/guilds/1/auto-moderation/rules", + FakeRest.resp(200, body: ~s([{"id":"5","name":"a"},{"id":"6","name":"b"}])) + ) + + assert {:ok, [%Dexcord.AutoModerationRule{id: 5}, %Dexcord.AutoModerationRule{id: 6}]} = + Api.list_auto_moderation_rules(1) + end + + # --- Moderation: scheduled events -------------------------------------- + + test "get_guild_scheduled_event forwards with_user_count and decodes the event" do + FakeRest.stub( + :get, + "/guilds/1/scheduled-events/2", + FakeRest.resp(200, body: ~s({"id":"2","name":"party","user_count":10})) + ) + + assert {:ok, %Dexcord.GuildScheduledEvent{id: 2, name: "party", user_count: 10}} = + Api.get_guild_scheduled_event(1, 2, with_user_count: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_user_count=true" + end + + test "delete_guild_scheduled_event returns nil (no reason support)" do + FakeRest.stub(:delete, "/guilds/1/scheduled-events/2", FakeRest.resp(204)) + + assert {:ok, nil} = Api.delete_guild_scheduled_event(1, 2) + end + + # --- Emojis / Stickers ------------------------------------------------- + + test "create_guild_emoji sends a base64 image in a plain JSON body (NOT multipart)" do + FakeRest.stub( + :post, + "/guilds/1/emojis", + FakeRest.resp(200, body: ~s({"id":"9","name":"blob"})) + ) + + image = "data:image/png;base64,iVBORw0KGgo=" + + assert {:ok, %Dexcord.Emoji{id: 9, name: "blob"}} = + Api.create_guild_emoji(1, %{"name" => "blob", "image" => image}, reason: "add") + + assert_receive {:rest_hit, info} + headers = Map.new(info.headers) + assert headers["content-type"] == "application/json" + assert JSON.decode!(info.body) == %{"name" => "blob", "image" => image} + end + + test "create_guild_sticker sends plain-form multipart (name/description/tags + file)" do + FakeRest.stub( + :post, + "/guilds/1/stickers", + FakeRest.resp(201, body: ~s({"id":"9","name":"wave"})) + ) + + file = %{filename: "wave.png", data: <<137, 80, 78, 71>>, content_type: "image/png"} + + assert {:ok, %Dexcord.Sticker{id: 9, name: "wave"}} = + Api.create_guild_sticker( + 1, + %{"name" => "wave", "description" => "a wave", "tags" => "wave"}, + files: [file], + reason: "add sticker" + ) + + assert_receive {:rest_hit, info} + headers = Map.new(info.headers) + assert "multipart/form-data; boundary=" <> boundary = headers["content-type"] + assert headers["x-audit-log-reason"] == "add%20sticker" + + named = info.body |> parse_multipart(boundary) |> by_name() + + assert named["name"].data == "wave" + assert named["name"].filename == nil + assert named["description"].data == "a wave" + assert named["tags"].data == "wave" + + assert %{"file" => file_part} = named + assert file_part.filename == "wave.png" + assert file_part.content_type == "image/png" + assert file_part.data == <<137, 80, 78, 71>> + end + + test "list_application_emojis returns the raw {items: [...]} envelope" do + FakeRest.stub( + :get, + "/applications/1/emojis", + FakeRest.resp(200, body: ~s({"items":[]})) + ) + + assert {:ok, %{"items" => []}} = Api.list_application_emojis(1) + end + + # --- Users ------------------------------------------------------------- + + test "edit_current_user PATCHes and decodes a User" do + FakeRest.stub( + :patch, + "/users/@me", + FakeRest.resp(200, body: ~s({"id":"42","username":"bot"})) + ) + + assert {:ok, %Dexcord.User{id: 42, username: "bot"}} = + Api.edit_current_user(%{"username" => "bot"}) + end + + test "get_current_user_guilds forwards the query and decodes PartialGuilds" do + FakeRest.stub( + :get, + "/users/@me/guilds", + FakeRest.resp(200, body: ~s([{"id":"1","name":"g1"},{"id":"2","name":"g2"}])) + ) + + assert {:ok, [%Dexcord.PartialGuild{id: 1, name: "g1"}, %Dexcord.PartialGuild{id: 2}]} = + Api.get_current_user_guilds(limit: 100, with_counts: true) + + assert_receive {:rest_hit, info} + assert URI.decode_query(info.query_string) == %{"limit" => "100", "with_counts" => "true"} + end + + test "leave_guild returns nil" do + FakeRest.stub(:delete, "/users/@me/guilds/1", FakeRest.resp(204)) + + assert {:ok, nil} = Api.leave_guild(1) + end + + # --- Misc: application-info re-point ------------------------------------ + + test "get_current_application now hits the canonical /applications/@me route" do + FakeRest.stub( + :get, + "/applications/@me", + FakeRest.resp(200, body: ~s({"id":"99","name":"App"})) + ) + + assert {:ok, %Dexcord.ApplicationInfo{id: 99}} = Api.get_current_application() + + assert_receive {:rest_hit, info} + assert info.path == "/applications/@me" + end + + test "get_current_bot_application keeps the oauth2 route" do + FakeRest.stub( + :get, + "/oauth2/applications/@me", + FakeRest.resp(200, body: ~s({"id":"99","name":"App"})) + ) + + assert {:ok, %Dexcord.ApplicationInfo{id: 99}} = Api.get_current_bot_application() + + assert_receive {:rest_hit, info} + assert info.path == "/oauth2/applications/@me" + end + + # --- multipart parsing helpers (copied from api_integration_test.exs) --- + + defp parse_multipart(body, boundary) do + body + |> String.split("--" <> boundary) + |> Enum.drop(1) + |> Enum.reject(fn seg -> seg == "--\r\n" or seg == "--" end) + |> Enum.map(fn seg -> + seg = String.replace_prefix(seg, "\r\n", "") + [raw_headers, rest] = String.split(seg, "\r\n\r\n", parts: 2) + data = String.replace_suffix(rest, "\r\n", "") + disposition = header_value(raw_headers, "content-disposition") + + %{ + name: extract(disposition, ~r/name="([^"]*)"/), + filename: extract(disposition, ~r/filename="([^"]*)"/), + content_type: header_value(raw_headers, "content-type"), + data: data + } + end) + end + + defp by_name(parts), do: Map.new(parts, fn part -> {part.name, part} end) + + defp header_value(raw_headers, name) do + raw_headers + |> String.split("\r\n") + |> Enum.find_value(fn line -> + case String.split(line, ": ", parts: 2) do + [key, value] -> if String.downcase(key) == name, do: value + _ -> nil + end + end) + end + + defp extract(nil, _re), do: nil + + defp extract(string, re) do + case Regex.run(re, string) do + [_, captured] -> captured + _ -> nil + end + end +end diff --git a/test/dexcord/api_webhooks_test.exs b/test/dexcord/api_webhooks_test.exs new file mode 100644 index 0000000..b8f9cae --- /dev/null +++ b/test/dexcord/api_webhooks_test.exs @@ -0,0 +1,169 @@ +defmodule Dexcord.ApiWebhooksTest do + @moduledoc """ + Representative wire tests for the `Dexcord.Api.Webhooks` and + `Dexcord.Api.Invites` groups plus the extended `Dexcord.Api.Interactions` + group (Phase 5 Task 4): the `wait` / `thread_id` / `with_response` specials, + multipart webhook-message edit, and token bucketing that never leaks the token. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + # --- Webhooks ----------------------------------------------------------- + + test "create_webhook wraps a binary as name, decodes a Webhook, carries a reason" do + FakeRest.stub( + :post, + "/channels/1/webhooks", + FakeRest.resp(200, body: ~s({"id":"9","name":"hook"})) + ) + + assert {:ok, %Dexcord.Webhook{id: 9, name: "hook"}} = + Api.create_webhook(1, "hook", reason: "notify") + + assert_receive {:rest_hit, info} + assert JSON.decode!(info.body) == %{"name" => "hook"} + assert Map.new(info.headers)["x-audit-log-reason"] == "notify" + end + + test "get_webhook_with_token decodes a Webhook" do + FakeRest.stub(:get, "/webhooks/9/tok", FakeRest.resp(200, body: ~s({"id":"9"}))) + + assert {:ok, %Dexcord.Webhook{id: 9}} = Api.get_webhook_with_token(9, "tok") + end + + test "execute_webhook with no wait sends no query and passes a 204 through as nil" do + FakeRest.stub(:post, "/webhooks/9/tok", FakeRest.resp(204)) + + assert {:ok, nil} = Api.execute_webhook(9, "tok", "hi") + + assert_receive {:rest_hit, info} + assert info.query_string == "" + assert JSON.decode!(info.body) == %{"content" => "hi"} + end + + test "execute_webhook with wait+thread_id forwards the query and decodes a Message" do + FakeRest.stub( + :post, + "/webhooks/9/tok", + FakeRest.resp(200, body: ~s({"id":"5","content":"hi"})) + ) + + assert {:ok, %Dexcord.Message{id: 5, content: "hi"}} = + Api.execute_webhook(9, "tok", "hi", wait: true, thread_id: 123) + + assert_receive {:rest_hit, info} + assert URI.decode_query(info.query_string) == %{"wait" => "true", "thread_id" => "123"} + end + + test "execute_slack_webhook returns the raw compat response" do + FakeRest.stub(:post, "/webhooks/9/tok/slack", FakeRest.resp(200, body: ~s({"ok":true}))) + + assert {:ok, %{"ok" => true}} = Api.execute_slack_webhook(9, "tok", %{"text" => "hi"}) + end + + test "edit_webhook_message with files sends a multipart body and decodes a Message" do + FakeRest.stub( + :patch, + "/webhooks/9/tok/messages/5", + FakeRest.resp(200, body: ~s({"id":"5","content":"edited"})) + ) + + file = %{filename: "a.png", data: <<1, 2, 3>>, content_type: "image/png"} + + assert {:ok, %Dexcord.Message{id: 5, content: "edited"}} = + Api.edit_webhook_message(9, "tok", 5, %{"content" => "edited"}, files: [file]) + + assert_receive {:rest_hit, info} + assert "multipart/form-data; boundary=" <> _ = Map.new(info.headers)["content-type"] + end + + # --- Invites ------------------------------------------------------------ + + test "get_invite forwards with_counts and decodes an Invite" do + FakeRest.stub( + :get, + "/invites/abc", + FakeRest.resp(200, body: ~s({"code":"abc","approximate_member_count":42})) + ) + + assert {:ok, %Dexcord.Invite{code: "abc", approximate_member_count: 42}} = + Api.get_invite("abc", with_counts: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_counts=true" + end + + test "delete_invite decodes the deleted Invite with a reason" do + FakeRest.stub(:delete, "/invites/abc", FakeRest.resp(200, body: ~s({"code":"abc"}))) + + assert {:ok, %Dexcord.Invite{code: "abc"}} = Api.delete_invite("abc", reason: "revoke") + + assert_receive {:rest_hit, info} + assert Map.new(info.headers)["x-audit-log-reason"] == "revoke" + end + + # --- Interactions ------------------------------------------------------- + + test "create_interaction_response forwards the with_response query" do + FakeRest.stub( + :post, + "/interactions/1/itok/callback", + FakeRest.resp(200, body: ~s({"resource":{}})) + ) + + assert {:ok, %{"resource" => %{}}} = + Api.create_interaction_response(1, "itok", %{"type" => 4}, with_response: true) + + assert_receive {:rest_hit, info} + assert info.query_string == "with_response=true" + end + + test "get_followup_message decodes a Message" do + FakeRest.stub( + :get, + "/webhooks/99/itok/messages/5", + FakeRest.resp(200, body: ~s({"id":"5","content":"fu"})) + ) + + assert {:ok, %Dexcord.Message{id: 5, content: "fu"}} = + Api.get_followup_message(99, "itok", 5) + end + + test "delete_followup_message returns nil" do + FakeRest.stub(:delete, "/webhooks/99/itok/messages/5", FakeRest.resp(204)) + + assert {:ok, nil} = Api.delete_followup_message(99, "itok", 5) + end + + # --- Token bucketing ---------------------------------------------------- + + test "the webhook token never appears in the ratelimit route key" do + secret = "super-secret-webhook-token" + key = Ratelimit.route_key(:post, "/webhooks/9/#{secret}/messages/5") + + refute key =~ secret + assert key =~ "webhooks/9/" + # The trailing message id collapses to a bounded placeholder. + assert String.ends_with?(key, "/messages/:id [message-delete]") or + String.ends_with?(key, "/messages/:id") + end +end diff --git a/test/dexcord/cache_cascade_test.exs b/test/dexcord/cache_cascade_test.exs index 9d7d8d1..86c4337 100644 --- a/test/dexcord/cache_cascade_test.exs +++ b/test/dexcord/cache_cascade_test.exs @@ -70,11 +70,11 @@ defmodule Dexcord.CacheCascadeTest do ) # The next dispatch caches fine against the recreated tables - no crash. - guild = %{"id" => "g1", "name" => "Test Guild"} + guild = %{"id" => "1", "name" => "Test Guild"} Dexcord.Dispatcher.dispatch(:GUILD_CREATE, guild) - wait_until(fn -> match?({:ok, _}, Dexcord.Cache.guild("g1")) end, 200) - assert {:ok, %{"id" => "g1", "name" => "Test Guild"}} = Dexcord.Cache.guild("g1") + wait_until(fn -> match?({:ok, _}, Dexcord.Cache.guild(1)) end, 200) + assert {:ok, %Dexcord.Guild{id: 1, name: "Test Guild"}} = Dexcord.Cache.guild(1) # The gateway was never restarted (still the same, still alive) - crash contained. assert Process.whereis(Dexcord.Gateway) == gateway diff --git a/test/dexcord/cache_fill_test.exs b/test/dexcord/cache_fill_test.exs new file mode 100644 index 0000000..6e9174a --- /dev/null +++ b/test/dexcord/cache_fill_test.exs @@ -0,0 +1,103 @@ +defmodule Dexcord.CacheFillTest do + @moduledoc """ + Unit tests for `Dexcord.Cache.fill/1` (api-surface.AC3.8): best-effort + hydration of an envelope's declared `hydrate` slots from ETS only. + + These tests start ONLY `Dexcord.Cache` - no FakeRest, no Finch pool. `fill/1` + reads ETS and nothing else, so any accidental HTTP round-trip would crash on + the missing connection pool; the suite passing is the proof that `fill/1` + never touches the network. + """ + use ExUnit.Case, async: false + + alias Dexcord.Cache + alias Dexcord.Events + + setup do + Dexcord.EnvSandbox.sandbox_env() + start_supervised!(Dexcord.Cache) + :ok + end + + # Seed the cache exactly as the dispatcher would: decode once, hand the cache + # both the decoded struct and the raw partial map. + defp feed(name, raw) do + Cache.handle_dispatch(name, Events.decode(name, raw), raw, %{cache_presences: false}) + end + + # The golden guild's own ids (see test/fixtures/guild_create.json). + @guild_id 900_000_000_000_000_000 + # channel 100000000000000000 is a type-0 text channel. + @channel_id 100_000_000_000_000_000 + # member/user 800000000000000000 is Nelly. + @user_id 800_000_000_000_000_000 + + defp reaction(overrides) do + raw = + Map.merge( + %{ + "user_id" => to_string(@user_id), + "channel_id" => to_string(@channel_id), + "message_id" => "100000000000000099", + "guild_id" => to_string(@guild_id), + "emoji" => %{"id" => nil, "name" => "👍"} + }, + overrides + ) + + Events.decode(:MESSAGE_REACTION_ADD, raw) + end + + describe "fill/1 hydration (AC3.8)" do + test "populates every declared slot from the cache on a full hit" do + feed(:GUILD_CREATE, Dexcord.Fixtures.load!("guild_create.json")) + + event = reaction(%{}) + # After decode the hydrate slots are always nil. + assert %Dexcord.Events.ReactionAdd{user: nil, channel: nil, guild: nil} = event + + filled = Cache.fill(event) + + assert %Dexcord.Events.ReactionAdd{ + user: %Dexcord.User{id: @user_id}, + channel: %Dexcord.TextChannel{id: @channel_id}, + guild: %Dexcord.Guild{id: @guild_id} + } = filled + end + + test "a cache miss leaves that slot nil while the others still fill" do + feed(:GUILD_CREATE, Dexcord.Fixtures.load!("guild_create.json")) + + # Unknown user id: not in the cache. + event = reaction(%{"user_id" => "111111111111111111"}) + + filled = Cache.fill(event) + + assert filled.user == nil + assert %Dexcord.TextChannel{id: @channel_id} = filled.channel + assert %Dexcord.Guild{id: @guild_id} = filled.guild + end + + test "an unseeded cache leaves every slot nil (never blocks on the network)" do + # No GUILD_CREATE seeded and, deliberately, no FakeRest/Finch started: if + # fill/1 ever reached for HTTP it would crash here instead of returning nils. + filled = Cache.fill(reaction(%{})) + + assert %Dexcord.Events.ReactionAdd{user: nil, channel: nil, guild: nil} = filled + end + + test "fill/1 is idempotent" do + feed(:GUILD_CREATE, Dexcord.Fixtures.load!("guild_create.json")) + + event = reaction(%{}) + once = Cache.fill(event) + + assert Cache.fill(once) == once + end + + test "a struct with no hydration slots passes through unchanged" do + user = %Dexcord.User{id: @user_id, username: "nelly"} + assert Cache.fill(user) == user + end + end +end diff --git a/test/dexcord/cache_integration_test.exs b/test/dexcord/cache_integration_test.exs index 29fb37e..e526ac8 100644 --- a/test/dexcord/cache_integration_test.exs +++ b/test/dexcord/cache_integration_test.exs @@ -52,27 +52,27 @@ defmodule Dexcord.CacheIntegrationTest do await_ready() guild = %{ - "id" => "g100", + "id" => "100", "name" => "test", - "channels" => [%{"id" => "c100", "type" => 0}], - "roles" => [%{"id" => "r100", "name" => "everyone"}], - "members" => [%{"user" => %{"id" => "u100", "username" => "alice"}, "nick" => "a"}], - "voice_states" => [%{"user_id" => "u100", "channel_id" => "vc100"}], - "presences" => [%{"user" => %{"id" => "u100"}, "status" => "online"}] + "channels" => [%{"id" => "101", "type" => 0}], + "roles" => [%{"id" => "102", "name" => "everyone"}], + "members" => [%{"user" => %{"id" => "103", "username" => "alice"}, "nick" => "a"}], + "voice_states" => [%{"user_id" => "103", "channel_id" => "104"}], + "presences" => [%{"user" => %{"id" => "103"}, "status" => "online"}] } FakeGateway.push_dispatch(fake, "GUILD_CREATE", guild, 10) # The handler observed a cache that ALREADY had the guild (inline write ordering). - assert_receive {:handler_saw, :GUILD_CREATE, {:ok, %{"id" => "g100"}}}, @timeout + assert_receive {:handler_saw, :GUILD_CREATE, {:ok, %Dexcord.Guild{id: 100}}}, @timeout - # And every fan-out table is populated. - assert {:ok, %{"guild_id" => "g100"}} = Cache.channel("c100") - assert {:ok, %{"name" => "everyone"}} = Cache.role("g100", "r100") - assert {:ok, %{"user_id" => "u100"}} = Cache.member("g100", "u100") - assert {:ok, %{"username" => "alice"}} = Cache.user("u100") - assert {:ok, %{"channel_id" => "vc100"}} = Cache.voice_state("g100", "u100") - assert {:ok, %{"status" => "online"}} = Cache.presence("g100", "u100") + # And every fan-out table is populated with decoded structs keyed by integers. + assert {:ok, %Dexcord.TextChannel{guild_id: 100}} = Cache.channel(101) + assert {:ok, %Dexcord.Role{name: "everyone"}} = Cache.role(100, 102) + assert {:ok, %Dexcord.Member{user_id: 103, user: nil}} = Cache.member(100, 103) + assert {:ok, %Dexcord.User{username: "alice"}} = Cache.user(103) + assert {:ok, %Dexcord.VoiceState{channel_id: 104}} = Cache.voice_state(100, 103) + assert {:ok, %Dexcord.Presence{status: "online"}} = Cache.presence(100, 103) end test "MESSAGE_CREATE author is cached before the handler observes it" do @@ -80,10 +80,11 @@ defmodule Dexcord.CacheIntegrationTest do start_bot(fake, intents: :all) await_ready() - msg = %{"id" => "m1", "author" => %{"id" => "au1", "username" => "bob"}, "content" => "hi"} + msg = %{"id" => "1", "author" => %{"id" => "200", "username" => "bob"}, "content" => "hi"} FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", msg, 11) - assert_receive {:handler_saw, :MESSAGE_CREATE, {:ok, %{"username" => "bob"}}}, @timeout + assert_receive {:handler_saw, :MESSAGE_CREATE, {:ok, %Dexcord.User{username: "bob"}}}, + @timeout end test "chunking: GUILD_CREATE -> client sends op 8 -> CHUNK populates members" do @@ -93,21 +94,21 @@ defmodule Dexcord.CacheIntegrationTest do await_ready() # A real (non-stub) GUILD_CREATE with no inline members triggers the op 8 request. - FakeGateway.push_dispatch(fake, "GUILD_CREATE", %{"id" => "g200", "name" => "big"}, 20) + FakeGateway.push_dispatch(fake, "GUILD_CREATE", %{"id" => "400", "name" => "big"}, 20) # The client asks for members (op 8) with the documented query/limit/presences. assert_receive {:fake_gw, :frame, _c, %{"op" => 8, "d" => d}}, @timeout - assert d["guild_id"] == "g200" + assert d["guild_id"] == "400" assert d["query"] == "" assert d["limit"] == 0 assert d["presences"] == false # The server answers with a chunk; the members flow through dispatch into cache. chunk = %{ - "guild_id" => "g200", + "guild_id" => "400", "members" => [ - %{"user" => %{"id" => "cu1", "username" => "x"}}, - %{"user" => %{"id" => "cu2", "username" => "y"}} + %{"user" => %{"id" => "401", "username" => "x"}}, + %{"user" => %{"id" => "402", "username" => "y"}} ] } @@ -116,8 +117,8 @@ defmodule Dexcord.CacheIntegrationTest do assert_receive {:handler_saw, :GUILD_MEMBERS_CHUNK, members} when length(members) == 2, @timeout - assert {:ok, _} = Cache.member("g200", "cu1") - assert {:ok, _} = Cache.member("g200", "cu2") + assert {:ok, %Dexcord.Member{}} = Cache.member(400, 401) + assert {:ok, %Dexcord.Member{}} = Cache.member(400, 402) end test "no op 8 is sent when request_guild_members is false" do @@ -125,8 +126,8 @@ defmodule Dexcord.CacheIntegrationTest do start_bot(fake, intents: :all, request_guild_members: false) await_ready() - FakeGateway.push_dispatch(fake, "GUILD_CREATE", %{"id" => "g300", "name" => "q"}, 30) - assert_receive {:handler_saw, :GUILD_CREATE, {:ok, _}}, @timeout + FakeGateway.push_dispatch(fake, "GUILD_CREATE", %{"id" => "500", "name" => "q"}, 30) + assert_receive {:handler_saw, :GUILD_CREATE, {:ok, %Dexcord.Guild{}}}, @timeout refute_receive {:fake_gw, :frame, _c, %{"op" => 8}}, 300 end diff --git a/test/dexcord/cache_test.exs b/test/dexcord/cache_test.exs index 12f3b83..04889ad 100644 --- a/test/dexcord/cache_test.exs +++ b/test/dexcord/cache_test.exs @@ -1,9 +1,10 @@ defmodule Dexcord.CacheTest do @moduledoc """ - Unit tests for `Dexcord.Cache.handle_dispatch/3` against real ETS. Each test - starts a fresh `Dexcord.Cache` (which owns and creates the tables), feeds it - string-keyed payload maps exactly as they arrive off the wire, and asserts table - contents through the public read API. + Unit tests for `Dexcord.Cache.handle_dispatch/4` against real ETS. Each test + starts a fresh `Dexcord.Cache` (which owns and creates the tables), feeds it a + raw string-keyed payload map decoded exactly as the dispatcher would (via + `feed/3`), and asserts table contents through the public read API - which now + returns model structs keyed by integer snowflakes (api-surface.AC2.20). """ use ExUnit.Case, async: false @@ -17,341 +18,424 @@ defmodule Dexcord.CacheTest do :ok end - defp user(id, name \\ "u"), do: %{"id" => id, "username" => "#{name}#{id}"} + # Feed a raw wire payload exactly as the dispatcher does: decode once, then hand + # the cache both the decoded struct and the raw partial map. + defp feed(name, raw, config) do + Cache.handle_dispatch(name, Dexcord.Events.decode(name, raw), raw, config) + end + + defp user(id, name \\ "u"), do: %{"id" => to_string(id), "username" => "#{name}#{id}"} defp member(uid, extra \\ %{}), do: Map.merge(%{"user" => user(uid), "nick" => "nick#{uid}"}, extra) - # A full-ish guild payload as GUILD_CREATE delivers it. + # A full-ish guild payload as GUILD_CREATE delivers it. Child ids are derived + # from `gid` so a test can reference them: channel `gid+1`, role `gid+2`, member + # user `gid+3`, its voice channel `gid+4`. defp guild_create(gid, opts \\ []) do %{ - "id" => gid, + "id" => to_string(gid), "name" => "guild#{gid}", - "emojis" => [%{"id" => "e1", "name" => "smile"}], - "channels" => Keyword.get(opts, :channels, [%{"id" => "c#{gid}", "type" => 0}]), + "emojis" => [%{"id" => to_string(gid + 90), "name" => "smile"}], + "channels" => Keyword.get(opts, :channels, [%{"id" => to_string(gid + 1), "type" => 0}]), "threads" => Keyword.get(opts, :threads, []), - "roles" => Keyword.get(opts, :roles, [%{"id" => "r#{gid}", "name" => "everyone"}]), - "members" => Keyword.get(opts, :members, [member("m#{gid}")]), + "roles" => Keyword.get(opts, :roles, [%{"id" => to_string(gid + 2), "name" => "everyone"}]), + "members" => Keyword.get(opts, :members, [member(gid + 3)]), "voice_states" => - Keyword.get(opts, :voice_states, [%{"user_id" => "m#{gid}", "channel_id" => "vc#{gid}"}]), + Keyword.get(opts, :voice_states, [ + %{"user_id" => to_string(gid + 3), "channel_id" => to_string(gid + 4)} + ]), "presences" => - Keyword.get(opts, :presences, [%{"user" => %{"id" => "m#{gid}"}, "status" => "online"}]) + Keyword.get(opts, :presences, [ + %{"user" => %{"id" => to_string(gid + 3)}, "status" => "online"} + ]) } end describe "READY" do - test "stores the bot user and unavailable guild stubs" do + test "stores the bot user (struct) and unavailable guild stubs" do data = %{ - "user" => user("bot1", "self"), + "user" => user(10, "self"), "guilds" => [ - %{"id" => "g1", "unavailable" => true}, - %{"id" => "g2", "unavailable" => true} + %{"id" => "1", "unavailable" => true}, + %{"id" => "2", "unavailable" => true} ] } - Cache.handle_dispatch(:READY, data, @off) + feed(:READY, data, @off) - assert {:ok, %{"id" => "bot1"}} = Cache.me() - assert {:ok, %{"unavailable" => true}} = Cache.guild("g1") - assert {:ok, %{"unavailable" => true}} = Cache.guild("g2") + assert {:ok, %Dexcord.User{id: 10}} = Cache.me() + assert {:ok, %Dexcord.UnavailableGuild{id: 1, unavailable: true}} = Cache.guild(1) + assert {:ok, %Dexcord.UnavailableGuild{id: 2}} = Cache.guild(2) end end describe "GUILD_CREATE" do - test "stores guild row with big arrays stripped and fans out children" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) + test "stores struct guild row with big arrays stripped and fans out children" do + feed(:GUILD_CREATE, guild_create(100), @on) - {:ok, g} = Cache.guild("g1") - # Big arrays stripped... - for k <- ~w(channels threads roles members presences voice_states) do - refute Map.has_key?(g, k), "expected #{k} stripped from guild row" + {:ok, g} = Cache.guild(100) + assert %Dexcord.Guild{id: 100} = g + # Big arrays stripped to empty... + for f <- [:channels, :threads, :roles, :members, :presences, :voice_states] do + assert Map.get(g, f) == [], "expected #{f} emptied on the guild row" end - # ...emojis stay inline. - assert [%{"name" => "smile"}] = g["emojis"] + # ...emojis stay inline as decoded structs. + assert [%Dexcord.Emoji{name: "smile"}] = g.emojis - # Fan-out: channel carries an injected guild_id. - assert {:ok, %{"guild_id" => "g1", "type" => 0}} = Cache.channel("cg1") - assert [%{"id" => "cg1"}] = Cache.channels("g1") + # AC2.20: wrote from string-snowflake wire data, read back with INTEGER key. + assert {:ok, %Dexcord.TextChannel{guild_id: 100}} = Cache.channel(101) + assert [%Dexcord.TextChannel{id: 101}] = Cache.channels(100) - assert {:ok, %{"name" => "everyone"}} = Cache.role("g1", "rg1") - assert [%{"id" => "rg1"}] = Cache.roles("g1") + assert {:ok, %Dexcord.Role{name: "everyone"}} = Cache.role(100, 102) + assert [%Dexcord.Role{id: 102}] = Cache.roles(100) - # Member: "user" moved to users table, member keeps "user_id". - {:ok, m} = Cache.member("g1", "mg1") - assert m["user_id"] == "mg1" - refute Map.has_key?(m, "user") - assert {:ok, %{"username" => _}} = Cache.user("mg1") + # Member: nested "user" moved to users table; row keeps user_id, user: nil. + assert {:ok, %Dexcord.Member{user_id: 103, user: nil, nick: "nick103"}} = + Cache.member(100, 103) - assert {:ok, %{"channel_id" => "vcg1"}} = Cache.voice_state("g1", "mg1") - assert {:ok, %{"status" => "online"}} = Cache.presence("g1", "mg1") + assert {:ok, %Dexcord.User{id: 103}} = Cache.user(103) + + assert {:ok, %Dexcord.VoiceState{channel_id: 104}} = Cache.voice_state(100, 103) + assert {:ok, %Dexcord.Presence{status: "online"}} = Cache.presence(100, 103) end test "presences are skipped when cache_presences is false" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @off) - assert Cache.presence("g1", "mg1") == :error - assert Cache.presences("g1") == [] + feed(:GUILD_CREATE, guild_create(100), @off) + assert Cache.presence(100, 103) == :error + assert Cache.presences(100) == [] # But members/voice_states still populate. - assert {:ok, _} = Cache.member("g1", "mg1") - assert {:ok, _} = Cache.voice_state("g1", "mg1") + assert {:ok, %Dexcord.Member{}} = Cache.member(100, 103) + assert {:ok, %Dexcord.VoiceState{}} = Cache.voice_state(100, 103) end end - describe "GUILD_UPDATE / GUILD_EMOJIS_UPDATE" do - test "GUILD_UPDATE merges into the existing row without touching child tables" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) - Cache.handle_dispatch(:GUILD_UPDATE, %{"id" => "g1", "name" => "renamed"}, @on) + describe "GUILD_UPDATE and child tables" do + test "updates the guild row without touching the per-guild child tables" do + feed(:GUILD_CREATE, guild_create(100), @on) + feed(:GUILD_UPDATE, %{"id" => "100", "name" => "renamed"}, @on) - {:ok, g} = Cache.guild("g1") - assert g["name"] == "renamed" + assert {:ok, %Dexcord.Guild{name: "renamed"}} = Cache.guild(100) # Members untouched by a guild update. - assert {:ok, _} = Cache.member("g1", "mg1") - end - - test "GUILD_EMOJIS_UPDATE replaces the inline emoji list" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) - - Cache.handle_dispatch( - :GUILD_EMOJIS_UPDATE, - %{"guild_id" => "g1", "emojis" => [%{"id" => "e9", "name" => "wave"}]}, - @on - ) - - {:ok, g} = Cache.guild("g1") - assert [%{"id" => "e9", "name" => "wave"}] = g["emojis"] - end - end - - describe "GUILD_DELETE cascade" do - test "purge wipes exactly that guild's rows across every per-guild table, and no others" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g2"), @on) - - Cache.handle_dispatch(:GUILD_DELETE, %{"id" => "g1"}, @on) - - # g1 gone everywhere. - assert Cache.guild("g1") == :error - assert Cache.members("g1") == [] - assert Cache.roles("g1") == [] - assert Cache.presences("g1") == [] - assert Cache.voice_states("g1") == [] - assert Cache.channels("g1") == [] - - # g2 fully intact. - assert {:ok, _} = Cache.guild("g2") - assert [_] = Cache.members("g2") - assert [_] = Cache.roles("g2") - assert [_] = Cache.presences("g2") - assert [_] = Cache.voice_states("g2") - assert [_] = Cache.channels("g2") - end - - test "unavailable:true keeps a stub instead of purging" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) - Cache.handle_dispatch(:GUILD_DELETE, %{"id" => "g1", "unavailable" => true}, @on) - - assert {:ok, %{"unavailable" => true}} = Cache.guild("g1") + assert {:ok, %Dexcord.Member{}} = Cache.member(100, 103) end end describe "channels and threads" do - test "CHANNEL_CREATE/UPDATE/DELETE and THREAD_* upsert and remove" do - Cache.handle_dispatch( - :CHANNEL_CREATE, - %{"id" => "c1", "guild_id" => "g1", "name" => "gen"}, - @off - ) + test "CHANNEL_CREATE/UPDATE/DELETE and THREAD_* upsert and remove structs" do + feed(:CHANNEL_CREATE, %{"id" => "1", "type" => 0, "guild_id" => "5", "name" => "gen"}, @off) + assert {:ok, %Dexcord.TextChannel{name: "gen"}} = Cache.channel(1) - assert {:ok, %{"name" => "gen"}} = Cache.channel("c1") - - Cache.handle_dispatch( + feed( :CHANNEL_UPDATE, - %{"id" => "c1", "guild_id" => "g1", "name" => "renamed"}, + %{"id" => "1", "type" => 0, "guild_id" => "5", "name" => "renamed"}, @off ) - assert {:ok, %{"name" => "renamed"}} = Cache.channel("c1") + assert {:ok, %Dexcord.TextChannel{name: "renamed"}} = Cache.channel(1) - Cache.handle_dispatch(:THREAD_CREATE, %{"id" => "t1", "guild_id" => "g1"}, @off) - assert {:ok, _} = Cache.channel("t1") + feed(:THREAD_CREATE, %{"id" => "2", "type" => 11, "guild_id" => "5"}, @off) + assert {:ok, %Dexcord.Thread{}} = Cache.channel(2) - Cache.handle_dispatch(:THREAD_DELETE, %{"id" => "t1"}, @off) - assert Cache.channel("t1") == :error + feed(:THREAD_DELETE, %{"id" => "2", "guild_id" => "5"}, @off) + assert Cache.channel(2) == :error - Cache.handle_dispatch(:CHANNEL_DELETE, %{"id" => "c1"}, @off) - assert Cache.channel("c1") == :error + feed(:CHANNEL_DELETE, %{"id" => "1", "type" => 0}, @off) + assert Cache.channel(1) == :error end end describe "threads/1" do - test "returns only thread-typed channels for the given guild" do + test "returns only Thread structs for the given guild" do g1 = - guild_create("g1", + guild_create(100, channels: [ - %{"id" => "c1", "type" => 0}, - %{"id" => "t1", "type" => 10}, - %{"id" => "t2", "type" => 11}, - %{"id" => "t3", "type" => 12} + %{"id" => "1", "type" => 0}, + %{"id" => "2", "type" => 10}, + %{"id" => "3", "type" => 11}, + %{"id" => "4", "type" => 12} ], threads: [] ) g2 = - guild_create("g2", - channels: [%{"id" => "c2", "type" => 0}, %{"id" => "t4", "type" => 11}], + guild_create(200, + channels: [%{"id" => "5", "type" => 0}, %{"id" => "6", "type" => 11}], threads: [] ) - Cache.handle_dispatch(:GUILD_CREATE, g1, @off) - Cache.handle_dispatch(:GUILD_CREATE, g2, @off) + feed(:GUILD_CREATE, g1, @off) + feed(:GUILD_CREATE, g2, @off) - assert Cache.threads("g1") |> Enum.map(& &1["id"]) |> Enum.sort() == ["t1", "t2", "t3"] - assert Cache.threads("g2") |> Enum.map(& &1["id"]) == ["t4"] - assert Cache.threads("nope") == [] + assert Cache.threads(100) |> Enum.map(& &1.id) |> Enum.sort() == [2, 3, 4] + assert Cache.threads(200) |> Enum.map(& &1.id) == [6] + assert Cache.threads(999) == [] end end describe "members" do - test "ADD/UPDATE move user out and merge; REMOVE deletes the member" do - Cache.handle_dispatch(:GUILD_MEMBER_ADD, member("u1", %{"guild_id" => "g1"}), @off) - {:ok, m} = Cache.member("g1", "u1") - assert m["user_id"] == "u1" - assert m["nick"] == "nicku1" - assert {:ok, _} = Cache.user("u1") + test "GUILD_MEMBER_ADD moves user out; GUILD_MEMBER_REMOVE deletes the member" do + feed(:GUILD_MEMBER_ADD, member(1, %{"guild_id" => "5"}), @off) - # Update merges (keeps prior fields, changes nick). - Cache.handle_dispatch( - :GUILD_MEMBER_UPDATE, - %{"guild_id" => "g1", "user" => user("u1"), "nick" => "new"}, - @off - ) + assert {:ok, %Dexcord.Member{user_id: 1, user: nil, nick: "nick1"}} = Cache.member(5, 1) + assert {:ok, %Dexcord.User{id: 1}} = Cache.user(1) - {:ok, m2} = Cache.member("g1", "u1") - assert m2["nick"] == "new" - - Cache.handle_dispatch( - :GUILD_MEMBER_REMOVE, - %{"guild_id" => "g1", "user" => user("u1")}, - @off - ) - - assert Cache.member("g1", "u1") == :error + feed(:GUILD_MEMBER_REMOVE, %{"guild_id" => "5", "user" => user(1)}, @off) + assert Cache.member(5, 1) == :error # User survives removal from one guild. - assert {:ok, _} = Cache.user("u1") + assert {:ok, %Dexcord.User{id: 1}} = Cache.user(1) end test "GUILD_MEMBERS_CHUNK bulk upserts members + users, and presences when present" do data = %{ - "guild_id" => "g1", - "members" => [member("u1"), member("u2")], - "presences" => [%{"user" => %{"id" => "u1"}, "status" => "idle"}] + "guild_id" => "5", + "members" => [member(1), member(2)], + "presences" => [%{"user" => %{"id" => "1"}, "status" => "idle"}] } - Cache.handle_dispatch(:GUILD_MEMBERS_CHUNK, data, @on) + feed(:GUILD_MEMBERS_CHUNK, data, @on) - assert length(Cache.members("g1")) == 2 - assert {:ok, _} = Cache.user("u1") - assert {:ok, _} = Cache.user("u2") - assert {:ok, %{"status" => "idle"}} = Cache.presence("g1", "u1") + assert length(Cache.members(5)) == 2 + assert {:ok, %Dexcord.User{}} = Cache.user(1) + assert {:ok, %Dexcord.User{}} = Cache.user(2) + assert {:ok, %Dexcord.Presence{status: "idle"}} = Cache.presence(5, 1) end end describe "roles" do test "ROLE_CREATE/UPDATE/DELETE" do - Cache.handle_dispatch( + feed( :GUILD_ROLE_CREATE, - %{"guild_id" => "g1", "role" => %{"id" => "r1", "name" => "mod"}}, + %{"guild_id" => "5", "role" => %{"id" => "9", "name" => "mod"}}, @off ) - assert {:ok, %{"name" => "mod"}} = Cache.role("g1", "r1") + assert {:ok, %Dexcord.Role{name: "mod"}} = Cache.role(5, 9) - Cache.handle_dispatch( + feed( :GUILD_ROLE_UPDATE, - %{"guild_id" => "g1", "role" => %{"id" => "r1", "name" => "admin"}}, + %{"guild_id" => "5", "role" => %{"id" => "9", "name" => "admin"}}, @off ) - assert {:ok, %{"name" => "admin"}} = Cache.role("g1", "r1") + assert {:ok, %Dexcord.Role{name: "admin"}} = Cache.role(5, 9) - Cache.handle_dispatch(:GUILD_ROLE_DELETE, %{"guild_id" => "g1", "role_id" => "r1"}, @off) - assert Cache.role("g1", "r1") == :error + feed(:GUILD_ROLE_DELETE, %{"guild_id" => "5", "role_id" => "9"}, @off) + assert Cache.role(5, 9) == :error end end describe "presences toggle" do test "PRESENCE_UPDATE writes only when cache_presences is true" do - p = %{"guild_id" => "g1", "user" => %{"id" => "u1"}, "status" => "dnd"} + p = %{"guild_id" => "5", "user" => %{"id" => "1"}, "status" => "dnd"} - Cache.handle_dispatch(:PRESENCE_UPDATE, p, @off) - assert Cache.presence("g1", "u1") == :error + feed(:PRESENCE_UPDATE, p, @off) + assert Cache.presence(5, 1) == :error - Cache.handle_dispatch(:PRESENCE_UPDATE, p, @on) - assert {:ok, %{"status" => "dnd"}} = Cache.presence("g1", "u1") + feed(:PRESENCE_UPDATE, p, @on) + assert {:ok, %Dexcord.Presence{status: "dnd"}} = Cache.presence(5, 1) end end describe "voice states" do test "upsert on channel_id, delete when channel_id is nil" do - Cache.handle_dispatch( - :VOICE_STATE_UPDATE, - %{"guild_id" => "g1", "user_id" => "u1", "channel_id" => "vc1"}, - @off - ) + feed(:VOICE_STATE_UPDATE, %{"guild_id" => "5", "user_id" => "1", "channel_id" => "7"}, @off) + assert {:ok, %Dexcord.VoiceState{channel_id: 7}} = Cache.voice_state(5, 1) - assert {:ok, %{"channel_id" => "vc1"}} = Cache.voice_state("g1", "u1") - - Cache.handle_dispatch( - :VOICE_STATE_UPDATE, - %{"guild_id" => "g1", "user_id" => "u1", "channel_id" => nil}, - @off - ) - - assert Cache.voice_state("g1", "u1") == :error + feed(:VOICE_STATE_UPDATE, %{"guild_id" => "5", "user_id" => "1", "channel_id" => nil}, @off) + assert Cache.voice_state(5, 1) == :error end end - describe "USER_UPDATE and MESSAGE_CREATE" do - test "USER_UPDATE merges into me" do - Cache.handle_dispatch(:READY, %{"user" => user("bot1", "self"), "guilds" => []}, @off) - Cache.handle_dispatch(:USER_UPDATE, %{"id" => "bot1", "username" => "renamed"}, @off) - assert {:ok, %{"username" => "renamed"}} = Cache.me() + describe "GUILD_UPDATE / GUILD_EMOJIS_UPDATE / GUILD_STICKERS_UPDATE" do + test "GUILD_UPDATE merge preserves gateway-only fields like joined_at (AC2.21)" do + golden = Dexcord.Fixtures.load!("guild_create.json") + gid = golden["id"] + feed(:GUILD_CREATE, golden, @off) + + original = Cache.guild!(gid) + assert %DateTime{} = original.joined_at + + feed(:GUILD_UPDATE, %{"id" => gid, "name" => "renamed"}, @off) + + updated = Cache.guild!(gid) + assert updated.name == "renamed" + # joined_at is absent from GUILD_UPDATE payloads: the merge must not drop it. + assert updated.joined_at == original.joined_at + assert updated.verification_level == original.verification_level end - test "MESSAGE_CREATE opportunistically upserts author and member fragment" do + test "GUILD_UPDATE on an unknown guild stores it decoded (minus children)" do + feed(:GUILD_UPDATE, %{"id" => "100", "name" => "fresh"}, @off) + assert {:ok, %Dexcord.Guild{id: 100, name: "fresh", members: []}} = Cache.guild(100) + end + + test "GUILD_EMOJIS_UPDATE replaces the inline emoji list" do + feed(:GUILD_CREATE, guild_create(100), @off) + + feed( + :GUILD_EMOJIS_UPDATE, + %{"guild_id" => "100", "emojis" => [%{"id" => "77", "name" => "wave"}]}, + @off + ) + + {:ok, g} = Cache.guild(100) + assert [%Dexcord.Emoji{name: "wave"}] = g.emojis + end + + test "GUILD_STICKERS_UPDATE replaces the inline sticker list" do + feed(:GUILD_CREATE, guild_create(100), @off) + + feed( + :GUILD_STICKERS_UPDATE, + %{"guild_id" => "100", "stickers" => [%{"id" => "88", "name" => "party"}]}, + @off + ) + + {:ok, g} = Cache.guild(100) + assert [%Dexcord.Sticker{name: "party"}] = g.stickers + end + end + + describe "GUILD_MEMBER_UPDATE merge" do + test "merges only present keys and creates the row when absent" do + feed( + :GUILD_MEMBER_ADD, + member(1, %{"guild_id" => "5", "joined_at" => "2020-01-01T00:00:00Z"}), + @off + ) + + {:ok, before} = Cache.member(5, 1) + assert before.nick == "nick1" + assert %DateTime{} = before.joined_at + + feed( + :GUILD_MEMBER_UPDATE, + %{"guild_id" => "5", "user" => user(1), "roles" => ["9", "10"]}, + @off + ) + + {:ok, updated} = Cache.member(5, 1) + assert updated.roles == [9, 10] + # nick / joined_at were not in the update payload: they must survive. + assert updated.nick == "nick1" + assert updated.joined_at == before.joined_at + assert updated.user == nil + assert updated.user_id == 1 + + # No existing row: the update creates one. + feed(:GUILD_MEMBER_UPDATE, %{"guild_id" => "5", "user" => user(2), "nick" => "new"}, @off) + {:ok, created} = Cache.member(5, 2) + assert created.nick == "new" + assert created.user_id == 2 + assert created.user == nil + end + end + + describe "USER_UPDATE merge" do + test "merges the partial into the cached me record, preserving absent fields" do + feed( + :READY, + %{ + "user" => %{"id" => "10", "username" => "self", "global_name" => "Selfy"}, + "guilds" => [] + }, + @off + ) + + feed(:USER_UPDATE, %{"id" => "10", "username" => "renamed"}, @off) + + {:ok, me} = Cache.me() + assert me.username == "renamed" + # global_name was absent from the update: merge must preserve it. + assert me.global_name == "Selfy" + assert me.id == 10 + end + end + + describe "MESSAGE_CREATE fold-in" do + test "folds author + member fragment and stamps user_id" do data = %{ - "guild_id" => "g1", - "author" => user("a1"), - "member" => %{"nick" => "frag", "roles" => ["r1"]} + "id" => "1", + "guild_id" => "5", + "author" => user(2), + "member" => %{"nick" => "frag", "roles" => ["9"]} } - Cache.handle_dispatch(:MESSAGE_CREATE, data, @off) + feed(:MESSAGE_CREATE, data, @off) - assert {:ok, _} = Cache.user("a1") - {:ok, m} = Cache.member("g1", "a1") - assert m["user_id"] == "a1" - assert m["nick"] == "frag" + assert {:ok, %Dexcord.User{id: 2}} = Cache.user(2) + {:ok, m} = Cache.member(5, 2) + assert m.user_id == 2 + assert m.user == nil + assert m.nick == "frag" + assert m.roles == [9] end - test "MESSAGE_CREATE ignores webhook authors" do - data = %{"webhook_id" => "wh1", "author" => %{"id" => "wh1", "username" => "hook"}} - Cache.handle_dispatch(:MESSAGE_CREATE, data, @off) - assert Cache.user("wh1") == :error + test "ignores webhook authors (writes nothing)" do + data = %{ + "id" => "1", + "webhook_id" => "99", + "author" => %{"id" => "99", "username" => "hook"} + } + + feed(:MESSAGE_CREATE, data, @off) + assert Cache.user(99) == :error + end + end + + describe "GUILD_DELETE cascade (AC2.22)" do + test "purge wipes exactly that guild's rows across every per-guild table, and no others" do + feed(:GUILD_CREATE, guild_create(100), @on) + feed(:GUILD_CREATE, guild_create(200), @on) + + feed(:GUILD_DELETE, %{"id" => "100"}, @on) + + # Guild 100 gone everywhere - including the struct-valued channels table. + assert Cache.guild(100) == :error + assert Cache.members(100) == [] + assert Cache.roles(100) == [] + assert Cache.presences(100) == [] + assert Cache.voice_states(100) == [] + assert Cache.channels(100) == [] + + # Guild 200 fully intact. + assert {:ok, %Dexcord.Guild{}} = Cache.guild(200) + assert [_] = Cache.members(200) + assert [_] = Cache.roles(200) + assert [_] = Cache.presences(200) + assert [_] = Cache.voice_states(200) + assert [_] = Cache.channels(200) + end + + test "unavailable:true keeps a stub instead of purging" do + feed(:GUILD_CREATE, guild_create(100), @on) + feed(:GUILD_DELETE, %{"id" => "100", "unavailable" => true}, @on) + assert {:ok, %Dexcord.UnavailableGuild{unavailable: true}} = Cache.guild(100) end end describe "read API shapes" do - test "bang variants return the value or raise" do - Cache.handle_dispatch(:GUILD_CREATE, guild_create("g1"), @on) - assert %{"id" => "g1"} = Cache.guild!("g1") - assert_raise RuntimeError, fn -> Cache.guild!("nope") end - assert_raise RuntimeError, fn -> Cache.member!("g1", "nope") end + test "bang variants return the struct or raise" do + feed(:GUILD_CREATE, guild_create(100), @on) + assert %Dexcord.Guild{id: 100} = Cache.guild!(100) + assert_raise RuntimeError, fn -> Cache.guild!(999) end + assert_raise RuntimeError, fn -> Cache.member!(100, 999) end end test "listings return plain lists (empty, never :error)" do assert Cache.guilds() == [] - assert Cache.channels("g1") == [] - assert Cache.members("g1") == [] + assert Cache.channels(5) == [] + assert Cache.members(5) == [] + end + + test "reads accept string and integer ids interchangeably" do + feed(:GUILD_CREATE, guild_create(100), @off) + assert {:ok, %Dexcord.Guild{id: 100}} = Cache.guild(100) + assert {:ok, %Dexcord.Guild{id: 100}} = Cache.guild("100") + # Non-castable ids read as a miss, never a crash. + assert Cache.guild("not-a-snowflake") == :error end end end diff --git a/test/dexcord/config_validation_test.exs b/test/dexcord/config_validation_test.exs index a591a36..841b494 100644 --- a/test/dexcord/config_validation_test.exs +++ b/test/dexcord/config_validation_test.exs @@ -75,4 +75,25 @@ defmodule Dexcord.ConfigValidationTest do assert %{slash_guild_ids: nil} = validate([]) end end + + describe "allowed_mentions" do + test "a keyword is normalized to a string-keyed wire map" do + assert %{allowed_mentions: %{"parse" => []}} = validate(allowed_mentions: [parse: []]) + end + + test "a struct is normalized via to_map" do + assert %{allowed_mentions: %{"parse" => [], "replied_user" => false}} = + validate(allowed_mentions: %Dexcord.AllowedMentions{}) + end + + test "absent defaults to nil" do + assert %{allowed_mentions: nil} = validate([]) + end + + test "an invalid type raises a friendly ArgumentError" do + assert_raise ArgumentError, ~r/:allowed_mentions must be/, fn -> + validate(allowed_mentions: 123) + end + end + end end diff --git a/test/dexcord/coverage_test.exs b/test/dexcord/coverage_test.exs new file mode 100644 index 0000000..f9a9b4f --- /dev/null +++ b/test/dexcord/coverage_test.exs @@ -0,0 +1,73 @@ +defmodule Dexcord.Api.CoverageTest do + @moduledoc """ + The coverage gate as a hermetic unit test: everything reads only committed + files (`priv/discord_openapi.json`, `priv/coverage_allowlist.exs`) and the + compiled route table — no network, no live Discord. + """ + use ExUnit.Case, async: true + + alias Dexcord.Api.Coverage + + describe "check/1 (the gate)" do + # api-surface.AC1.8: the pinned spec is fully covered by declarations + + # allowlist. THIS is the assertion CI runs as `mix dexcord.coverage`. + test "passes against the real pinned spec" do + assert {:ok, stats} = Coverage.check() + assert stats.declared > 0 + assert stats.spec > 0 + assert stats.allowlisted > 0 + end + + # api-surface.AC1.9: dropping any one declared route re-exposes it as missing + # (this is exactly what a deleted route-table entry would do). The mix task's + # non-zero exit is a thin shell wrapper over this {:missing, _} return. + test "reports missing when a declared route is removed" do + declared = Coverage.declared_routes() + dropped = tl(declared) + + # The dropped head must be a spec route (otherwise removing it changes + # nothing) — pick one that is guaranteed present in the spec. + assert {:missing, missing} = Coverage.check(dropped) + assert is_list(missing) + assert missing != [] + # missing is sorted and deduped strings. + assert missing == Enum.sort(missing) + assert Enum.all?(missing, &is_binary/1) + end + end + + describe "normalization" do + test "spec and declared routes agree on param collapse for get_channel" do + assert "GET /channels/*" in Coverage.spec_routes() + assert "GET /channels/*" in Coverage.declared_routes() + end + + test "routes are normalized VERB /path strings" do + for route <- Coverage.declared_routes() do + assert route =~ ~r{^(GET|POST|PUT|PATCH|DELETE) /} + end + end + end + + describe "allowlist matching" do + # allowlisted?/2 is private; exercise it through check/1 with a hand-built + # declared set so the match semantics are observable. + test "an unlisted, undeclared spec route is reported missing" do + # Declaring nothing surfaces every non-allowlisted spec route as missing. + assert {:missing, missing} = Coverage.check([]) + assert missing != [] + # Allowlisted families (e.g. lobbies) must NOT appear in the missing set. + refute Enum.any?(missing, &String.starts_with?(&1, "GET /lobbies")) + refute Enum.any?(missing, &String.starts_with?(&1, "POST /partner-sdk")) + end + + test "exact and trailing-* patterns both suppress their routes" do + # An exact allowlist entry ("GET /oauth2/@me") and a prefix entry + # ("GET /lobbies*") both keep their routes out of the missing set even + # when nothing is declared. + assert {:missing, missing} = Coverage.check([]) + refute "GET /oauth2/@me" in missing + refute "GET /lobbies/*" in missing + end + end +end diff --git a/test/dexcord/endpoint_macro_test.exs b/test/dexcord/endpoint_macro_test.exs new file mode 100644 index 0000000..6b6d903 --- /dev/null +++ b/test/dexcord/endpoint_macro_test.exs @@ -0,0 +1,175 @@ +defmodule Dexcord.Api.EndpointMacroTest do + @moduledoc """ + Round-trip tests for the `endpoint/4` macro: the generated functions issue the + right method/path/query/headers/body through the real `Dexcord.Api` pipeline + against the scripted `Dexcord.FakeRest` server, and decode responses per their + declared `returns:`. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api.Error + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + alias Dexcord.Test.Endpoints + alias Dexcord.Test.Widget + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + describe "AC1.1: method + interpolated path + typed decode" do + test "get_widget/1 issues GET and decodes the returns struct" do + FakeRest.stub(:get, "/channels/1/widget", FakeRest.resp(200, body: ~s({"id":"77"}))) + + assert {:ok, %Widget{id: 77}} = Endpoints.get_widget(1) + + assert_receive {:rest_hit, info} + assert info.method == "GET" + assert info.path == "/channels/1/widget" + end + end + + describe "AC1.2: query params" do + test "declared query keys are URL-encoded when present" do + FakeRest.stub(:get, "/channels/1/widget", FakeRest.resp(200, body: ~s({"id":"1"}))) + + assert {:ok, %Widget{}} = Endpoints.get_widget(1, limit: 5, around: 99) + + assert_receive {:rest_hit, info} + assert URI.decode_query(info.query_string) == %{"limit" => "5", "around" => "99"} + end + + test "no query opts sends an empty query string" do + FakeRest.stub(:get, "/channels/1/widget", FakeRest.resp(200, body: ~s({"id":"1"}))) + + assert {:ok, %Widget{}} = Endpoints.get_widget(1) + + assert_receive {:rest_hit, info} + assert info.query_string == "" + end + end + + describe "AC1.3: reason header" do + test "reason: endpoints send a URL-encoded X-Audit-Log-Reason" do + FakeRest.stub(:delete, "/channels/1/widget/2", FakeRest.resp(204)) + + assert {:ok, nil} = Endpoints.delete_widget(1, 2, reason: "spam & bad") + + assert_receive {:rest_hit, info} + assert Map.new(info.headers)["x-audit-log-reason"] == "spam%20%26%20bad" + end + + test "without :reason no header is sent" do + FakeRest.stub(:delete, "/channels/1/widget/2", FakeRest.resp(204)) + + assert {:ok, nil} = Endpoints.delete_widget(1, 2) + + assert_receive {:rest_hit, info} + refute Map.has_key?(Map.new(info.headers), "x-audit-log-reason") + end + end + + describe "AC1.6: error decoding" do + test "a 4xx decodes to an Error struct with status/code/message" do + body = ~s({"code":50013,"message":"Missing Permissions"}) + FakeRest.stub(:get, "/channels/1/widget", FakeRest.resp(403, body: body)) + + assert {:error, %Error{status: 403, code: 50013, message: "Missing Permissions"}} = + Endpoints.get_widget(1) + end + end + + describe "multipart + binary wrap" do + test "files: opts encode multipart with payload_json carrying attachments" do + FakeRest.stub(:post, "/channels/1/widget", FakeRest.resp(200, body: ~s({"id":"1"}))) + + assert {:ok, %Widget{}} = + Endpoints.create_widget(1, [name: "w"], files: [%{filename: "a.png", data: <<1>>}]) + + assert_receive {:rest_hit, info} + "multipart/form-data; boundary=" <> boundary = Map.new(info.headers)["content-type"] + + payload = payload_json(info.body, boundary) + assert payload["name"] == "w" + assert payload["attachments"] == [%{"id" => 0, "filename" => "a.png"}] + end + + test "a binary body wraps to the declared key" do + FakeRest.stub(:post, "/channels/1/widget", FakeRest.resp(200, body: ~s({"id":"1"}))) + + assert {:ok, %Widget{}} = Endpoints.create_widget(1, "hello") + + assert_receive {:rest_hit, info} + assert JSON.decode!(info.body) == %{"name" => "hello"} + end + end + + describe "list returns and default :raw" do + test "a [Module] return decodes to a list of structs" do + FakeRest.stub( + :get, + "/channels/1/widgets", + FakeRest.resp(200, body: ~s([{"id":"1"},{"id":"2"}])) + ) + + assert {:ok, [%Widget{id: 1}, %Widget{id: 2}]} = Endpoints.list_widgets(1) + end + + test "an endpoint without returns: yields the raw decoded map" do + FakeRest.stub(:get, "/things/5", FakeRest.resp(200, body: ~s({"id":"5","k":"v"}))) + + assert {:ok, %{"id" => "5", "k" => "v"}} = Endpoints.raw_thing(5) + end + end + + describe "__endpoints__/0 route table" do + test "lists every declaration in order with parsed segments" do + specs = Endpoints.__endpoints__() + + assert Enum.map(specs, & &1.name) == [ + :get_widget, + :create_widget, + :delete_widget, + :list_widgets, + :raw_thing + ] + + get_widget = Enum.find(specs, &(&1.name == :get_widget)) + assert get_widget.method == :get + assert get_widget.segments == ["channels", {:param, :channel_id}, "widget"] + assert get_widget.query == [:limit, :around] + assert get_widget.returns == Dexcord.Test.Widget + + list_widgets = Enum.find(specs, &(&1.name == :list_widgets)) + assert list_widgets.returns == [Dexcord.Test.Widget] + + raw_thing = Enum.find(specs, &(&1.name == :raw_thing)) + assert raw_thing.returns == :raw + end + end + + # --- helpers ------------------------------------------------------------ + + defp payload_json(body, boundary) do + body + |> String.split("--" <> boundary) + |> Enum.find_value(fn seg -> + if seg =~ ~s(name="payload_json") do + [_headers, rest] = String.split(seg, "\r\n\r\n", parts: 2) + rest |> String.replace_suffix("\r\n", "") |> JSON.decode!() + end + end) + end +end diff --git a/test/dexcord/endpoint_test.exs b/test/dexcord/endpoint_test.exs new file mode 100644 index 0000000..f9ef0a4 --- /dev/null +++ b/test/dexcord/endpoint_test.exs @@ -0,0 +1,213 @@ +defmodule Dexcord.Api.EndpointTest do + @moduledoc """ + Pure unit tests for the endpoint runtime engine helpers — path/query/body + building and response decoding. No HTTP: these exercise the functions in + isolation with hand-built spec maps. + """ + use ExUnit.Case, async: true + + alias Dexcord.Api.Endpoint + alias Dexcord.Api.Error + + # A spec fragment for /channels/:channel_id/messages. + defp messages_spec do + %{segments: ["channels", {:param, :channel_id}, "messages"]} + end + + describe "build_path/2 (AC1.5)" do + test "accepts integer, decimal string, and struct-with-id, all identical" do + spec = messages_spec() + + assert Endpoint.build_path(spec, [123]) == {:ok, "/channels/123/messages"} + assert Endpoint.build_path(spec, ["123"]) == {:ok, "/channels/123/messages"} + + assert Endpoint.build_path(spec, [%Dexcord.Test.Widget{id: 123}]) == + {:ok, "/channels/123/messages"} + end + + test "a non-numeric snowflake returns an Error naming the param, no HTTP" do + assert {:error, %Error{status: nil, message: message}} = + Endpoint.build_path(messages_spec(), ["12x"]) + + assert message =~ ":channel_id" + end + + test "non-id params are URL-encoded strings" do + spec = %{ + segments: [ + "channels", + {:param, :channel_id}, + "reactions", + {:param, :emoji}, + "@me" + ] + } + + assert {:ok, path} = Endpoint.build_path(spec, [1, "🔥"]) + assert path == "/channels/1/reactions/%F0%9F%94%A5/@me" + + assert {:ok, colon_path} = Endpoint.build_path(spec, [1, "name:123"]) + assert colon_path == "/channels/1/reactions/name%3A123/@me" + end + end + + describe "build_query/2 (AC1.2)" do + test "encodes only declared, present, non-nil keys" do + assert Endpoint.build_query([:limit, :around], limit: 50) == "?limit=50" + end + + test "no matching opts yields an empty string" do + assert Endpoint.build_query([:limit, :around], []) == "" + assert Endpoint.build_query([:limit], limit: nil) == "" + end + + test "struct-with-id values collapse to the id" do + assert Endpoint.build_query([:around], around: %Dexcord.Test.Widget{id: 99}) == + "?around=99" + end + + test "unknown opt keys are ignored" do + assert Endpoint.build_query([:limit], limit: 5, mystery: "x") == "?limit=5" + end + end + + describe "encode_body/2 (AC2.9)" do + test "keyword: absent key omitted, explicit nil kept as JSON null" do + spec = %{binary_wrap: nil} + + assert Endpoint.encode_body([content: "hi", nonce: nil], spec) == + %{"content" => "hi", "nonce" => nil} + + refute Map.has_key?(Endpoint.encode_body([content: "hi"], spec), "nonce") + end + + test "nested structs in a keyword value encode via to_map" do + spec = %{binary_wrap: nil} + result = Endpoint.encode_body([embeds: [%Dexcord.Test.Gadget{id: 1, name: "t"}]], spec) + + assert %{"embeds" => [embed]} = result + assert embed["name"] == "t" + end + + test "binary body with binary_wrap wraps to the configured key" do + assert Endpoint.encode_body("hi", %{binary_wrap: :content}) == %{"content" => "hi"} + end + + test "a top-level (non-keyword) list maps each struct element" do + spec = %{binary_wrap: nil} + result = Endpoint.encode_body([%Dexcord.Test.Gadget{id: 1, name: "a"}], spec) + + assert [%{"name" => "a"}] = result + end + + test "nil body encodes to nil" do + assert Endpoint.encode_body(nil, %{binary_wrap: nil}) == nil + end + end + + describe "prepare_body/3 with files (AC1.4)" do + defp files_spec do + %{body: true, files: true, binary_wrap: nil} + end + + test "returns a multipart tuple with attachments injected and description propagated" do + files = [%{filename: "a.png", data: <<1>>, description: "alt"}] + + assert {:multipart, payload, [file]} = + Endpoint.prepare_body(files_spec(), [content: "hi"], files: files) + + assert payload["content"] == "hi" + + assert payload["attachments"] == [ + %{"id" => 0, "filename" => "a.png", "description" => "alt"} + ] + + assert file.filename == "a.png" + assert file.content_type == "application/octet-stream" + refute Map.has_key?(file, :description) + end + + test "without :files in opts returns a plain map" do + assert Endpoint.prepare_body(files_spec(), [content: "hi"], []) == %{"content" => "hi"} + end + + test "a body:false spec always yields nil" do + assert Endpoint.prepare_body(%{body: false}, [content: "x"], []) == nil + end + end + + describe "prepare_body/3 with form-field files (sticker create)" do + defp form_spec do + %{body: true, files: :form, binary_wrap: nil} + end + + test "stringifies every payload value onto bare form parts" do + file = %{filename: "s.png", data: <<1>>} + + assert {:form_multipart, fields, out_file} = + Endpoint.prepare_body(form_spec(), [name: "sticker", tags: "cat"], files: [file]) + + assert fields == %{"name" => "sticker", "tags" => "cat"} + assert out_file.filename == "s.png" + refute Map.has_key?(out_file, :description) + end + + test "a nil body is legitimate and yields empty form fields" do + file = %{filename: "s.png", data: <<1>>} + + assert {:form_multipart, %{}, out_file} = + Endpoint.prepare_body(form_spec(), nil, files: [file]) + + assert out_file.filename == "s.png" + end + + test "a non-map, non-nil payload raises rather than silently vanishing" do + file = %{filename: "s.png", data: <<1>>} + # A top-level list body encodes to a list, which must not be coerced to %{}. + assert_raise FunctionClauseError, fn -> + Endpoint.prepare_body(form_spec(), [%Dexcord.Test.Gadget{id: 1, name: "a"}], + files: [file] + ) + end + end + end + + describe "decode_response/2 (AC1.6 + passthrough)" do + test "decodes a single map to the returns struct" do + assert {:ok, %Dexcord.User{id: 1}} = + Endpoint.decode_response({:ok, %{"id" => "1"}}, Dexcord.User) + end + + test "decodes a list to a list of structs" do + assert {:ok, [%Dexcord.User{id: 1}, %Dexcord.User{id: 2}]} = + Endpoint.decode_response( + {:ok, [%{"id" => "1"}, %{"id" => "2"}]}, + [Dexcord.User] + ) + end + + test "an error tuple passes through untouched" do + err = {:error, %Error{status: 403, message: "no"}} + assert Endpoint.decode_response(err, Dexcord.User) == err + end + + test "a raw body passes through" do + assert Endpoint.decode_response({:ok, {:raw, "x"}}, Dexcord.User) == {:ok, {:raw, "x"}} + end + + test "a nil body passes through" do + assert Endpoint.decode_response({:ok, nil}, Dexcord.User) == {:ok, nil} + end + + test "returns: nil normalizes a non-204 JSON body to {:ok, nil}" do + # A `returns: nil` endpoint must not leak the raw decoded map when the + # server answers with a JSON body instead of an empty 204. + assert Endpoint.decode_response({:ok, %{"unexpected" => "body"}}, nil) == {:ok, nil} + assert Endpoint.decode_response({:ok, nil}, nil) == {:ok, nil} + end + + test "returns: :raw leaves the decoded map alone" do + assert Endpoint.decode_response({:ok, %{"id" => "1"}}, :raw) == {:ok, %{"id" => "1"}} + end + end +end diff --git a/test/dexcord/enum_test.exs b/test/dexcord/enum_test.exs new file mode 100644 index 0000000..c0704a0 --- /dev/null +++ b/test/dexcord/enum_test.exs @@ -0,0 +1,49 @@ +defmodule Dexcord.EnumTest do + use ExUnit.Case, async: true + + alias Dexcord.Test.Color + + # api-surface.AC2.4: unknown enum int -> {:unknown, n}; encode(decode(n)) lossless. + + test "decode/1 maps known integers to their named atoms" do + assert Color.decode(0) == :red + assert Color.decode(1) == :green + assert Color.decode(4) == :blue + end + + test "decode/1 maps a gap value (unnamed but in range) to {:unknown, n}" do + assert Color.decode(2) == {:unknown, 2} + end + + test "decode/1 maps out-of-range and negative integers to {:unknown, n}" do + assert Color.decode(999) == {:unknown, 999} + assert Color.decode(-5) == {:unknown, -5} + end + + test "encode(decode(n)) is lossless for known and unknown values, including negative" do + for n <- [0, 1, 4, 2, 999, -5] do + assert Color.encode(Color.decode(n)) == n + end + end + + test "encode/1 maps named atoms to their wire integer" do + assert Color.encode(:red) == 0 + assert Color.encode(:green) == 1 + assert Color.encode(:blue) == 4 + end + + test "encode/1 raises FunctionClauseError for an unnamed atom" do + # apply/3 keeps the invalid atom opaque to the compile-time type checker, + # so this deliberate misuse doesn't emit a typing-violation warning. + assert_raise FunctionClauseError, fn -> apply(Color, :encode, [:not_a_color]) end + end + + test "values/0 returns the full name => integer table" do + assert Color.values() == %{red: 0, green: 1, blue: 4} + end + + test "the module generates a :t type" do + {:ok, types} = Code.Typespec.fetch_types(Color) + assert Enum.any?(types, fn {_kind, {name, _def, _args}} -> name == :t end) + end +end diff --git a/test/dexcord/ergonomics_helpers_test.exs b/test/dexcord/ergonomics_helpers_test.exs new file mode 100644 index 0000000..8c4d7f5 --- /dev/null +++ b/test/dexcord/ergonomics_helpers_test.exs @@ -0,0 +1,131 @@ +defmodule Dexcord.ErgonomicsHelpersTest do + @moduledoc """ + Pure helper tests for the discord.py taste layer (api-surface.AC3.4): mentions, + `String.Chars` interpolation, `Dexcord.PartialEmoji.to_string/1`, `created_at/1`, + and `Dexcord.Util.format_dt/2`. + """ + use ExUnit.Case, async: true + + alias Dexcord.Util + + # Discord's documented example snowflake -> 2016-04-30T11:18:25.796Z. + @known_snowflake 175_928_847_299_117_063 + @known_unix_ms 1_462_015_105_796 + + describe "mention/1" do + test "User renders <@id>" do + assert Dexcord.User.mention(%Dexcord.User{id: 1}) == "<@1>" + end + + test "Member renders <@id> from user_id" do + assert Dexcord.Member.mention(%Dexcord.Member{user_id: 7}) == "<@7>" + end + + test "Member renders <@id> from the nested user when user_id is nil" do + assert Dexcord.Member.mention(%Dexcord.Member{user: %Dexcord.User{id: 8}}) == "<@8>" + end + + test "Role renders <@&id>" do + assert Dexcord.Role.mention(%Dexcord.Role{id: 2}) == "<@&2>" + end + + test "Dexcord.Channel.mention/1 renders <#id> for any channel struct" do + assert Dexcord.Channel.mention(%Dexcord.TextChannel{id: 3}) == "<#3>" + assert Dexcord.Channel.mention(%Dexcord.VoiceChannel{id: 4}) == "<#4>" + assert Dexcord.Channel.mention(%Dexcord.Thread{id: 5}) == "<#5>" + assert Dexcord.Channel.mention(%Dexcord.DMChannel{id: 6}) == "<#6>" + end + end + + describe "String.Chars interpolation" do + test "a user interpolates as a real ping" do + assert "hey #{%Dexcord.User{id: 1}}" == "hey <@1>" + end + + test "a member interpolates as a real ping" do + assert "#{%Dexcord.Member{user_id: 9}}" == "<@9>" + end + + test "a role interpolates as <@&id>" do + assert "#{%Dexcord.Role{id: 2}}" == "<@&2>" + end + + test "a text channel interpolates as <#id>" do + assert "#{%Dexcord.TextChannel{id: 3}}" == "<#3>" + end + + test "a thread interpolates as <#id>" do + assert "#{%Dexcord.Thread{id: 5}}" == "<#5>" + end + end + + describe "PartialEmoji.to_string/1" do + test "a custom (static) emoji renders <:name:id>" do + emoji = %Dexcord.PartialEmoji{id: 100, name: "blob", animated: false} + assert Dexcord.PartialEmoji.to_string(emoji) == "<:blob:100>" + end + + test "an animated emoji renders " do + emoji = %Dexcord.PartialEmoji{id: 200, name: "party", animated: true} + assert Dexcord.PartialEmoji.to_string(emoji) == "" + end + + test "a unicode emoji (nil id) renders its raw name" do + emoji = %Dexcord.PartialEmoji{id: nil, name: "🔥"} + assert Dexcord.PartialEmoji.to_string(emoji) == "🔥" + end + + test "interpolates via String.Chars" do + assert "#{%Dexcord.PartialEmoji{id: 100, name: "blob"}}" == "<:blob:100>" + assert "#{%Dexcord.PartialEmoji{id: nil, name: "🔥"}}" == "🔥" + end + end + + describe "Dexcord.Util.format_dt/2" do + test "renders every documented style" do + for style <- ~w(t T d D f F s S R) do + assert Util.format_dt(1_700_000_000, style) == "" + end + end + + test "defaults to the f style" do + assert Util.format_dt(1_700_000_000) == "" + end + + test "accepts a DateTime and converts to unix" do + dt = DateTime.from_unix!(1_700_000_000) + assert Util.format_dt(dt) == "" + assert Util.format_dt(dt, "R") == "" + end + end + + describe "created_at/1" do + test "round-trips a known snowflake vector on a User" do + assert {:ok, dt} = Dexcord.User.created_at(%Dexcord.User{id: @known_snowflake}) + assert DateTime.to_unix(dt, :millisecond) == @known_unix_ms + end + + test "works on a Message via its id" do + assert {:ok, dt} = Dexcord.Message.created_at(%Dexcord.Message{id: @known_snowflake}) + assert DateTime.to_unix(dt, :millisecond) == @known_unix_ms + end + + test "works on a Member via user_id" do + assert {:ok, dt} = + Dexcord.Member.created_at(%Dexcord.Member{user_id: @known_snowflake}) + + assert DateTime.to_unix(dt, :millisecond) == @known_unix_ms + end + + test "works on a guild channel struct" do + assert {:ok, dt} = + Dexcord.TextChannel.created_at(%Dexcord.TextChannel{id: @known_snowflake}) + + assert DateTime.to_unix(dt, :millisecond) == @known_unix_ms + end + + test "an emoji with a nil id returns :error" do + assert Dexcord.Emoji.created_at(%Dexcord.Emoji{id: nil}) == :error + end + end +end diff --git a/test/dexcord/ergonomics_integration_test.exs b/test/dexcord/ergonomics_integration_test.exs index 6b17e59..e1341f6 100644 --- a/test/dexcord/ergonomics_integration_test.exs +++ b/test/dexcord/ergonomics_integration_test.exs @@ -71,10 +71,10 @@ defmodule Dexcord.ErgonomicsIntegrationTest do FakeRest.stub( :get, "/oauth2/applications/@me", - FakeRest.resp(200, body: ~s({"id":"app-erg"})) + FakeRest.resp(200, body: ~s({"id":"555"})) ) - FakeRest.stub(:put, "/applications/app-erg/commands", FakeRest.resp(200, body: "[]")) + FakeRest.stub(:put, "/applications/555/commands", FakeRest.resp(200, body: "[]")) fake = start_supervised!({FakeGateway, test_pid: self(), hello_interval: 80}) :ok = start_bot(fake) @@ -102,7 +102,8 @@ defmodule Dexcord.ErgonomicsIntegrationTest do user_msg = %{"content" => "!ping a b", "author" => %{"id" => "u1", "bot" => false}} FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", user_msg, 10) - assert_receive {:command, "ping", ["a", "b"], ^user_msg}, @timeout + assert_receive {:command, "ping", ["a", "b"], %Dexcord.Message{content: "!ping a b"}}, + @timeout bot_msg = %{"content" => "!ping", "author" => %{"id" => "b1", "bot" => true}} FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", bot_msg, 11) @@ -115,8 +116,10 @@ defmodule Dexcord.ErgonomicsIntegrationTest do itx = %{"id" => "i2", "type" => 2, "token" => "tok", "data" => %{"name" => "ping"}} FakeGateway.push_dispatch(fake, "INTERACTION_CREATE", itx, 20) - assert_receive {:slash, "ping", ^itx}, @timeout - assert_receive {:raw, :INTERACTION_CREATE, ^itx}, @timeout + assert_receive {:slash, "ping", %Dexcord.Interaction{type: :application_command}}, @timeout + + assert_receive {:raw, :INTERACTION_CREATE, %Dexcord.Interaction{type: :application_command}}, + @timeout end test "an autocomplete interaction (type 4) reaches the raw handler but is not auto-routed", @@ -124,7 +127,10 @@ defmodule Dexcord.ErgonomicsIntegrationTest do itx = %{"id" => "i4", "type" => 4, "data" => %{"name" => "ping"}} FakeGateway.push_dispatch(fake, "INTERACTION_CREATE", itx, 21) - assert_receive {:raw, :INTERACTION_CREATE, ^itx}, @timeout + assert_receive {:raw, :INTERACTION_CREATE, + %Dexcord.Interaction{type: :application_command_autocomplete}}, + @timeout + refute_receive {:slash, _, _}, 300 end diff --git a/test/dexcord/events_envelope_test.exs b/test/dexcord/events_envelope_test.exs new file mode 100644 index 0000000..e04c443 --- /dev/null +++ b/test/dexcord/events_envelope_test.exs @@ -0,0 +1,252 @@ +defmodule Dexcord.EventsEnvelopeTest do + use ExUnit.Case, async: true + + alias Dexcord.Events + + describe "message_events.ex" do + test "ReactionAdd decodes member, unicode emoji, burst; hydrate slots default nil" do + raw = %{ + "user_id" => "111", + "channel_id" => "222", + "message_id" => "333", + "guild_id" => "444", + "member" => %{"nick" => "bee", "user" => %{"id" => "111", "username" => "bee"}}, + "emoji" => %{"id" => nil, "name" => "🔥"}, + "message_author_id" => "555", + "burst" => true, + "burst_colors" => ["#ffffff"], + "type" => 0 + } + + assert %Events.ReactionAdd{} = ev = Events.ReactionAdd.from_map(raw) + assert ev.user_id == 111 + assert ev.channel_id == 222 + assert ev.message_id == 333 + assert ev.guild_id == 444 + assert %Dexcord.Member{nick: "bee"} = ev.member + assert %Dexcord.User{username: "bee"} = ev.member.user + assert %Dexcord.PartialEmoji{name: "🔥", id: nil} = ev.emoji + assert ev.message_author_id == 555 + assert ev.burst == true + assert ev.burst_colors == ["#ffffff"] + assert ev.type == :normal + + # hydrate slots exist, default to nil, and are declared + assert ev.user == nil + assert ev.channel == nil + assert ev.guild == nil + + slots = Enum.map(Events.ReactionAdd.__hydrations__(), & &1.name) + assert :user in slots + assert :channel in slots + assert :guild in slots + end + + test "MessageDelete decodes minimal payload with hydrate slots" do + ev = Events.MessageDelete.from_map(%{"id" => "1", "channel_id" => "2", "guild_id" => "3"}) + assert %Events.MessageDelete{id: 1, channel_id: 2, guild_id: 3} = ev + assert ev.channel == nil + assert ev.guild == nil + end + + test "MessageDeleteBulk decodes ids list" do + ev = Events.MessageDeleteBulk.from_map(%{"ids" => ["1", "2"], "channel_id" => "9"}) + assert ev.ids == [1, 2] + assert ev.channel_id == 9 + end + + test "PollVoteAdd decodes answer_id" do + ev = + Events.PollVoteAdd.from_map(%{ + "user_id" => "1", + "channel_id" => "2", + "message_id" => "3", + "guild_id" => "4", + "answer_id" => 7 + }) + + assert ev.answer_id == 7 + assert ev.user == nil + end + end + + describe "guild_events.ex" do + test "GuildMemberUpdate: nick present-null -> nil, absent premium_since -> :absent" do + raw = %{ + "guild_id" => "1", + "roles" => ["10", "11"], + "user" => %{"id" => "2", "username" => "x"}, + "nick" => nil, + "joined_at" => "2021-01-01T00:00:00.000000+00:00" + } + + ev = Events.GuildMemberUpdate.from_map(raw) + assert ev.guild_id == 1 + assert ev.roles == [10, 11] + assert %Dexcord.User{username: "x"} = ev.user + assert ev.nick == nil + assert ev.premium_since == :absent + assert ev.communication_disabled_until == :absent + end + + test "GuildRoleCreate decodes nested role + guild hydrate" do + ev = + Events.GuildRoleCreate.from_map(%{ + "guild_id" => "1", + "role" => %{"id" => "5", "name" => "admin"} + }) + + assert ev.guild_id == 1 + assert %Dexcord.Role{name: "admin"} = ev.role + assert ev.guild == nil + end + + test "GuildRoleDelete decodes role_id" do + ev = Events.GuildRoleDelete.from_map(%{"guild_id" => "1", "role_id" => "5"}) + assert ev.role_id == 5 + end + + test "GuildMembersChunk decodes nested members" do + ev = + Events.GuildMembersChunk.from_map(%{ + "guild_id" => "1", + "members" => [%{"user" => %{"id" => "2"}}], + "chunk_index" => 0, + "chunk_count" => 1 + }) + + assert [%Dexcord.Member{}] = ev.members + assert ev.chunk_index == 0 + end + end + + describe "channel_events.ex" do + test "ThreadMembersUpdate: added_members with nested member" do + raw = %{ + "id" => "1", + "guild_id" => "2", + "member_count" => 3, + "added_members" => [ + %{"id" => "1", "user_id" => "9", "member" => %{"user" => %{"id" => "9"}}} + ], + "removed_member_ids" => ["8"] + } + + ev = Events.ThreadMembersUpdate.from_map(raw) + assert ev.member_count == 3 + assert [%Dexcord.ThreadMember{} = tm] = ev.added_members + assert %Dexcord.Member{} = tm.member + assert ev.removed_member_ids == [8] + end + + test "ChannelPinsUpdate tristate last_pin_timestamp absent -> :absent" do + ev = Events.ChannelPinsUpdate.from_map(%{"guild_id" => "1", "channel_id" => "2"}) + assert ev.last_pin_timestamp == :absent + assert ev.channel == nil + end + + test "ThreadDelete decodes type enum" do + ev = + Events.ThreadDelete.from_map(%{ + "id" => "1", + "guild_id" => "2", + "parent_id" => "3", + "type" => 11 + }) + + assert ev.id == 1 + assert ev.type == :public_thread + end + end + + describe "misc_events.ex" do + test "TypingStart: integer timestamp stays integer" do + raw = %{ + "channel_id" => "1", + "guild_id" => "2", + "user_id" => "3", + "timestamp" => 1_700_000_000, + "member" => %{"user" => %{"id" => "3"}} + } + + ev = Events.TypingStart.from_map(raw) + assert ev.timestamp == 1_700_000_000 + assert is_integer(ev.timestamp) + assert %Dexcord.Member{} = ev.member + assert ev.user == nil + end + + test "InviteCreate: expires_at null stays nil" do + raw = %{ + "channel_id" => "1", + "code" => "abc", + "created_at" => "2021-01-01T00:00:00.000000+00:00", + "guild_id" => "2", + "max_age" => 3600, + "max_uses" => 0, + "temporary" => false, + "uses" => 0, + "expires_at" => nil + } + + ev = Events.InviteCreate.from_map(raw) + assert ev.code == "abc" + assert ev.expires_at == nil + assert ev.channel == nil + end + + test "VoiceServerUpdate decodes token/endpoint" do + ev = + Events.VoiceServerUpdate.from_map(%{ + "token" => "t", + "guild_id" => "1", + "endpoint" => "smart.example" + }) + + assert ev.token == "t" + assert ev.endpoint == "smart.example" + end + + test "AutoModerationActionExecution decodes nested action + enum" do + ev = + Events.AutoModerationActionExecution.from_map(%{ + "guild_id" => "1", + "action" => %{"type" => 1, "metadata" => %{"channel_id" => "9"}}, + "rule_id" => "2", + "rule_trigger_type" => 1, + "user_id" => "3", + "content" => "bad word" + }) + + assert %Dexcord.AutoModerationAction{type: :block_message} = ev.action + assert ev.rule_trigger_type == :keyword + assert ev.content == "bad word" + end + + test "WebhooksUpdate decodes with channel/guild hydrates" do + ev = Events.WebhooksUpdate.from_map(%{"guild_id" => "1", "channel_id" => "2"}) + assert ev.guild_id == 1 + assert ev.channel_id == 2 + assert ev.channel == nil + assert ev.guild == nil + end + end + + describe "ready.ex" do + test "Ready decodes user + unavailable guilds" do + ev = + Events.Ready.from_map(%{ + "v" => 10, + "user" => %{"id" => "1", "username" => "bot"}, + "guilds" => [%{"id" => "9", "unavailable" => true}], + "session_id" => "sess", + "resume_gateway_url" => "wss://example" + }) + + assert ev.v == 10 + assert %Dexcord.User{username: "bot"} = ev.user + assert [%Dexcord.UnavailableGuild{id: 9, unavailable: true}] = ev.guilds + assert ev.session_id == "sess" + end + end +end diff --git a/test/dexcord/events_test.exs b/test/dexcord/events_test.exs new file mode 100644 index 0000000..0169a62 --- /dev/null +++ b/test/dexcord/events_test.exs @@ -0,0 +1,109 @@ +defmodule Dexcord.EventsTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureLog + + alias Dexcord.Events + + describe "api-surface.AC2.17: every documented event is classified" do + test "no atom in EventNames.all/0 has payload_type :unknown" do + for name <- Dexcord.EventNames.all() do + assert Events.payload_type(name) != :unknown, + "#{name} is unclassified in Dexcord.Events" + end + end + + test "the three-way classification partitions exactly EventNames.all/0" do + # Recompute the counts independently of the table internals by classifying + # every documented name and tallying — adding a new event name without + # classifying it makes some name :unknown and fails the AC2.17 test above; + # this asserts the totals sum to the whitelist size with no overlap. + names = Dexcord.EventNames.all() + + grouped = + Enum.group_by(names, fn name -> + case Events.payload_type(name) do + {:full, _} -> :full + {:envelope, _} -> :envelope + :passthrough -> :passthrough + :unknown -> :unknown + end + end) + + full = Map.get(grouped, :full, []) + envelope = Map.get(grouped, :envelope, []) + passthrough = Map.get(grouped, :passthrough, []) + unknown = Map.get(grouped, :unknown, []) + + assert unknown == [] + assert length(full) + length(envelope) + length(passthrough) == length(names) + assert length(names) == 67 + end + end + + describe "payload_type/1 classification" do + test "full-object events" do + assert Events.payload_type(:MESSAGE_CREATE) == {:full, Dexcord.Message} + assert Events.payload_type(:GUILD_DELETE) == {:full, Dexcord.UnavailableGuild} + assert Events.payload_type(:INTERACTION_CREATE) == {:full, Dexcord.Interaction} + end + + test "envelope events" do + assert Events.payload_type(:MESSAGE_DELETE) == {:envelope, Dexcord.Events.MessageDelete} + assert Events.payload_type(:READY) == {:envelope, Dexcord.Events.Ready} + end + + test "passthrough events" do + assert Events.payload_type(:RESUMED) == :passthrough + assert Events.payload_type(:ENTITLEMENT_CREATE) == :passthrough + assert Events.payload_type(:STAGE_INSTANCE_UPDATE) == :passthrough + end + end + + describe "decode/2 smoke tests" do + test "MESSAGE_CREATE decodes a full message struct" do + raw = Dexcord.Fixtures.load!("message_create_guild.json") + assert %Dexcord.Message{} = Events.decode(:MESSAGE_CREATE, raw) + end + + test "MESSAGE_DELETE decodes to its envelope" do + decoded = Events.decode(:MESSAGE_DELETE, %{"id" => "1", "channel_id" => "2"}) + assert %Dexcord.Events.MessageDelete{id: 1, channel_id: 2} = decoded + end + + test "RESUMED passes the raw map through untouched" do + assert Events.decode(:RESUMED, %{}) == %{} + assert Events.decode(:RESUMED, %{"trace" => ["x"]}) == %{"trace" => ["x"]} + end + + test "unknown_event passes the raw map through" do + assert Events.decode({:unknown_event, "WEIRD"}, %{"x" => 1}) == %{"x" => 1} + end + end + + describe "GUILD_DELETE semantics" do + test "removed-from-guild (no unavailable key) -> unavailable: false" do + decoded = Events.decode(:GUILD_DELETE, %{"id" => "1"}) + assert %Dexcord.UnavailableGuild{id: 1, unavailable: false} = decoded + end + + test "outage (unavailable: true) -> unavailable: true" do + decoded = Events.decode(:GUILD_DELETE, %{"id" => "1", "unavailable" => true}) + assert %Dexcord.UnavailableGuild{id: 1, unavailable: true} = decoded + end + end + + describe "nil -> raw fallback" do + test "a non-map payload decodes to nil and falls back to the raw payload, logging the event name only" do + log = + capture_log(fn -> + assert Events.decode(:MESSAGE_CREATE, "not a map") == "not a map" + end) + + # The warning names the event... + assert log =~ "MESSAGE_CREATE" + # ...but never leaks the payload content. + refute log =~ "not a map" + end + end +end diff --git a/test/dexcord/flags_test.exs b/test/dexcord/flags_test.exs new file mode 100644 index 0000000..370fd63 --- /dev/null +++ b/test/dexcord/flags_test.exs @@ -0,0 +1,53 @@ +defmodule Dexcord.FlagsTest do + use ExUnit.Case, async: true + + alias Dexcord.Test.TestFlags + + # api-surface.AC2.5: flags fields keep the raw integer; has?/2, to_list/1, + # from_list/1 correct; unknown bits survive round-trip in the int. + # + # TestFlags: alpha: bit 0 (value 1), beta: bit 1 (value 2), gamma: bit 4 (value 16). + + test "has?/2 reports whether a named flag is set in the integer" do + # 0b10001 = 17 = alpha (1) | gamma (16) + assert TestFlags.has?(0b10001, :alpha) + refute TestFlags.has?(0b10001, :beta) + assert TestFlags.has?(0b10001, :gamma) + end + + test "to_list/1 returns set named flags ordered by bit value" do + # 0b10011 = 19 = alpha (1) | beta (2) | gamma (16) + assert TestFlags.to_list(0b10011) == [:alpha, :beta, :gamma] + end + + test "from_list/1 combines named flags into an integer" do + # alpha (1) | gamma (16) = 17 = 0b10001 + assert TestFlags.from_list([:alpha, :gamma]) == 0b10001 + end + + test "from_list(to_list(int)) recovers exactly the known bits" do + int = 0b10011 + assert TestFlags.from_list(TestFlags.to_list(int)) == int + end + + test "to_list/1 omits unknown bits, leaving the raw int untouched elsewhere" do + # 0b1000_0001 = 129: bit 0 (alpha) is named, bit 7 is unnamed. + assert TestFlags.to_list(0b1000_0001) == [:alpha] + # from_list of that view drops the unknown bit — the module never mutates + # the raw int; the round-trip contract lives on the raw integer field + # (verified again at the struct level in Task 5). + assert TestFlags.from_list(TestFlags.to_list(0b1000_0001)) == 0b0000_0001 + end + + test "all/0 returns the full name => integer-value map" do + assert TestFlags.all() == %{alpha: 1, beta: 2, gamma: 16} + end + + test "from_list/1 raises KeyError on an unknown flag name" do + assert_raise KeyError, fn -> TestFlags.from_list([:nope]) end + end + + test "has?/2 raises FunctionClauseError on an unknown flag name" do + assert_raise FunctionClauseError, fn -> apply(TestFlags, :has?, [1, :nope]) end + end +end diff --git a/test/dexcord/gateway_integration_test.exs b/test/dexcord/gateway_integration_test.exs index 04b8cdb..f7625bb 100644 --- a/test/dexcord/gateway_integration_test.exs +++ b/test/dexcord/gateway_integration_test.exs @@ -9,6 +9,8 @@ defmodule Dexcord.GatewayIntegrationTest do """ use ExUnit.Case, async: false + import ExUnit.CaptureLog + alias Dexcord.FakeGateway alias Dexcord.Gateway alias Dexcord.Intents @@ -96,12 +98,51 @@ defmodule Dexcord.GatewayIntegrationTest do # A dispatch reaches the user handler... FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "hi"}, 7) - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "hi"}}}, @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "hi"}}}, + @event_timeout # ...and the next heartbeat carries the latest seq (7). assert_receive {:fake_gw, :frame, _c, %{"op" => 1, "d" => 7}}, @event_timeout end + # --- AC2.18 / AC2.19: typed delivery, decode-failure fallback, unknown events --- + + # A payload the decoder cannot turn into a struct must not stall dispatch: it + # logs, falls back to the RAW payload, and the very next well-formed event still + # arrives fully typed. Also proves the unknown-event contract: an undocumented + # event name arrives as `{{:unknown_event, name}, raw_map}`. + test "AC2.19: a malformed payload logs, delivers raw, and dispatch keeps flowing typed" do + fake = start_fake(hello_interval: 80) + start_bot(fake) + + assert_frame(2) + assert_receive {:handler_event, {:READY, _}}, @event_timeout + + # A MESSAGE_CREATE whose `d` is a bare string can't decode to a %Message{}: the + # decoder returns nil, Events.decode logs a warning and falls back to the raw + # payload. The handler receives that raw string unchanged. + log = + capture_log(fn -> + FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", "garbage", 7) + assert_receive {:handler_event, {:MESSAGE_CREATE, "garbage"}}, @event_timeout + end) + + assert log =~ "MESSAGE_CREATE" + + # Dispatch survived the bad payload: a following well-formed event decodes typed. + FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "recovered"}, 8) + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "recovered"}}}, + @event_timeout + + # AC2.18 unknown half: an undocumented event arrives as {{:unknown_event, name}, raw}. + FakeGateway.push_dispatch(fake, "TOTALLY_NEW_EVENT", %{"x" => 1}, 9) + + assert_receive {:handler_event, {{:unknown_event, "TOTALLY_NEW_EVENT"}, %{"x" => 1}}}, + @event_timeout + end + # --- REVIEW FIX 1: heartbeat timer must re-arm while gap-waiting in :hello_wait - # Verifies review finding #1. Deterministic and green in isolation @@ -218,7 +259,9 @@ defmodule Dexcord.GatewayIntegrationTest do # The statem survived: a following real dispatch still reaches the user handler. FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "alive"}, 6) - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "alive"}}}, @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "alive"}}}, + @event_timeout # And it never crashed/reconnected: heartbeats keep flowing on the same connection. assert_receive {:fake_gw, :frame, _c, %{"op" => 1}}, @event_timeout @@ -249,7 +292,10 @@ defmodule Dexcord.GatewayIntegrationTest do # Advance the live session's seq. FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "x"}, 42) - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "x"}}}, @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "x"}}}, + @event_timeout + wait_until(fn -> Session.last_seq() == 42 end) # Force a resume; the server rejects it with op 9 d:false, clearing the session. @@ -259,7 +305,9 @@ defmodule Dexcord.GatewayIntegrationTest do # Trickle a dispatch from the abandoned session during the reidentify wait. FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "noise"}, 9_999) - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "noise"}}}, @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "noise"}}}, + @event_timeout # It was delivered, but its foreign seq must not have repopulated last_seq. assert Session.last_seq() == nil, "abandoned-session frame repopulated last_seq" @@ -503,15 +551,23 @@ defmodule Dexcord.GatewayIntegrationTest do assert_frame(2) assert_receive {:handler_event, {:READY, _}}, @event_timeout FakeGateway.push_dispatch(fake, "MESSAGE_CREATE", %{"content" => "seed"}, 5) - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "seed"}}}, @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "seed"}}}, + @event_timeout FakeGateway.push_op7(fake) {_rc, 6, _} = assert_reconnect_frame() # Replays arrive in order, before RESUMED. - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "a"}}}, @event_timeout - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "b"}}}, @event_timeout - assert_receive {:handler_event, {:MESSAGE_CREATE, %{"content" => "c"}}}, @event_timeout + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "a"}}}, + @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "b"}}}, + @event_timeout + + assert_receive {:handler_event, {:MESSAGE_CREATE, %Dexcord.Message{content: "c"}}}, + @event_timeout + assert_receive {:handler_event, {:RESUMED, _}}, @event_timeout # Seq advanced to the last replayed dispatch. diff --git a/test/dexcord/messageable_test.exs b/test/dexcord/messageable_test.exs new file mode 100644 index 0000000..4374ed2 --- /dev/null +++ b/test/dexcord/messageable_test.exs @@ -0,0 +1,79 @@ +defmodule Dexcord.MessageableTest do + @moduledoc """ + Pure resolve tests for the `Dexcord.Messageable` protocol (api-surface.AC3.1's + match-time layer). No HTTP: every case exercises `Dexcord.Messageable.resolve/1` + directly, including the deliberate non-implementations that must fail BEFORE any + network call. + """ + use ExUnit.Case, async: true + + alias Dexcord.Messageable + + describe "channel-like targets resolve to {:channel, id}" do + test "guild text/announcement/voice/stage channels" do + assert Messageable.resolve(%Dexcord.TextChannel{id: 1}) == {:channel, 1} + assert Messageable.resolve(%Dexcord.AnnouncementChannel{id: 2}) == {:channel, 2} + assert Messageable.resolve(%Dexcord.VoiceChannel{id: 3}) == {:channel, 3} + assert Messageable.resolve(%Dexcord.StageChannel{id: 4}) == {:channel, 4} + end + + test "threads and DM/group-DM channels" do + assert Messageable.resolve(%Dexcord.Thread{id: 9}) == {:channel, 9} + assert Messageable.resolve(%Dexcord.DMChannel{id: 10}) == {:channel, 10} + assert Messageable.resolve(%Dexcord.GroupDMChannel{id: 11}) == {:channel, 11} + end + + test "a message resolves to its channel" do + assert Messageable.resolve(%Dexcord.Message{id: 100, channel_id: 5}) == {:channel, 5} + end + + test "an interaction resolves to its channel" do + assert Messageable.resolve(%Dexcord.Interaction{id: 1, channel_id: 7}) == {:channel, 7} + end + + test "a bare non-negative integer passes through" do + assert Messageable.resolve(123) == {:channel, 123} + assert Messageable.resolve(0) == {:channel, 0} + end + end + + describe "user-like targets resolve to {:dm_user, id}" do + test "a user" do + assert Messageable.resolve(%Dexcord.User{id: 42}) == {:dm_user, 42} + end + + test "a member via its nested user" do + assert Messageable.resolve(%Dexcord.Member{user: %Dexcord.User{id: 43}}) == + {:dm_user, 43} + end + + test "a member via its user_id back-reference (cache shape, no nested user)" do + assert Messageable.resolve(%Dexcord.Member{user: nil, user_id: 44}) == {:dm_user, 44} + end + end + + describe "deliberate non-implementations fail at resolve time" do + # These structs are built via `struct/2` rather than a literal so the + # set-theoretic type checker can't statically flag the (intentional) + # protocol violation we're asserting on at runtime. + test "category channels are not Messageable" do + target = struct(Dexcord.CategoryChannel, id: 1) + assert_raise Protocol.UndefinedError, fn -> Messageable.resolve(target) end + end + + test "forum channels are not Messageable" do + target = struct(Dexcord.ForumChannel, id: 1) + assert_raise Protocol.UndefinedError, fn -> Messageable.resolve(target) end + end + + test "media channels are not Messageable" do + target = struct(Dexcord.MediaChannel, id: 1) + assert_raise Protocol.UndefinedError, fn -> Messageable.resolve(target) end + end + + test "directory channels are not Messageable" do + target = struct(Dexcord.DirectoryChannel, id: 1) + assert_raise Protocol.UndefinedError, fn -> Messageable.resolve(target) end + end + end +end diff --git a/test/dexcord/migration_guide_samples_test.exs b/test/dexcord/migration_guide_samples_test.exs new file mode 100644 index 0000000..939c63d --- /dev/null +++ b/test/dexcord/migration_guide_samples_test.exs @@ -0,0 +1,193 @@ +defmodule AlamedyaDiscord.Handler do + # KEEP IN SYNC: mirrored verbatim in + # test/dexcord/migration_guide_samples_test.exs (§3 of docs/alamedya-migration-v2.md). + use Dexcord.Handler + + @self_id 1_135_637_126_222_987_365 + + # Webhook messages carry a `webhook_id` and a synthetic author — skip them + # first, before touching `author.bot`. + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{webhook_id: id}}) when not is_nil(id), + do: :ignore + + # Any bot (including ourselves) — never react. + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{author: %Dexcord.User{bot: true}}}), + do: :ignore + + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{author: %Dexcord.User{id: @self_id}}}), + do: :ignore + + def handle_event({:MESSAGE_CREATE, %Dexcord.Message{} = msg}) do + cond do + mentions_self?(msg) -> Dexcord.Message.reply(msg, "you rang?") + msg.content == "ping!" -> Dexcord.Message.reply(msg, "helo") + true -> :ignore + end + end + + defp mentions_self?(%Dexcord.Message{mentions: mentions}), + do: Enum.any?(mentions, fn %Dexcord.User{id: id} -> id == @self_id end) +end + +defmodule Dexcord.MigrationGuideSamplesTest do + @moduledoc """ + Mechanically pins `docs/alamedya-migration-v2.md` to the final API (AC5.1's + "code samples are valid against the final API"). + + The §3 handler module above is embedded VERBATIM from the guide (the keep-in-sync + comment appears in both files). We decode real MESSAGE_CREATE fixtures and drive + it through `Dexcord.FakeRest`, asserting it routes: bot/webhook/self authors send + nothing; a `"ping!"` or self-mention replies to the source channel. The second + block asserts every API the guide's snippets call still exists at the right + arity / with the right struct fields, so a rename in the library breaks this test + rather than silently rotting the guide. + """ + use ExUnit.Case, async: false + + alias Dexcord.Events + alias Dexcord.FakeRest + alias Dexcord.Api.Ratelimit + + @token "test.token.value" + @self_id 1_135_637_126_222_987_365 + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: AlamedyaDiscord.Handler, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(Dexcord.Cache) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + defp message(raw), do: Events.decode(:MESSAGE_CREATE, raw) + + describe "§3 MESSAGE_CREATE handler routes on typed struct fields" do + test "a webhook message is skipped before any send" do + msg = + message(%{ + "webhook_id" => "999", + "channel_id" => "100", + "content" => "ping!", + "author" => %{"id" => "555", "username" => "hook"} + }) + + assert :ignore = AlamedyaDiscord.Handler.handle_event({:MESSAGE_CREATE, msg}) + refute_receive {:rest_hit, _}, 50 + end + + test "a bot author is skipped" do + msg = + message(%{ + "channel_id" => "100", + "content" => "ping!", + "author" => %{"id" => "7", "bot" => true} + }) + + assert :ignore = AlamedyaDiscord.Handler.handle_event({:MESSAGE_CREATE, msg}) + refute_receive {:rest_hit, _}, 50 + end + + test "the bot's own message (@self_id) is skipped" do + msg = + message(%{ + "channel_id" => "100", + "content" => "ping!", + "author" => %{"id" => Integer.to_string(@self_id)} + }) + + assert :ignore = AlamedyaDiscord.Handler.handle_event({:MESSAGE_CREATE, msg}) + refute_receive {:rest_hit, _}, 50 + end + + test "a plain \"ping!\" replies \"helo\" to the source channel with a message_reference" do + FakeRest.stub(:post, "/channels/100/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + + msg = + message(%{ + "id" => "500", + "channel_id" => "100", + "content" => "ping!", + "author" => %{"id" => "7", "bot" => false} + }) + + assert {:ok, %Dexcord.Message{}} = + AlamedyaDiscord.Handler.handle_event({:MESSAGE_CREATE, msg}) + + assert_receive {:rest_hit, %{method: "POST", path: "/channels/100/messages", body: body}} + decoded = JSON.decode!(body) + assert decoded["content"] == "helo" + assert decoded["message_reference"] == %{"message_id" => "500"} + end + + test "a self-mention replies \"you rang?\"" do + FakeRest.stub(:post, "/channels/101/messages", FakeRest.resp(200, body: ~s({"id":"2"}))) + + msg = + message(%{ + "id" => "501", + "channel_id" => "101", + "content" => "hey bot", + "author" => %{"id" => "8", "bot" => false}, + "mentions" => [%{"id" => Integer.to_string(@self_id)}] + }) + + assert {:ok, %Dexcord.Message{}} = + AlamedyaDiscord.Handler.handle_event({:MESSAGE_CREATE, msg}) + + assert_receive {:rest_hit, %{path: "/channels/101/messages", body: body}} + assert JSON.decode!(body)["content"] == "you rang?" + end + end + + describe "every API the guide's snippets use exists at the documented shape" do + test "functions the guide calls are exported at the right arity" do + for {mod, fun, arity} <- [ + # §3 — reply/3 (reply(msg, body, opts \\ [])) + {Dexcord.Message, :reply, 3}, + # §4 — thread cache reads + {Dexcord.Cache, :threads, 1}, + {Dexcord.Cache, :channel, 1}, + # §6 — slash respond + {Dexcord.Slash, :respond, 2}, + # §7 — the send funnel + history stream + {Dexcord.Api, :send, 2}, + {Dexcord.Api, :message_history, 2}, + # §8 — hydration + {Dexcord.Cache, :fill, 1}, + # §5 — snowflake boundary cast + {Dexcord.Snowflake, :cast, 1}, + {Dexcord.Snowflake, :cast!, 1} + ] do + Code.ensure_loaded!(mod) + + assert function_exported?(mod, fun, arity), + "#{inspect(mod)}.#{fun}/#{arity} is called by the guide but is not exported" + end + end + + test "struct fields the guide accesses exist on their structs" do + assert_fields(%Dexcord.Message{}, [:author, :content, :channel_id, :webhook_id, :mentions]) + assert_fields(%Dexcord.User{}, [:id, :bot, :username]) + assert_fields(%Dexcord.Thread{}, [:id, :parent_id, :thread_metadata]) + assert_fields(%Dexcord.ThreadMetadata{}, [:archived]) + assert_fields(%Dexcord.Interaction{}, [:id, :token, :data]) + assert_fields(%Dexcord.Events.Ready{}, [:user, :guilds]) + assert_fields(%Dexcord.UnavailableGuild{}, [:id, :unavailable]) + assert_fields(%Dexcord.Events.ReactionAdd{}, [:user_id, :user, :channel, :guild]) + end + end + + defp assert_fields(struct, fields) do + for field <- fields do + assert Map.has_key?(struct, field), + "#{inspect(struct.__struct__)} is missing the `#{field}` field the guide accesses" + end + end +end diff --git a/test/dexcord/model_application_command_test.exs b/test/dexcord/model_application_command_test.exs new file mode 100644 index 0000000..a03e848 --- /dev/null +++ b/test/dexcord/model_application_command_test.exs @@ -0,0 +1,133 @@ +defmodule Dexcord.ModelApplicationCommandTest do + use ExUnit.Case, async: true + + alias Dexcord.ApplicationCommand + alias Dexcord.ApplicationCommand.{Option, OptionChoice} + alias Dexcord.{ApplicationCommandPermission, GuildApplicationCommandPermissions} + + describe "ApplicationCommand recursive options" do + setup do + map = %{ + "id" => "1000000000000000001", + "type" => 1, + "application_id" => "2000000000000000002", + "guild_id" => "3000000000000000003", + "name" => "config", + "description" => "Configure the bot", + "default_member_permissions" => "8", + "nsfw" => false, + "integration_types" => [0, 1], + "contexts" => [0, 1, 2], + "version" => "9000000000000000009", + "options" => [ + %{ + "type" => 2, + "name" => "channels", + "description" => "Channel settings", + "options" => [ + %{ + "type" => 1, + "name" => "set", + "description" => "Set a channel", + "options" => [ + %{ + "type" => 3, + "name" => "kind", + "description" => "Which kind", + "required" => true, + "choices" => [ + %{"name" => "Logs", "value" => "logs"}, + %{"name" => "Welcome", "value" => "welcome"} + ] + } + ] + } + ] + } + ] + } + + %{command: ApplicationCommand.from_map(map)} + end + + test "decodes the top-level command with typed enums", %{command: cmd} do + assert %ApplicationCommand{ + id: 1_000_000_000_000_000_001, + type: :chat_input, + application_id: 2_000_000_000_000_000_002, + guild_id: 3_000_000_000_000_000_003, + name: "config", + nsfw: false, + integration_types: [:guild_install, :user_install], + contexts: [:guild, :bot_dm, :private_channel], + version: 9_000_000_000_000_000_009 + } = cmd + end + + test "default_member_permissions decodes the wire string to an integer bitfield", %{ + command: cmd + } do + assert cmd.default_member_permissions == 8 + end + + test "decodes the sub_command_group -> sub_command -> string option tree recursively", %{ + command: cmd + } do + assert [%Option{type: :sub_command_group, name: "channels"} = group] = cmd.options + assert [%Option{type: :sub_command, name: "set"} = sub] = group.options + + assert [ + %Option{ + type: :string, + name: "kind", + required: true, + choices: [ + %OptionChoice{name: "Logs", value: "logs"}, + %OptionChoice{name: "Welcome", value: "welcome"} + ] + } + ] = sub.options + end + + test "to_map re-encodes default_member_permissions as a wire string", %{command: cmd} do + assert %{"default_member_permissions" => "8"} = ApplicationCommand.to_map(cmd) + end + + test "type defaults to :chat_input when the wire omits it" do + cmd = ApplicationCommand.from_map(%{"name" => "ping", "description" => "pong"}) + assert cmd.type == :chat_input + end + end + + describe "GuildApplicationCommandPermissions" do + test "decodes permission overrides with typed permission targets" do + map = %{ + "id" => "1000000000000000001", + "application_id" => "2000000000000000002", + "guild_id" => "3000000000000000003", + "permissions" => [ + %{"id" => "4000000000000000004", "type" => 1, "permission" => true}, + %{"id" => "5000000000000000005", "type" => 2, "permission" => false} + ] + } + + assert %GuildApplicationCommandPermissions{ + id: 1_000_000_000_000_000_001, + application_id: 2_000_000_000_000_000_002, + guild_id: 3_000_000_000_000_000_003, + permissions: [ + %ApplicationCommandPermission{ + id: 4_000_000_000_000_000_004, + type: :role, + permission: true + }, + %ApplicationCommandPermission{ + id: 5_000_000_000_000_000_005, + type: :user, + permission: false + } + ] + } = GuildApplicationCommandPermissions.from_map(map) + end + end +end diff --git a/test/dexcord/model_channel_test.exs b/test/dexcord/model_channel_test.exs new file mode 100644 index 0000000..98b95da --- /dev/null +++ b/test/dexcord/model_channel_test.exs @@ -0,0 +1,124 @@ +defmodule Dexcord.ModelChannelTest do + use ExUnit.Case, async: true + + # Literal expectation table — deliberately NOT reusing + # Dexcord.Channel.__type_map__/0 (that would test the code with itself). + @expected %{ + 0 => Dexcord.TextChannel, + 1 => Dexcord.DMChannel, + 2 => Dexcord.VoiceChannel, + 3 => Dexcord.GroupDMChannel, + 4 => Dexcord.CategoryChannel, + 5 => Dexcord.AnnouncementChannel, + 10 => Dexcord.Thread, + 11 => Dexcord.Thread, + 12 => Dexcord.Thread, + 13 => Dexcord.StageChannel, + 14 => Dexcord.DirectoryChannel, + 15 => Dexcord.ForumChannel, + 16 => Dexcord.MediaChannel + } + + setup_all do + {:ok, channels: Dexcord.Fixtures.load!("channel_types.json")} + end + + describe "api-surface.AC2.14: factory dispatch on wire type" do + test "every documented type decodes to exactly its expected struct", %{channels: channels} do + # The fixture covers all 13 documented types, once each. + assert Enum.map(channels, & &1["type"]) |> Enum.sort() == + Enum.sort(Map.keys(@expected)) + + for obj <- channels do + type = obj["type"] + expected = Map.fetch!(@expected, type) + decoded = Dexcord.Channel.from_map(obj) + + assert decoded.__struct__ == expected, + "type #{type} decoded to #{inspect(decoded.__struct__)}, expected #{inspect(expected)}" + end + end + + test "threads 10/11/12 all decode to %Dexcord.Thread{}", %{channels: channels} do + threads = Enum.filter(channels, &(&1["type"] in [10, 11, 12])) + assert length(threads) == 3 + + for obj <- threads do + assert %Dexcord.Thread{} = Dexcord.Channel.from_map(obj) + end + end + end + + describe "unknown-type fallback" do + test "a recognized-shape map with an unknown type int preserves the raw payload" do + raw = %{"type" => 99, "id" => "1"} + + assert %Dexcord.UnknownChannel{type: {:unknown, 99}, raw: ^raw} = + Dexcord.Channel.from_map(raw) + end + + test "a map with no type field decodes to UnknownChannel with nil type" do + assert %Dexcord.UnknownChannel{type: nil} = Dexcord.Channel.from_map(%{"id" => "1"}) + end + + test "a non-map decodes to nil" do + assert Dexcord.Channel.from_map("junk") == nil + end + end + + describe "field-depth decoding" do + test "forum available_tags decode to ForumTag structs with moderated set", %{ + channels: channels + } do + forum = channel_of_type(channels, 15) + decoded = Dexcord.Channel.from_map(forum) + + assert [%Dexcord.ForumTag{} = tag] = decoded.available_tags + assert tag.moderated == true + assert tag.name == "resolved" + end + + test "thread metadata and member timestamps decode to DateTime", %{channels: channels} do + thread = channel_of_type(channels, 11) + decoded = Dexcord.Channel.from_map(thread) + + assert %Dexcord.ThreadMetadata{} = decoded.thread_metadata + assert decoded.thread_metadata.archived == false + assert %Dexcord.ThreadMember{} = decoded.member + assert %DateTime{} = decoded.member.join_timestamp + end + + test "text channel overwrites decode allow to an integer", %{channels: channels} do + text = channel_of_type(channels, 0) + decoded = Dexcord.Channel.from_map(text) + + assert [%Dexcord.Overwrite{} = ow] = decoded.permission_overwrites + assert is_integer(ow.allow) + assert ow.allow == 1024 + assert ow.deny == 2048 + end + + test "DM channel decodes its recipients to User structs", %{channels: channels} do + dm = channel_of_type(channels, 1) + decoded = Dexcord.Channel.from_map(dm) + + assert [%Dexcord.User{} = user] = decoded.recipients + assert user.username == "Nelly" + end + end + + describe "type field carries the enum atom" do + test "decoded structs carry type as the enum atom", %{channels: channels} do + by_type = Map.new(channels, &{&1["type"], Dexcord.Channel.from_map(&1)}) + + assert by_type[0].type == :guild_text + assert by_type[1].type == :dm + assert by_type[2].type == :guild_voice + assert by_type[11].type == :public_thread + assert by_type[15].type == :guild_forum + end + end + + defp channel_of_type(channels, type), + do: Enum.find(channels, &(&1["type"] == type)) +end diff --git a/test/dexcord/model_component_test.exs b/test/dexcord/model_component_test.exs new file mode 100644 index 0000000..5cc1556 --- /dev/null +++ b/test/dexcord/model_component_test.exs @@ -0,0 +1,175 @@ +defmodule Dexcord.ModelComponentTest do + use ExUnit.Case, async: true + + # Literal expectation table — deliberately NOT reusing + # Dexcord.Component.__type_map__/0 (that would test the code with itself). + @expected %{ + 1 => Dexcord.Component.ActionRow, + 2 => Dexcord.Component.Button, + 3 => Dexcord.Component.StringSelect, + 4 => Dexcord.Component.TextInput, + 5 => Dexcord.Component.EntitySelect, + 6 => Dexcord.Component.EntitySelect, + 7 => Dexcord.Component.EntitySelect, + 8 => Dexcord.Component.EntitySelect, + 9 => Dexcord.Component.Section, + 10 => Dexcord.Component.TextDisplay, + 11 => Dexcord.Component.Thumbnail, + 12 => Dexcord.Component.MediaGallery, + 13 => Dexcord.Component.File, + 14 => Dexcord.Component.Separator, + 17 => Dexcord.Component.Container, + 18 => Dexcord.Component.Label, + 19 => Dexcord.Component.FileUpload, + 21 => Dexcord.Component.RadioGroup, + 22 => Dexcord.Component.CheckboxGroup, + 23 => Dexcord.Component.Checkbox + } + + describe "factory dispatch on wire type" do + test "every mapped type decodes to exactly its expected struct" do + assert Dexcord.Component.__type_map__() == @expected + + for {type, mod} <- @expected do + decoded = Dexcord.Component.from_map(%{"type" => type}) + + assert decoded.__struct__ == mod, + "type #{type} decoded to #{inspect(decoded.__struct__)}, expected #{inspect(mod)}" + end + end + + test "entity-select types 5/6/7/8 all decode to one struct" do + for type <- [5, 6, 7, 8] do + assert %Dexcord.Component.EntitySelect{} = + Dexcord.Component.from_map(%{"type" => type}) + end + end + end + + describe "recursive decode through the factory" do + test "an action row with a button + string select decodes recursively" do + wire = %{ + "type" => 1, + "id" => 7, + "components" => [ + %{"type" => 2, "style" => 1, "label" => "Click", "custom_id" => "btn"}, + %{ + "type" => 3, + "custom_id" => "sel", + "options" => [ + %{"label" => "One", "value" => "1", "default" => true} + ] + } + ] + } + + assert %Dexcord.Component.ActionRow{ + id: 7, + components: [ + %Dexcord.Component.Button{style: :primary, label: "Click", custom_id: "btn"}, + %Dexcord.Component.StringSelect{ + custom_id: "sel", + options: [ + %Dexcord.Component.SelectOption{label: "One", value: "1", default: true} + ] + } + ] + } = Dexcord.Component.from_map(wire) + end + + test "a Components-V2 container decodes its nested tree" do + wire = %{ + "type" => 17, + "accent_color" => 16_711_680, + "components" => [ + %{ + "type" => 9, + "components" => [%{"type" => 10, "content" => "Hello"}], + "accessory" => %{ + "type" => 11, + "media" => %{"url" => "https://example.test/a.png"}, + "description" => "art" + } + }, + %{"type" => 14, "divider" => true, "spacing" => 1} + ] + } + + assert %Dexcord.Component.Container{ + accent_color: 16_711_680, + components: [ + %Dexcord.Component.Section{ + components: [%Dexcord.Component.TextDisplay{content: "Hello"}], + accessory: %Dexcord.Component.Thumbnail{ + media: %Dexcord.Component.UnfurledMediaItem{ + url: "https://example.test/a.png" + }, + description: "art" + } + }, + %Dexcord.Component.Separator{divider: true, spacing: 1} + ] + } = Dexcord.Component.from_map(wire) + end + end + + describe "id is a plain integer, not a snowflake" do + test "component id stays an integer" do + assert %Dexcord.Component.TextDisplay{id: 42} = + Dexcord.Component.from_map(%{"type" => 10, "id" => 42, "content" => "x"}) + end + end + + describe "unknown-type fallback" do + test "an unknown type int preserves the raw payload" do + raw = %{"type" => 99, "custom_id" => "x"} + + assert %Dexcord.Component.Unknown{type: {:unknown, 99}, raw: ^raw} = + Dexcord.Component.from_map(raw) + end + + test "an unassigned type (15) also falls back to Unknown" do + assert %Dexcord.Component.Unknown{type: {:unknown, 15}} = + Dexcord.Component.from_map(%{"type" => 15}) + end + + test "a map with no type field decodes to Unknown with nil type" do + assert %Dexcord.Component.Unknown{type: nil} = + Dexcord.Component.from_map(%{"custom_id" => "x"}) + end + + test "Unknown re-encodes to its raw payload verbatim" do + raw = %{"type" => 99, "custom_id" => "x"} + unknown = Dexcord.Component.from_map(raw) + assert Dexcord.Component.Unknown.to_map(unknown) == raw + end + end + + describe "Message.components retype" do + test "a message map with components decodes typed via Message.from_map/1" do + wire = %{ + "id" => "123", + "content" => "hi", + "components" => [ + %{ + "type" => 1, + "components" => [%{"type" => 2, "style" => 2, "custom_id" => "b"}] + } + ] + } + + assert %Dexcord.Message{ + components: [ + %Dexcord.Component.ActionRow{ + components: [%Dexcord.Component.Button{style: :secondary, custom_id: "b"}] + } + ] + } = Dexcord.Message.from_map(wire) + end + + test "a message with no components defaults to []" do + assert %Dexcord.Message{components: []} = + Dexcord.Message.from_map(%{"id" => "1"}) + end + end +end diff --git a/test/dexcord/model_enums_test.exs b/test/dexcord/model_enums_test.exs new file mode 100644 index 0000000..ec4f106 --- /dev/null +++ b/test/dexcord/model_enums_test.exs @@ -0,0 +1,65 @@ +defmodule Dexcord.ModelEnumsTest do + use ExUnit.Case, async: true + + describe "ChannelType" do + test "decodes a live value" do + assert Dexcord.ChannelType.decode(16) == :guild_media + end + + test "removed store type 6 is unknown" do + assert Dexcord.ChannelType.decode(6) == {:unknown, 6} + end + end + + describe "MessageType" do + test "decodes a live value" do + assert Dexcord.MessageType.decode(46) == :poll_result + end + + test "real gap 13 is unknown" do + assert Dexcord.MessageType.decode(13) == {:unknown, 13} + end + end + + describe "Permissions" do + test "has? checks a named bit" do + assert Dexcord.Permissions.has?(8, :administrator) + end + + test "highest bit value is 2^52" do + assert Dexcord.Permissions.all()[:bypass_slowmode] == Bitwise.bsl(1, 52) + end + end + + describe "UserFlags" do + test "to_list picks bit 0 and bit 16, ordered by value" do + assert Dexcord.UserFlags.to_list(0b1_0000_0000_0000_0001) == [:staff, :verified_bot] + end + end + + describe "AuditLogEvent" do + test "decodes the highest live value" do + assert Dexcord.AuditLogEvent.decode(193) == :voice_channel_status_delete + end + + test "sparse gap 2 is unknown" do + assert Dexcord.AuditLogEvent.decode(2) == {:unknown, 2} + end + end + + describe "ComponentType" do + test "decodes a live value" do + assert Dexcord.ComponentType.decode(23) == :checkbox + end + + test "unassigned type 15 is unknown" do + assert Dexcord.ComponentType.decode(15) == {:unknown, 15} + end + end + + describe "AutoModerationTriggerType" do + test "removed trigger type 2 is unknown" do + assert Dexcord.AutoModerationTriggerType.decode(2) == {:unknown, 2} + end + end +end diff --git a/test/dexcord/model_guild_test.exs b/test/dexcord/model_guild_test.exs new file mode 100644 index 0000000..2041740 --- /dev/null +++ b/test/dexcord/model_guild_test.exs @@ -0,0 +1,120 @@ +defmodule Dexcord.ModelGuildTest do + use ExUnit.Case, async: true + + setup_all do + {:ok, guild: Dexcord.Guild.from_map(Dexcord.Fixtures.load!("guild_create.json"))} + end + + describe "api-surface.AC2.10: GUILD_CREATE decodes to a fully-typed %Dexcord.Guild{}" do + test "channels decode to the per-type struct for each wire type", %{guild: guild} do + assert [ + %Dexcord.TextChannel{}, + %Dexcord.VoiceChannel{}, + %Dexcord.CategoryChannel{}, + %Dexcord.ForumChannel{} + ] = guild.channels + end + + test "threads decode to %Dexcord.Thread{} with typed metadata", %{guild: guild} do + assert [%Dexcord.Thread{} = thread] = guild.threads + assert %Dexcord.ThreadMetadata{} = thread.thread_metadata + assert thread.thread_metadata.invitable == true + assert %Dexcord.ThreadMember{} = thread.member + end + + test "roles decode typed, with the booster role's premium_subscriber tag true", %{ + guild: guild + } do + assert [%Dexcord.Role{} = everyone, %Dexcord.Role{} = booster] = guild.roles + assert %Dexcord.RoleTags{} = booster.tags + assert booster.tags.premium_subscriber == true + # @everyone has no tags object at all. + assert everyone.tags == nil + end + + test "members decode typed with nested %Dexcord.User{}", %{guild: guild} do + assert [%Dexcord.Member{} = first, %Dexcord.Member{} = second] = guild.members + assert %Dexcord.User{username: "Nelly"} = first.user + assert %Dexcord.User{username: "Wumpus"} = second.user + assert second.nick == "The Wumpus" + assert %DateTime{} = second.premium_since + end + + test "joined_at is a DateTime and member_count is an integer", %{guild: guild} do + assert %DateTime{} = guild.joined_at + assert guild.member_count == 3 + end + + test "voice_states decode typed, with guild_id nil (GUILD_CREATE omits it)", %{guild: guild} do + assert [%Dexcord.VoiceState{} = vs] = guild.voice_states + assert vs.guild_id == nil + assert vs.user_id == 800_000_000_000_000_000 + assert vs.self_mute == true + end + + test "presences decode typed with a raw user map and typed client_status", %{guild: guild} do + assert [%Dexcord.Presence{} = presence] = guild.presences + assert is_map(presence.user) + refute is_struct(presence.user) + assert presence.user["id"] == "800000000000000000" + assert %Dexcord.ClientStatus{web: "online"} = presence.client_status + end + + test "guild core scalar/enum/flag fields decode", %{guild: guild} do + assert guild.id == 900_000_000_000_000_000 + assert guild.name == "Dexcord Test Guild" + assert guild.verification_level == :medium + assert guild.premium_tier == :tier_2 + assert guild.system_channel_flags == 5 + assert guild.features == ["COMMUNITY", "NEWS", "WELCOME_SCREEN_ENABLED"] + assert guild.large == false + assert guild.unavailable == false + end + + test "roles/emojis/stickers decode to their typed structs", %{guild: guild} do + assert [%Dexcord.Emoji{name: "wumpus"}] = guild.emojis + assert [%Dexcord.Sticker{name: "hello"}] = guild.stickers + end + end + + describe "Dexcord.UnavailableGuild" do + test "unavailable true when the key is present" do + assert %Dexcord.UnavailableGuild{unavailable: true} = + Dexcord.UnavailableGuild.from_map(%{"id" => "1", "unavailable" => true}) + end + + test "unavailable false when the key is absent (GUILD_DELETE removed-from-guild signal)" do + guild = Dexcord.UnavailableGuild.from_map(%{"id" => "1"}) + assert guild.unavailable == false + assert guild.id == 1 + end + end + + describe "Dexcord.PartialGuild" do + test "permissions string decodes to an integer and owner is a boolean" do + partial = + Dexcord.PartialGuild.from_map(%{ + "id" => "42", + "name" => "My Guild", + "owner" => true, + "permissions" => "2147483647", + "features" => ["COMMUNITY"] + }) + + assert partial.owner == true + assert partial.permissions == 2_147_483_647 + assert partial.features == ["COMMUNITY"] + end + end + + describe "Guild.merge_map/2 (feeds Phase 6 AC2.21)" do + test "a partial update touches only the present key", %{guild: guild} do + merged = Dexcord.Guild.merge_map(guild, %{"name" => "renamed"}) + + assert merged.name == "renamed" + assert merged.joined_at == guild.joined_at + assert merged.channels == guild.channels + assert merged.members == guild.members + end + end +end diff --git a/test/dexcord/model_interaction_test.exs b/test/dexcord/model_interaction_test.exs new file mode 100644 index 0000000..1b00fa3 --- /dev/null +++ b/test/dexcord/model_interaction_test.exs @@ -0,0 +1,148 @@ +defmodule Dexcord.ModelInteractionTest do + use ExUnit.Case, async: true + + alias Dexcord.Fixtures + alias Dexcord.Interaction + + alias Dexcord.Interaction.{ + ApplicationCommandData, + DataOption, + MessageComponentData, + ModalSubmitData + } + + describe "api-surface.AC2.15: INTERACTION_CREATE guild variant" do + setup do + %{interaction: Interaction.from_map(Fixtures.load!("interaction_slash_guild.json"))} + end + + test "decodes to a typed Interaction", %{interaction: interaction} do + assert %Interaction{ + id: 1_000_000_000_000_000_001, + type: :application_command, + guild_id: 800_000_000_000_000_001, + context: :guild + } = interaction + end + + test "member is a full Member with a nested User and integer permissions", %{ + interaction: interaction + } do + assert %Dexcord.Member{ + user: %Dexcord.User{id: 600_000_000_000_000_001, username: "invoker"}, + permissions: 2_147_483_647, + nick: "The Invoker" + } = interaction.member + end + + test "the guild variant carries member, not top-level user", %{interaction: interaction} do + assert interaction.user == nil + end + + test "app_permissions decodes to an integer bitfield", %{interaction: interaction} do + assert interaction.app_permissions == 8 + end + + test "data is a typed ApplicationCommandData with typed options", %{interaction: interaction} do + assert %ApplicationCommandData{ + name: "whois", + type: :chat_input, + options: [%DataOption{name: "target", type: :user, value: "600000000000000002"}] + } = interaction.data + end + end + + describe "api-surface.AC2.15: INTERACTION_CREATE DM variant" do + setup do + %{interaction: Interaction.from_map(Fixtures.load!("interaction_slash_dm.json"))} + end + + test "user is present at top level, member absent", %{interaction: interaction} do + assert %Dexcord.User{id: 600_000_000_000_000_003, username: "dmuser"} = interaction.user + assert interaction.member == nil + assert interaction.guild_id == nil + end + + test "context decodes to :bot_dm", %{interaction: interaction} do + assert interaction.context == :bot_dm + end + + test "data is still a typed ApplicationCommandData", %{interaction: interaction} do + assert %ApplicationCommandData{name: "ping", type: :chat_input} = interaction.data + end + end + + describe "api-surface.AC2.16: resolved interaction data decodes to partials" do + setup do + interaction = Interaction.from_map(Fixtures.load!("interaction_slash_guild.json")) + %{resolved: interaction.data.resolved} + end + + test "resolved is a typed ResolvedData struct", %{resolved: resolved} do + assert %Dexcord.ResolvedData{} = resolved + end + + test "users are full Users keyed by INTEGER snowflakes", %{resolved: resolved} do + assert %{600_000_000_000_000_002 => %Dexcord.User{username: "target"}} = resolved.users + end + + test "members are PartialMembers with no :user key on the struct", %{resolved: resolved} do + member = resolved.members[600_000_000_000_000_002] + + assert %Dexcord.PartialMember{nick: "Targeted", permissions: 104_324_673} = member + refute Map.has_key?(member, :user) + end + + test "channels are PartialChannels with the classic four fields set", %{resolved: resolved} do + channel = resolved.channels[700_000_000_000_000_001] + + assert %Dexcord.PartialChannel{ + id: 700_000_000_000_000_001, + name: "general", + type: :guild_text, + permissions: 2_147_483_647 + } = channel + end + + test "the thread channel variant carries thread_metadata", %{resolved: resolved} do + thread = resolved.channels[720_000_000_000_000_001] + + assert %Dexcord.PartialChannel{ + name: "help-thread", + thread_metadata: %Dexcord.ThreadMetadata{auto_archive_duration: 1440} + } = thread + end + end + + describe "component and modal data variants" do + test "component fixture decodes to MessageComponentData" do + interaction = Interaction.from_map(Fixtures.load!("interaction_component.json")) + + assert %Interaction{type: :message_component} = interaction + + assert %MessageComponentData{custom_id: "confirm_button", component_type: :button} = + interaction.data + + assert %Dexcord.Message{content: "Are you sure?"} = interaction.message + end + + test "modal fixture decodes to ModalSubmitData with a raw components list" do + interaction = Interaction.from_map(Fixtures.load!("interaction_modal.json")) + + assert %Interaction{type: :modal_submit} = interaction + + assert %ModalSubmitData{custom_id: "feedback_modal", components: components} = + interaction.data + + assert [%{"type" => 18}] = components + end + end + + describe "PING interactions carry no data variant" do + test "a type-1 interaction with no data decodes with data == nil" do + interaction = Interaction.from_map(%{"id" => "1", "type" => 1, "token" => "t"}) + + assert %Interaction{type: :ping, data: nil} = interaction + end + end +end diff --git a/test/dexcord/model_member_test.exs b/test/dexcord/model_member_test.exs new file mode 100644 index 0000000..4f6acb9 --- /dev/null +++ b/test/dexcord/model_member_test.exs @@ -0,0 +1,118 @@ +defmodule Dexcord.ModelMemberTest do + use ExUnit.Case, async: true + + @member_map %{ + "user" => %{"id" => "80351110224678912", "username" => "Nelly", "discriminator" => "1337"}, + "nick" => "NOT nelly", + "roles" => ["11111111111111111", "22222222222222222"], + "joined_at" => "2015-04-26T06:26:56.936000+00:00", + "premium_since" => "2016-05-15T06:26:56.936000+00:00", + "deaf" => false, + "mute" => false, + "flags" => 0, + "pending" => false, + "permissions" => "2147483648" + } + + describe "Member.from_map/1" do + test "full member decodes nested user, snowflake roles, and datetime joined_at" do + member = Dexcord.Member.from_map(@member_map) + + assert %Dexcord.User{} = member.user + assert member.user.id == 80_351_110_224_678_912 + assert member.nick == "NOT nelly" + assert member.roles == [11_111_111_111_111_111, 22_222_222_222_222_222] + assert %DateTime{} = member.joined_at + assert %DateTime{} = member.premium_since + assert member.deaf == false + assert member.flags == 0 + assert member.permissions == 2_147_483_648 + end + end + + describe "PartialMember.from_map/1" do + test "a member map without user decodes and the struct has no :user key" do + map = Map.delete(@member_map, "user") + pm = Dexcord.PartialMember.from_map(map) + + assert %Dexcord.PartialMember{} = pm + assert pm.nick == "NOT nelly" + assert pm.roles == [11_111_111_111_111_111, 22_222_222_222_222_222] + refute Map.has_key?(Map.from_struct(pm), :user) + end + end + + describe "shared field group parity" do + test "Member and PartialMember __fields__ differ by exactly the :user entry" do + member_names = Enum.map(Dexcord.Member.__fields__(), & &1.name) + partial_names = Enum.map(Dexcord.PartialMember.__fields__(), & &1.name) + + assert member_names -- partial_names == [:user] + assert partial_names -- member_names == [] + end + end + + describe "ThreadMember.from_map/1" do + test "thread member with nested member decodes" do + map = %{ + "id" => "1", + "user_id" => "2", + "join_timestamp" => "2021-01-01T00:00:00.000000+00:00", + "flags" => 1, + "member" => @member_map + } + + tm = Dexcord.ThreadMember.from_map(map) + + assert tm.id == 1 + assert tm.user_id == 2 + assert %DateTime{} = tm.join_timestamp + assert tm.flags == 1 + assert %Dexcord.Member{} = tm.member + assert %Dexcord.User{} = tm.member.user + end + end + + describe "ThreadMetadata / ForumTag / DefaultReaction" do + test "thread metadata decodes timestamps and booleans" do + map = %{ + "archived" => true, + "auto_archive_duration" => 1440, + "archive_timestamp" => "2021-01-01T00:00:00.000000+00:00", + "locked" => false, + "invitable" => true, + "create_timestamp" => "2021-01-01T00:00:00.000000+00:00" + } + + tmeta = Dexcord.ThreadMetadata.from_map(map) + assert tmeta.archived == true + assert tmeta.auto_archive_duration == 1440 + assert %DateTime{} = tmeta.archive_timestamp + assert tmeta.locked == false + assert tmeta.invitable == true + end + + test "forum tag decodes emoji fields" do + tag = + Dexcord.ForumTag.from_map(%{ + "id" => "1", + "name" => "help", + "moderated" => true, + "emoji_id" => "2", + "emoji_name" => nil + }) + + assert tag.id == 1 + assert tag.name == "help" + assert tag.moderated == true + assert tag.emoji_id == 2 + assert tag.emoji_name == nil + end + + test "default reaction decodes" do + dr = Dexcord.DefaultReaction.from_map(%{"emoji_id" => nil, "emoji_name" => "🔥"}) + assert dr.emoji_id == nil + assert dr.emoji_name == "🔥" + end + end +end diff --git a/test/dexcord/model_message_parts_test.exs b/test/dexcord/model_message_parts_test.exs new file mode 100644 index 0000000..7b38e08 --- /dev/null +++ b/test/dexcord/model_message_parts_test.exs @@ -0,0 +1,238 @@ +defmodule Dexcord.ModelMessagePartsTest do + use ExUnit.Case, async: true + + describe "Embed.from_map/1" do + test "a rich embed decodes fully typed with fields order preserved" do + map = %{ + "title" => "Hello", + "type" => "rich", + "description" => "world", + "url" => "https://example.com", + "timestamp" => "2021-01-01T00:00:00.000000+00:00", + "color" => 16_711_680, + "footer" => %{ + "text" => "the footer", + "icon_url" => "https://example.com/i.png", + "proxy_icon_url" => "https://proxy/i.png" + }, + "author" => %{ + "name" => "an author", + "url" => "https://example.com/a", + "icon_url" => "https://example.com/ai.png" + }, + "image" => %{"url" => "https://example.com/img.png", "height" => 100, "width" => 200}, + "fields" => [ + %{"name" => "first", "value" => "1", "inline" => true}, + %{"name" => "second", "value" => "2", "inline" => false} + ] + } + + embed = Dexcord.Embed.from_map(map) + + assert embed.title == "Hello" + assert embed.type == "rich" + assert embed.color == 16_711_680 + assert %DateTime{} = embed.timestamp + + assert %Dexcord.EmbedFooter{} = embed.footer + assert embed.footer.text == "the footer" + + assert %Dexcord.EmbedAuthor{} = embed.author + assert embed.author.name == "an author" + + assert %Dexcord.EmbedImage{} = embed.image + assert embed.image.height == 100 + assert embed.image.width == 200 + + assert [%Dexcord.EmbedField{} = f1, %Dexcord.EmbedField{} = f2] = embed.fields + assert f1.name == "first" + assert f1.inline == true + assert f2.name == "second" + assert f2.inline == false + end + + test "fields defaults to empty list when absent" do + embed = Dexcord.Embed.from_map(%{"title" => "t"}) + assert embed.fields == [] + end + end + + describe "Attachment.from_map/1" do + test "duration_secs decodes to a float" do + map = %{ + "id" => "1", + "filename" => "voice-message.ogg", + "size" => 12_345, + "url" => "https://cdn/x.ogg", + "proxy_url" => "https://proxy/x.ogg", + "duration_secs" => 2.5 + } + + att = Dexcord.Attachment.from_map(map) + + assert att.id == 1 + assert att.filename == "voice-message.ogg" + assert att.duration_secs == 2.5 + assert is_float(att.duration_secs) + end + + test "voice-message attachment keeps the raw flags int and waveform" do + map = %{ + "id" => "1", + "filename" => "voice-message.ogg", + "size" => 100, + "url" => "https://cdn/x.ogg", + "proxy_url" => "https://proxy/x.ogg", + "waveform" => "FzYACAAAAAAAAAAAAAAAAAAAAAAA", + "flags" => 8194 + } + + att = Dexcord.Attachment.from_map(map) + + assert att.waveform == "FzYACAAAAAAAAAAAAAAAAAAAAAAA" + assert att.flags == 8194 + end + end + + describe "Reaction.from_map/1" do + test "reaction with unicode partial emoji and count_details decodes" do + map = %{ + "count" => 3, + "count_details" => %{"burst" => 1, "normal" => 2}, + "me" => false, + "me_burst" => false, + "emoji" => %{"id" => nil, "name" => "🔥"}, + "burst_colors" => ["#ff0000"] + } + + reaction = Dexcord.Reaction.from_map(map) + + assert reaction.count == 3 + assert %Dexcord.ReactionCountDetails{} = reaction.count_details + assert reaction.count_details.burst == 1 + assert reaction.count_details.normal == 2 + assert %Dexcord.PartialEmoji{} = reaction.emoji + assert reaction.emoji.id == nil + assert reaction.emoji.name == "🔥" + assert reaction.burst_colors == ["#ff0000"] + end + end + + describe "AllowedMentions.to_map/1" do + test "default struct encodes declared defaults and omits nils" do + out = %Dexcord.AllowedMentions{} |> Dexcord.AllowedMentions.to_map() + assert out == %{"parse" => [], "replied_user" => false} + end + + test "decodes a populated allowed_mentions map" do + map = %{ + "parse" => ["users"], + "roles" => ["1"], + "users" => ["2"], + "replied_user" => true + } + + am = Dexcord.AllowedMentions.from_map(map) + assert am.parse == ["users"] + assert am.roles == [1] + assert am.users == [2] + assert am.replied_user == true + end + end + + describe "AllowedMentions.normalize/1" do + test "nil stays nil" do + assert Dexcord.AllowedMentions.normalize(nil) == nil + end + + test "a struct normalizes to its wire map (all set fields explicit)" do + assert Dexcord.AllowedMentions.normalize(%Dexcord.AllowedMentions{}) == + %{"parse" => [], "replied_user" => false} + end + + test "a keyword stringifies its keys, keeping only provided keys" do + assert Dexcord.AllowedMentions.normalize(parse: [], users: [5]) == + %{"parse" => [], "users" => [5]} + end + + test "a map with atom or string keys stringifies keys" do + assert Dexcord.AllowedMentions.normalize(%{parse: []}) == %{"parse" => []} + assert Dexcord.AllowedMentions.normalize(%{"users" => ["5"]}) == %{"users" => ["5"]} + end + end + + describe "AllowedMentions.merge/2" do + test "a nil default yields the per-send value" do + assert Dexcord.AllowedMentions.merge(nil, %{"users" => ["5"]}) == %{"users" => ["5"]} + end + + test "a nil per-send yields the default" do + assert Dexcord.AllowedMentions.merge(%{"parse" => []}, nil) == %{"parse" => []} + end + + test "field-wise: per-send keys win, default fills the rest" do + assert Dexcord.AllowedMentions.merge(%{"parse" => []}, %{"users" => ["5"]}) == + %{"parse" => [], "users" => ["5"]} + end + + test "a per-send key overrides the same key in the default" do + assert Dexcord.AllowedMentions.merge(%{"parse" => ["users"]}, %{"parse" => []}) == + %{"parse" => []} + end + end + + describe "Embed builder (AC3.6)" do + test "the builder chain produces the exact wire map" do + embed = + Dexcord.Embed.new() + |> Dexcord.Embed.title("t") + |> Dexcord.Embed.color(0xFF00FF) + |> Dexcord.Embed.field("a", "b", inline: true) + |> Dexcord.Embed.footer("f") + |> Dexcord.Embed.timestamp(~U[2026-07-04 12:00:00Z]) + + assert Dexcord.Embed.to_map(embed) == %{ + "title" => "t", + "color" => 0xFF00FF, + "fields" => [%{"name" => "a", "value" => "b", "inline" => true}], + "footer" => %{"text" => "f"}, + "timestamp" => "2026-07-04T12:00:00Z" + } + end + + test "description/url/image/thumbnail/author land on the struct and encode" do + embed = + Dexcord.Embed.new() + |> Dexcord.Embed.description("d") + |> Dexcord.Embed.url("https://e.com") + |> Dexcord.Embed.image("https://e.com/i.png") + |> Dexcord.Embed.thumbnail("https://e.com/t.png") + |> Dexcord.Embed.author("me", url: "https://e.com/me", icon_url: "https://e.com/me.png") + + assert Dexcord.Embed.to_map(embed) == %{ + "description" => "d", + "url" => "https://e.com", + "image" => %{"url" => "https://e.com/i.png"}, + "thumbnail" => %{"url" => "https://e.com/t.png"}, + "author" => %{ + "name" => "me", + "url" => "https://e.com/me", + "icon_url" => "https://e.com/me.png" + }, + "fields" => [] + } + end + + test "fields append in order" do + embed = + Dexcord.Embed.new() + |> Dexcord.Embed.field("a", "1") + |> Dexcord.Embed.field("b", "2", inline: true) + + assert [ + %Dexcord.EmbedField{name: "a", value: "1", inline: false}, + %Dexcord.EmbedField{name: "b", value: "2", inline: true} + ] = embed.fields + end + end +end diff --git a/test/dexcord/model_message_test.exs b/test/dexcord/model_message_test.exs new file mode 100644 index 0000000..0fefe2d --- /dev/null +++ b/test/dexcord/model_message_test.exs @@ -0,0 +1,168 @@ +defmodule Dexcord.ModelMessageTest do + use ExUnit.Case, async: true + + describe "api-surface.AC2.11: MESSAGE_CREATE member-without-user + full author" do + setup do + {:ok, msg: Dexcord.Message.from_map(Dexcord.Fixtures.load!("message_create_guild.json"))} + end + + test "member decodes to a %PartialMember{} with nick/roles/joined_at", %{msg: msg} do + assert %Dexcord.PartialMember{} = msg.member + assert msg.member.nick == "The Big N" + assert msg.member.roles == [197_038_439_483_310_087, 197_038_439_483_310_088] + assert %DateTime{} = msg.member.joined_at + # PartialMember genuinely has no :user field. + refute Map.has_key?(Map.from_struct(msg.member), :user) + end + + test "author decodes to a full %User{}", %{msg: msg} do + assert %Dexcord.User{} = msg.author + assert msg.author.id == 80_351_110_224_678_912 + assert msg.author.username == "Nelly" + end + + test "mentions decode to %User{} list, ignoring the embedded member key", %{msg: msg} do + assert [%Dexcord.User{} = mentioned] = msg.mentions + assert mentioned.id == 104_937_225_384_493_056 + assert mentioned.username == "friend" + # The per-mention embedded partial member is ignored (unknown-key tolerance). + refute Map.has_key?(Map.from_struct(mentioned), :member) + end + + test "mention_roles decode to snowflake integers", %{msg: msg} do + assert msg.mention_roles == [197_038_439_483_310_088] + end + + test "attachments and embeds decode to typed structs", %{msg: msg} do + assert [%Dexcord.Attachment{filename: "cat.png"}] = msg.attachments + assert [%Dexcord.Embed{title: "An Embed"}] = msg.embeds + end + end + + describe "api-surface.AC2.12: webhook-authored message" do + setup do + {:ok, msg: Dexcord.Message.from_map(Dexcord.Fixtures.load!("message_webhook.json"))} + end + + test "author is a synthetic %User{} whose id equals webhook_id", %{msg: msg} do + assert %Dexcord.User{} = msg.author + assert msg.author.id == msg.webhook_id + assert msg.webhook_id == 223_704_706_495_545_344 + end + + test "there is no member", %{msg: msg} do + assert msg.member == nil + end + + test "application_id decodes to a snowflake", %{msg: msg} do + assert msg.application_id == 223_704_706_495_545_344 + end + + test "decode does not raise", %{msg: msg} do + assert %Dexcord.Message{} = msg + end + end + + describe "api-surface.AC2.13: referenced_message tristate" do + test "present-null referenced_message decodes to nil (deleted reply)" do + map = Dexcord.Fixtures.load!("message_reply_deleted.json") + msg = Dexcord.Message.from_map(map) + + assert msg.referenced_message == nil + assert msg.type == :reply + assert %Dexcord.MessageReference{} = msg.message_reference + assert msg.message_reference.message_id == 1_052_425_005_447_712_818 + end + + test "absent referenced_message decodes to :absent" do + map = + "message_reply_deleted.json" + |> Dexcord.Fixtures.load!() + |> Map.delete("referenced_message") + + msg = Dexcord.Message.from_map(map) + assert msg.referenced_message == :absent + end + + test "present referenced_message decodes to a self-nested %Message{}" do + map = + "message_reply_deleted.json" + |> Dexcord.Fixtures.load!() + |> Map.put("referenced_message", %{ + "id" => "1052425005447712818", + "channel_id" => "155361364909801472", + "author" => %{"id" => "80351110224678912", "username" => "Nelly"}, + "content" => "the original message", + "type" => 0 + }) + + msg = Dexcord.Message.from_map(map) + assert %Dexcord.Message{} = msg.referenced_message + assert msg.referenced_message.content == "the original message" + assert %Dexcord.User{} = msg.referenced_message.author + end + end + + describe "DM message" do + setup do + {:ok, msg: Dexcord.Message.from_map(Dexcord.Fixtures.load!("message_dm.json"))} + end + + test "has no guild_id and no member, channel_type 1", %{msg: msg} do + assert msg.guild_id == nil + assert msg.member == nil + assert msg.channel_type == 1 + end + end + + describe "scalar decoding" do + setup do + {:ok, msg: Dexcord.Message.from_map(Dexcord.Fixtures.load!("message_create_guild.json"))} + end + + test "timestamp decodes to %DateTime{}", %{msg: msg} do + assert %DateTime{} = msg.timestamp + end + + test "default-type message decodes type to :default atom", %{msg: msg} do + assert msg.type == :default + end + end + + describe "satellites" do + test "MessageCall decodes participants and ended_timestamp" do + call = + Dexcord.MessageCall.from_map(%{ + "participants" => ["80351110224678912", "104937225384493056"], + "ended_timestamp" => "2022-12-14T09:00:00.000000+00:00" + }) + + assert call.participants == [80_351_110_224_678_912, 104_937_225_384_493_056] + assert %DateTime{} = call.ended_timestamp + end + + test "MessageSnapshot wraps a partial (author-less) message" do + snap = + Dexcord.MessageSnapshot.from_map(%{ + "message" => %{"content" => "forwarded content", "type" => 0} + }) + + assert %Dexcord.Message{} = snap.message + assert snap.message.content == "forwarded content" + assert snap.message.author == nil + end + + test "ChannelMention decodes type to the enum atom" do + cm = + Dexcord.ChannelMention.from_map(%{ + "id" => "155361364909801472", + "guild_id" => "197038439483310086", + "type" => 0, + "name" => "general" + }) + + assert cm.type == :guild_text + assert cm.id == 155_361_364_909_801_472 + end + end +end diff --git a/test/dexcord/model_resources_test.exs b/test/dexcord/model_resources_test.exs new file mode 100644 index 0000000..6c2d44a --- /dev/null +++ b/test/dexcord/model_resources_test.exs @@ -0,0 +1,293 @@ +defmodule Dexcord.ModelResourcesTest do + use ExUnit.Case, async: true + + describe "Dexcord.Webhook (fetched-by-token shape has no user)" do + test "decodes a webhook without a user" do + map = %{ + "id" => "100000000000000001", + "type" => 1, + "guild_id" => "200000000000000002", + "channel_id" => "300000000000000003", + "name" => "Spidey Bot", + "avatar" => "abc123", + "token" => "secrettoken", + "application_id" => nil + } + + assert %Dexcord.Webhook{ + id: 100_000_000_000_000_001, + type: :incoming, + guild_id: 200_000_000_000_000_002, + channel_id: 300_000_000_000_000_003, + name: "Spidey Bot", + user: nil, + application_id: nil + } = Dexcord.Webhook.from_map(map) + end + end + + describe "Dexcord.Invite and Dexcord.InviteMetadata (shared fields)" do + test "invite decodes the shared fields with typed enums" do + map = %{ + "type" => 0, + "code" => "abc123", + "target_type" => 1, + "approximate_member_count" => 42, + "flags" => 1 + } + + assert %Dexcord.Invite{ + type: :guild, + code: "abc123", + target_type: :stream, + approximate_member_count: 42 + } = Dexcord.Invite.from_map(map) + end + + test "invite metadata carries both invite fields and the metadata fields" do + map = %{ + "code" => "abc123", + "type" => 0, + "uses" => 5, + "max_uses" => 100, + "max_age" => 3600, + "temporary" => false, + "created_at" => "2026-07-05T00:00:00.000000+00:00" + } + + meta = Dexcord.InviteMetadata.from_map(map) + assert meta.code == "abc123" + assert meta.type == :guild + assert meta.uses == 5 + assert meta.max_uses == 100 + assert meta.max_age == 3600 + assert meta.temporary == false + assert %DateTime{} = meta.created_at + end + end + + describe "Dexcord.AuditLogEntry (string-typed numeric options)" do + test "options.count stays a string (Discord sends these numbers as strings)" do + map = %{ + "id" => "100000000000000001", + "target_id" => "200000000000000002", + "user_id" => "300000000000000003", + "action_type" => 21, + "guild_id" => "400000000000000004", + "options" => %{ + "count" => "15", + "delete_member_days" => "7", + "members_removed" => "4" + }, + "changes" => [ + %{"key" => "name", "old_value" => "old", "new_value" => "new"} + ] + } + + entry = Dexcord.AuditLogEntry.from_map(map) + + assert %Dexcord.AuditLogEntry{ + id: 100_000_000_000_000_001, + target_id: "200000000000000002", + user_id: 300_000_000_000_000_003, + action_type: :member_prune, + guild_id: 400_000_000_000_000_004 + } = entry + + assert %Dexcord.AuditLogEntryInfo{ + count: "15", + delete_member_days: "7", + members_removed: "4" + } = entry.options + + assert [%Dexcord.AuditLogChange{key: "name", old_value: "old", new_value: "new"}] = + entry.changes + end + end + + describe "Dexcord.Poll (results is tristate)" do + test "absent results decodes to :absent, not an empty struct" do + map = %{ + "question" => %{"text" => "Best hero?"}, + "answers" => [ + %{"answer_id" => 1, "poll_media" => %{"text" => "Spider-Man"}}, + %{"answer_id" => 2, "poll_media" => %{"text" => "Iron Man"}} + ], + "allow_multiselect" => false, + "layout_type" => 1 + } + + poll = Dexcord.Poll.from_map(map) + + assert %Dexcord.Poll{ + question: %Dexcord.PollMedia{text: "Best hero?"}, + layout_type: :default, + results: :absent + } = poll + + assert [ + %Dexcord.PollAnswer{ + answer_id: 1, + poll_media: %Dexcord.PollMedia{text: "Spider-Man"} + }, + %Dexcord.PollAnswer{answer_id: 2} + ] = poll.answers + end + + test "present results decodes to a PollResults struct" do + map = %{ + "question" => %{"text" => "Best hero?"}, + "results" => %{ + "is_finalized" => true, + "answer_counts" => [%{"id" => 1, "count" => 3, "me_voted" => true}] + } + } + + poll = Dexcord.Poll.from_map(map) + + assert %Dexcord.PollResults{ + is_finalized: true, + answer_counts: [%Dexcord.PollAnswerCount{id: 1, count: 3, me_voted: true}] + } = poll.results + end + end + + describe "Dexcord.GuildScheduledEvent (recurrence rule with enum lists)" do + test "recurrence rule decodes by_weekday as an enum list and honors the :end key" do + map = %{ + "id" => "100000000000000001", + "guild_id" => "200000000000000002", + "name" => "Weekly Standup", + "privacy_level" => 2, + "status" => 1, + "entity_type" => 2, + "entity_metadata" => %{"location" => "The Void"}, + "recurrence_rule" => %{ + "start" => "2026-07-05T00:00:00.000000+00:00", + "end" => "2026-12-05T00:00:00.000000+00:00", + "frequency" => 2, + "interval" => 1, + "by_weekday" => [0, 2, 4], + "by_month" => [7] + } + } + + event = Dexcord.GuildScheduledEvent.from_map(map) + + assert %Dexcord.GuildScheduledEvent{ + privacy_level: :guild_only, + status: :scheduled, + entity_type: :voice, + entity_metadata: %Dexcord.GuildScheduledEvent.EntityMetadata{location: "The Void"} + } = event + + rule = event.recurrence_rule + assert %Dexcord.GuildScheduledEvent.RecurrenceRule{frequency: :weekly, interval: 1} = rule + assert rule.by_weekday == [:monday, :wednesday, :friday] + assert rule.by_month == [:july] + assert %DateTime{} = rule.start + assert %DateTime{} = Map.fetch!(rule, :end) + end + end + + describe "Dexcord.Integration (string account id)" do + test "account.id stays a string, not a snowflake" do + map = %{ + "id" => "100000000000000001", + "name" => "Twitch", + "type" => "twitch", + "enabled" => true, + "expire_behavior" => 1, + "guild_id" => "200000000000000002", + "account" => %{"id" => "twitch_user_123", "name" => "streamer"} + } + + integration = Dexcord.Integration.from_map(map) + + assert %Dexcord.Integration{ + id: 100_000_000_000_000_001, + type: "twitch", + expire_behavior: :kick, + guild_id: 200_000_000_000_000_002, + account: %Dexcord.IntegrationAccount{id: "twitch_user_123", name: "streamer"} + } = integration + end + end + + describe "Dexcord.Activity (created_at is unix ms, not a datetime)" do + test "created_at stays an integer" do + map = %{"name" => "Coding", "type" => 0, "created_at" => 1_720_000_000_000} + activity = Dexcord.Activity.from_map(map) + + assert %Dexcord.Activity{name: "Coding", type: :playing, created_at: 1_720_000_000_000} = + activity + end + end + + describe "retypes" do + test "Guild.welcome_screen decodes to a WelcomeScreen struct" do + map = %{ + "welcome_screen" => %{ + "description" => "Welcome!", + "welcome_channels" => [ + %{"channel_id" => "100000000000000001", "description" => "rules"} + ] + } + } + + guild = Dexcord.Guild.from_map(map) + + assert %Dexcord.WelcomeScreen{ + description: "Welcome!", + welcome_channels: [ + %Dexcord.WelcomeScreenChannel{channel_id: 100_000_000_000_000_001} + ] + } = guild.welcome_screen + end + + test "Guild.guild_scheduled_events decodes to a list of GuildScheduledEvent" do + map = %{ + "guild_scheduled_events" => [ + %{"id" => "100000000000000001", "name" => "Event", "entity_type" => 3} + ] + } + + guild = Dexcord.Guild.from_map(map) + + assert [%Dexcord.GuildScheduledEvent{entity_type: :external}] = + guild.guild_scheduled_events + end + + test "Message.poll decodes to a Poll struct" do + map = %{"poll" => %{"question" => %{"text" => "Q?"}, "layout_type" => 1}} + message = Dexcord.Message.from_map(map) + assert %Dexcord.Poll{layout_type: :default} = message.poll + end + + test "Member gains guild_id (GUILD_MEMBER_ADD carries it)" do + map = %{ + "guild_id" => "100000000000000001", + "user" => %{"id" => "200000000000000002", "username" => "peter"}, + "nick" => "Spidey" + } + + assert %Dexcord.Member{ + guild_id: 100_000_000_000_000_001, + nick: "Spidey", + user: %Dexcord.User{username: "peter"} + } = Dexcord.Member.from_map(map) + end + + test "Thread gains newly_created" do + map = %{"id" => "100000000000000001", "type" => 11, "newly_created" => true} + assert %Dexcord.Thread{newly_created: true} = Dexcord.Thread.from_map(map) + end + + test "ThreadMember gains guild_id" do + map = %{"id" => "100000000000000001", "guild_id" => "200000000000000002"} + + assert %Dexcord.ThreadMember{guild_id: 200_000_000_000_000_002} = + Dexcord.ThreadMember.from_map(map) + end + end +end diff --git a/test/dexcord/model_role_test.exs b/test/dexcord/model_role_test.exs new file mode 100644 index 0000000..e8bdfd7 --- /dev/null +++ b/test/dexcord/model_role_test.exs @@ -0,0 +1,84 @@ +defmodule Dexcord.ModelRoleTest do + use ExUnit.Case, async: true + + describe "Role.from_map/1 — RoleTags presence booleans (api-surface.AC2.7)" do + test "premium_subscriber present-null is true, absent tag is false" do + map = %{ + "id" => "1", + "name" => "Booster", + "permissions" => "0", + "tags" => %{"bot_id" => "2", "premium_subscriber" => nil} + } + + role = Dexcord.Role.from_map(map) + + assert %Dexcord.RoleTags{} = role.tags + assert role.tags.premium_subscriber == true + assert role.tags.available_for_purchase == false + assert role.tags.guild_connections == false + assert role.tags.bot_id == 2 + end + end + + describe "Role.from_map/1 — string permission bitfields (wire_string)" do + test "a >2^31 permission string decodes to an integer" do + role = Dexcord.Role.from_map(%{"id" => "1", "name" => "r", "permissions" => "2147483648"}) + assert role.permissions == 2_147_483_648 + end + + test "permission bits are queryable after decode" do + role = Dexcord.Role.from_map(%{"id" => "1", "name" => "r", "permissions" => "8"}) + assert Dexcord.Permissions.has?(role.permissions, :administrator) + end + end + + describe "Role.to_map/1" do + test "re-encodes permissions as a decimal string and premium_subscriber as present-null" do + map = %{ + "id" => "1", + "name" => "Booster", + "permissions" => "8", + "tags" => %{"premium_subscriber" => nil} + } + + out = map |> Dexcord.Role.from_map() |> Dexcord.Role.to_map() + + assert out["permissions"] == "8" + assert Map.has_key?(out["tags"], "premium_subscriber") + assert out["tags"]["premium_subscriber"] == nil + end + end + + describe "Role colors" do + test "colors struct decodes nested" do + map = %{ + "id" => "1", + "name" => "r", + "colors" => %{"primary_color" => 1, "secondary_color" => 2, "tertiary_color" => 3} + } + + role = Dexcord.Role.from_map(map) + assert %Dexcord.RoleColors{} = role.colors + assert role.colors.primary_color == 1 + assert role.colors.secondary_color == 2 + assert role.colors.tertiary_color == 3 + end + end + + describe "Overwrite" do + test "allow/deny round-trip as decimal strings" do + map = %{"id" => "1", "type" => 0, "allow" => "1024", "deny" => "0"} + + ow = Dexcord.Overwrite.from_map(map) + assert ow.id == 1 + assert ow.type == :role + assert ow.allow == 1024 + assert ow.deny == 0 + + out = Dexcord.Overwrite.to_map(ow) + assert out["allow"] == "1024" + assert out["deny"] == "0" + assert out["type"] == 0 + end + end +end diff --git a/test/dexcord/model_user_test.exs b/test/dexcord/model_user_test.exs new file mode 100644 index 0000000..65b0cf1 --- /dev/null +++ b/test/dexcord/model_user_test.exs @@ -0,0 +1,188 @@ +defmodule Dexcord.ModelUserTest do + use ExUnit.Case, async: true + + describe "User.from_map/1" do + test "full user map decodes field-correct with snowflake normalized to integer" do + map = %{ + "id" => "80351110224678912", + "username" => "Nelly", + "discriminator" => "1337", + "global_name" => "Nelly", + "avatar" => "8342729096ea3675442027381ff50dfe", + "bot" => false, + "system" => false, + "mfa_enabled" => true, + "banner" => "06c16474723fe537c283b8efa61a30c8", + "accent_color" => 16_711_680, + "locale" => "en-US", + "verified" => true, + "email" => "nelly@discord.com", + "flags" => 64, + "premium_type" => 1, + "public_flags" => 64, + "avatar_decoration_data" => %{ + "asset" => "a_fb5bc2b3625390fd53a5e6ecb268e5ba", + "sku_id" => "1144058844004233369" + }, + "collectibles" => %{"nameplate" => %{"sku_id" => "x"}}, + "primary_guild" => %{ + "identity_guild_id" => "123456789", + "identity_enabled" => true, + "tag" => "PONY", + "badge" => "abc123" + } + } + + user = Dexcord.User.from_map(map) + + assert user.id == 80_351_110_224_678_912 + assert user.username == "Nelly" + assert user.discriminator == "1337" + assert user.global_name == "Nelly" + assert user.avatar == "8342729096ea3675442027381ff50dfe" + assert user.bot == false + assert user.system == false + assert user.mfa_enabled == true + assert user.banner == "06c16474723fe537c283b8efa61a30c8" + assert user.accent_color == 16_711_680 + assert user.locale == "en-US" + assert user.verified == true + assert user.email == "nelly@discord.com" + assert user.flags == 64 + assert user.premium_type == :nitro_classic + assert user.public_flags == 64 + assert user.collectibles == %{"nameplate" => %{"sku_id" => "x"}} + + assert %Dexcord.AvatarDecorationData{} = user.avatar_decoration_data + assert user.avatar_decoration_data.asset == "a_fb5bc2b3625390fd53a5e6ecb268e5ba" + assert user.avatar_decoration_data.sku_id == 1_144_058_844_004_233_369 + + assert %Dexcord.PrimaryGuild{} = user.primary_guild + assert user.primary_guild.identity_guild_id == 123_456_789 + assert user.primary_guild.identity_enabled == true + assert user.primary_guild.tag == "PONY" + assert user.primary_guild.badge == "abc123" + end + + test "minimal user map defaults other fields to nil and bot to false" do + map = %{ + "id" => "1", + "username" => "min", + "discriminator" => "0", + "global_name" => "min", + "avatar" => nil + } + + user = Dexcord.User.from_map(map) + + assert user.id == 1 + assert user.username == "min" + assert user.avatar == nil + assert user.bot == false + assert user.system == false + assert user.mfa_enabled == nil + assert user.flags == nil + assert user.premium_type == nil + assert user.avatar_decoration_data == nil + assert user.primary_guild == nil + end + + test "round-trip re-encodes id as a string" do + map = %{"id" => "80351110224678912", "username" => "Nelly", "discriminator" => "1337"} + out = map |> Dexcord.User.from_map() |> Dexcord.User.to_map() + assert out["id"] == "80351110224678912" + assert out["username"] == "Nelly" + end + end + + describe "PartialEmoji.from_map/1" do + test "unicode emoji decodes with nil id and the character as name" do + emoji = Dexcord.PartialEmoji.from_map(%{"id" => nil, "name" => "🔥"}) + assert emoji.id == nil + assert emoji.name == "🔥" + assert emoji.animated == false + end + + test "custom emoji decodes id, name, and animated" do + emoji = + Dexcord.PartialEmoji.from_map(%{"id" => "123", "name" => "blob", "animated" => true}) + + assert emoji.id == 123 + assert emoji.name == "blob" + assert emoji.animated == true + end + + test "deleted custom emoji with null name decodes name to nil" do + emoji = Dexcord.PartialEmoji.from_map(%{"id" => "123", "name" => nil}) + assert emoji.id == 123 + assert emoji.name == nil + end + end + + describe "Emoji.from_map/1" do + test "guild emoji decodes with roles and nested user" do + map = %{ + "id" => "41771983429993937", + "name" => "LUL", + "roles" => ["41771983429993000"], + "user" => %{"id" => "96008815106887111", "username" => "Cool", "discriminator" => "0001"}, + "require_colons" => true, + "managed" => false, + "animated" => false, + "available" => true + } + + emoji = Dexcord.Emoji.from_map(map) + + assert emoji.id == 41_771_983_429_993_937 + assert emoji.name == "LUL" + assert emoji.roles == [41_771_983_429_993_000] + assert %Dexcord.User{} = emoji.user + assert emoji.user.id == 96_008_815_106_887_111 + assert emoji.require_colons == true + assert emoji.animated == false + assert emoji.available == true + end + + test "roles defaults to empty list when absent" do + emoji = Dexcord.Emoji.from_map(%{"id" => "1", "name" => "x"}) + assert emoji.roles == [] + end + end + + describe "Sticker.from_map/1 and StickerItem.from_map/1" do + test "sticker decodes type and format_type enums and nested user" do + map = %{ + "id" => "749054660769218631", + "pack_id" => "847199849233514549", + "name" => "Wave", + "description" => "Wumpus waves hello", + "tags" => "wumpus, hello, sup, hi, oh, greeting, wave, welcome", + "type" => 1, + "format_type" => 3, + "available" => true, + "guild_id" => "1", + "user" => %{"id" => "2", "username" => "u", "discriminator" => "0"}, + "sort_value" => 12 + } + + sticker = Dexcord.Sticker.from_map(map) + + assert sticker.id == 749_054_660_769_218_631 + assert sticker.pack_id == 847_199_849_233_514_549 + assert sticker.name == "Wave" + assert sticker.type == :standard + assert sticker.format_type == :lottie + assert sticker.available == true + assert %Dexcord.User{} = sticker.user + assert sticker.sort_value == 12 + end + + test "sticker item decodes minimal shape" do + item = Dexcord.StickerItem.from_map(%{"id" => "1", "name" => "Wave", "format_type" => 1}) + assert item.id == 1 + assert item.name == "Wave" + assert item.format_type == :png + end + end +end diff --git a/test/dexcord/pagination_test.exs b/test/dexcord/pagination_test.exs new file mode 100644 index 0000000..85d63dd --- /dev/null +++ b/test/dexcord/pagination_test.exs @@ -0,0 +1,177 @@ +defmodule Dexcord.PaginationTest do + @moduledoc """ + Wire tests for the lazy pagination streams on `Dexcord.Api` + (api-surface.AC3.7), driven through `Dexcord.FakeRest`. + + The point of these streams is laziness: a page is fetched only when the + consumer walks that far, so `Stream.take/2` on a fresh stream makes exactly + one wire hit. These tests assert that directly (the second hit is refuted). + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Paginate + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(Dexcord.Cache) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + # Build a JSON array body of message objects with the given ids (as strings). + defp messages_json(ids), + do: "[" <> Enum.map_join(ids, ",", fn id -> ~s({"id":"#{id}"}) end) <> "]" + + # Build a JSON array of member objects (nested user id) with the given ids. + defp members_json(ids), + do: "[" <> Enum.map_join(ids, ",", fn id -> ~s({"user":{"id":"#{id}"}}) end) <> "]" + + describe "AC3.7: message_history pages lazily across multiple hits" do + test "two sequential pages yield every message in order, before= cursor on hit 2" do + page1_ids = Enum.to_list(200..101//-1) + page2_ids = [100, 99, 98] + assert length(page1_ids) == 100 + + FakeRest.stub( + :get, + "/channels/1/messages", + FakeRest.resp(200, body: messages_json(page1_ids)) + ) + + FakeRest.stub( + :get, + "/channels/1/messages", + FakeRest.resp(200, body: messages_json(page2_ids)) + ) + + result = Api.message_history(1) |> Enum.to_list() + + assert Enum.map(result, & &1.id) == page1_ids ++ page2_ids + + # Exactly two hits; the second carries before=. + assert_receive {:rest_hit, %{method: "GET", path: "/channels/1/messages", query_string: q1}} + refute q1 =~ "before=" + assert_receive {:rest_hit, %{path: "/channels/1/messages", query_string: q2}} + assert q2 =~ "before=101" + refute_receive {:rest_hit, %{path: "/channels/1/messages"}}, 50 + end + + test "LAZINESS: Stream.take(5) makes exactly one wire hit" do + # A single, sticky 100-message page: if the stream were eager it would + # loop forever hitting the wire. take(5) must fetch exactly one page. + full_page = Enum.to_list(500..401//-1) + assert length(full_page) == 100 + + FakeRest.stub( + :get, + "/channels/7/messages", + FakeRest.resp(200, body: messages_json(full_page)) + ) + + result = Api.message_history(7) |> Stream.take(5) |> Enum.to_list() + + assert Enum.map(result, & &1.id) == [500, 499, 498, 497, 496] + assert_receive {:rest_hit, %{path: "/channels/7/messages"}} + refute_receive {:rest_hit, %{path: "/channels/7/messages"}}, 50 + end + + test "after: flips to ascending paging with after= cursor" do + page1_ids = Enum.to_list(101..200) + page2_ids = [201, 202, 203] + assert length(page1_ids) == 100 + + FakeRest.stub( + :get, + "/channels/2/messages", + FakeRest.resp(200, body: messages_json(page1_ids)) + ) + + FakeRest.stub( + :get, + "/channels/2/messages", + FakeRest.resp(200, body: messages_json(page2_ids)) + ) + + result = Api.message_history(2, after: 100) |> Enum.to_list() + + assert Enum.map(result, & &1.id) == page1_ids ++ page2_ids + + assert_receive {:rest_hit, %{path: "/channels/2/messages", query_string: q1}} + assert q1 =~ "after=100" + assert_receive {:rest_hit, %{path: "/channels/2/messages", query_string: q2}} + assert q2 =~ "after=200" + refute q2 =~ "before=" + end + + test "limit: caps the total number of messages via Stream.take" do + full_page = Enum.to_list(300..201//-1) + + FakeRest.stub( + :get, + "/channels/3/messages", + FakeRest.resp(200, body: messages_json(full_page)) + ) + + result = Api.message_history(3, limit: 3) |> Enum.to_list() + + assert Enum.map(result, & &1.id) == [300, 299, 298] + assert_receive {:rest_hit, %{path: "/channels/3/messages"}} + refute_receive {:rest_hit, %{path: "/channels/3/messages"}}, 50 + end + end + + describe "guild_members_stream pages ascending on the last user_id" do + test "cursor is the last member's user id" do + page1_ids = Enum.to_list(1..1000) + page2_ids = [1001, 1002] + assert length(page1_ids) == 1000 + + FakeRest.stub(:get, "/guilds/9/members", FakeRest.resp(200, body: members_json(page1_ids))) + FakeRest.stub(:get, "/guilds/9/members", FakeRest.resp(200, body: members_json(page2_ids))) + + result = Api.guild_members_stream(9) |> Enum.to_list() + + assert Enum.map(result, & &1.user.id) == page1_ids ++ page2_ids + + assert_receive {:rest_hit, %{path: "/guilds/9/members", query_string: q1}} + assert q1 =~ "after=0" + assert_receive {:rest_hit, %{path: "/guilds/9/members", query_string: q2}} + assert q2 =~ "after=1000" + end + end + + describe "error mid-stream" do + test "an {:error, _} page raises Dexcord.Api.Paginate.PageError" do + full_page = Enum.to_list(200..101//-1) + + FakeRest.stub( + :get, + "/channels/5/messages", + FakeRest.resp(200, body: messages_json(full_page)) + ) + + FakeRest.stub( + :get, + "/channels/5/messages", + FakeRest.resp(500, body: ~s({"message":"boom"})) + ) + + assert_raise Paginate.PageError, fn -> + Api.message_history(5) |> Enum.to_list() + end + end + end +end diff --git a/test/dexcord/permissions_compute_test.exs b/test/dexcord/permissions_compute_test.exs new file mode 100644 index 0000000..543d704 --- /dev/null +++ b/test/dexcord/permissions_compute_test.exs @@ -0,0 +1,153 @@ +defmodule Dexcord.PermissionsComputeTest do + @moduledoc """ + Pure tests for `Dexcord.Guild.member_permissions/2,3` — the documented + overwrite-resolution algorithm (owner → base roles → ADMINISTRATOR short-circuit + → @everyone/role/member overwrites → timeout rule), verified verbatim. + """ + use ExUnit.Case, async: true + + import Bitwise + + alias Dexcord.Guild + alias Dexcord.Member + alias Dexcord.Overwrite + alias Dexcord.Permissions + alias Dexcord.Role + + @send Permissions.from_list([:send_messages]) + @view Permissions.from_list([:view_channel]) + @rmh Permissions.from_list([:read_message_history]) + @admin Permissions.from_list([:administrator]) + @ban Permissions.from_list([:ban_members]) + @all Permissions.all() |> Map.values() |> Enum.reduce(0, &bor/2) + @timeout_allowed bor(@view, @rmh) + + # Guild id 1; @everyone role has id == guild id (== 1). + defp guild(roles), do: %Guild{id: 1, owner_id: 100, roles: roles} + defp everyone(perms), do: %Role{id: 1, permissions: perms} + defp role(id, perms), do: %Role{id: id, permissions: perms} + defp member(user_id, role_ids), do: %Member{user_id: user_id, roles: role_ids} + + test "the guild owner gets all permissions" do + g = guild([everyone(0)]) + m = member(100, []) + assert Guild.member_permissions(g, m) == @all + end + + test "an ADMINISTRATOR member gets all permissions, even with a deny-everything channel overwrite" do + g = guild([everyone(0), role(20, @admin)]) + m = member(200, [20]) + + channel = %Dexcord.TextChannel{ + id: 5, + permission_overwrites: [%Overwrite{id: 1, type: :role, allow: 0, deny: @all}] + } + + assert Guild.member_permissions(g, m, channel) == @all + end + + test "base permissions are the OR of @everyone and every member role" do + g = guild([everyone(@view), role(10, @send), role(11, @ban)]) + m = member(200, [10, 11]) + + assert Guild.member_permissions(g, m) == (@view ||| @send ||| @ban) + end + + test "unknown member role ids are skipped" do + g = guild([everyone(@view), role(10, @send)]) + m = member(200, [10, 999]) + + assert Guild.member_permissions(g, m) == (@view ||| @send) + end + + test "an @everyone channel overwrite that denies SEND_MESSAGES removes it" do + g = guild([everyone(@view), role(10, @send)]) + m = member(200, [10]) + + channel = %Dexcord.TextChannel{ + id: 5, + permission_overwrites: [%Overwrite{id: 1, type: :role, allow: 0, deny: @send}] + } + + result = Guild.member_permissions(g, m, channel) + refute Permissions.has?(result, :send_messages) + assert Permissions.has?(result, :view_channel) + end + + test "a role overwrite allow restores a bit denied by @everyone (deny then allow order)" do + g = guild([everyone(@view), role(10, @send)]) + m = member(200, [10]) + + channel = %Dexcord.TextChannel{ + id: 5, + permission_overwrites: [ + %Overwrite{id: 1, type: :role, allow: 0, deny: @send}, + %Overwrite{id: 10, type: :role, allow: @send, deny: 0} + ] + } + + result = Guild.member_permissions(g, m, channel) + assert Permissions.has?(result, :send_messages) + end + + test "a member overwrite deny wins over a role overwrite allow (member applied last)" do + g = guild([everyone(@view), role(10, 0)]) + m = member(200, [10]) + + channel = %Dexcord.TextChannel{ + id: 5, + permission_overwrites: [ + %Overwrite{id: 10, type: :role, allow: @send, deny: 0}, + %Overwrite{id: 200, type: :member, allow: 0, deny: @send} + ] + } + + result = Guild.member_permissions(g, m, channel) + refute Permissions.has?(result, :send_messages) + end + + test "within the role tier, an allow OR-aggregates over a deny of the same bit (allow wins)" do + g = guild([everyone(@view), role(10, 0), role(11, 0)]) + m = member(200, [10, 11]) + + channel = %Dexcord.TextChannel{ + id: 5, + permission_overwrites: [ + %Overwrite{id: 10, type: :role, allow: 0, deny: @send}, + %Overwrite{id: 11, type: :role, allow: @send, deny: 0} + ] + } + + result = Guild.member_permissions(g, m, channel) + assert Permissions.has?(result, :send_messages) + end + + test "a timed-out member keeps exactly VIEW_CHANNEL | READ_MESSAGE_HISTORY" do + future = DateTime.add(DateTime.utc_now(), 3600, :second) + g = guild([everyone(@view ||| @send ||| @rmh)]) + + m = %Member{user_id: 200, roles: [], communication_disabled_until: future} + + assert Guild.member_permissions(g, m) == @timeout_allowed + end + + test "a timed-out administrator is unaffected (owner/admin skip the timeout rule)" do + future = DateTime.add(DateTime.utc_now(), 3600, :second) + g = guild([everyone(0), role(20, @admin)]) + + m = %Member{user_id: 200, roles: [20], communication_disabled_until: future} + + assert Guild.member_permissions(g, m) == @all + end + + test "a PAST communication_disabled_until is NOT a timeout" do + past = DateTime.add(DateTime.utc_now(), -3600, :second) + g = guild([everyone(@view ||| @send)]) + + m = %Member{user_id: 200, roles: [], communication_disabled_until: past} + + result = Guild.member_permissions(g, m) + assert Permissions.has?(result, :send_messages) + assert Permissions.has?(result, :view_channel) + end +end diff --git a/test/dexcord/prefix_test.exs b/test/dexcord/prefix_test.exs index 80f2104..1416aee 100644 --- a/test/dexcord/prefix_test.exs +++ b/test/dexcord/prefix_test.exs @@ -74,31 +74,42 @@ defmodule Dexcord.PrefixTest do end test "skips the bot's own messages via the cached self id, even without a bot flag" do - Dexcord.Cache.handle_dispatch(:READY, %{"user" => %{"id" => "me-id"}}, %{}) - msg = %{"content" => "!ping", "author" => %{"id" => "me-id"}} + raw = %{"user" => %{"id" => "123"}} + Dexcord.Cache.handle_dispatch(:READY, Dexcord.Events.decode(:READY, raw), raw, %{}) + msg = Dexcord.Message.from_map(%{"content" => "!ping", "author" => %{"id" => "123"}}) assert Prefix.dispatch(msg, prefix: "!", to: Router) == :ignore refute_received {:routed, _, _, _} end test "routes a matching command to the router" do - msg = %{"content" => "!ping a b", "author" => %{"id" => "7", "bot" => false}} + msg = + Dexcord.Message.from_map(%{ + "content" => "!ping a b", + "author" => %{"id" => "7", "bot" => false} + }) + assert Prefix.dispatch(msg, prefix: "!", to: Router) == :handled assert_received {:routed, "ping", ["a", "b"], ^msg} end test "the injected catch-all returns :ignore for unknown commands" do - msg = %{"content" => "!unknown", "author" => %{"id" => "7"}} + msg = Dexcord.Message.from_map(%{"content" => "!unknown", "author" => %{"id" => "7"}}) assert Prefix.dispatch(msg, prefix: "!", to: Router) == :ignore end test "returns :ignore on a non-match without touching the router" do - msg = %{"content" => "no prefix", "author" => %{"id" => "7"}} + msg = Dexcord.Message.from_map(%{"content" => "no prefix", "author" => %{"id" => "7"}}) assert Prefix.dispatch(msg, prefix: "!", to: Router) == :ignore refute_received {:routed, _, _, _} end test "skips messages whose author is a bot" do - msg = %{"content" => "!ping", "author" => %{"id" => "7", "bot" => true}} + msg = + Dexcord.Message.from_map(%{ + "content" => "!ping", + "author" => %{"id" => "7", "bot" => true} + }) + assert Prefix.dispatch(msg, prefix: "!", to: Router) == :ignore refute_received {:routed, _, _, _} end diff --git a/test/dexcord/ratelimit_test.exs b/test/dexcord/ratelimit_test.exs index b1fb053..616f80c 100644 --- a/test/dexcord/ratelimit_test.exs +++ b/test/dexcord/ratelimit_test.exs @@ -93,6 +93,53 @@ defmodule Dexcord.Api.RatelimitTest do assert Ratelimit.route_key(:get, "/channels/1/messages?limit=50") == "GET /channels/1/messages" end + + test "invite codes collapse to :code and never leak literally" do + key = Ratelimit.route_key(:delete, "/invites/aBcD3fG") + assert key == "DELETE /invites/:code" + refute key =~ "aBcD3fG" + + # query strings are stripped before the code collapses. + assert Ratelimit.route_key(:get, "/invites/xyz?with_counts=true") == + "GET /invites/:code" + end + + test "new route shapes collapse via the digits fallback and keep majors" do + # sticker-pack id is a minor param. + assert Ratelimit.route_key(:get, "/sticker-packs/123456789") == + "GET /sticker-packs/:id" + + # application emojis: application id and emoji id are both minor. + assert Ratelimit.route_key(:post, "/applications/123/emojis") == + "POST /applications/:id/emojis" + + assert Ratelimit.route_key(:delete, "/applications/123/emojis/456") == + "DELETE /applications/:id/emojis/:id" + + # guild id is a major param and stays literal even on emoji routes. + assert Ratelimit.route_key(:get, "/guilds/1/emojis/2") == + "GET /guilds/1/emojis/:id" + end + + # AC1.10 (final): the full Phase 5 surface must still produce bounded, + # id-collapsed keys — no unbounded snowflake leakage on the new route shapes. + test "new Phase 5 route shapes collapse to bounded keys" do + # scheduled-event users: guild is major (literal), the event id collapses. + assert Ratelimit.route_key(:get, "/guilds/1/scheduled-events/2/users") == + "GET /guilds/1/scheduled-events/:id/users" + + # poll answer voters: channel is major; message id and answer id collapse. + assert Ratelimit.route_key(:get, "/channels/1/polls/2/answers/3") == + "GET /channels/1/polls/:id/answers/:id" + + # automod rules: guild is major; the rule id collapses. + assert Ratelimit.route_key(:get, "/guilds/1/auto-moderation/rules/2") == + "GET /guilds/1/auto-moderation/rules/:id" + + # voice-status: channel is major, no trailing id to collapse. + assert Ratelimit.route_key(:put, "/channels/1/voice-status") == + "PUT /channels/1/voice-status" + end end describe "bucket + global math (injected clock)" do diff --git a/test/dexcord/registrar_integration_test.exs b/test/dexcord/registrar_integration_test.exs index 0100a7e..a88b95f 100644 --- a/test/dexcord/registrar_integration_test.exs +++ b/test/dexcord/registrar_integration_test.exs @@ -37,43 +37,47 @@ defmodule Dexcord.RegistrarIntegrationTest do Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) FakeRest.subscribe(self()) + # The application/guild ids are numeric snowflakes: `get_current_application` + # now decodes to `%Dexcord.ApplicationInfo{}` (id cast to an integer) and the + # `bulk_overwrite_*` endpoints cast their `*_id` path params via + # `Dexcord.Snowflake.cast/1`. As of Phase 5 Task 5, `get_current_application` + # is re-pointed to the docs-canonical `GET /applications/@me`. FakeRest.stub( :get, - "/oauth2/applications/@me", - FakeRest.resp(200, body: ~s({"id":"app-99"})) + "/applications/@me", + FakeRest.resp(200, body: ~s({"id":"99"})) ) :ok end test "global mode overwrites application commands and caches the app id" do - FakeRest.stub(:put, "/applications/app-99/commands", FakeRest.resp(200, body: "[]")) + FakeRest.stub(:put, "/applications/99/commands", FakeRest.resp(200, body: "[]")) config = %{slash: Commands, slash_guild_ids: nil} assert Registrar.run(config) == :ok - assert Dexcord.Config.application_id() == "app-99" + assert Dexcord.Config.application_id() == 99 - assert_receive {:rest_hit, %{method: "GET", path: "/oauth2/applications/@me"}} + assert_receive {:rest_hit, %{method: "GET", path: "/applications/@me"}} - assert_receive {:rest_hit, - %{method: "PUT", path: "/applications/app-99/commands", body: body}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands", body: body}} assert JSON.decode!(body) == [%{"name" => "ping", "description" => "Pong!"}] end test "guild mode overwrites each guild and NEVER wipes global commands" do - FakeRest.stub(:put, "/applications/app-99/guilds/g1/commands", FakeRest.resp(200, body: "[]")) - FakeRest.stub(:put, "/applications/app-99/guilds/g2/commands", FakeRest.resp(200, body: "[]")) + FakeRest.stub(:put, "/applications/99/guilds/1/commands", FakeRest.resp(200, body: "[]")) + FakeRest.stub(:put, "/applications/99/guilds/2/commands", FakeRest.resp(200, body: "[]")) - config = %{slash: Commands, slash_guild_ids: ["g1", "g2"]} + config = %{slash: Commands, slash_guild_ids: ["1", "2"]} assert Registrar.run(config) == :ok - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/guilds/g1/commands"}} - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/guilds/g2/commands"}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/guilds/1/commands"}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/guilds/2/commands"}} # The global-commands route must never be hit in guild (dev) mode. - refute_received {:rest_hit, %{path: "/applications/app-99/commands"}} + refute_received {:rest_hit, %{path: "/applications/99/commands"}} end test "empty commands in global mode SKIPS the overwrite (never wipes) and logs a hint" do @@ -84,9 +88,9 @@ defmodule Dexcord.RegistrarIntegrationTest do assert Registrar.run(config) == :ok end) - assert_receive {:rest_hit, %{method: "GET", path: "/oauth2/applications/@me"}} + assert_receive {:rest_hit, %{method: "GET", path: "/applications/@me"}} # The global-commands overwrite must NOT be attempted for an empty list. - refute_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/commands"}} + refute_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}} assert log =~ "commands/0 is empty in global mode" assert log =~ "bulk_overwrite_global_commands" @@ -97,7 +101,7 @@ defmodule Dexcord.RegistrarIntegrationTest do Dexcord.EnvSandbox.sandbox_env() Application.put_env(:dexcord, :registrar_retry_delays, [0, 0]) - FakeRest.stub(:put, "/applications/app-99/commands", FakeRest.resp(403, body: "forbidden")) + FakeRest.stub(:put, "/applications/99/commands", FakeRest.resp(403, body: "forbidden")) config = %{slash: Commands, slash_guild_ids: nil} @@ -108,10 +112,10 @@ defmodule Dexcord.RegistrarIntegrationTest do end) # Exactly 3 overwrite attempts hit the fake, and not a 4th. - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/commands"}} - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/commands"}} - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/commands"}} - refute_receive {:rest_hit, %{method: "PUT", path: "/applications/app-99/commands"}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}} + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}} + refute_receive {:rest_hit, %{method: "PUT", path: "/applications/99/commands"}} assert log =~ "giving up after 3 attempt(s)" end diff --git a/test/dexcord/registrar_tree_test.exs b/test/dexcord/registrar_tree_test.exs index f2d2f42..b21ff4f 100644 --- a/test/dexcord/registrar_tree_test.exs +++ b/test/dexcord/registrar_tree_test.exs @@ -41,9 +41,9 @@ defmodule Dexcord.RegistrarTreeTest do Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) FakeRest.subscribe(self()) - FakeRest.stub(:get, "/oauth2/applications/@me", FakeRest.resp(200, body: ~s({"id":"app-1"}))) + FakeRest.stub(:get, "/applications/@me", FakeRest.resp(200, body: ~s({"id":"1"}))) # Registration fails forever with a 4xx. - FakeRest.stub(:put, "/applications/app-1/commands", FakeRest.resp(403, body: "forbidden")) + FakeRest.stub(:put, "/applications/1/commands", FakeRest.resp(403, body: "forbidden")) :ok end @@ -63,10 +63,10 @@ defmodule Dexcord.RegistrarTreeTest do # Exactly 3 overwrite attempts hit the fake (2 retries), then the Registrar # gives up - no 4th attempt. - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-1/commands"}}, 2_000 - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-1/commands"}}, 2_000 - assert_receive {:rest_hit, %{method: "PUT", path: "/applications/app-1/commands"}}, 2_000 - refute_receive {:rest_hit, %{method: "PUT", path: "/applications/app-1/commands"}}, 500 + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/1/commands"}}, 2_000 + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/1/commands"}}, 2_000 + assert_receive {:rest_hit, %{method: "PUT", path: "/applications/1/commands"}}, 2_000 + refute_receive {:rest_hit, %{method: "PUT", path: "/applications/1/commands"}}, 500 # The whole tree is unharmed: supervisor and gateway are alive. assert Process.alive?(sup) diff --git a/test/dexcord/send_test.exs b/test/dexcord/send_test.exs new file mode 100644 index 0000000..e99e4d9 --- /dev/null +++ b/test/dexcord/send_test.exs @@ -0,0 +1,219 @@ +defmodule Dexcord.SendTest do + @moduledoc """ + Wire tests for the `Dexcord.Api.send/2,3` funnel and its lazy DM dance + (api-surface.AC3.1, api-surface.AC3.2), driven through `Dexcord.FakeRest`. + """ + use ExUnit.Case, async: false + + alias Dexcord.Api + alias Dexcord.Api.Ratelimit + alias Dexcord.FakeRest + + @token "test.token.value" + + setup do + Dexcord.EnvSandbox.sandbox_env() + Dexcord.Config.put(%{token: @token, handler: nil, intents: 0}) + + start_supervised!({Finch, name: Dexcord.Finch}) + start_supervised!(Ratelimit) + start_supervised!(Dexcord.Cache) + start_supervised!(FakeRest) + + Application.put_env(:dexcord, :api_base_url, FakeRest.base_url()) + FakeRest.subscribe(self()) + + :ok + end + + describe "AC3.1: send/2 resolves channels, threads, messages, and bare ids to the right route" do + test "a text channel struct" do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.TextChannel{id: 1}, "hi") + + assert_receive {:rest_hit, %{method: "POST", path: "/channels/1/messages"}} + end + + test "a thread struct" do + FakeRest.stub(:post, "/channels/9/messages", FakeRest.resp(200, body: ~s({"id":"2"}))) + + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.Thread{id: 9}, "hi") + + assert_receive {:rest_hit, %{path: "/channels/9/messages"}} + end + + test "a bare snowflake" do + FakeRest.stub(:post, "/channels/123/messages", FakeRest.resp(200, body: ~s({"id":"3"}))) + + assert {:ok, %Dexcord.Message{}} = Api.send(123, "hi") + + assert_receive {:rest_hit, %{path: "/channels/123/messages"}} + end + + test "a message resolves to its channel" do + FakeRest.stub(:post, "/channels/5/messages", FakeRest.resp(200, body: ~s({"id":"4"}))) + + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.Message{channel_id: 5}, "hi") + + assert_receive {:rest_hit, %{path: "/channels/5/messages"}} + end + end + + describe "AC3.2: send/2 to a user lazily creates the DM once, then reuses it" do + setup do + FakeRest.stub( + :post, + "/users/@me/channels", + FakeRest.resp(200, body: ~s({"id":"77","type":1})) + ) + + FakeRest.stub(:post, "/channels/77/messages", FakeRest.resp(200, body: ~s({"id":"9"}))) + :ok + end + + test "first send hits create-DM then the message; second send skips create-DM" do + # First send: create the DM channel, then post the message. + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.User{id: 42}, "yo") + + assert_receive {:rest_hit, %{method: "POST", path: "/users/@me/channels"}} + assert_receive {:rest_hit, %{method: "POST", path: "/channels/77/messages"}} + + # Second send: the DM channel id is cached, so ONLY a message hit occurs. + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.User{id: 42}, "again") + + assert_receive {:rest_hit, %{method: "POST", path: "/channels/77/messages"}} + refute_receive {:rest_hit, %{path: "/users/@me/channels"}}, 50 + end + + test "a member (via nested user) reuses the same cached DM as the user" do + assert {:ok, %Dexcord.Message{}} = Api.send(%Dexcord.User{id: 42}, "first") + assert_receive {:rest_hit, %{path: "/users/@me/channels"}} + assert_receive {:rest_hit, %{path: "/channels/77/messages"}} + + assert {:ok, %Dexcord.Message{}} = + Api.send(%Dexcord.Member{user: %Dexcord.User{id: 42}}, "second") + + assert_receive {:rest_hit, %{path: "/channels/77/messages"}} + refute_receive {:rest_hit, %{path: "/users/@me/channels"}}, 50 + end + end + + describe "AC3.3: Message.reply/2,3 sets message_reference and mention_author" do + setup do + FakeRest.stub(:post, "/channels/5/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + %{msg: %Dexcord.Message{id: 555, channel_id: 5}} + end + + test "reply/2 sets message_reference to the source message", %{msg: msg} do + assert {:ok, %Dexcord.Message{}} = Dexcord.Message.reply(msg, "pong") + + assert_receive {:rest_hit, %{path: "/channels/5/messages", body: body}} + decoded = JSON.decode!(body) + + assert decoded["content"] == "pong" + assert decoded["message_reference"] == %{"message_id" => "555"} + refute Map.has_key?(decoded, "allowed_mentions") + end + + test "mention_author: false sets allowed_mentions.replied_user to false", %{msg: msg} do + assert {:ok, %Dexcord.Message{}} = Dexcord.Message.reply(msg, "pong", mention_author: false) + + assert_receive {:rest_hit, %{path: "/channels/5/messages", body: body}} + decoded = JSON.decode!(body) + + assert decoded["allowed_mentions"] == %{"replied_user" => false} + assert decoded["message_reference"] == %{"message_id" => "555"} + end + + test "mention_author: true preserves other allowed_mentions keys and wins over the body", + %{msg: msg} do + body = %{ + "content" => "pong", + "allowed_mentions" => %{"users" => ["7"], "replied_user" => false} + } + + assert {:ok, %Dexcord.Message{}} = Dexcord.Message.reply(msg, body, mention_author: true) + + assert_receive {:rest_hit, %{path: "/channels/5/messages", body: raw}} + decoded = JSON.decode!(raw) + + assert decoded["allowed_mentions"] == %{"users" => ["7"], "replied_user" => true} + end + end + + describe "AC3.5: config-level allowed_mentions default merges field-wise per send" do + setup do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + :ok + end + + defp put_config_with(allowed_mentions) do + Dexcord.Config.put(%{ + token: @token, + handler: nil, + intents: 0, + allowed_mentions: allowed_mentions + }) + end + + test "the configured default applies when the send carries none" do + put_config_with(%{"parse" => []}) + + assert {:ok, %Dexcord.Message{}} = Api.send(1, "hi") + + assert_receive {:rest_hit, %{path: "/channels/1/messages", body: body}} + decoded = JSON.decode!(body) + + assert decoded["allowed_mentions"] == %{"parse" => []} + end + + test "a per-send value merges field-wise over the default (both keys survive)" do + put_config_with(%{"parse" => []}) + + body = %{"content" => "hi", "allowed_mentions" => %{"users" => ["5"]}} + assert {:ok, %Dexcord.Message{}} = Api.send(1, body) + + assert_receive {:rest_hit, %{path: "/channels/1/messages", body: raw}} + decoded = JSON.decode!(raw) + + assert decoded["allowed_mentions"] == %{"parse" => [], "users" => ["5"]} + end + + test "no config default and no per-send value leaves allowed_mentions absent" do + # Base config (no :allowed_mentions) is already in place from the top setup. + assert {:ok, %Dexcord.Message{}} = Api.send(1, "hi") + + assert_receive {:rest_hit, %{path: "/channels/1/messages", body: body}} + decoded = JSON.decode!(body) + + refute Map.has_key?(decoded, "allowed_mentions") + end + end + + describe "AC3.6: an Embed built with the builder rides Api.send as valid wire JSON" do + setup do + FakeRest.stub(:post, "/channels/1/messages", FakeRest.resp(200, body: ~s({"id":"1"}))) + :ok + end + + test "send/2 with embeds: [embed] serializes the embed" do + embed = + Dexcord.Embed.new() + |> Dexcord.Embed.title("t") + |> Dexcord.Embed.field("a", "b", inline: true) + + assert {:ok, %Dexcord.Message{}} = Api.send(1, embeds: [embed]) + + assert_receive {:rest_hit, %{path: "/channels/1/messages", body: body}} + decoded = JSON.decode!(body) + + assert decoded["embeds"] == [ + %{ + "title" => "t", + "fields" => [%{"name" => "a", "value" => "b", "inline" => true}] + } + ] + end + end +end diff --git a/test/dexcord/slash_test.exs b/test/dexcord/slash_test.exs index cf9376c..cacc85e 100644 --- a/test/dexcord/slash_test.exs +++ b/test/dexcord/slash_test.exs @@ -5,6 +5,7 @@ defmodule Dexcord.SlashTest do alias Dexcord.Api.Ratelimit alias Dexcord.FakeRest + alias Dexcord.Interaction alias Dexcord.Slash @token "test.token.value" @@ -50,25 +51,37 @@ defmodule Dexcord.SlashTest do end test "dispatch/2 routes a type-2 interaction to handle_interaction/2 by name" do - itx = %{"type" => 2, "data" => %{"name" => "ping"}, "id" => "1"} + itx = Interaction.from_map(%{"type" => 2, "data" => %{"name" => "ping"}, "id" => "1"}) assert Slash.dispatch(itx, Commands) == :ok - assert_received {:handled, "ping", ^itx} + assert_received {:handled, "ping", %Dexcord.Interaction{type: :application_command}} end test "dispatch/2 routes a type-3 (component) interaction to handle_component/2 by custom_id" do - itx = %{"type" => 3, "data" => %{"custom_id" => "refresh"}, "id" => "2"} + itx = + Interaction.from_map(%{"type" => 3, "data" => %{"custom_id" => "refresh"}, "id" => "2"}) + assert Slash.dispatch(itx, Commands) == :ok - assert_received {:component, "refresh", ^itx} + assert_received {:component, "refresh", %Dexcord.Interaction{type: :message_component}} end test "dispatch/2 routes a type-5 (modal) interaction to handle_modal/2 by custom_id" do - itx = %{"type" => 5, "data" => %{"custom_id" => "feedback"}, "id" => "3"} + itx = + Interaction.from_map(%{"type" => 5, "data" => %{"custom_id" => "feedback"}, "id" => "3"}) + assert Slash.dispatch(itx, Commands) == :ok - assert_received {:modal, "feedback", ^itx} + assert_received {:modal, "feedback", %Dexcord.Interaction{type: :modal_submit}} + end + + test "the raw-map degraded path still routes on the integer type" do + # A malformed interaction the dispatcher could not decode into a struct is + # routed here as a raw map; it must still reach the callback by integer type. + raw = %{"type" => 2, "data" => %{"name" => "ping"}, "id" => "1"} + assert Slash.dispatch(raw, Commands) == :ok + assert_received {:handled, "ping", ^raw} end test "the injected catch-all logs a warning for an unhandled command name" do - itx = %{"type" => 2, "data" => %{"name" => "unknown"}} + itx = Interaction.from_map(%{"type" => 2, "data" => %{"name" => "unknown"}}) log = capture_log(fn -> @@ -80,8 +93,8 @@ defmodule Dexcord.SlashTest do end test "the injected component/modal catch-alls log at debug, not warning" do - component = %{"type" => 3, "data" => %{"custom_id" => "nope"}} - modal = %{"type" => 5, "data" => %{"custom_id" => "nope"}} + component = Interaction.from_map(%{"type" => 3, "data" => %{"custom_id" => "nope"}}) + modal = Interaction.from_map(%{"type" => 5, "data" => %{"custom_id" => "nope"}}) log = capture_log([level: :debug], fn -> @@ -95,12 +108,12 @@ defmodule Dexcord.SlashTest do end test "a module defining only handle_interaction/2 still routes components/modals via defaults" do - itx2 = %{"type" => 2, "data" => %{"name" => "ping"}} + itx2 = Interaction.from_map(%{"type" => 2, "data" => %{"name" => "ping"}}) Slash.dispatch(itx2, LegacyCommands) - assert_received {:legacy, ^itx2} + assert_received {:legacy, %Dexcord.Interaction{type: :application_command}} - component = %{"type" => 3, "data" => %{"custom_id" => "x"}} - modal = %{"type" => 5, "data" => %{"custom_id" => "y"}} + component = Interaction.from_map(%{"type" => 3, "data" => %{"custom_id" => "x"}}) + modal = Interaction.from_map(%{"type" => 5, "data" => %{"custom_id" => "y"}}) capture_log([level: :debug], fn -> assert Slash.dispatch(component, LegacyCommands) == :ignore @@ -123,26 +136,30 @@ defmodule Dexcord.SlashTest do :ok end - @itx %{ - "id" => "int-id", - "token" => "int-token", - "application_id" => "app-id", - "data" => %{"name" => "ping"} - } + # Interaction/application ids must be numeric snowflakes: the typed endpoint + # surface casts `:interaction_id` / `:application_id` path params via + # `Dexcord.Snowflake.cast/1`. The interaction token is an opaque string. The + # response helpers take a decoded `%Dexcord.Interaction{}`, so build one. + @itx Interaction.from_map(%{ + "id" => "100", + "token" => "int-token", + "application_id" => "200", + "data" => %{"name" => "ping"} + }) test "respond/2 with a string sends a type-4 content response to the callback route" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond(@itx, "pong") assert_receive {:rest_hit, info} assert info.method == "POST" - assert info.path == "/interactions/int-id/int-token/callback" + assert info.path == "/interactions/100/int-token/callback" assert JSON.decode!(info.body) == %{"type" => 4, "data" => %{"content" => "pong"}} end test "respond/2 with a map maps ephemeral: true to flags 64 and passes embeds through" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond(@itx, %{content: "secret", embeds: [%{title: "x"}], ephemeral: true}) @@ -155,8 +172,21 @@ defmodule Dexcord.SlashTest do assert body["data"]["embeds"] == [%{"title" => "x"}] end + test "respond/2 encodes struct data values (e.g. an %Embed{}) to wire maps" do + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) + + assert {:ok, nil} = Slash.respond(@itx, %{embeds: [%Dexcord.Embed{title: "t"}]}) + + assert_receive {:rest_hit, info} + body = JSON.decode!(info.body) + # The %Embed{} struct is run through its own to_map/1: string-keyed wire map, + # title preserved, empty-list defaults (e.g. fields) retained as the encoder emits. + assert [%{"title" => "t"} = embed] = body["data"]["embeds"] + assert embed["fields"] == [] + end + test "respond/2 accepts string-keyed data maps too" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond(@itx, %{"content" => "hi", "ephemeral" => true}) @@ -166,7 +196,7 @@ defmodule Dexcord.SlashTest do end test "respond/2 passes a caller-supplied integer flags through unchanged" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond(@itx, %{content: "hi", flags: 4}) @@ -176,7 +206,7 @@ defmodule Dexcord.SlashTest do end test "respond/2 OR-s a caller flags with the ephemeral bit (4 + 64 = 68)" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond(@itx, %{content: "hi", flags: 4, ephemeral: true}) @@ -186,7 +216,7 @@ defmodule Dexcord.SlashTest do end test "respond_later/1 sends a bare type-5 deferred response" do - FakeRest.stub(:post, "/interactions/int-id/int-token/callback", FakeRest.resp(204)) + FakeRest.stub(:post, "/interactions/100/int-token/callback", FakeRest.resp(204)) assert {:ok, nil} = Slash.respond_later(@itx) @@ -195,28 +225,28 @@ defmodule Dexcord.SlashTest do end test "followup/2 posts to the application webhook route" do - FakeRest.stub(:post, "/webhooks/app-id/int-token", FakeRest.resp(200, body: ~s({"id":"9"}))) + FakeRest.stub(:post, "/webhooks/200/int-token", FakeRest.resp(200, body: ~s({"id":"9"}))) - assert {:ok, %{"id" => "9"}} = Slash.followup(@itx, "more") + assert {:ok, %Dexcord.Message{id: 9}} = Slash.followup(@itx, "more") assert_receive {:rest_hit, info} assert info.method == "POST" - assert info.path == "/webhooks/app-id/int-token" + assert info.path == "/webhooks/200/int-token" assert JSON.decode!(info.body) == %{"content" => "more"} end test "edit_response/2 patches the original message route" do FakeRest.stub( :patch, - "/webhooks/app-id/int-token/messages/@original", + "/webhooks/200/int-token/messages/@original", FakeRest.resp(200, body: ~s({"id":"9"})) ) - assert {:ok, %{"id" => "9"}} = Slash.edit_response(@itx, %{content: "edited"}) + assert {:ok, %Dexcord.Message{id: 9}} = Slash.edit_response(@itx, %{content: "edited"}) assert_receive {:rest_hit, info} assert info.method == "PATCH" - assert info.path == "/webhooks/app-id/int-token/messages/@original" + assert info.path == "/webhooks/200/int-token/messages/@original" assert JSON.decode!(info.body) == %{"content" => "edited"} end end diff --git a/test/dexcord/snowflake_test.exs b/test/dexcord/snowflake_test.exs index 434e0de..09e6d4a 100644 --- a/test/dexcord/snowflake_test.exs +++ b/test/dexcord/snowflake_test.exs @@ -1,6 +1,7 @@ defmodule Dexcord.SnowflakeTest do use ExUnit.Case, async: true + require Dexcord.Snowflake alias Dexcord.Snowflake # Discord's documented example snowflake and its encoded creation time. @@ -33,12 +34,69 @@ defmodule Dexcord.SnowflakeTest do assert Snowflake.from_datetime("nope") == :error end - test "string and integer inputs are equivalent" do - assert Snowflake.to_unix(to_string(@vector)) == Snowflake.to_unix(@vector) - assert Snowflake.to_datetime(to_string(@vector)) == Snowflake.to_datetime(@vector) + describe "cast/1" do + test "passes an in-range integer through" do + assert Snowflake.cast(@vector) == {:ok, @vector} + assert Snowflake.cast(0) == {:ok, 0} + end + + test "parses a decimal string" do + assert Snowflake.cast(to_string(@vector)) == {:ok, @vector} + end + + test "rejects a string with trailing junk" do + assert Snowflake.cast("123abc") == :error + end + + test "rejects a negative integer" do + assert Snowflake.cast(-1) == :error + end + + test "rejects an out-of-range integer" do + assert Snowflake.cast(0x1_0000_0000_0000_0000) == :error + end + + test "accepts a struct/map with an :id field" do + assert Snowflake.cast(%{id: 123}) == {:ok, 123} + end + + test "rejects junk values" do + assert Snowflake.cast(:atom) == :error + assert Snowflake.cast(%{}) == :error + assert Snowflake.cast("") == :error + end end - test "to_datetime/1 returns :error for a non-numeric string" do + describe "cast!/1" do + test "returns the integer for valid input" do + assert Snowflake.cast!(to_string(@vector)) == @vector + end + + test "raises ArgumentError with the offending value inspected" do + assert_raise ArgumentError, ~r/not a snowflake: "123abc"/, fn -> + Snowflake.cast!("123abc") + end + end + end + + describe "dump/1" do + test "produces the decimal string" do + assert Snowflake.dump(@vector) == to_string(@vector) + end + end + + describe "is_snowflake/1" do + defp check(x) when Snowflake.is_snowflake(x), do: :snowflake + defp check(_), do: :not_a_snowflake + + test "is usable in a guard position" do + assert check(123) == :snowflake + assert check(-1) == :not_a_snowflake + assert check("123") == :not_a_snowflake + end + end + + test "to_datetime/1 returns :error for a junk string" do assert Snowflake.to_datetime("not-a-snowflake") == :error end end diff --git a/test/dexcord/struct_test.exs b/test/dexcord/struct_test.exs new file mode 100644 index 0000000..669524f --- /dev/null +++ b/test/dexcord/struct_test.exs @@ -0,0 +1,495 @@ +defmodule Dexcord.StructTest do + use ExUnit.Case, async: true + + alias Dexcord.Test.Contraption + alias Dexcord.Test.Doohickey + alias Dexcord.Test.Gadget + alias Dexcord.Test.Overridden + alias Dexcord.Test.Widget + + # A representative full wire map (string-keyed), reused across decode/encode tests. + defp full_wire_map do + %{ + "id" => "123", + "name" => "w", + "count" => 7, + "enabled" => true, + "created_at" => "2026-07-04T12:00:00Z", + "gadget" => %{"id" => 9, "name" => "g"}, + "gadgets" => [%{"id" => 1, "name" => "a"}, %{"id" => 2, "name" => "b"}], + "tag_ids" => ["111", 222] + } + end + + describe "compile shape (Task 4)" do + test "struct has all declared keys with correct defaults" do + w = %Widget{} + assert w.id == nil + assert w.name == nil + assert w.count == nil + assert w.enabled == nil + assert w.created_at == nil + assert w.gadget == nil + assert w.gadgets == [] + assert w.tag_ids == [] + assert w.color == nil + assert w.flags == nil + assert w.parent == :absent + assert w.premium == false + assert w.note == "n/a" + assert w.extra == nil + assert w.gadget_full == nil + end + + test "__fields__/0 returns the table in declaration order with resolved types" do + fields = Widget.__fields__() + names = Enum.map(fields, & &1.name) + + assert names == [ + :id, + :name, + :count, + :enabled, + :created_at, + :gadget, + :gadgets, + :tag_ids, + :color, + :flags, + :parent, + :premium, + :note, + :extra + ] + + by_name = Map.new(fields, &{&1.name, &1}) + + # nested struct type resolved to a real atom module + assert by_name[:gadget].type == {:struct, Dexcord.Test.Gadget} + assert by_name[:gadgets].type == {:list, {:struct, Dexcord.Test.Gadget}} + assert by_name[:tag_ids].type == {:list, :snowflake} + assert by_name[:color].type == {:enum, Dexcord.Test.Color} + assert by_name[:flags].type == {:flags, Dexcord.Test.TestFlags} + + # self-referencing struct resolves __MODULE__ to the module atom + assert by_name[:parent].type == {:struct, Dexcord.Test.Widget} + assert by_name[:parent].tristate == true + + # presence sugar + assert by_name[:premium].presence == true + assert by_name[:premium].type == :boolean + + # keys are strings + assert by_name[:id].key == "id" + assert by_name[:created_at].key == "created_at" + + # defaults + assert by_name[:gadgets].default == [] + assert by_name[:tag_ids].default == [] + assert by_name[:parent].default == :absent + assert by_name[:premium].default == false + assert by_name[:note].default == "n/a" + end + + test "__hydrations__/0 returns the hydrate table" do + assert Widget.__hydrations__() == [ + %{name: :gadget_full, from: :id, type: Dexcord.Test.Gadget} + ] + end + + test "the module generates a :t type" do + {:ok, types} = Code.Typespec.fetch_types(Widget) + assert Enum.any?(types, fn {_kind, {name, _def, _args}} -> name == :t end) + end + + test "an unknown field type raises ArgumentError at compile time" do + assert_raise ArgumentError, ~r/unknown field type/, fn -> + Code.compile_string(""" + defmodule Dexcord.Test.BogusCompile do + use Dexcord.Struct + + discord_struct do + field :x, :bogus_type + end + end + """) + end + end + + test "tristate and presence are mutually exclusive at compile time" do + assert_raise ArgumentError, ~r/mutually exclusive/, fn -> + Code.compile_string(""" + defmodule Dexcord.Test.BadModifiers do + use Dexcord.Struct + + discord_struct do + field :x, :string, tristate: true, presence: true + end + end + """) + end + end + + test "use Dexcord.Struct without any field/hydrate raises at compile time" do + assert_raise ArgumentError, ~r/requires a `discord_struct`? do/, fn -> + Code.compile_string(""" + defmodule Dexcord.Test.EmptyStruct do + use Dexcord.Struct + end + """) + end + end + end + + describe "from_map decode (Task 5)" do + test "api-surface.AC2.1: decodes declared fields including nested structs and lists" do + w = Widget.from_map(full_wire_map()) + + # snowflake string normalized to integer + assert w.id == 123 + assert w.name == "w" + assert w.count == 7 + assert w.enabled == true + + # datetime parsed + assert %DateTime{} = w.created_at + assert w.created_at == ~U[2026-07-04 12:00:00Z] + + # nested struct + assert w.gadget == %Gadget{id: 9, name: "g"} + + # list of nested structs + assert w.gadgets == [%Gadget{id: 1, name: "a"}, %Gadget{id: 2, name: "b"}] + + # list of snowflakes: string and int both normalized to integer + assert w.tag_ids == [111, 222] + end + + test "api-surface.AC2.2: unknown keys ignored, decode identical, zero atoms minted" do + base = Widget.from_map(full_wire_map()) + + with_junk = + Widget.from_map( + Map.merge(full_wire_map(), %{ + "totally_unknown_key_ab12cd" => 1, + "another_unknown_key_ab12cd" => %{"deep" => true} + }) + ) + + assert with_junk == base + + # zero atoms minted from wire data: these strings never became atoms. + assert_raise ArgumentError, fn -> + String.to_existing_atom("totally_unknown_key_ab12cd") + end + + assert_raise ArgumentError, fn -> + String.to_existing_atom("another_unknown_key_ab12cd") + end + end + + test "api-surface.AC2.3: wrong-shaped values never raise, fall to default/raw" do + # list where a map is expected -> nil (declared default for :gadget) + assert Widget.from_map(%{"gadget" => [1, 2]}).gadget == nil + + # map where a list is expected -> declared default [] + assert Widget.from_map(%{"gadgets" => %{"not" => "a list"}}).gadgets == [] + + # junk scalars keep the raw wire value + assert Widget.from_map(%{"count" => "not-a-number"}).count == "not-a-number" + assert Widget.from_map(%{"enabled" => "yes"}).enabled == "yes" + assert Widget.from_map(%{"created_at" => "not-a-date"}).created_at == "not-a-date" + assert Widget.from_map(%{"id" => "12x"}).id == "12x" + assert Widget.from_map(%{"color" => "red"}).color == "red" + # a decimal-string flags value now decodes to an integer (permission + # bitfields arrive as strings); a non-numeric junk string stays raw. + assert Widget.from_map(%{"flags" => "3x"}).flags == "3x" + + # whole-value junk -> nil + assert Widget.from_map("hello") == nil + assert Widget.from_map(42) == nil + + # empty map -> all-defaults struct + assert Widget.from_map(%{}) == %Widget{} + end + + test "enum and flags decode through the struct" do + assert Widget.from_map(%{"color" => 4}).color == :blue + assert Widget.from_map(%{"color" => 2}).color == {:unknown, 2} + + # flags keep the raw integer, unknown bit (bit 7) preserved + assert Widget.from_map(%{"flags" => 0b1000_0001}).flags == 129 + end + + test "api-surface.AC2.6: tristate field decodes to :absent | nil | value" do + # key absent -> :absent + assert Widget.from_map(%{}).parent == :absent + + # present with null -> nil + assert Widget.from_map(%{"parent" => nil}).parent == nil + + # present with value -> self-referencing struct decodes + w = Widget.from_map(%{"parent" => %{"id" => 1, "name" => "p"}}) + assert %Widget{} = w.parent + assert w.parent.id == 1 + assert w.parent.name == "p" + end + + test "api-surface.AC2.7: presence field decodes key-presence to boolean" do + # key absent -> false + assert Widget.from_map(%{}).premium == false + + # present with null -> true (present-with-null still counts as present) + assert Widget.from_map(%{"premium" => nil}).premium == true + + # present with value -> true + assert Widget.from_map(%{"premium" => true}).premium == true + end + + test "list element null does not crash" do + w = Widget.from_map(%{"gadgets" => [%{"id" => 1, "name" => "a"}, nil]}) + assert w.gadgets == [%Gadget{id: 1, name: "a"}, nil] + end + end + + describe "to_map encode (Task 6)" do + test "api-surface.AC2.9: value encodings for a decoded struct" do + w = Widget.from_map(full_wire_map()) + m = Widget.to_map(w) + + # snowflake integer -> decimal string + assert m["id"] == "123" + + # DateTime -> ISO8601 + assert m["created_at"] == "2026-07-04T12:00:00Z" + + # nested struct/list encode as plain maps + assert m["gadget"] == %{"id" => "9", "name" => "g"} + assert m["gadgets"] == [%{"id" => "1", "name" => "a"}, %{"id" => "2", "name" => "b"}] + + # scalars pass through + assert m["name"] == "w" + assert m["count"] == 7 + assert m["enabled"] == true + + # empty-list default still encodes (real information) + assert m["tag_ids"] == ["111", "222"] + + # nil fields are omitted + refute Map.has_key?(m, "color") + refute Map.has_key?(m, "flags") + refute Map.has_key?(m, "extra") + + # :absent tristate is omitted (AC2.6 encode direction) + refute Map.has_key?(m, "parent") + + # presence false -> omitted + refute Map.has_key?(m, "premium") + end + + test "enum encodes losslessly for known and unknown values" do + known = Widget.to_map(%Widget{color: :blue}) + assert known["color"] == 4 + + unknown = Widget.to_map(%Widget{color: {:unknown, 2}}) + assert unknown["color"] == 2 + end + + test "flags raw int encodes back as the raw int" do + m = Widget.to_map(%Widget{flags: 129}) + assert m["flags"] == 129 + end + + test "presence true encodes as present-with-null" do + m = Widget.to_map(%Widget{premium: true}) + assert Map.has_key?(m, "premium") + assert m["premium"] == nil + end + + test "full round-trip is stable for a junk-free map" do + first = Widget.from_map(full_wire_map()) + round_tripped = first |> Widget.to_map() |> Widget.from_map() + assert round_tripped == first + end + end + + describe "DSL extensions (Phase 2, Task 1)" do + test ":number decodes float and integer verbatim, junk stays raw" do + assert Doohickey.from_map(%{"ratio" => 3.5}).ratio == 3.5 + assert Doohickey.from_map(%{"ratio" => 3}).ratio == 3 + assert Doohickey.from_map(%{"ratio" => "not-a-number"}).ratio == "not-a-number" + end + + test ":number resolves to the :number field type" do + by_name = Map.new(Doohickey.__fields__(), &{&1.name, &1}) + assert by_name[:ratio].type == :number + end + + test "string-wire flags decode: decimal string -> integer, junk stays raw, int passes through" do + assert Doohickey.from_map(%{"bits" => "17"}).bits == 17 + assert Doohickey.from_map(%{"bits" => "12x"}).bits == "12x" + assert Doohickey.from_map(%{"bits" => 5}).bits == 5 + end + + test "wire_string: true encodes integer flags to a decimal string" do + m = Doohickey.to_map(%Doohickey{perms: 17}) + assert m["perms"] == "17" + end + + test "without wire_string a flags field encodes as the raw integer" do + m = Doohickey.to_map(%Doohickey{bits: 17}) + assert m["bits"] == 17 + end + + test "wire_string is recorded on the field table" do + by_name = Map.new(Doohickey.__fields__(), &{&1.name, &1}) + assert by_name[:perms].wire_string == true + assert by_name[:bits].wire_string == false + end + + test "include_fields injects the shared group in declaration position" do + names = Enum.map(Doohickey.__fields__(), & &1.name) + assert names == [:id, :ratio, :perms, :bits, :shared_a, :shared_b, :tail] + end + + test "included fields carry their declared types and defaults" do + by_name = Map.new(Doohickey.__fields__(), &{&1.name, &1}) + assert by_name[:shared_a].type == :string + assert by_name[:shared_b].type == {:list, :snowflake} + assert by_name[:shared_b].default == [] + assert %Doohickey{}.shared_b == [] + end + + test "included fields decode identically to hand-declared fields" do + d = Doohickey.from_map(%{"shared_a" => "x", "shared_b" => ["1", 2], "tail" => "end"}) + assert d.shared_a == "x" + assert d.shared_b == [1, 2] + assert d.tail == "end" + end + end + + describe "{:id_map, type} dictionary field (Phase 3, Task 1)" do + test "resolves the id_map type and its default on the field table" do + by_name = Map.new(Contraption.__fields__(), &{&1.name, &1}) + assert by_name[:things].type == {:id_map, {:struct, Dexcord.Test.Gadget}} + assert by_name[:things].default == %{} + end + + test "decodes a snowflake-keyed dictionary to integer keys and typed values" do + c = Contraption.from_map(%{"things" => %{"111" => %{"id" => "1", "name" => "a"}}}) + assert c.things == %{111 => %Gadget{id: 1, name: "a"}} + end + + test "a non-snowflake key stays a string key, value still decodes" do + c = Contraption.from_map(%{"things" => %{"abc" => %{"id" => "1", "name" => "a"}}}) + assert c.things == %{"abc" => %Gadget{id: 1, name: "a"}} + end + + test "a junk (non-map) value decodes leniently to nil for a struct inner" do + c = Contraption.from_map(%{"things" => %{"111" => "not-a-map"}}) + assert c.things == %{111 => nil} + end + + test "a non-map id_map value falls to the declared default" do + assert Contraption.from_map(%{"things" => [1, 2]}).things == %{} + assert Contraption.from_map(%{"things" => "junk"}).things == %{} + end + + test "absent id_map key uses the declared default" do + assert Contraption.from_map(%{}).things == %{} + end + + test "re-encode stringifies integer keys and encodes struct values" do + c = Contraption.from_map(%{"things" => %{"111" => %{"id" => "1", "name" => "a"}}}) + m = Contraption.to_map(c) + assert m["things"] == %{"111" => %{"id" => "1", "name" => "a"}} + end + + test "re-encode keeps a non-integer key verbatim" do + c = %Contraption{things: %{"abc" => %Gadget{id: 1, name: "a"}}} + m = Contraption.to_map(c) + assert m["things"] == %{"abc" => %{"id" => "1", "name" => "a"}} + end + + test "id_map field participates in a valid :t typespec" do + {:ok, types} = Code.Typespec.fetch_types(Contraption) + assert Enum.any?(types, fn {_kind, {name, _def, _args}} -> name == :t end) + end + end + + describe "overridable generated functions via super/1 (Phase 3, Task 1)" do + test "an override post-processes the DSL-generated from_map decode" do + o = Overridden.from_map(%{"id" => "5", "name" => "luna"}) + assert o.id == 5 + assert o.name == "LUNA" + end + + test "the override still returns nil for non-map input (super passes through)" do + assert Overridden.from_map("junk") == nil + end + + test "generated to_map and merge_map stay callable on the overriding module" do + o = Overridden.from_map(%{"id" => "5", "name" => "x"}) + assert Overridden.to_map(o)["name"] == "X" + assert Overridden.merge_map(o, %{"id" => "9"}).id == 9 + end + end + + describe "merge_map partial update (Task 6)" do + test "api-surface.AC2.8: updates only keys present in the partial map" do + w = Widget.from_map(full_wire_map()) + merged = Widget.merge_map(w, %{"name" => "renamed"}) + + assert merged.name == "renamed" + + # every other field is untouched + assert merged.id == w.id + assert merged.count == w.count + assert merged.enabled == w.enabled + assert merged.created_at == w.created_at + assert merged.gadget == w.gadget + assert merged.gadgets == w.gadgets + assert merged.tag_ids == w.tag_ids + assert merged.parent == :absent + assert merged.premium == w.premium + assert merged.gadget_full == w.gadget_full + end + + test "present-as-null sets nil, absent key leaves value alone" do + w = Widget.from_map(full_wire_map()) + assert w.parent == :absent + + # present with null -> nil + assert Widget.merge_map(w, %{"parent" => nil}).parent == nil + + # absent key leaves the existing :absent alone (not reset) + assert Widget.merge_map(w, %{}).parent == :absent + + # count present-as-null -> nil, and only count changes + merged = Widget.merge_map(w, %{"count" => nil}) + assert merged.count == nil + assert merged.name == w.name + end + + test "non-map partial returns the struct unchanged" do + w = Widget.from_map(full_wire_map()) + assert Widget.merge_map(w, "junk") == w + end + + test "unknown key in partial changes nothing and mints no atom" do + w = Widget.from_map(full_wire_map()) + merged = Widget.merge_map(w, %{"totally_unknown_merge_key_xy99" => 1}) + assert merged == w + + assert_raise ArgumentError, fn -> + String.to_existing_atom("totally_unknown_merge_key_xy99") + end + end + + test "merge decode parity: nested struct decodes like from_map" do + w = Widget.from_map(full_wire_map()) + merged = Widget.merge_map(w, %{"gadget" => %{"id" => 5, "name" => "x"}}) + assert merged.gadget == %Gadget{id: 5, name: "x"} + end + end +end diff --git a/test/fixtures/channel_types.json b/test/fixtures/channel_types.json new file mode 100644 index 0000000..c97db1e --- /dev/null +++ b/test/fixtures/channel_types.json @@ -0,0 +1,222 @@ +[ + { + "id": "100000000000000000", + "type": 0, + "guild_id": "900000000000000000", + "name": "general", + "position": 0, + "flags": 0, + "topic": "Welcome to the server", + "last_message_id": "100000000000000099", + "rate_limit_per_user": 5, + "nsfw": false, + "parent_id": "104000000000000000", + "default_auto_archive_duration": 1440, + "permission_overwrites": [ + { + "id": "900000000000000000", + "type": 0, + "allow": "1024", + "deny": "2048" + } + ] + }, + { + "id": "101000000000000000", + "type": 1, + "flags": 0, + "last_message_id": "101000000000000099", + "recipients": [ + { + "id": "800000000000000000", + "username": "Nelly", + "discriminator": "1337", + "global_name": "Nelly" + } + ] + }, + { + "id": "102000000000000000", + "type": 2, + "guild_id": "900000000000000000", + "name": "Voice Chat", + "position": 1, + "flags": 0, + "bitrate": 64000, + "user_limit": 10, + "rtc_region": null, + "video_quality_mode": 1, + "parent_id": "104000000000000000" + }, + { + "id": "103000000000000000", + "type": 3, + "flags": 0, + "name": "Study Group", + "icon": "a1b2c3d4e5f6", + "owner_id": "800000000000000000", + "last_message_id": "103000000000000099", + "recipients": [ + { + "id": "800000000000000000", + "username": "Nelly", + "discriminator": "1337" + }, + { + "id": "800000000000000001", + "username": "Wumpus", + "discriminator": "0001" + } + ] + }, + { + "id": "104000000000000000", + "type": 4, + "guild_id": "900000000000000000", + "name": "Text Channels", + "position": 0, + "flags": 0 + }, + { + "id": "105000000000000000", + "type": 5, + "guild_id": "900000000000000000", + "name": "announcements", + "position": 2, + "flags": 0, + "topic": "Server announcements", + "last_message_id": "105000000000000099", + "nsfw": false + }, + { + "id": "110000000000000000", + "type": 10, + "guild_id": "900000000000000000", + "name": "announcement-thread", + "flags": 0, + "parent_id": "105000000000000000", + "owner_id": "800000000000000000", + "message_count": 3, + "member_count": 2, + "total_message_sent": 3, + "thread_metadata": { + "archived": false, + "auto_archive_duration": 1440, + "archive_timestamp": "2021-01-01T00:00:00.000000+00:00", + "locked": false + } + }, + { + "id": "111000000000000000", + "type": 11, + "guild_id": "900000000000000000", + "name": "public-thread", + "flags": 0, + "parent_id": "100000000000000000", + "owner_id": "800000000000000000", + "message_count": 12, + "member_count": 5, + "total_message_sent": 12, + "rate_limit_per_user": 0, + "applied_tags": ["150000000000000001"], + "thread_metadata": { + "archived": false, + "auto_archive_duration": 4320, + "archive_timestamp": "2021-02-01T00:00:00.000000+00:00", + "locked": false, + "invitable": true + }, + "member": { + "id": "111000000000000000", + "user_id": "800000000000000000", + "join_timestamp": "2021-02-01T12:30:00.000000+00:00", + "flags": 1 + } + }, + { + "id": "112000000000000000", + "type": 12, + "guild_id": "900000000000000000", + "name": "private-thread", + "flags": 0, + "parent_id": "100000000000000000", + "owner_id": "800000000000000000", + "message_count": 1, + "member_count": 1, + "total_message_sent": 1, + "thread_metadata": { + "archived": true, + "auto_archive_duration": 10080, + "archive_timestamp": "2021-03-01T00:00:00.000000+00:00", + "locked": true, + "invitable": false + } + }, + { + "id": "113000000000000000", + "type": 13, + "guild_id": "900000000000000000", + "name": "Stage", + "position": 3, + "flags": 0, + "bitrate": 40000, + "user_limit": 0, + "rtc_region": "us-east", + "video_quality_mode": 1 + }, + { + "id": "114000000000000000", + "type": 14, + "guild_id": "900000000000000000", + "name": "Server Directory", + "position": 4, + "flags": 0 + }, + { + "id": "115000000000000000", + "type": 15, + "guild_id": "900000000000000000", + "name": "help-forum", + "position": 5, + "flags": 16, + "topic": "Read the pinned guidelines before posting", + "last_message_id": "115000000000000099", + "rate_limit_per_user": 10, + "default_auto_archive_duration": 4320, + "default_sort_order": 0, + "default_forum_layout": 1, + "available_tags": [ + { + "id": "150000000000000001", + "name": "resolved", + "moderated": true, + "emoji_id": null, + "emoji_name": "✅" + } + ], + "default_reaction_emoji": { + "emoji_id": null, + "emoji_name": "👍" + } + }, + { + "id": "116000000000000000", + "type": 16, + "guild_id": "900000000000000000", + "name": "gallery", + "position": 6, + "flags": 0, + "topic": "Share your art", + "default_auto_archive_duration": 1440, + "default_sort_order": 1, + "available_tags": [ + { + "id": "160000000000000001", + "name": "sketch", + "moderated": false, + "emoji_id": null, + "emoji_name": "🎨" + } + ] + } +] diff --git a/test/fixtures/guild_create.json b/test/fixtures/guild_create.json new file mode 100644 index 0000000..407f668 --- /dev/null +++ b/test/fixtures/guild_create.json @@ -0,0 +1,256 @@ +{ + "id": "900000000000000000", + "name": "Dexcord Test Guild", + "icon": "abc123icon", + "splash": null, + "discovery_splash": null, + "owner_id": "800000000000000000", + "afk_channel_id": null, + "afk_timeout": 300, + "widget_enabled": true, + "widget_channel_id": null, + "verification_level": 2, + "default_message_notifications": 1, + "explicit_content_filter": 2, + "features": ["COMMUNITY", "NEWS", "WELCOME_SCREEN_ENABLED"], + "mfa_level": 1, + "application_id": null, + "system_channel_id": "100000000000000000", + "system_channel_flags": 5, + "rules_channel_id": null, + "max_presences": null, + "max_members": 250000, + "vanity_url_code": null, + "description": "A guild for exercising the decoder.", + "banner": null, + "premium_tier": 2, + "premium_subscription_count": 7, + "preferred_locale": "en-US", + "public_updates_channel_id": null, + "max_video_channel_users": 25, + "max_stage_video_channel_users": 50, + "nsfw_level": 0, + "premium_progress_bar_enabled": true, + "safety_alerts_channel_id": null, + "roles": [ + { + "id": "900000000000000000", + "name": "@everyone", + "color": 0, + "hoist": false, + "position": 0, + "permissions": "104324673", + "managed": false, + "mentionable": false, + "flags": 0 + }, + { + "id": "901000000000000001", + "name": "Server Booster", + "color": 16038978, + "hoist": true, + "position": 3, + "permissions": "1071698660928", + "managed": true, + "mentionable": false, + "flags": 0, + "tags": { + "premium_subscriber": null + } + } + ], + "emojis": [ + { + "id": "950000000000000001", + "name": "wumpus", + "roles": [], + "require_colons": true, + "managed": false, + "animated": false, + "available": true + } + ], + "stickers": [ + { + "id": "960000000000000001", + "name": "hello", + "description": "a waving sticker", + "tags": "wave", + "type": 2, + "format_type": 1, + "available": true, + "guild_id": "900000000000000000", + "sort_value": 0 + } + ], + "joined_at": "2021-05-01T12:00:00.000000+00:00", + "large": false, + "unavailable": false, + "member_count": 3, + "members": [ + { + "user": { + "id": "800000000000000000", + "username": "Nelly", + "discriminator": "1337", + "global_name": "Nelly" + }, + "nick": null, + "roles": ["901000000000000001"], + "joined_at": "2021-05-01T12:00:00.000000+00:00", + "deaf": false, + "mute": false, + "flags": 0 + }, + { + "user": { + "id": "800000000000000001", + "username": "Wumpus", + "discriminator": "0001", + "global_name": "Wumpus", + "bot": true + }, + "nick": "The Wumpus", + "roles": [], + "joined_at": "2021-05-02T09:15:00.000000+00:00", + "premium_since": "2021-06-01T00:00:00.000000+00:00", + "deaf": false, + "mute": false, + "flags": 0 + } + ], + "channels": [ + { + "id": "100000000000000000", + "type": 0, + "guild_id": "900000000000000000", + "name": "general", + "position": 0, + "flags": 0, + "topic": "Welcome to the server", + "last_message_id": "100000000000000099", + "rate_limit_per_user": 5, + "nsfw": false, + "parent_id": "104000000000000000", + "permission_overwrites": [ + { + "id": "900000000000000000", + "type": 0, + "allow": "1024", + "deny": "2048" + } + ] + }, + { + "id": "102000000000000000", + "type": 2, + "guild_id": "900000000000000000", + "name": "Voice Chat", + "position": 1, + "flags": 0, + "bitrate": 64000, + "user_limit": 10, + "rtc_region": null, + "video_quality_mode": 1, + "parent_id": "104000000000000000" + }, + { + "id": "104000000000000000", + "type": 4, + "guild_id": "900000000000000000", + "name": "Text Channels", + "position": 0, + "flags": 0 + }, + { + "id": "115000000000000000", + "type": 15, + "guild_id": "900000000000000000", + "name": "help-forum", + "position": 5, + "flags": 16, + "topic": "Read the pinned guidelines before posting", + "last_message_id": "115000000000000099", + "rate_limit_per_user": 10, + "default_auto_archive_duration": 4320, + "default_sort_order": 0, + "default_forum_layout": 1, + "available_tags": [ + { + "id": "150000000000000001", + "name": "resolved", + "moderated": true, + "emoji_id": null, + "emoji_name": "✅" + } + ], + "default_reaction_emoji": { + "emoji_id": null, + "emoji_name": "👍" + } + } + ], + "threads": [ + { + "id": "111000000000000000", + "type": 11, + "guild_id": "900000000000000000", + "name": "public-thread", + "flags": 0, + "parent_id": "100000000000000000", + "owner_id": "800000000000000000", + "message_count": 12, + "member_count": 5, + "total_message_sent": 12, + "rate_limit_per_user": 0, + "applied_tags": ["150000000000000001"], + "thread_metadata": { + "archived": false, + "auto_archive_duration": 4320, + "archive_timestamp": "2021-02-01T00:00:00.000000+00:00", + "locked": false, + "invitable": true + }, + "member": { + "id": "111000000000000000", + "user_id": "800000000000000000", + "join_timestamp": "2021-02-01T12:30:00.000000+00:00", + "flags": 1 + } + } + ], + "voice_states": [ + { + "channel_id": "102000000000000000", + "user_id": "800000000000000000", + "session_id": "abc123session", + "deaf": false, + "mute": false, + "self_deaf": false, + "self_mute": true, + "self_video": false, + "suppress": false, + "request_to_speak_timestamp": null + } + ], + "presences": [ + { + "user": { + "id": "800000000000000000" + }, + "status": "online", + "activities": [ + { + "name": "Visual Studio Code", + "type": 0 + } + ], + "client_status": { + "web": "online" + } + } + ], + "stage_instances": [], + "guild_scheduled_events": [], + "soundboard_sounds": [] +} diff --git a/test/fixtures/interaction_component.json b/test/fixtures/interaction_component.json new file mode 100644 index 0000000..cb62e82 --- /dev/null +++ b/test/fixtures/interaction_component.json @@ -0,0 +1,32 @@ +{ + "id": "1000000000000000003", + "application_id": "900000000000000001", + "type": 3, + "token": "aW50ZXJhY3Rpb24tdG9rZW4tY29tcA", + "version": 1, + "guild_id": "800000000000000001", + "channel_id": "700000000000000001", + "app_permissions": "2147483647", + "context": 0, + "member": { + "user": { + "id": "600000000000000001", + "username": "clicker", + "discriminator": "0", + "global_name": "Clicker" + }, + "roles": [], + "joined_at": "2021-01-01T00:00:00.000000+00:00", + "permissions": "2147483647" + }, + "data": { + "custom_id": "confirm_button", + "component_type": 2 + }, + "message": { + "id": "500000000000000001", + "channel_id": "700000000000000001", + "content": "Are you sure?", + "type": 0 + } +} diff --git a/test/fixtures/interaction_modal.json b/test/fixtures/interaction_modal.json new file mode 100644 index 0000000..9c8fcb9 --- /dev/null +++ b/test/fixtures/interaction_modal.json @@ -0,0 +1,36 @@ +{ + "id": "1000000000000000004", + "application_id": "900000000000000001", + "type": 5, + "token": "aW50ZXJhY3Rpb24tdG9rZW4tbW9kYWw", + "version": 1, + "guild_id": "800000000000000001", + "channel_id": "700000000000000001", + "app_permissions": "2147483647", + "context": 0, + "member": { + "user": { + "id": "600000000000000001", + "username": "submitter", + "discriminator": "0", + "global_name": "Submitter" + }, + "roles": [], + "joined_at": "2021-01-01T00:00:00.000000+00:00", + "permissions": "2147483647" + }, + "data": { + "custom_id": "feedback_modal", + "components": [ + { + "type": 18, + "label": "Your feedback", + "component": { + "type": 4, + "custom_id": "feedback_input", + "value": "This is great!" + } + } + ] + } +} diff --git a/test/fixtures/interaction_slash_dm.json b/test/fixtures/interaction_slash_dm.json new file mode 100644 index 0000000..13425f7 --- /dev/null +++ b/test/fixtures/interaction_slash_dm.json @@ -0,0 +1,23 @@ +{ + "id": "1000000000000000002", + "application_id": "900000000000000001", + "type": 2, + "token": "aW50ZXJhY3Rpb24tdG9rZW4tZG0", + "version": 1, + "channel_id": "710000000000000009", + "app_permissions": "0", + "locale": "en-US", + "context": 1, + "attachment_size_limit": 26214400, + "user": { + "id": "600000000000000003", + "username": "dmuser", + "discriminator": "0", + "global_name": "DM User" + }, + "data": { + "id": "950000000000000002", + "name": "ping", + "type": 1 + } +} diff --git a/test/fixtures/interaction_slash_guild.json b/test/fixtures/interaction_slash_guild.json new file mode 100644 index 0000000..c10f747 --- /dev/null +++ b/test/fixtures/interaction_slash_guild.json @@ -0,0 +1,94 @@ +{ + "id": "1000000000000000001", + "application_id": "900000000000000001", + "type": 2, + "token": "aW50ZXJhY3Rpb24tdG9rZW4", + "version": 1, + "guild_id": "800000000000000001", + "channel_id": "700000000000000001", + "app_permissions": "8", + "locale": "en-US", + "guild_locale": "en-US", + "context": 0, + "attachment_size_limit": 26214400, + "channel": { + "id": "700000000000000001", + "name": "general", + "type": 0, + "permissions": "2147483647", + "last_message_id": "710000000000000001", + "nsfw": false, + "parent_id": "690000000000000001", + "guild_id": "800000000000000001", + "flags": 0, + "rate_limit_per_user": 0, + "topic": "welcome", + "position": 3 + }, + "member": { + "user": { + "id": "600000000000000001", + "username": "invoker", + "discriminator": "0", + "global_name": "Invoker" + }, + "nick": "The Invoker", + "roles": ["810000000000000001"], + "joined_at": "2021-01-01T00:00:00.000000+00:00", + "deaf": false, + "mute": false, + "permissions": "2147483647" + }, + "data": { + "id": "950000000000000001", + "name": "whois", + "type": 1, + "options": [ + { + "name": "target", + "type": 6, + "value": "600000000000000002" + } + ], + "resolved": { + "users": { + "600000000000000002": { + "id": "600000000000000002", + "username": "target", + "discriminator": "0", + "global_name": "Target User", + "avatar": "abc123" + } + }, + "members": { + "600000000000000002": { + "nick": "Targeted", + "roles": ["810000000000000002"], + "joined_at": "2021-06-01T00:00:00.000000+00:00", + "permissions": "104324673" + } + }, + "channels": { + "700000000000000001": { + "id": "700000000000000001", + "name": "general", + "type": 0, + "permissions": "2147483647" + }, + "720000000000000001": { + "id": "720000000000000001", + "name": "help-thread", + "type": 11, + "permissions": "1024", + "parent_id": "700000000000000001", + "thread_metadata": { + "archived": false, + "auto_archive_duration": 1440, + "archive_timestamp": "2022-01-01T00:00:00.000000+00:00", + "locked": false + } + } + } + } + } +} diff --git a/test/fixtures/message_create_guild.json b/test/fixtures/message_create_guild.json new file mode 100644 index 0000000..cd79ce3 --- /dev/null +++ b/test/fixtures/message_create_guild.json @@ -0,0 +1,70 @@ +{ + "id": "1052425005447712818", + "channel_id": "155361364909801472", + "guild_id": "197038439483310086", + "author": { + "id": "80351110224678912", + "username": "Nelly", + "discriminator": "1337", + "global_name": "Nelly", + "avatar": "8342729096ea3675442027381ff50dfe", + "bot": false, + "public_flags": 131072 + }, + "member": { + "nick": "The Big N", + "roles": ["197038439483310087", "197038439483310088"], + "joined_at": "2021-04-11T15:00:00.000000+00:00", + "deaf": false, + "mute": false, + "flags": 0, + "pending": false + }, + "content": "Hey <@104937225384493056> check this out", + "timestamp": "2022-12-14T05:06:00.000000+00:00", + "edited_timestamp": null, + "tts": false, + "mention_everyone": false, + "mentions": [ + { + "id": "104937225384493056", + "username": "friend", + "discriminator": "0001", + "global_name": "Friend", + "avatar": null, + "member": { + "nick": "buddy", + "roles": ["197038439483310087"], + "joined_at": "2021-05-01T00:00:00.000000+00:00" + } + } + ], + "mention_roles": ["197038439483310088"], + "attachments": [ + { + "id": "1052425005447712819", + "filename": "cat.png", + "size": 45678, + "url": "https://cdn.discordapp.com/attachments/1/2/cat.png", + "proxy_url": "https://media.discordapp.net/attachments/1/2/cat.png", + "height": 512, + "width": 512, + "content_type": "image/png" + } + ], + "embeds": [ + { + "title": "An Embed", + "type": "rich", + "description": "A described embed", + "color": 5814783, + "timestamp": "2022-12-14T05:00:00.000000+00:00" + } + ], + "reactions": [], + "nonce": "1052425005447712818", + "pinned": false, + "type": 0, + "channel_type": 0, + "flags": 0 +} diff --git a/test/fixtures/message_dm.json b/test/fixtures/message_dm.json new file mode 100644 index 0000000..02e1167 --- /dev/null +++ b/test/fixtures/message_dm.json @@ -0,0 +1,24 @@ +{ + "id": "1052425005447713100", + "channel_id": "319674150115823165", + "author": { + "id": "80351110224678912", + "username": "Nelly", + "discriminator": "1337", + "global_name": "Nelly", + "avatar": "8342729096ea3675442027381ff50dfe" + }, + "content": "a private message", + "timestamp": "2022-12-14T08:00:00.000000+00:00", + "edited_timestamp": null, + "tts": false, + "mention_everyone": false, + "mentions": [], + "mention_roles": [], + "attachments": [], + "embeds": [], + "pinned": false, + "type": 0, + "channel_type": 1, + "flags": 0 +} diff --git a/test/fixtures/message_reply_deleted.json b/test/fixtures/message_reply_deleted.json new file mode 100644 index 0000000..27fc6bf --- /dev/null +++ b/test/fixtures/message_reply_deleted.json @@ -0,0 +1,31 @@ +{ + "id": "1052425005447713000", + "channel_id": "155361364909801472", + "guild_id": "197038439483310086", + "author": { + "id": "80351110224678912", + "username": "Nelly", + "discriminator": "1337", + "global_name": "Nelly", + "avatar": "8342729096ea3675442027381ff50dfe" + }, + "content": "replying to a message that got deleted", + "timestamp": "2022-12-14T07:00:00.000000+00:00", + "edited_timestamp": null, + "tts": false, + "mention_everyone": false, + "mentions": [], + "mention_roles": [], + "attachments": [], + "embeds": [], + "pinned": false, + "type": 19, + "flags": 0, + "message_reference": { + "type": 0, + "message_id": "1052425005447712818", + "channel_id": "155361364909801472", + "guild_id": "197038439483310086" + }, + "referenced_message": null +} diff --git a/test/fixtures/message_webhook.json b/test/fixtures/message_webhook.json new file mode 100644 index 0000000..c1cf254 --- /dev/null +++ b/test/fixtures/message_webhook.json @@ -0,0 +1,27 @@ +{ + "id": "1052425005447712900", + "channel_id": "155361364909801472", + "guild_id": "197038439483310086", + "webhook_id": "223704706495545344", + "author": { + "id": "223704706495545344", + "username": "GitHub", + "discriminator": "0000", + "avatar": null, + "bot": true + }, + "content": "A new commit was pushed", + "timestamp": "2022-12-14T06:00:00.000000+00:00", + "edited_timestamp": null, + "tts": false, + "mention_everyone": false, + "mentions": [], + "mention_roles": [], + "attachments": [], + "embeds": [], + "reactions": [], + "pinned": false, + "type": 0, + "application_id": "223704706495545344", + "flags": 0 +} diff --git a/test/support/cache_probe_handler.ex b/test/support/cache_probe_handler.ex index 0fe5a73..386d130 100644 --- a/test/support/cache_probe_handler.ex +++ b/test/support/cache_probe_handler.ex @@ -20,8 +20,13 @@ defmodule Dexcord.FakeGateway.CacheProbeHandler do end end - defp probe(:GUILD_CREATE, %{"id" => id}), do: Dexcord.Cache.guild(id) - defp probe(:MESSAGE_CREATE, %{"author" => %{"id" => uid}}), do: Dexcord.Cache.user(uid) - defp probe(:GUILD_MEMBERS_CHUNK, %{"guild_id" => gid}), do: Dexcord.Cache.members(gid) + defp probe(:GUILD_CREATE, %Dexcord.Guild{id: id}), do: Dexcord.Cache.guild(id) + + defp probe(:MESSAGE_CREATE, %Dexcord.Message{author: %Dexcord.User{id: uid}}), + do: Dexcord.Cache.user(uid) + + defp probe(:GUILD_MEMBERS_CHUNK, %Dexcord.Events.GuildMembersChunk{guild_id: gid}), + do: Dexcord.Cache.members(gid) + defp probe(_name, _data), do: :na end diff --git a/test/support/dsl_fixtures.ex b/test/support/dsl_fixtures.ex new file mode 100644 index 0000000..a164307 --- /dev/null +++ b/test/support/dsl_fixtures.ex @@ -0,0 +1,107 @@ +defmodule Dexcord.Test.Color do + @moduledoc false + use Dexcord.Enum, red: 0, green: 1, blue: 4 +end + +defmodule Dexcord.Test.TestFlags do + @moduledoc false + use Dexcord.Flags, alpha: 0, beta: 1, gamma: 4 +end + +defmodule Dexcord.Test.Gadget do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + end +end + +defmodule Dexcord.Test.Widget do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + field :count, :integer + field :enabled, :boolean + field :created_at, :datetime + field :gadget, {:struct, Dexcord.Test.Gadget} + field :gadgets, {:list, {:struct, Dexcord.Test.Gadget}}, default: [] + field :tag_ids, {:list, :snowflake}, default: [] + field :color, {:enum, Dexcord.Test.Color} + field :flags, {:flags, Dexcord.Test.TestFlags} + field :parent, {:struct, __MODULE__}, tristate: true + field :premium, :presence + field :note, :string, default: "n/a" + field :extra, :raw + hydrate :gadget_full, from: :id, type: Dexcord.Test.Gadget + end +end + +# A plain-data field-group provider for exercising `include_fields/1`. +defmodule Dexcord.Test.SharedGroup do + @moduledoc false + + def __included_fields__ do + [ + {:shared_a, :string, []}, + {:shared_b, {:list, :snowflake}, [default: []]} + ] + end +end + +# Exercises Task 1's DSL extensions: `:number`, string-wire flags, the +# `wire_string:` opt, and `include_fields/1` interleaved with own fields. +defmodule Dexcord.Test.Doohickey do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :ratio, :number + field :perms, {:flags, Dexcord.Test.TestFlags}, wire_string: true + field :bits, {:flags, Dexcord.Test.TestFlags} + include_fields Dexcord.Test.SharedGroup + field :tail, :string + end +end + +# Exercises Phase 3 Task 1: the `{:id_map, type}` snowflake-keyed dictionary +# field type (Discord's resolved-data shape). +defmodule Dexcord.Test.Contraption do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :things, {:id_map, {:struct, Dexcord.Test.Gadget}}, default: %{} + end +end + +# Exercises Phase 3 Task 1: overriding a generated function and calling +# `super/1` to post-process the DSL-generated decode. +defmodule Dexcord.Test.Overridden do + @moduledoc false + use Dexcord.Struct + + discord_struct do + field :id, :snowflake + field :name, :string + end + + # NOTE: the override must NOT pattern-match `%__MODULE__{}` — `defstruct` is + # injected by `@before_compile` and is not yet defined where the body compiles. + # Match the struct as a plain map (a struct IS a map) guarded by `is_struct/2`. + def from_map(map) do + case super(map) do + %{name: name} = struct when is_struct(struct, __MODULE__) and is_binary(name) -> + %{struct | name: String.upcase(name)} + + other -> + other + end + end +end diff --git a/test/support/endpoint_fixtures.ex b/test/support/endpoint_fixtures.ex new file mode 100644 index 0000000..64f7852 --- /dev/null +++ b/test/support/endpoint_fixtures.ex @@ -0,0 +1,22 @@ +defmodule Dexcord.Test.Endpoints do + @moduledoc false + use Dexcord.Api.Endpoint + + endpoint :get_widget, :get, "/channels/:channel_id/widget", + query: [:limit, :around], + returns: Dexcord.Test.Widget + + endpoint :create_widget, :post, "/channels/:channel_id/widget", + body: true, + binary_wrap: :name, + files: true, + returns: Dexcord.Test.Widget + + endpoint :delete_widget, :delete, "/channels/:channel_id/widget/:widget_id", + reason: true, + returns: nil + + endpoint :list_widgets, :get, "/channels/:channel_id/widgets", returns: [Dexcord.Test.Widget] + + endpoint :raw_thing, :get, "/things/:thing_id" +end diff --git a/test/support/fixtures.ex b/test/support/fixtures.ex new file mode 100644 index 0000000..565f41a --- /dev/null +++ b/test/support/fixtures.ex @@ -0,0 +1,9 @@ +defmodule Dexcord.Fixtures do + @moduledoc false + + @fixtures_dir Path.expand("../fixtures", __DIR__) + + def load!(name) do + @fixtures_dir |> Path.join(name) |> File.read!() |> JSON.decode!() + end +end