Initial commit with IRC and bot example.
Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
## Vim
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
!*.svg # comment out if you don't need vector files
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-rt-v][a-z]
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# Persistent undo
|
||||
[._]*.un~
|
||||
|
||||
## Elixir
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
omnibot-*.tar
|
||||
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Omnibot
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `omnibot` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:omnibot, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at [https://hexdocs.pm/omnibot](https://hexdocs.pm/omnibot).
|
||||
|
||||
7
config/config.exs
Normal file
7
config/config.exs
Normal file
@@ -0,0 +1,7 @@
|
||||
use Mix.Config
|
||||
|
||||
config :logger,
|
||||
backends: [:console],
|
||||
compile_time_purge_matching: [
|
||||
[level_lower_than: :debug]
|
||||
]
|
||||
52
lib/config.ex
Normal file
52
lib/config.ex
Normal file
@@ -0,0 +1,52 @@
|
||||
defmodule Omnibot.Config do
|
||||
alias Omnibot.Irc.Msg
|
||||
|
||||
@enforce_keys [:server]
|
||||
defstruct [
|
||||
:server,
|
||||
nick: "omnibot",
|
||||
user: "omnibot",
|
||||
real: "omnibot",
|
||||
port: 6667,
|
||||
ssl: false,
|
||||
modules: [],
|
||||
module_paths: []
|
||||
]
|
||||
|
||||
@doc ~S"""
|
||||
Gets all channels that the bot should join via its modules.
|
||||
"""
|
||||
def all_channels(cfg) do
|
||||
Enum.flat_map(cfg.modules, fn {_, [channels: channels]} -> channels end)
|
||||
|> MapSet.new()
|
||||
|> MapSet.to_list()
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Gets a list of all `{module, mod_cfg}` pairs from the given configuration
|
||||
that are listening to the given channel.
|
||||
"""
|
||||
def channel_modules(cfg, channel) do
|
||||
cfg.modules
|
||||
|> Enum.filter(fn {_, cfg} -> Enum.member?(cfg[:channels] || [], channel) end)
|
||||
end
|
||||
|
||||
def msg_prefix(cfg) do
|
||||
%Msg.Prefix {
|
||||
nick: cfg.nick,
|
||||
user: cfg.user,
|
||||
}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Make a new message with the given command and parameters using the given
|
||||
configuration to build the prefix.
|
||||
"""
|
||||
def msg(cfg, command, params \\ []) do
|
||||
%Msg {
|
||||
prefix: msg_prefix(cfg),
|
||||
command: command,
|
||||
params: params,
|
||||
}
|
||||
end
|
||||
end
|
||||
57
lib/contrib/fortune.ex
Normal file
57
lib/contrib/fortune.ex
Normal file
@@ -0,0 +1,57 @@
|
||||
defmodule Omnibot.Contrib.Fortune do
|
||||
use GenServer
|
||||
alias Omnibot.Irc
|
||||
require Logger
|
||||
|
||||
@fortunes [
|
||||
"Reply hazy, try again",
|
||||
"Excellent Luck",
|
||||
"Good Luck",
|
||||
"Average Luck",
|
||||
"Bad Luck",
|
||||
"Good news will come to you by mail",
|
||||
"´_ゝ`",
|
||||
"タ━━━━━━(゚∀゚)━━━━━━ !!!!",
|
||||
"You will meet a dark handsome stranger",
|
||||
"Better not tell you now",
|
||||
"Outlook good",
|
||||
"Very Bad Luck",
|
||||
"Godly Luck",
|
||||
]
|
||||
|
||||
## Client API
|
||||
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts[:cfg], opts)
|
||||
end
|
||||
|
||||
def privmsg(module, channel, nick, line) do
|
||||
GenServer.cast(module, {:privmsg, {channel, nick, line}})
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
|
||||
@impl true
|
||||
def init(cfg) do
|
||||
#Logger.debug("Starting fortune module")
|
||||
#IO.inspect(self())
|
||||
{:ok, cfg}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:unload, _from, cfg) do
|
||||
Logger.info("Unloading")
|
||||
{:reply, :ok, cfg}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:privmsg, {channel, nick, line}}, cfg) do
|
||||
if IO.inspect(line) == "!fortune" do
|
||||
fortune = Enum.random(@fortunes)
|
||||
reply = "#{nick}: #{fortune}"
|
||||
Irc.send_to(Irc, channel, reply)
|
||||
end
|
||||
|
||||
{:noreply, cfg}
|
||||
end
|
||||
end
|
||||
99
lib/irc.ex
Normal file
99
lib/irc.ex
Normal file
@@ -0,0 +1,99 @@
|
||||
# REWRITE
|
||||
defmodule Omnibot.Irc do
|
||||
require Logger
|
||||
alias Omnibot.Irc.Msg
|
||||
alias Omnibot.{Config, State}
|
||||
use GenServer
|
||||
|
||||
## Client API
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, :ok, opts)
|
||||
end
|
||||
|
||||
def send_msg(irc, msg) do
|
||||
GenServer.cast(irc, {:send_msg, msg})
|
||||
end
|
||||
|
||||
def send_msg(irc, command, params) when is_list(params) do
|
||||
cfg = State.cfg()
|
||||
GenServer.cast(irc, {:send_msg, Config.msg(cfg, command, params)})
|
||||
end
|
||||
|
||||
def send_msg(irc, command, param), do: send_msg(irc, command, [param])
|
||||
|
||||
def send_to(irc, channel, text), do: send_msg(irc, "PRIVMSG", [channel, text])
|
||||
|
||||
def join(irc, channel), do: send_msg(irc, "JOIN", channel)
|
||||
|
||||
def part(irc, channel), do: send_msg(irc, "PART", channel)
|
||||
|
||||
def sync_channels(irc), do: GenServer.cast(irc, :sync_channels)
|
||||
|
||||
## Server callbacks
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
cfg = State.cfg()
|
||||
_ssl = cfg.ssl
|
||||
|
||||
{:ok, socket} =
|
||||
:gen_tcp.connect(to_charlist(cfg.server), cfg.port, [:binary, active: false, packet: :line])
|
||||
|
||||
# Wait for first message
|
||||
#{:ok, _} = :gen_tcp.recv(socket, 0)
|
||||
send_msg(self(), "NICK", cfg.nick)
|
||||
send_msg(self(), "USER", [cfg.user, "0", "*", cfg.real])
|
||||
:inet.setopts(socket, [active: true])
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
defp write(socket, msg) do
|
||||
msg = String.Chars.to_string(msg)
|
||||
Logger.debug(">>> #{msg}")
|
||||
:gen_tcp.send(socket, "#{msg}\r\n")
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:send_msg, msg}, socket) do
|
||||
write(socket, msg)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:tcp, line}, socket) do
|
||||
Logger.debug(line)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast(:sync_channels, socket) do
|
||||
cfg = State.cfg()
|
||||
desired = MapSet.new(Config.all_channels(cfg))
|
||||
present = MapSet.new(State.channels())
|
||||
to_join = MapSet.difference(desired, present)
|
||||
|> MapSet.to_list()
|
||||
to_part = MapSet.difference(present, desired)
|
||||
|> MapSet.to_list()
|
||||
|
||||
Enum.each(to_join, fn channel -> join(self(), channel) end)
|
||||
Enum.each(to_part, fn channel -> part(self(), channel) end)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:tcp, _socket, line}, socket) do
|
||||
Logger.debug(String.trim(line))
|
||||
msg = Msg.parse(line)
|
||||
|
||||
# Send the message to the router
|
||||
irc = self()
|
||||
{:ok, _task} = Task.Supervisor.start_child(
|
||||
Omnibot.RouterSupervisor,
|
||||
fn -> Omnibot.Router.route(irc, msg) end
|
||||
)
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
109
lib/irc/msg.ex
Normal file
109
lib/irc/msg.ex
Normal file
@@ -0,0 +1,109 @@
|
||||
alias Omnibot.Irc
|
||||
alias Omnibot.Util
|
||||
|
||||
defmodule Omnibot.Irc.Msg do
|
||||
defmodule Prefix do
|
||||
defstruct [:nick, :user, :host]
|
||||
|
||||
@prefix_regex ~r/(?<nick>[^!]+)(!(?<user>[^@]+)(@(?<host>.+))?)?/
|
||||
def parse(prefix) do
|
||||
cap = Regex.named_captures(@prefix_regex, prefix)
|
||||
|
||||
if cap do
|
||||
%{
|
||||
"nick" => nick,
|
||||
"user" => user,
|
||||
"host" => host
|
||||
} = cap
|
||||
|
||||
%Irc.Msg.Prefix{
|
||||
nick: nick,
|
||||
user: if(user == "", do: nil, else: user),
|
||||
host: if(host == "", do: nil, else: host)
|
||||
}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@enforce_keys [:command]
|
||||
defstruct prefix: nil, command: nil, params: []
|
||||
|
||||
@msg_regex ~r/
|
||||
^(:(?P<prefix>[^ ]+)\ )?
|
||||
(?<command>[a-zA-Z]+|[0-9]{3})
|
||||
(?<params>(\ [^: \r\n]+)*)
|
||||
(?<trailing>\ :[^\r\n]+)?
|
||||
/x
|
||||
|
||||
def parse(msg) do
|
||||
%{
|
||||
"prefix" => prefix,
|
||||
"command" => command,
|
||||
"params" => params,
|
||||
"trailing" => trailing
|
||||
} = Regex.named_captures(@msg_regex, msg)
|
||||
|
||||
prefix = Irc.Msg.Prefix.parse(prefix)
|
||||
|
||||
params =
|
||||
String.slice(params, 1..-1)
|
||||
|> String.split(" ")
|
||||
|> Enum.filter(fn s -> String.length(s) > 0 end)
|
||||
|
||||
trailing =
|
||||
trailing
|
||||
|> String.slice(2..-1)
|
||||
|> Util.string_or_nil()
|
||||
|
||||
params = if trailing, do: params ++ [trailing], else: params
|
||||
|
||||
%Irc.Msg{
|
||||
prefix: prefix,
|
||||
command: command,
|
||||
params: params,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Irc.Msg.Prefix do
|
||||
def to_string(prefix) do
|
||||
nick = prefix.nick || ""
|
||||
user = if prefix.user, do: "!#{prefix.user}", else: ""
|
||||
host = if prefix.host, do: "@#{prefix.host}", else: ""
|
||||
"#{nick}#{user}#{host}"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars, for: Irc.Msg do
|
||||
def to_string(msg) do
|
||||
prefix =
|
||||
case String.Chars.to_string(msg.prefix) do
|
||||
"" -> ""
|
||||
p -> ":#{p}"
|
||||
end
|
||||
|
||||
# Figure out where "trailing" parameters begin, e.g.
|
||||
# ["privmsg", "param", "some message", "with another param"]
|
||||
#
|
||||
# becomes
|
||||
#
|
||||
# {["privmsg", "param"], ["some message", "with another param"]}
|
||||
#
|
||||
{params, trailing} = Enum.split_while(msg.params, fn param -> !String.contains?(param, " ") end)
|
||||
|
||||
# If trailing parameters exist, then join them with a space character and prefix with a colon,
|
||||
# appending it as another list item to the non-trailing parameter list
|
||||
#
|
||||
# If trailing parameters don't exist, don't append anything
|
||||
params = params ++
|
||||
if length(trailing) > 0,
|
||||
do: [":" <> Enum.join(trailing, " ")],
|
||||
else: []
|
||||
|
||||
([prefix, msg.command] ++ params)
|
||||
|> Enum.filter(fn n -> String.length(n) > 0 end)
|
||||
|> Enum.join(" ")
|
||||
end
|
||||
end
|
||||
52
lib/module_supervisor.ex
Normal file
52
lib/module_supervisor.ex
Normal file
@@ -0,0 +1,52 @@
|
||||
defmodule Omnibot.ModuleSupervisor do
|
||||
@moduledoc false
|
||||
|
||||
use Supervisor
|
||||
require Logger
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
Supervisor.start_link(__MODULE__, opts[:cfg], opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(cfg) do
|
||||
compile_files(cfg.module_paths || [])
|
||||
|
||||
# Map the modules in the configuration to the children
|
||||
children =
|
||||
for mod <- cfg.modules do
|
||||
case mod do
|
||||
{name, cfg} -> {name, cfg: cfg, name: name}
|
||||
name -> {name, cfg: [], name: name}
|
||||
end
|
||||
end
|
||||
|
||||
Supervisor.init(children, strategy: :one_for_one)
|
||||
end
|
||||
|
||||
defp compile_files([]), do: nil
|
||||
|
||||
defp compile_files([{path, opts} | module_paths]) do
|
||||
case {File.exists?(path), File.dir?(path)} do
|
||||
{_, true} -> compile_dir(path, opts[:recurse] || false)
|
||||
{true, false} -> Code.require_file(path)
|
||||
{_, _} -> Logger.error("module path '#{path}' does not exist, it will not be loaded")
|
||||
end
|
||||
|
||||
compile_files(module_paths)
|
||||
end
|
||||
|
||||
defp compile_files([path | module_paths]) do
|
||||
compile_files([{path, []} | module_paths])
|
||||
end
|
||||
|
||||
defp compile_dir(path, recurse) do
|
||||
files =
|
||||
File.ls!(path)
|
||||
|> Enum.map(fn file -> {Path.join(path, file), [recurse: recurse]} end)
|
||||
|> Enum.filter(fn {file, [recurse: recurse]} ->
|
||||
(!File.dir?(file) || recurse) && File.exists?(file)
|
||||
end)
|
||||
compile_files(files)
|
||||
end
|
||||
end
|
||||
10
lib/omnibot.ex
Normal file
10
lib/omnibot.ex
Normal file
@@ -0,0 +1,10 @@
|
||||
defmodule Omnibot do
|
||||
@moduledoc false
|
||||
|
||||
use Application
|
||||
|
||||
@impl true
|
||||
def start(_type, _args) do
|
||||
Omnibot.Supervisor.start_link(name: Omnibot.Supervisor)
|
||||
end
|
||||
end
|
||||
61
lib/router.ex
Normal file
61
lib/router.ex
Normal file
@@ -0,0 +1,61 @@
|
||||
defmodule Omnibot.Router do
|
||||
require Logger
|
||||
alias Omnibot.{Config, Irc, Irc.Msg, State}
|
||||
|
||||
def route(irc, msg) do
|
||||
case String.upcase(msg.command) do
|
||||
"PRIVMSG" -> handle(irc, :privmsg, msg)
|
||||
"JOIN" -> handle(irc, :join, msg)
|
||||
"KICK" -> handle(irc, :kick, msg)
|
||||
"PART" -> handle(irc, :part, msg)
|
||||
"PING" -> handle(irc, :ping, msg)
|
||||
"001" -> handle(irc, :welcome, msg)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def handle(_irc, :privmsg, msg) do
|
||||
# TODO : get channel, pass along to modules
|
||||
[channel | params] = msg.params
|
||||
line = Enum.join(params, " ")
|
||||
nick = msg.prefix.nick
|
||||
|
||||
# Find modules that want this message
|
||||
State.cfg()
|
||||
|> Config.channel_modules(channel)
|
||||
|> Enum.each(fn {module, _} -> module.privmsg(module, channel, nick, line) end)
|
||||
end
|
||||
|
||||
def handle(_irc, :join, %Msg {prefix: %Msg.Prefix{nick: nick}, params: [channel | _]}) do
|
||||
cfg = State.cfg()
|
||||
if nick == cfg.nick do
|
||||
State.add_channel(channel)
|
||||
end
|
||||
end
|
||||
|
||||
def handle(irc, :kick, %Msg {params: [channel, who | _]}) do
|
||||
cfg = State.cfg()
|
||||
if who == cfg.nick do
|
||||
State.remove_channel(State, channel)
|
||||
# sync_channels here because this is a state that we (should) not have put ourselves into
|
||||
Irc.sync_channels(irc)
|
||||
end
|
||||
end
|
||||
|
||||
def handle(_irc, :part, %Msg {prefix: %Msg.Prefix{nick: nick}, params: [channel | _]}) do
|
||||
cfg = State.cfg()
|
||||
if nick == cfg.nick do
|
||||
State.remove_channel(State, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def handle(irc, :ping, msg) do
|
||||
cfg = State.cfg()
|
||||
reply = Config.msg(cfg, "PONG", msg.params)
|
||||
Irc.send_msg(irc, reply)
|
||||
end
|
||||
|
||||
def handle(irc, :welcome, _msg) do
|
||||
Irc.sync_channels(irc)
|
||||
end
|
||||
end
|
||||
74
lib/state.ex
Normal file
74
lib/state.ex
Normal file
@@ -0,0 +1,74 @@
|
||||
defmodule Omnibot.State do
|
||||
use GenServer
|
||||
|
||||
@enforce_keys [:cfg]
|
||||
defstruct [:cfg, channels: MapSet.new()]
|
||||
|
||||
## Client API
|
||||
|
||||
def start_link(opts) do
|
||||
cfg = opts[:cfg]
|
||||
GenServer.start_link(__MODULE__, %Omnibot.State{
|
||||
cfg: cfg
|
||||
}, opts)
|
||||
end
|
||||
|
||||
@doc "Gets the current configuration from the default State process."
|
||||
def cfg(), do: cfg(__MODULE__)
|
||||
|
||||
@doc "Gets the current configuration from the given State process."
|
||||
def cfg(state) do
|
||||
GenServer.call(state, :cfg)
|
||||
end
|
||||
|
||||
@doc "Gets all channels that the bot is present in from the default State process."
|
||||
def channels(), do: channels(__MODULE__)
|
||||
|
||||
@doc "Gets all channels that the bot is present in from the given State process."
|
||||
def channels(state) do
|
||||
GenServer.call(state, :channels)
|
||||
end
|
||||
|
||||
@doc "Adds a channel to the list of joined channels of the default State process, if it is not already present."
|
||||
def add_channel(channel), do: add_channel(__MODULE__, channel)
|
||||
|
||||
@doc "Adds a channel to the list of joined channels of the given State process, if it is not already present."
|
||||
def add_channel(state, channel) do
|
||||
GenServer.cast(state, {:add_channel, channel})
|
||||
end
|
||||
|
||||
@doc "Removes a channel from the list of joined channels of the default State process, if it exists."
|
||||
def remove_channel(channel), do: remove_channel(__MODULE__, channel)
|
||||
|
||||
@doc "Removes a channel from the list of joined channels of the given State process, if it exists."
|
||||
def remove_channel(state, channel) do
|
||||
GenServer.cast(state, {:remove_channel, channel})
|
||||
end
|
||||
|
||||
## Server API
|
||||
|
||||
@impl true
|
||||
def init(state) do
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:cfg, _from, state) do
|
||||
{:reply, state.cfg, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:channels, _from, state) do
|
||||
{:reply, state.channels, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:add_channel, channel}, state) do
|
||||
{:noreply, %{state | channels: state.channels |> MapSet.put(channel)}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:remove_channel, channel}, state) do
|
||||
{:noreply, %{state | channels: state.channels |> MapSet.delete(channel)}}
|
||||
end
|
||||
end
|
||||
29
lib/supervisor.ex
Normal file
29
lib/supervisor.ex
Normal file
@@ -0,0 +1,29 @@
|
||||
defmodule Omnibot.Supervisor do
|
||||
@moduledoc false
|
||||
|
||||
use Supervisor
|
||||
|
||||
def start_link(opts) do
|
||||
Supervisor.start_link(__MODULE__, :ok, opts)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(:ok) do
|
||||
{_, bindings} = Code.eval_file("omnibot.exs")
|
||||
cfg = bindings[:config]
|
||||
|
||||
children = [
|
||||
{Task.Supervisor, name: Omnibot.RouterSupervisor, strategy: :one_for_one},
|
||||
{Omnibot.State, cfg: cfg, name: Omnibot.State},
|
||||
{Omnibot.Irc, name: Omnibot.Irc},
|
||||
{Omnibot.ModuleSupervisor, cfg: cfg, name: Omnibot.ModuleSupervisor}
|
||||
]
|
||||
|
||||
# TODO : how to handle config reloading?
|
||||
# TODO : how to start up modules?
|
||||
|
||||
# :one_for_all here because the RouterSupervisor and IRC server are co-dependent
|
||||
Supervisor.init(children, strategy: :one_for_all)
|
||||
end
|
||||
end
|
||||
|
||||
5
lib/util.ex
Normal file
5
lib/util.ex
Normal file
@@ -0,0 +1,5 @@
|
||||
defmodule Omnibot.Util do
|
||||
def string_empty?(s), do: String.length(s) == 0
|
||||
|
||||
def string_or_nil(s), do: if(string_empty?(s), do: nil, else: s)
|
||||
end
|
||||
37
mix.exs
Normal file
37
mix.exs
Normal file
@@ -0,0 +1,37 @@
|
||||
defmodule Omnibot.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
app: :omnibot,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.10",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
aliases: aliases(),
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger],
|
||||
mod: {Omnibot, []}
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[
|
||||
c: ["clean", "compile --warnings-as-errors"],
|
||||
test: ["test --no-start"],
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
]
|
||||
end
|
||||
end
|
||||
14
omnibot.example.exs
Normal file
14
omnibot.example.exs
Normal file
@@ -0,0 +1,14 @@
|
||||
alias Omnibot.Config
|
||||
|
||||
config = %Config {
|
||||
nick: "omnibot_testing",
|
||||
server: "irc.bonerjamz.us",
|
||||
port: 6667,
|
||||
ssl: false,
|
||||
|
||||
modules: [
|
||||
{Omnibot.Contrib.Fortune, channels: ["#idleville"]},
|
||||
],
|
||||
|
||||
module_paths: [{"modules", recurse: true}]
|
||||
}
|
||||
24
test/config_test.exs
Normal file
24
test/config_test.exs
Normal file
@@ -0,0 +1,24 @@
|
||||
defmodule ConfigTest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Omnibot.Config
|
||||
|
||||
test "config all_channels works correctly" do
|
||||
cfg = %Config {
|
||||
server: "test",
|
||||
modules: [
|
||||
{Test, channels: ["#foo", "#bar"]},
|
||||
{Test, channels: ["#foo"]},
|
||||
{Test, channels: ["#bar"]},
|
||||
{Test, channels: ["#baz"]},
|
||||
]
|
||||
}
|
||||
|
||||
channels = Config.all_channels(cfg)
|
||||
|
||||
assert length(channels) == 3
|
||||
assert Enum.any?(channels, fn channel -> channel == "#foo" end)
|
||||
assert Enum.any?(channels, fn channel -> channel == "#bar" end)
|
||||
assert Enum.any?(channels, fn channel -> channel == "#baz" end)
|
||||
end
|
||||
end
|
||||
76
test/irc/msg_test.exs
Normal file
76
test/irc/msg_test.exs
Normal file
@@ -0,0 +1,76 @@
|
||||
alias Omnibot.Irc
|
||||
alias Omnibot.Msg
|
||||
|
||||
defmodule Omnibot.Irc.MsgTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# doctest Irc
|
||||
|
||||
test "irc message parsing" do
|
||||
assert %Irc.Msg{
|
||||
prefix: %Irc.Msg.Prefix{nick: "example.com"},
|
||||
command: "PRIVMSG",
|
||||
params: [],
|
||||
} == Irc.Msg.parse(":example.com PRIVMSG")
|
||||
|
||||
assert %Irc.Msg{
|
||||
prefix: %Irc.Msg.Prefix{nick: "example.com"},
|
||||
command: "PRIVMSG",
|
||||
params: ["#channel", "message text"],
|
||||
} == Irc.Msg.parse(":example.com PRIVMSG #channel :message text")
|
||||
|
||||
assert %Irc.Msg{
|
||||
prefix: %Irc.Msg.Prefix{nick: "example.com"},
|
||||
command: "PRIVMSG",
|
||||
params: ["#channel", "message", "text"],
|
||||
} == Irc.Msg.parse(":example.com PRIVMSG #channel message text")
|
||||
end
|
||||
|
||||
test "irc message prefix parsing" do
|
||||
alias Irc.Msg.Prefix
|
||||
assert Prefix.parse(":example.com") != %Prefix{}
|
||||
|
||||
%Prefix{
|
||||
nick: "example.com"
|
||||
} = Prefix.parse("example.com")
|
||||
|
||||
%Prefix{
|
||||
nick: "nick"
|
||||
} = Prefix.parse("nick")
|
||||
|
||||
%Prefix{
|
||||
nick: "nick",
|
||||
user: "username"
|
||||
} = Prefix.parse("nick!username")
|
||||
|
||||
%Prefix{
|
||||
nick: "nick",
|
||||
user: "username",
|
||||
host: "example.com"
|
||||
} = Prefix.parse("nick!username@example.com")
|
||||
end
|
||||
|
||||
test "irc message prefix to_string" do
|
||||
alias Irc.Msg.Prefix
|
||||
|
||||
prefixes = [
|
||||
"example.com",
|
||||
"nick!username",
|
||||
"nick!username@example.com"
|
||||
]
|
||||
|
||||
for prefix <- prefixes,
|
||||
do: assert(Prefix.parse(prefix) |> to_string() == prefix)
|
||||
end
|
||||
|
||||
test "irc message to_string" do
|
||||
alias Irc.Msg
|
||||
|
||||
msgs = [
|
||||
":example.com PRIVMSG #command",
|
||||
":example.com PRIVMSG #channel :message text"
|
||||
]
|
||||
|
||||
for msg <- msgs, do: assert(Msg.parse(msg) |> to_string() == msg)
|
||||
end
|
||||
end
|
||||
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@@ -0,0 +1 @@
|
||||
ExUnit.start()
|
||||
15
test/util_test.exs
Normal file
15
test/util_test.exs
Normal file
@@ -0,0 +1,15 @@
|
||||
alias Omnibot.Util
|
||||
|
||||
defmodule Omnibot.UtilTest do
|
||||
use ExUnit.Case
|
||||
|
||||
test "string_empty?" do
|
||||
assert Util.string_empty?("")
|
||||
assert !Util.string_empty?("asdf")
|
||||
end
|
||||
|
||||
test "string_or_nil" do
|
||||
assert Util.string_or_nil("") == nil
|
||||
assert Util.string_or_nil("asdf") == "asdf"
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user