api-surface #1
5 changed files with 506 additions and 0 deletions
feat(model): Guild family, VoiceState, Presence, golden GUILD_CREATE fixture
commit
227f6d8422
87
lib/dexcord/model/guild.ex
Normal file
87
lib/dexcord/model/guild.ex
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
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, :raw
|
||||
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, :raw}, default: []
|
||||
field :soundboard_sounds, {:list, :raw}, default: []
|
||||
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
|
||||
23
lib/dexcord/model/presence.ex
Normal file
23
lib/dexcord/model/presence.ex
Normal file
|
|
@ -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
|
||||
20
lib/dexcord/model/voice_state.ex
Normal file
20
lib/dexcord/model/voice_state.ex
Normal file
|
|
@ -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
|
||||
120
test/dexcord/model_guild_test.exs
Normal file
120
test/dexcord/model_guild_test.exs
Normal file
|
|
@ -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
|
||||
256
test/fixtures/guild_create.json
vendored
Normal file
256
test/fixtures/guild_create.json
vendored
Normal file
|
|
@ -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": []
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue