Skip to content

Commit

Permalink
Merge pull request #37 from Awlexus/add_component_handler
Browse files Browse the repository at this point in the history
Add component handlers to nosedrum for handling message component interactions
  • Loading branch information
jchristgit authored May 2, 2024
2 parents 22a8185 + bf499d0 commit 62c9b23
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 1 deletion.
88 changes: 88 additions & 0 deletions lib/nosedrum/component_handler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
defmodule Nosedrum.ComponentHandler do
@moduledoc """
Handles incoming interactions triggered by interacting with a component.
## Register components
Before the component handler can handle incoming interactions, you must
register some [custom ids](https://discord.com/developers/docs/interactions/message-components#custom-id)
with the `c:Nosedrum.ComponentHandler.register_components/3` callback. The arguments for the callback
are one or more custom ids, a module or a pid that will handle the interaction
and additional data, that will be passed on to the module or pid that handles
the interaction. The additional data can be any arbitrary term.
### Module handlers
The registered module should implement the `Nosedrum.ComponentInteraction`
behaviour. The component handler will call the `c:Nosedrum.ComponentInteraction.message_component_interaction/2`
callback of the module when a matching interaction is found.
### Process handlers
Once a pid is registered, it will receive a message of the type `t:message_component_interaction/0`
every time a matching interaction is found.
## Handle incomming interactions
The `c:Nosedrum.ComponentHandler.handle_component_interaction/1` callback will look up the correct module or
pid and relay the interaction and additional data. The recommended place to
handle these interactions would be when handling the `:INTERACTION_CREATE`
event in the consumer. The following example uses the
`Nosedrum.ComponentHandler.ETS` implementation:
```elixir
# The ready event would be a possible place where you could register static
# component handlers.
def handle_event({:READY, _, _}) do
Nosedrum.ComponentHandler.ETS.register_component(["next_button", "previous_button"],
MyApp.ButtonHandler, nil)
end
# Handle the interaction create in your consumer module.
def handle_event({:INTERACTION_CREATE, interaction, _}) do
case interaction.type do
1 -> Nostrum.Api.create_interaction_response(interaction, %{type: 1})
2 -> Nosedrum.Storage.Dispatcher.handle_interaction(interaction)
x when x in 3..5 -> Nosedrum.ComponentHandler.ETS.handle_component_interaction(interaction)
end
end
```
```elixir
# Start the Component handler in your Application. Ideally before your Consumer.
defmodule MyApp.Application do
# ...
def start(type, args) do
children = [
# ...
Nosedrum.ComponentHandler.ETS
]
options = [strategy: :rest_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, options)
end
end
```
```elixir
# A simple module for handling component interactions
defmodule MyApp.ButtonHandler do
@behaviour Nosedrum.ComponentInteraction
def message_component_interaction(interaction, _) do
case interaction.data.custom_id do
"next_button" -> [content: "The next button was clicked"]
"prev_button" -> [content: "The previous button was clicked"]
end
end
end
```
"""

@type custom_ids ::
Nostrum.Struct.Component.custom_id() | [Nostrum.Struct.Component.custom_id()]
@type component_handler :: module() | pid()
@type additional_data :: term()
@type message_component_interaction ::
{:message_component_interaction, Nostrum.Struct.Interaction.t(), additional_data()}

@callback register_components(custom_ids, component_handler, additional_data()) :: :ok
@callback handle_component_interaction(Nostrum.Struct.Interaction.t()) ::
:ok | {:error, Nostrum.Error.ApiError.t() | :not_found}
end
100 changes: 100 additions & 0 deletions lib/nosedrum/component_handler/ets.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
defmodule Nosedrum.ComponentHandler.ETS do
@moduledoc """
ETS-based implementation of a `Nosedrum.ComponentHandler`.
When a pid is registered as a handler for components it will be monitored
and automatically removed once the process exits. Registered module handlers
on the other hand are never cleared. You can access the named ETS-table manually
to remove any keys if necessary.
## Options
* :name - name used for registering the process under and also the name of the ETS-table.
Must be a atom, because it will be passed to `:ets.new/2` as name. Defaults to `Nosedrum.ComponentHandler.ETS`
> ### Note {: .info}
> When using a different name than the default you must pass it as first argument of
> the callbacks.
>
> ```elixir
> # Assuming name was set to MyApp.ComponentHandler
> Nosedrum.Storage.ETS.register_components(MyApp.ComponentHandler,
> ["a", "b", "c"], MyApp.ComponentHandlers.ABCHandler)
> ```
"""

@doc false
use GenServer

@behaviour Nosedrum.ComponentHandler

alias Nosedrum.Storage

@impl Nosedrum.ComponentHandler
def register_components(server \\ __MODULE__, component_ids, module, additional_data) do
GenServer.call(server, {:register_components, component_ids, module, additional_data})
end

@impl Nosedrum.ComponentHandler
def handle_component_interaction(
server \\ __MODULE__,
%Nostrum.Struct.Interaction{} = interaction
) do
component_id = interaction.data.custom_id

case :ets.match(server, {component_id, :"$1", :"$2"}) do
[[pid, additional_data]] when is_pid(pid) ->
send(pid, {:message_component_interaction, interaction, additional_data})
:ok

[[module, additional_data]] when is_atom(module) ->
with response <- module.message_component_interaction(interaction, additional_data),
{:ok} <- Storage.respond(interaction, response),
{_defer_type, callback_tuple} <- Keyword.get(response, :type) do
Storage.followup(interaction, callback_tuple)
end

[] ->
{:error, :not_found}
end
end

@doc false
def start_link(opts) do
name = Keyword.get(opts, :name, __MODULE__)
GenServer.start_link(__MODULE__, opts, name: name)
end

@impl GenServer
def init(opts) do
name = Keyword.get(opts, :name, __MODULE__)
table = :ets.new(name, [:named_table, :public, :ordered_set, read_concurrency: true])
{:ok, table}
end

@impl GenServer
def handle_call(
{:register_components, component_ids, component_handler, additional_data},
_from,
table
) do
if is_pid(component_handler) do
# Get notified then a stateful component handler exists
Process.monitor(component_handler)
end

entries =
component_ids
|> List.wrap()
|> Enum.map(&{&1, component_handler, additional_data})

:ets.insert(table, entries)
{:reply, :ok, table}
end

@impl GenServer
def handle_info({:DOWN, _, :process, pid, _}, table) do
# Remove stateful component handlers when the process exits
:ets.match_delete(table, {:_, pid})
{:noreply, table}
end
end
20 changes: 20 additions & 0 deletions lib/nosedrum/component_interaction.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Nosedrum.ComponentInteraction do
@moduledoc """
Behaviour for processing an interaction triggered from a message component.
Modules implementing this behaviour can be registered to a command handler
via `c:Nosedrum.ComponentHandler.register_components/3`. See the module
documentation for `Nosedrum.ComponentHandler` for more information.
"""

@doc """
Handle message component interactions.
Behaves the same way as the `Nosedrum.ApplicationCommand.commmand/1` callback.
"""
@callback message_component_interaction(
interaction :: Nostrum.Struct.Interaction.t(),
Nosedrum.ComponentHandler.additional_data()
) ::
Nosedrum.ApplicationCommand.response()
end
7 changes: 6 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ defmodule Nosedrum.MixProject do
Nosedrum.Storage,
Nosedrum.Storage.Dispatcher
],
"Component Handler": [
Nosedrum.ComponentHandler,
Nosedrum.ComponentInteraction
],
Functionality: [Nosedrum.Converters, Nosedrum.Helpers, Nosedrum.TextCommand.Predicates],
Behaviours: [
Nosedrum.TextCommand,
Expand All @@ -52,7 +56,8 @@ defmodule Nosedrum.MixProject do
Nosedrum.TextCommand.Invoker.Split,
Nosedrum.MessageCache.Agent,
Nosedrum.MessageCache.ETS,
Nosedrum.TextCommand.Storage.ETS
Nosedrum.TextCommand.Storage.ETS,
Nosedrum.ComponentHandler.ETS
]
]
]
Expand Down
10 changes: 10 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,33 @@
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.5", "643213503b1c766ec0496d828c90c424471ea54da77c8a168c725686377b9545", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "f799e9b5cd1891577d8c773d245668aa74a2fcd15eb277f51a0131690ebfb3fd"},
"curve25519": {:hex, :curve25519, "1.0.5", "f801179424e4012049fcfcfcda74ac04f65d0ffceeb80e7ef1d3352deb09f5bb", [:mix], [], "hexpm", "0fba3ad55bf1154d4d5fc3ae5fb91b912b77b13f0def6ccb3a5d58168ff4192d"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ed25519": {:hex, :ed25519, "1.4.1", "479fb83c3e31987c9cad780e6aeb8f2015fb5a482618cdf2a825c9aff809afc4", [:mix], [], "hexpm", "0dacb84f3faa3d8148e81019ca35f9d8dcee13232c32c9db5c2fb8ff48c80ec7"},
"equivalex": {:hex, :equivalex, "1.0.3", "170d9a82ae066e0020dfe1cf7811381669565922eb3359f6c91d7e9a1124ff74", [:mix], [], "hexpm", "46fa311adb855117d36e461b9c0ad2598f72110ad17ad73d7533c78020e045fc"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ex_doc": {:hex, :ex_doc, "0.32.1", "21e40f939515373bcdc9cffe65f3b3543f05015ac6c3d01d991874129d173420", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "5142c9db521f106d61ff33250f779807ed2a88620e472ac95dc7d59c380113da"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"forecastle": {:hex, :forecastle, "0.1.2", "f8dab08962c7a33010ebd39182513129f17b8814aa16fa453ddd536040882daf", [:mix], [], "hexpm", "8efaeb2e7d0fa24c605605e42562e2dbb0ffd11dc1dd99ef77d78884536ce501"},
"gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"},
"gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"},
"hackney": {:hex, :hackney, "1.17.0", "717ea195fd2f898d9fe9f1ce0afcc2621a41ecfe137fae57e7fe6e9484b9aa99", [:rebar3], [{:certifi, "~>2.5", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "64c22225f1ea8855f584720c0e5b3cd14095703af1c9fbc845ba042811dc671c"},
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"kcl": {:hex, :kcl, "1.4.2", "8b73a55a14899dc172fcb05a13a754ac171c8165c14f65043382d567922f44ab", [:mix], [{:curve25519, ">= 1.0.4", [hex: :curve25519, repo: "hexpm", optional: false]}, {:ed25519, "~> 1.3", [hex: :ed25519, repo: "hexpm", optional: false]}, {:poly1305, "~> 1.0", [hex: :poly1305, repo: "hexpm", optional: false]}, {:salsa20, "~> 1.0", [hex: :salsa20, repo: "hexpm", optional: false]}], "hexpm", "9f083dd3844d902df6834b258564a82b21a15eb9f6acdc98e8df0c10feeabf05"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nostrum": {:hex, :nostrum, "0.9.0", "ab062cec67d9fc207a1f0483683f10f5ff7985e5f5cf7bba6d073ef1fab41d41", [:mix], [{:castle, "~> 0.3.0", [hex: :castle, repo: "hexpm", optional: false]}, {:certifi, "~> 2.13", [hex: :certifi, repo: "hexpm", optional: false]}, {:gun, "~> 2.0", [hex: :gun, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:kcl, "~> 1.4", [hex: :kcl, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "8c43c6c2e213f0b43d1628c407a1e83f4baa1edcd9efe03c34d5c9ab4419846d"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
"poly1305": {:hex, :poly1305, "1.0.4", "7cdc8961a0a6e00a764835918cdb8ade868044026df8ef5d718708ea6cc06611", [:mix], [{:chacha20, "~> 1.0", [hex: :chacha20, repo: "hexpm", optional: false]}, {:equivalex, "~> 1.0", [hex: :equivalex, repo: "hexpm", optional: false]}], "hexpm", "e14e684661a5195e149b3139db4a1693579d4659d65bba115a307529c47dbc3b"},
"salsa20": {:hex, :salsa20, "1.0.4", "404cbea1fa8e68a41bcc834c0a2571ac175580fec01cc38cc70c0fb9ffc87e9b", [:mix], [], "hexpm", "745ddcd8cfa563ddb0fd61e7ce48d5146279a2cf7834e1da8441b369fdc58ac6"},
}

0 comments on commit 62c9b23

Please sign in to comment.