summaryrefslogtreecommitdiff
path: root/lib/irc/parser
diff options
context:
space:
mode:
authorhref <href@random.sh>2019-12-30 11:30:01 +0100
committerhref <href@random.sh>2019-12-30 11:30:01 +0100
commitbc506bcd62cfb1bc62a81600c214fd849b54248d (patch)
tree8c7a2f474c38389591610c4ad294664ebdfe5bdd /lib/irc/parser
Initial commit...
Diffstat (limited to 'lib/irc/parser')
-rw-r--r--lib/irc/parser/isupport.ex41
-rw-r--r--lib/irc/parser/line.ex143
-rw-r--r--lib/irc/parser/mode.ex32
-rw-r--r--lib/irc/parser/numeric.ex72
4 files changed, 288 insertions, 0 deletions
diff --git a/lib/irc/parser/isupport.ex b/lib/irc/parser/isupport.ex
new file mode 100644
index 0000000..9dd7f30
--- /dev/null
+++ b/lib/irc/parser/isupport.ex
@@ -0,0 +1,41 @@
+defmodule Irc.Parser.Isupport do
+ @moduledoc """
+ ISUPPORT list parser
+ """
+
+ def parse(list, acc \\ %{}) do
+ list = List.flatten(list)
+ Enum.reduce(list, acc, fn(entry, acc) ->
+ case String.split(entry, "=", parts: 2) do
+ [entry] ->
+ if String.contains?(entry, " ") do
+ acc
+ else
+ Map.put(acc, entry, true)
+ end
+ [entry, value] -> Map.put(acc, entry, parse_value(value))
+ end
+ end)
+ end
+
+ defp parse_value(value) do
+ if String.contains?(value, ":") do
+ Enum.reduce(String.split(value, ","), %{}, fn(key_and_value, acc) ->
+ case String.split(key_and_value, ":", parts: 2) do
+ [key] -> Map.put(acc, key, nil)
+ [key, value] -> Map.put(acc, key, format_value(value))
+ end
+ end)
+ else
+ format_value(value)
+ end
+ end
+
+ def format_value(value) do
+ case Integer.parse(value) do
+ {i, ""} -> i
+ _ -> value
+ end
+ end
+
+end
diff --git a/lib/irc/parser/line.ex b/lib/irc/parser/line.ex
new file mode 100644
index 0000000..3ba2016
--- /dev/null
+++ b/lib/irc/parser/line.ex
@@ -0,0 +1,143 @@
+defmodule Irc.Parser.Line do
+ @moduledoc """
+ IRC line parser
+ """
+
+ defstruct tags: %{}, source: nil, command: nil, args: []
+
+ @doc "Parse a server-to-client line."
+ def parse(line) do
+ {tags, rest} = parse_tags(line)
+ {source, rest} = parse_source(rest)
+ {command, rest} = parse_cmd(rest)
+ args = parse_args(rest)
+ %__MODULE__{tags: tags, source: source, command: command, args: args}
+ end
+
+ # ARGS
+
+ def parse_args(input), do: parse_args(input, [])
+
+ def parse_args(input, acc) do
+ case parse_arg(input, []) do
+ {:continue, new, rest} -> parse_args(rest, [new | acc])
+ {:finished, new} ->
+ list = new ++ acc
+ format_args(list)
+ end
+ end
+
+ # Final argument is the only argument.
+ def parse_arg([?: | rest], acc) when length(acc) == 0 do
+ final = parse_final_arg(rest, [])
+ {:finished, [final]}
+ end
+
+ def parse_arg([0x20, ?: | rest], acc) do
+ final = parse_final_arg(rest, [])
+ {:finished, [final, acc]}
+ end
+
+ def parse_arg([0x20 | rest], acc) do
+ {:continue, acc, rest}
+ end
+
+ def parse_arg([char | rest], acc) do
+ parse_arg(rest, [char | acc])
+ end
+
+ def parse_arg([], acc) do
+ {:finished, [acc]}
+ end
+
+ def parse_final_arg([char | rest], acc) do
+ parse_final_arg(rest, [char | acc])
+ end
+
+ def parse_final_arg([], acc) do
+ acc
+ end
+
+ def format_args(list) when is_list(list) do
+ list
+ |> Enum.map(fn(arg) -> String.trim(acc_to_str(arg)) end)
+ |> Enum.reverse()
+ end
+
+ # COMMAND
+
+ def parse_cmd(input), do: parse_cmd(input, [])
+
+ def parse_cmd([0x20 | rest], acc) do
+ {acc_to_str(acc), rest}
+ end
+
+ def parse_cmd([char | rest], acc) do
+ parse_cmd(rest, [char | acc])
+ end
+
+ # SOURCE
+
+ def parse_source([?: | input]), do: parse_source(input, [])
+ def parse_source(input), do: {nil, input}
+
+ def parse_source([0x20 | rest], acc) do
+ {acc_to_str(acc), rest}
+ end
+ def parse_source([char | rest], acc), do: parse_source(rest, [char | acc])
+
+ # TAGS
+
+ def parse_tags([?@ | input]), do: parse_tags(input, [])
+ def parse_tags(input), do: {%{}, input}
+
+ def parse_tags(input, acc) do
+ case parse_tag(input) do
+ {:continue, new, rest} -> parse_tags([new | acc], rest)
+ {:finished, new, rest} -> {format_tags([new | acc]), rest}
+ end
+ end
+
+ def parse_tag(input) do
+ parse_tag({[], nil}, input)
+ end
+
+ def parse_tag({acc, nil}, [?= | rest]) do
+ parse_tag({acc, []}, rest)
+ end
+ def parse_tag({acc, nil}, [?; | rest]) do
+ {:continue, {acc, true}, rest}
+ end
+ def parse_tag({acc, nil}, [char | rest]) do
+ parse_tag({[char | acc], nil}, rest)
+ end
+
+ def parse_tag({key, acc}, [?; | rest]) do
+ {:continue, {key, acc}, rest}
+ end
+
+ def parse_tag({key, acc}, [0x20 | rest]) do
+ {:finished, {key, acc}, rest}
+ end
+ def parse_tag({key, acc}, [char | rest]) do
+ parse_tag({key, [char | acc]}, rest)
+ end
+
+ def format_tags(list) do
+ list
+ |> Enum.map(fn({k,v}) ->
+ {acc_to_str(k), acc_to_str(v)}
+ end)
+ |> Enum.into(Map.new)
+ end
+
+ def acc_to_str([]), do: nil
+ def acc_to_str(list) when is_list(list) do
+ list
+ |> Enum.reverse()
+ |> to_string()
+ end
+ def acc_to_str(other), do: other
+
+end
+
diff --git a/lib/irc/parser/mode.ex b/lib/irc/parser/mode.ex
new file mode 100644
index 0000000..d4c0a16
--- /dev/null
+++ b/lib/irc/parser/mode.ex
@@ -0,0 +1,32 @@
+defmodule Irc.Parser.Mode do
+ @moduledoc """
+ Mode change parser
+ """
+
+ def changes(string, previous) do
+ list = to_charlist(string)
+ acc_changes(list, nil, previous)
+ end
+
+ defp acc_changes([?+ | rest], _, acc) do
+ acc_changes(rest, true, acc)
+ end
+
+ defp acc_changes([?- | rest], _, acc) do
+ acc_changes(rest, false, acc)
+ end
+
+ defp acc_changes([0x20 | _], _, acc), do: acc
+
+ defp acc_changes([char | rest], action, acc) do
+ acc = if action do
+ [char | acc]
+ else
+ List.delete(acc, char)
+ end
+ acc_changes(rest, action, acc)
+ end
+
+ defp acc_changes([], _, acc), do: acc
+
+end
diff --git a/lib/irc/parser/numeric.ex b/lib/irc/parser/numeric.ex
new file mode 100644
index 0000000..3a6e27f
--- /dev/null
+++ b/lib/irc/parser/numeric.ex
@@ -0,0 +1,72 @@
+defmodule Irc.Parser.Numeric do
+ require Logger
+
+ @moduledoc """
+ Helpers for using IRC Numerics.
+
+ * Use macros to translate from `rpl_MOTDSTART` (a macro is generated for every numeric name).
+ * Convert a numeric to its name (name_numeric())
+ * Convert a name to its numeric (to_numeric())
+ * Check if a numeric is an error (error_numeric?())
+
+ """
+
+ defmacro __using__(_) do
+ quote do
+ require Irc.Numeric
+ import Irc.Numeric
+ end
+ end
+
+ @numerics File.read!(Path.join(:code.priv_dir(:irc), "numerics.txt"))
+ |> String.split("\n")
+ |> Enum.map(fn(line) ->
+ case String.split(String.trim(line), " ", parts: 2) do
+ ["#"<>_, _] -> nil
+ ["#"<>_] -> nil
+ [numeric, full_name = "ERR_" <> name] -> {numeric, name, true, full_name}
+ [numeric, full_name = "RPL_" <> name] -> {numeric, name, false, full_name}
+ line ->
+ Logger.debug "Invalid line in numeric.txt: #{inspect line}"
+ end
+ end)
+ |> Enum.filter(fn(n) -> n end)
+
+ for {num, name, error, full_name} <- @numerics do
+ prefix = if error, do: "err_", else: "rpl_"
+ fname = "#{prefix}#{name}" |> String.to_atom()
+
+ defmacro unquote(fname)() do
+ unquote(num)
+ end
+
+ def error_numeric?(unquote(num)) do
+ unquote(error)
+ end
+ def error_numeric?(unquote(name)) do
+ unquote(error)
+ end
+ def error_numeric?(unquote(full_name)) do
+ unquote(error)
+ end
+
+ def name_numeric(unquote(num)) do
+ unquote(full_name)
+ end
+
+ def to_numeric(unquote(num)) do
+ unquote(num)
+ end
+ def to_numeric(unquote(name)) do
+ unquote(num)
+ end
+ def to_numeric(unquote(fname)) do
+ unquote(num)
+ end
+ def to_numeric(unquote(full_name)) do
+ unquote(num)
+ end
+
+ end
+
+end