api-surface #1
4 changed files with 516 additions and 0 deletions
feat(api): full channels + messages endpoint rollout
commit
217361de42
|
|
@ -21,4 +21,84 @@ defmodule Dexcord.Api.Channels do
|
|||
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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,84 @@ defmodule Dexcord.Api.Messages do
|
|||
"/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`.
|
||||
|
||||
|
|
|
|||
178
test/dexcord/api_channels_test.exs
Normal file
178
test/dexcord/api_channels_test.exs
Normal file
|
|
@ -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
|
||||
180
test/dexcord/api_messages_test.exs
Normal file
180
test/dexcord/api_messages_test.exs
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue