diff --git a/lib/config.ex b/lib/config.ex index 19f33d4..c6ccc69 100644 --- a/lib/config.ex +++ b/lib/config.ex @@ -6,7 +6,7 @@ defmodule Omnibot.Config do plugins and their configuration. ## Configuration keys - + - server: the IRC server to connect to. **(required)** - nick: the IRC nickname to use for the bot. **(optional, default "omnibot")** - user: the IRC username to use for the bot. **(optional, default "omnibot")** @@ -18,12 +18,12 @@ defmodule Omnibot.Config do - plugin_paths: a list of locations to look for additional plugins. **(optional, default [])** ## Plugins configuration - + Inside the `plugins` key listed above, a list of plugins is expected. Plugins are either a single atom, or a tuple of the plugin and a keyword list of configuration. ### Plugin channels - + All plugins have a `channels` configuration value, determining which channels this plugin is active in. This value may also be set to the atom value `:all`, which indicates that it is active on all channels. @@ -32,7 +32,7 @@ defmodule Omnibot.Config do knows which channels to join. ## Example configuration - + iex> %Omnibot.Config { ...> nick: "omnibot_testing", ...> server: "irc.bonerjamz.us", @@ -87,11 +87,12 @@ defmodule Omnibot.Config do """ def all_channels(cfg) do Enum.flat_map(cfg.plugins, fn - {_, [channels: :all]} -> [] - {_, [channels: channels]} -> channels + {_, cfg} -> if cfg[:channels] in [nil, :all], + do: [], + else: cfg[:channels] end) - |> MapSet.new() - |> MapSet.to_list() + |> MapSet.new() + |> MapSet.to_list() end @doc """ @@ -108,9 +109,9 @@ defmodule Omnibot.Config do %Omnibot.Irc.Msg.Prefix {:nick => "omnibot", :user => "omnibot", :host => nil} """ def msg_prefix(cfg) do - %Msg.Prefix { + %Msg.Prefix{ nick: cfg.nick, - user: cfg.user, + user: cfg.user } end @@ -135,10 +136,29 @@ defmodule Omnibot.Config do } """ def msg(cfg, command, params \\ []) do - %Msg { + %Msg{ prefix: msg_prefix(cfg), command: command, - params: params, + params: params } end + + def channel_plugins(cfg, channel) do + cfg.plugins + |> Enum.filter(fn {_plug, cf} -> + cf[:channels] == :all or channel in Keyword.get(cf, :channels, []) + end) + end + + def load(path) do + with {_, bindings} <- Code.eval_file(path) do + cfg = bindings[:config] + plugins = cfg.plugins + |> Enum.map(fn + plug when is_atom(plug) -> {plug, plug.default_config()} + {plug, cfg}-> {plug, cfg ++ plug.default_config()} + end) + %Omnibot.Config{cfg | plugins: plugins} + end + end end diff --git a/lib/irc/irc.ex b/lib/irc/irc.ex index cc1e99c..d341381 100644 --- a/lib/irc/irc.ex +++ b/lib/irc/irc.ex @@ -1,7 +1,7 @@ defmodule Omnibot.Irc do require Logger alias Omnibot.Irc.Msg - alias Omnibot.{Config, State} + alias Omnibot.Config use GenServer ## Client API @@ -29,8 +29,13 @@ defmodule Omnibot.Irc do def cfg(irc), do: GenServer.call(irc, :cfg) - defp route_msg(irc, msg) do - plugins = Msg.channel(msg) |> State.channel_plugins() + defp route_msg(irc, cfg, msg) do + # TODO : + # * Plugins that are loaded are not having their defaults applied, since + # the add_loaded_plugin in State would handle that. May be a good idea to + # make a Config.load/1 function that will handle loading of a + # configuration file instead of doing it in the root Omnibot module + plugins = Config.channel_plugins(cfg, Msg.channel(msg)) Task.Supervisor.async_stream_nolink( Omnibot.RouterSupervisor, @@ -85,12 +90,12 @@ defmodule Omnibot.Irc do end @impl true - def handle_info({:tcp, _socket, line}, state) do + def handle_info({:tcp, _info_socket, line}, state = {_socket, cfg}) do Logger.debug(String.trim(line)) msg = Msg.parse(line) # Send the message to the router - route_msg(self(), msg) + route_msg(self(), cfg, msg) {:noreply, state} end diff --git a/lib/plugin_manager.ex b/lib/plugin_manager.ex index 1a03336..4f74ce7 100644 --- a/lib/plugin_manager.ex +++ b/lib/plugin_manager.ex @@ -3,7 +3,6 @@ defmodule Omnibot.PluginManager do use Supervisor require Logger - alias Omnibot.State def start_link(opts \\ []) do Supervisor.start_link(__MODULE__, opts[:cfg], opts) @@ -13,23 +12,10 @@ defmodule Omnibot.PluginManager do def init(cfg) do compile_files(cfg.plugin_paths || []) - # These are plugins that need to be loaded for core functionality of the bot - core = [ - Omnibot.Core, - ] - # Map the plugins in the configuration to the children children = - for plug <- (core ++ cfg.plugins) do - {name, cfg} = case plug do - {name, cfg} -> {name, cfg ++ name.default_config()} - name -> {name, name.default_config()} - end - {Omnibot.Plugin.Supervisor, plugin: name, cfg: cfg} - end - - # Add each child to the "loaded plugins" list in the State - Enum.each(children, fn {_plugin, opts} -> State.add_loaded_plugin({opts[:plugin], opts[:cfg]}) end) + for {plugin, cfg} <- cfg.plugins, + do: {Omnibot.Plugin.Supervisor, plugin: plugin, cfg: cfg} Supervisor.init(children, strategy: :one_for_one) end diff --git a/lib/supervisor.ex b/lib/supervisor.ex index 5e1a2c8..d6524db 100644 --- a/lib/supervisor.ex +++ b/lib/supervisor.ex @@ -3,6 +3,7 @@ defmodule Omnibot.Supervisor do use Supervisor require IEx + alias Omnibot.Config def start_link(opts) do Supervisor.start_link(__MODULE__, :ok, opts) @@ -10,10 +11,7 @@ defmodule Omnibot.Supervisor do @impl true def init(:ok) do - - {_, bindings} = System.get_env("OMNIBOT_CFG", "omnibot.exs") - |> Code.eval_file() - cfg = bindings[:config] + cfg = System.get_env("OMNIBOT_CFG", "omnibot.exs") |> Config.load() # TODO : move cfg to its own process so reloading it is as simple as killing the process children = [ diff --git a/omnibot.example.exs b/omnibot.example.exs index e89c1a4..ca6355f 100644 --- a/omnibot.example.exs +++ b/omnibot.example.exs @@ -7,6 +7,7 @@ config = %Config { ssl: false, plugins: [ + Omnibot.Core, {Omnibot.Contrib.OnConnect, commands: [ ["privmsg", "nickserv", "register", "password123", "omnibot@omni.bot"], ["privmsg", "nickserv", "identify", "password123"] diff --git a/test/config_test.exs b/test/config_test.exs new file mode 100644 index 0000000..2a63dda --- /dev/null +++ b/test/config_test.exs @@ -0,0 +1,63 @@ +defmodule Omnibot.ConfigTest do + use ExUnit.Case, async: true + doctest Omnibot.Config + alias Omnibot.Config + + test "channel_plugins works correctly" do + cfg = %Config { + server: "test", + plugins: [ + {FooBar, channels: ["#foo", "#bar"]}, + {Foo, channels: ["#foo"]}, + {Bar, channels: ["#bar"]}, + {Baz, channels: ["#baz"]}, + {All, channels: :all}, + ] + } + + plugins = Config.channel_plugins(cfg, "#foo") + |> Enum.map(fn {plugin, _} -> plugin end) + assert length(plugins) == 3 + assert Enum.member?(plugins, FooBar) + assert Enum.member?(plugins, Foo) + assert Enum.member?(plugins, All) + + plugins = Config.channel_plugins(cfg, "#bar") + |> Enum.map(fn {plugin, _} -> plugin end) + assert length(plugins) == 3 + assert Enum.member?(plugins, FooBar) + assert Enum.member?(plugins, Bar) + assert Enum.member?(plugins, All) + + plugins = Config.channel_plugins(cfg, "#baz") + |> Enum.map(fn {plugin, _} -> plugin end) + assert length(plugins) == 2 + assert Enum.member?(plugins, Baz) + assert Enum.member?(plugins, All) + + plugins = Config.channel_plugins(cfg, nil) + |> Enum.map(fn {plugin, _} -> plugin end) + assert length(plugins) == 1 + assert Enum.member?(plugins, All) + end + + test "all_channels works correctly" do + cfg = %Config { + server: "testing", + plugins: [ + {FooBar, channels: ["#foo", "#bar"]}, + {Foo, channels: ["#foo"]}, + {Bar, channels: ["#bar"]}, + {Baz, channels: ["#baz"]}, + {All, channels: :all}, + ], + } + + channels = Config.all_channels(cfg) + + assert length(channels) == 3 + assert Enum.member?(channels, "#foo") + assert Enum.member?(channels, "#bar") + assert Enum.member?(channels, "#baz") + end +end diff --git a/test/state_test.exs b/test/state_test.exs deleted file mode 100644 index 4b7f9a1..0000000 --- a/test/state_test.exs +++ /dev/null @@ -1,65 +0,0 @@ -defmodule StateTest do - use ExUnit.Case - - alias Omnibot.State - - setup do - state = start_supervised!(State) - {:ok, state: state} - end - - test "state channel_plugins works correctly", %{state: state} do - plugins = [ - {FooBar, channels: ["#foo", "#bar"]}, - {Foo, channels: ["#foo"]}, - {Bar, channels: ["#bar"]}, - {Baz, channels: ["#baz"]}, - {All, channels: :all}, - ] - - plugins |> Enum.each(fn plugin -> State.add_loaded_plugin(state, plugin) end) - - plugins = State.channel_plugins(state, "#foo") - |> Enum.map(fn {plugin, _} -> plugin end) - assert length(plugins) == 3 - assert Enum.member?(plugins, FooBar) - assert Enum.member?(plugins, Foo) - assert Enum.member?(plugins, All) - - plugins = State.channel_plugins(state, "#bar") - |> Enum.map(fn {plugin, _} -> plugin end) - assert length(plugins) == 3 - assert Enum.member?(plugins, FooBar) - assert Enum.member?(plugins, Bar) - assert Enum.member?(plugins, All) - - plugins = State.channel_plugins(state, "#baz") - |> Enum.map(fn {plugin, _} -> plugin end) - assert length(plugins) == 2 - assert Enum.member?(plugins, Baz) - assert Enum.member?(plugins, All) - - plugins = State.channel_plugins(state, nil) - |> Enum.map(fn {plugin, _} -> plugin end) - assert length(plugins) == 1 - assert Enum.member?(plugins, All) - end - - test "state all_channels works correctly", %{state: state} do - plugins = [ - {FooBar, channels: ["#foo", "#bar"]}, - {Foo, channels: ["#foo"]}, - {Bar, channels: ["#bar"]}, - {Baz, channels: ["#baz"]}, - {All, channels: :all}, - ] - - plugins |> Enum.each(fn plugin -> State.add_loaded_plugin(state, plugin) end) - channels = State.all_channels(state) - - assert length(channels) == 3 - assert Enum.member?(channels, "#foo") - assert Enum.member?(channels, "#bar") - assert Enum.member?(channels, "#baz") - end -end