api-surface #1

Merged
luna merged 51 commits from api-surface into mistress 2026-07-05 14:43:13 +00:00
16 changed files with 914 additions and 3 deletions
Showing only changes of commit 9c8c8de498 - Show all commits

feat(model): webhook/invite/audit/automod/scheduled/poll/application/team + retypes

Luna 2026-07-05 04:45:45 -03:00

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -184,6 +184,7 @@ defmodule Dexcord.Thread do
field :member, {:struct, Dexcord.ThreadMember}
field :applied_tags, {:list, :snowflake}, default: []
field :last_pin_timestamp, :datetime
field :newly_created, :boolean, default: false
end
end

View file

@ -22,6 +22,7 @@ defmodule Dexcord.ThreadMember do
field :join_timestamp, :datetime
field :flags, :integer
field :member, {:struct, Dexcord.Member}
field :guild_id, :snowflake
end
end

View file

@ -37,7 +37,7 @@ defmodule Dexcord.Guild do
field :max_stage_video_channel_users, :integer
field :approximate_member_count, :integer
field :approximate_presence_count, :integer
field :welcome_screen, :raw
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
@ -54,7 +54,7 @@ defmodule Dexcord.Guild do
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, :raw}, default: []
field :guild_scheduled_events, {:list, {:struct, Dexcord.GuildScheduledEvent}}, default: []
field :soundboard_sounds, {:list, :raw}, default: []
end
end

View file

@ -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

View file

@ -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

View file

@ -4,6 +4,7 @@ defmodule Dexcord.Model.MemberShared do
def __included_fields__ do
[
{:guild_id, :snowflake, []},
{:nick, :string, []},
{:avatar, :string, []},
{:banner, :string, []},

View file

@ -40,7 +40,7 @@ defmodule Dexcord.Message do
field :position, :integer
field :role_subscription_data, :raw
field :resolved, {:struct, Dexcord.ResolvedData}
field :poll, :raw
field :poll, {:struct, Dexcord.Poll}
field :call, {:struct, Dexcord.MessageCall}
field :shared_client_theme, :raw
end

67
lib/dexcord/model/misc.ex Normal file
View file

@ -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

55
lib/dexcord/model/poll.ex Normal file
View file

@ -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

View file

@ -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

24
lib/dexcord/model/team.ex Normal file
View file

@ -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

View file

@ -0,0 +1,19 @@
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
end

View file

@ -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