Skip to content

Commit

Permalink
refactor: use a handler module instead of options map to allow more d…
Browse files Browse the repository at this point in the history
…ynamic
  • Loading branch information
SpaceEEC committed May 12, 2019
1 parent eb88c9c commit 92ca71e
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 207 deletions.
91 changes: 30 additions & 61 deletions lib/extensions/command/command.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ defmodule Crux.Extensions.Command do
@moduledoc ~S"""
Behaviour module used to compose command pipelines.
## Example
## Examples
### Commands / Middlewares
#### A Simple Ping Command
### A Simple Ping Command
```elixir
defmodule MyBot.Command.Ping do
use Crux.Extensions.Command
Expand All @@ -19,7 +17,7 @@ defmodule Crux.Extensions.Command do
end
```
#### A Simple Middleware
### A Simple Middleware Command
```elixir
defmodule MyBot.Middleware.FetchPicture do
use Crux.Extensions.Command
Expand All @@ -41,7 +39,7 @@ defmodule Crux.Extensions.Command do
end
```
#### Using the Middleware
### Using the Middleware Command
```elixir
defmodule MyBot.Command.Dog do
use Crux.Extensions.Command
Expand All @@ -56,30 +54,6 @@ defmodule Crux.Extensions.Command do
end
end
```
### Starting `Crux.Extensions.Command`
```elixir
defmodule MyBot.Application do
use Application
@commands [MyBot.Command.Ping, MyBot.Command.Dog]
def start(_, _) do
children = [
# Other modules, gateway, rest, etc...
{Crux.Extensions.Command, %{
producers: fn -> fetch_producer_pids() end,
prefix: "!",
commands: @commands,
rest: MyBot.Rest
}}
]
opts = [strategy: :one_for_one]
Supervisor.start_link(children, opts)
end
end
```
"""

defstruct assigns: %{},
Expand All @@ -88,7 +62,6 @@ defmodule Crux.Extensions.Command do
message: nil,
shard_id: nil,
halted: false,
rest: nil,
response: nil,
response_channel: nil

Expand All @@ -102,7 +75,6 @@ defmodule Crux.Extensions.Command do
message: Crux.Structs.Message.t(),
shard_id: non_neg_integer(),
halted: boolean(),
rest: module(),
response: term(),
response_channel: Crux.Rest.Util.channel_id_resolvable()
}
Expand All @@ -124,46 +96,25 @@ defmodule Crux.Extensions.Command do
"""
@type command :: command_mod() | {command_mod(), command_opts()}

@typedoc """
Available options used to start handling commands.
Notes:
* `prefix`: Omitting or `nil` results in commands being triggered without a prefix.
* `commands`: A list of modules which must implement `c:triggers/0`.
* `producers`: A function returning the current producers to subsribe to.
* `rest`: The module (using `Crux.Rest`) which should be used to send a response.
"""
@type options :: %{
optional(:prefix) => String.t() | nil,
commands: [command()],
producers: (() -> [pid()]),
rest: module()
}

defmacro __using__(_ \\ []) do
quote do
@behaviour Crux.Extensions.Command

import Crux.Extensions.Command, except: [start_link: 1, child_spec: 1]
import Crux.Extensions.Command
end
end

# TODO:
# error handler ?

# before (send) handler ?
# after (send) handler ?

@doc false
def start_link(arg), do: Crux.Extensions.Command.Supervisor.start_link(arg)
@doc false
def child_spec(arg), do: Crux.Extensions.Command.Supervisor.child_spec(arg)

@doc """
Sets the response content for this command.
"""
@spec set_response(t(), term()) :: t()
def set_response(%__MODULE__{} = command, response) do
%{command | response: response}
end

@doc """
Sets the response channel for this command.
"""
@spec set_response_channel(t(), Crux.Rest.Util.channel_id_resolvable() | nil) :: t()
def set_response_channel(%__MODULE__{} = command, response_channel) do
response_channel =
Expand All @@ -172,21 +123,39 @@ defmodule Crux.Extensions.Command do
%{command | response_channel: response_channel}
end

@doc """
Halts this command, no other commands will be executed fater this one.
"""
@spec halt(t()) :: t()
def halt(%__MODULE__{} = command) do
%{command | halted: true}
end

@doc """
Assigns an arbitrary value to an atom key, which is accessible
under the `assigns` field of a `Command`.
"""
@spec assign(t(), key :: atom(), value :: term()) :: t()
def assign(%__MODULE__{assigns: assigns} = command, key, value) when is_atom(key) do
%{command | assigns: Map.put(assigns, key, value)}
end

@doc """
Returns a list of required command modules to run before this one.
"""
@callback required() :: [command_mod() | {command_mod(), command_opts()}]

@doc """
Exeucting this command module.
"""
@callback call(t(), command_opts()) :: t()

@callback triggers() :: [String.t()]
@doc """
List of possible triggers for this command module.
Only used and required for primarily handled commands.
"""
@callback triggers() :: [String.t() | nil]

@optional_callbacks required: 0, triggers: 0
end
123 changes: 10 additions & 113 deletions lib/extensions/command/consumer.ex
Original file line number Diff line number Diff line change
@@ -1,128 +1,25 @@
defmodule Crux.Extensions.Command.Consumer do
# Module being used as consumer for incoming MESSAGE_CREATE
# events forwarding them to `Crux.Extensions.Command.Handler`.
@moduledoc false

alias Crux.Extensions.Command

@type command_info :: {Command.command_mod(), Command.command_opts()}
@type command_infos :: [command_info()]

@doc """
Starts a consumer task handling a CREATE_MESSAGE event.
Will ignore other events.
"""
@spec start_link(Command.options(), Crux.Base.Processor.event()) :: {:ok, pid()} | :ignore
def start_link(opts, {:MESSAGE_CREATE, _, _} = event) do
Task.start_link(__MODULE__, :handle_event, [opts, event])
def start_link(handler, {:MESSAGE_CREATE, _message, _shard_id} = event) do
Task.start_link(Crux.Extensions.Command.Handler, :handle_event, [event, handler])
end

# Not a MESSAGE_CREATE event, therefore not a command, therefore ignore it
def start_link(_, _), do: :ignore
def start_link(_handler, {_type, _message, _shard_id}), do: :ignore

def child_spec(opts) do
@doc false
def child_spec(handler) when is_atom(handler) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
restart: :transient
start: {__MODULE__, :start_link, [handler]},
restart: :temporary
}
end

@doc """
Handles incoming CREATE_MESSAGE events, executing possible commands.
"""
def handle_event(opts, {_, %{content: content} = message, shard_id}) do
case handle_prefix(opts.prefix, content) do
{:ok, content} ->
for {command_mod, command} <- match_commands(opts, content, message, shard_id) do
command = run_command(command_mod, command)

if command.response && command.response_channel do
command.rest.create_message!(
command.response_channel,
command.response
)
end
end

_ ->
nil
end
end

### Command (matching)

@doc """
Removes the prefix, if not nil, from content and returns an `{:ok, content}` tuple.
Returns `:error` if prefix did not match.
"""
@spec handle_prefix(
prefix :: String.t() | nil,
content :: String.t()
) :: {:ok, String.t()} | :error
def handle_prefix(nil, content), do: {:ok, content}

def handle_prefix(prefix, content) do
content_down = String.downcase(content)

if String.starts_with?(content_down, prefix) do
content = String.slice(content, String.length(prefix)..-1)

{:ok, content}
else
:error
end
end

@doc """
Gets all commands matching the given content.
"""
@spec match_commands(
opts :: Command.options(),
content :: String.t(),
message :: Crux.Structs.Message.t(),
shard_id :: non_neg_integer()
) :: [{Command.command_info(), Command.t()}]
def match_commands(%{command_infos: command_infos, rest: rest}, content, message, shard_id) do
[command | args] = String.split(content, ~r{ +})

command = String.downcase(command)

for {command_mod, _} = command_info <- command_infos,
^command <- command_mod.triggers() do
{command_info,
%Command{
trigger: command,
args: args,
message: message,
shard_id: shard_id,
rest: rest,
response_channel: message.channel_id
}}
end
end

### Command (running)

@doc """
Executes a command, including all required commands (and their required commands).
"""
@spec run_command(Command.command(), Command.t()) :: Command.t()
def run_command(_command, %Command{halted: true} = command), do: command

def run_command(command_mod, %Command{} = command) when is_atom(command_mod) do
run_command({command_mod, []}, command)
end

def run_command({command_mod, command_arg}, %Command{} = command) do
if function_exported?(command_mod, :required, 0) do
Enum.reduce(command_mod.required(), command, &run_command/2)
else
command
end
|> case do
%{halted: true} = command ->
command

command ->
command_mod.call(command, command_arg)
end
end
end
Loading

0 comments on commit 92ca71e

Please sign in to comment.