-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #37 from Awlexus/add_component_handler
Add component handlers to nosedrum for handling message component interactions
- Loading branch information
Showing
5 changed files
with
224 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters