defmodule Irc.Parser.Line do @moduledoc """ IRC line parser/encoder """ alias Irc.Line require Logger @spec parse(string) :: Line.t() @doc "Parse a server-to-client line." def parse(line, owner_module \\ nil, owner \\ nil) def parse(line, context, _) when is_map(context) do module = Irc.Context.module(context) pid = Irc.Context.pid(context) parse(line) |> set_owner(module, pid) end def parse(line, owner_module, owner) do {tags, rest} = parse_tags(line) {source, rest} = parse_source(rest, owner_module, owner) {command, rest} = parse_cmd(rest) args = parse_args(rest) private = %{ owner: owner, owner_module: owner_module, at: DateTime.utc_now() } %Line{__private__: private, tags: tags, source: source, command: command, args: args} end @spec set_owner(Line.t(), module(), pid()) :: Line.t() def set_owner(line = %Line{__private__: private = %{owner: nil, owner_module: nil}}, module, owner) when is_atom(module) and is_pid(owner) do private = Map.put(private, :owner_module, module) |> Map.put(:owner, owner) %Line{line | __private__: private} end def set_owner(line = %Line{__private__: private = %{owner: opid, owner_module: omod}}, module, owner) when is_atom(module) and is_pid(owner) do Logger.debug("Parser.Line: tried to re-own an already owned line #{inspect {{omod,opid},{module,owner}}}") line end @spec encode(Line.t()) :: String.t @doc "Encode a line" def encode(line = %Line{}, conn \\ %Irc.Connection{}) do [line.source, line.tags, line.command, line.args] src = if line.source, do: ":"<>to_string(line.source) tags = Enum.reduce(line.tags, [], fn({k,v}, acc) -> ["#{k}=#{v}" | acc] end) |> Enum.join(";") tags = if Irc.Context.capab?(conn, "message-tags") && tags != "", do: "@"<>tags [tags, src, line.command, encode_args(line.args)] |> Enum.filter(fn(x) -> x end) |> Enum.join(" ") end # ARGS defp parse_args(input), do: parse_args(input, []) defp 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. defp parse_arg([?: | rest], acc) when length(acc) == 0 do final = parse_final_arg(rest, []) {:finished, [final]} end defp parse_arg([0x20, ?: | rest], acc) do final = parse_final_arg(rest, []) {:finished, [final, acc]} end defp parse_arg([0x20 | rest], acc) do {:continue, acc, rest} end defp parse_arg([char | rest], acc) do parse_arg(rest, [char | acc]) end defp parse_arg([], acc) do {:finished, [acc]} end defp parse_final_arg([char | rest], acc) do parse_final_arg(rest, [char | acc]) end defp parse_final_arg([], acc) do acc end defp format_args(list) when is_list(list) do list |> Enum.map(fn(arg) -> String.trim(acc_to_str(arg)) end) |> Enum.reverse() end # COMMAND defp parse_cmd(input), do: parse_cmd(input, []) defp parse_cmd([0x20 | rest], acc) do {acc_to_str(acc), rest} end defp parse_cmd([char | rest], acc) do parse_cmd(rest, [char | acc]) end # SOURCE defp parse_source([?: | input], m, p), do: parse_source(input, [], m, p) defp parse_source(input, _, _), do: {nil, input} defp parse_source([0x20 | rest], acc, m, p) do string = acc_to_str(acc) result = case Irc.Mask.parse(string, m, p) do {:error, _} -> string {:ok, mask} -> mask end {result, rest} end defp parse_source([char | rest], acc, m, p), do: parse_source(rest, [char | acc], m, p) # TAGS defp parse_tags([?@ | input]), do: parse_tags(input, []) defp parse_tags(input), do: {%{}, input} defp 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 defp parse_tag(input) do parse_tag({[], nil}, input) end defp parse_tag({acc, nil}, [?= | rest]) do parse_tag({acc, []}, rest) end defp parse_tag({acc, nil}, [?; | rest]) do {:continue, {acc, true}, rest} end defp parse_tag({acc, nil}, [char | rest]) do parse_tag({[char | acc], nil}, rest) end defp parse_tag({key, acc}, [?; | rest]) do {:continue, {key, acc}, rest} end defp parse_tag({key, acc}, [0x20 | rest]) do {:finished, {key, acc}, rest} end defp parse_tag({key, acc}, [char | rest]) do parse_tag({key, [char | acc]}, rest) end defp format_tags(list) do list |> Enum.map(fn({k,v}) -> {acc_to_str(k), acc_to_str(v)} end) |> Enum.into(Map.new) end defp acc_to_str([]), do: nil defp acc_to_str(list) when is_list(list) do list |> Enum.reverse() |> to_string() end defp acc_to_str(other), do: other defp encode_args(args) do encode_args(args, []) |> Enum.reverse() |> Enum.join(" ") end defp encode_args([arg], acc) do [":"<>arg | acc] end defp encode_args([arg | rest], acc) do encode_args(rest, [arg | acc]) end defp encode_args([], acc), do: acc end