api-surface #1

Merged
luna merged 51 commits from api-surface into mistress 2026-07-05 14:43:13 +00:00
5 changed files with 318 additions and 0 deletions
Showing only changes of commit e065b4d6c7 - Show all commits

feat(model): Attachment, Embed family, Reaction, AllowedMentions

Luna 2026-07-05 03:54:08 -03:00

View file

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

View file

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

116
lib/dexcord/model/embed.ex Normal file
View file

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

View file

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

View file

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