diff --git a/lib/plugin/agent.ex b/lib/plugin/agent.ex new file mode 100644 index 0000000..e55eef8 --- /dev/null +++ b/lib/plugin/agent.ex @@ -0,0 +1,40 @@ +defmodule Omnibot.Plugin.Agent do + defmacro __using__([]) do + quote do + alias Omnibot.Plugin + use Agent + + def start_link(opts) do + cfg = opts[:cfg] + state = opts[:state] || on_init(cfg) + Plugin.Agent.start_link(cfg, state, opts) + end + + def cfg, do: Plugin.Agent.cfg(__MODULE__) + def state, do: Plugin.Agent.state(__MODULE__) + + def update_state(update, timeout \\ 5000), + do: Plugin.Agent.update_state(__MODULE__, update, timeout) + end + end + + def start_link(cfg, state, opts) do + Agent.start_link(fn -> {cfg, state} end, opts) + end + + def cfg(agent) do + Agent.get(agent, fn {cfg, _} -> cfg end) + end + + def state(agent) do + Agent.get(agent, fn {_, state} -> state end) + end + + def update_state(agent, update, timeout \\ 5000) do + Agent.update( + agent, + fn {cfg, state} -> {cfg, apply(update, [state])} end, + timeout + ) + end +end diff --git a/lib/plugin/base.ex b/lib/plugin/base.ex new file mode 100644 index 0000000..8ce8eac --- /dev/null +++ b/lib/plugin/base.ex @@ -0,0 +1,129 @@ +defmodule Omnibot.Plugin.Base do + defmodule Hooks do + defmacro __before_compile__(_env) do + quote generated: true do + @impl true + def on_channel_msg(_irc, _channel, _nick, _line), do: nil + + @impl true + def on_channel_msg(_irc, _channel, _nick, _cmd, _params), do: nil + + @impl true + def on_join(_irc, _channel, _nick), do: nil + + @impl true + def on_part(_irc, _channel, _nick), do: nil + + @impl true + def on_kick(_irc, _channel, _nick, _target), do: nil + + @impl true + def on_init(_cfg), do: nil + + @impl true + def default_config(), do: @default_config + + def commands(), do: MapSet.to_list(@commands) + end + end + end + + defmacro __using__([]) do + quote do + alias Omnibot.{Irc, Plugin} + import Omnibot.Plugin.Base + + @behaviour Plugin.Base + + @impl Plugin.Base + def on_msg(irc, msg) do + # TODO - instead of using a router for modules, consider using a PubSub with a Registry: + # https://hexdocs.pm/elixir/master/Registry.html#module-using-as-a-pubsub + route_msg(irc, msg) + end + + defp route_msg(irc, msg) do + nick = msg.prefix.nick + case String.upcase(msg.command) do + "PRIVMSG" -> + [channel | params] = msg.params + line = Enum.join(params, " ") + + case String.split(line, " ") do + [cmd | params] -> if Enum.member?(commands(), cmd), + do: on_channel_msg(irc, channel, nick, cmd, params), + else: on_channel_msg(irc, channel, nick, line) + _ -> on_channel_msg(irc, channel, nick, line) + end + + "JOIN" -> + [channel | _] = msg.params + on_join(irc, channel, nick) + + "PART" -> + [channel | _] = msg.params + on_part(irc, channel, nick) + + "KICK" -> + [channel, target | _] = msg.params + on_kick(irc, channel, nick, target) + + _ -> + nil + end + end + + defoverridable Plugin.Base + + @commands MapSet.new() + @default_config [] + @before_compile Omnibot.Plugin.Base.Hooks + end + end + + @callback on_msg(irc :: pid(), msg :: %Omnibot.Irc.Msg{}) :: any + @callback on_channel_msg(irc :: pid(), channel :: String.t(), nick :: String.t(), line :: String.t()) :: any + @callback on_channel_msg( + irc :: pid(), + channel :: String.t(), + nick :: String.t(), + cmd :: String.t(), + params :: [String.t()] + ) :: any + @callback on_join(irc :: pid(), channel :: String.t(), nick :: String.t()) :: any + @callback on_part(irc :: pid(), channel :: String.t(), nick :: String.t()) :: any + @callback on_kick(irc :: pid(), channel :: String.t(), nick :: String.t(), target :: String.t()) :: any + @callback on_init(cfg :: any) :: any + @callback default_config() :: any + + defmacro command(cmd, opts) do + quote generated: true do + @commands MapSet.put(@commands, unquote(cmd)) + @impl Omnibot.Plugin.Base + def on_channel_msg(var!(irc), var!(channel), var!(nick), unquote(cmd), var!(params)) do + unquote(opts[:do]) + end + end + end + + defmacro command(cmd, params, opts) do + params = + Enum.map( + params, + fn param -> + case param do + {_, _, _} -> quote(do: var!(unquote(param))) + lit -> Macro.escape(lit) + end + end + ) + + quote generated: true do + @commands MapSet.put(@commands, unquote(cmd)) + @impl Omnibot.Plugin.Base + def on_channel_msg(var!(irc), var!(channel), var!(nick), unquote(cmd), unquote(params)) do + unquote(opts[:do]) + end + end + end +end diff --git a/lib/plugin/gen_server.ex b/lib/plugin/gen_server.ex new file mode 100644 index 0000000..3b68d60 --- /dev/null +++ b/lib/plugin/gen_server.ex @@ -0,0 +1,50 @@ +defmodule Omnibot.Plugin.GenServer do + defmacro __using__([]) do + quote do + alias Omnibot.Plugin + use GenServer + + def start_link(opts) do + cfg = opts[:cfg] + state = opts[:state] + GenServer.start_link(__MODULE__, {cfg, state}, opts) + end + + def cfg() do + GenServer.call(__MODULE__, :cfg) + end + + def state() do + GenServer.call(__MODULE__, :state) + end + + def update_state(update) do + GenServer.cast(__MODULE__, {:state, update}) + end + + ## Server callbacks + + @impl GenServer + def init({cfg, state}) do + state = state || on_init(cfg) + {:ok, {cfg, state}} + end + + @impl GenServer + def handle_call(:cfg, _from, {cfg, state}) do + {:reply, cfg, {cfg, state}} + end + + @impl GenServer + def handle_call(:state, _from, {cfg, state}) do + {:reply, state, {cfg, state}} + end + + @impl GenServer + def handle_cast({:state, update}, {cfg, state}) do + {:noreply, {cfg, apply(update, [state])}} + end + end + end +end + diff --git a/lib/plugin/meta.ex b/lib/plugin/meta.ex new file mode 100644 index 0000000..0270b37 --- /dev/null +++ b/lib/plugin/meta.ex @@ -0,0 +1,37 @@ +defmodule Omnibot.Plugin.Meta do + defmodule Hooks do + defmacro __before_compile(_env) do + quote do + def children(_cfg), do: [] + end + end + end + + defmacro __using__([]) do + quote do + use Omnibot.Plugin.Base + use Supervisor + + @behaviour Omnibot.Plugin.Meta + + ## Client API + + def start_link(opts) do + Supervisor.start_link(opts) + end + + ## Server callbacks + def init(opts) do + cfg = opts[:cfg] + children = children(cfg) + Supervisor.init(children, opts) + end + + defoverridable Omnibot.Plugin.Meta + + @before_compile Omnibot.Plugin.Meta.Hooks + end + end + + @callback children(cfg :: any) :: [{atom(), [{atom(), any}]}] +end diff --git a/lib/plugin/plugin.ex b/lib/plugin/plugin.ex new file mode 100644 index 0000000..a80abd6 --- /dev/null +++ b/lib/plugin/plugin.ex @@ -0,0 +1,8 @@ +defmodule Omnibot.Plugin do + defmacro __using__([]) do + quote do + use Omnibot.Plugin.Base + use Omnibot.Plugin.Agent + end + end +end diff --git a/lib/plugin/supervisor.ex b/lib/plugin/supervisor.ex new file mode 100644 index 0000000..63158ab --- /dev/null +++ b/lib/plugin/supervisor.ex @@ -0,0 +1,40 @@ +defmodule Omnibot.Plugin.Supervisor do + defmacro __using__(_opts) do + quote do + import Omnibot.Plugin.Supervisor + alias Omnibot.Plugin + use Supervisor + + @behaviour Omnibot.Plugin.Supervisor + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts[:cfg], opts) + end + + def init(_cfg) do + Supervisor.init(children(), strategy: :one_for_one) + end + + end + end + + @callback children() :: [any] +end + +# TODO : +# - figure out the best way to allow for including of supervisors and agents into a bot module +# - have to `use Agent` both places, this is not optimal +# - probably just lacks child_spec/1 ? +# - Do away with actual Plugin.Agent set of functions (outside of macro), +# and make it behaviours + `use Plugin.Agent` instead? +# Allow for ergonomic supervisor declarations, maybe like: +# +# Plugin.supervisor [ +# SomeAgent, +# SomeGenSever, +# SomeWorker, +# ], strategy: one_for_all +# +# +# And it implements all of the stuff for you? This may be too broad for how I'm doing things +# - rename MODULES to PLUGINS