summaryrefslogtreecommitdiff
path: root/lib/exirc/utils.ex
blob: 2573cc35e654f8c1396371349ceb8b716b6975d2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
defmodule ExIrc.Utils do

  ######################
  # IRC Message Parsing
  ######################

  @doc """
  Parse an IRC message

  Example:

      data    = ':irc.example.org 005 nick NETWORK=Freenode PREFIX=(ov)@+ CHANTYPES=#&'
      message = ExIrc.Utils.parse data
      assert "irc.example.org" = message.server
  """
  @spec parse(raw_data :: char_list) :: IrcMessage.t
  def parse(raw_data) do
    data = :string.substr(raw_data, 1, length(raw_data))
    case data do
      [?:|_] ->
          [[?:|from]|rest] = :string.tokens(data, ' ')
          get_cmd(rest, parse_from(from, %IrcMessage{ctcp: false}))
      data ->
          get_cmd(:string.tokens(data, ' '), %IrcMessage{ctcp: false})
    end
  end

  @prefix_pattern ~r/^(?<nick>[^!\s]+)(?:!(?:(?<user>[^@\s]+)@)?(?:(?<host>[\S]+)))?$/
  defp parse_from(from, msg) do
    from_str = IO.iodata_to_binary(from)
    parts    = Regex.run(@prefix_pattern, from_str, capture: :all_but_first)
    case parts do
      [nick, user, host] ->
        %{msg | nick: nick, user: user, host: host}
      [nick, host] ->
        %{msg | nick: nick, host: host}
      [nick] ->
        if String.contains?(nick, ".") do
          %{msg | server: nick}
        else
          %{msg | nick: nick}
        end
    end
  end

  # Parse command from message
  defp get_cmd([cmd, arg1, [?:, 1 | ctcp_trail] | restargs], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do
    get_cmd([cmd, arg1, [1 | ctcp_trail] | restargs], msg)
  end

  defp get_cmd([cmd, target, [1 | ctcp_cmd] | cmd_args], msg) when cmd == 'PRIVMSG' or cmd == 'NOTICE' do
    args = cmd_args
      |> Enum.map(&Enum.take_while(&1, fn c -> c != 0o001 end))
      |> Enum.map(&List.to_string/1)
    case args do
      args when args != [] ->
        %{msg |
          cmd:  to_string(ctcp_cmd),
          args: [to_string(target), args |> Enum.join(" ")],
          ctcp: true
        }
      _ ->
        %{msg | cmd: to_string(cmd), ctcp: :invalid}
    end
  end

  defp get_cmd([cmd | rest], msg) do
    get_args(rest, %{msg | cmd: to_string(cmd)})
  end


  # Parse command args from message
  defp get_args([], msg) do
    args = msg.args
    |> Enum.reverse
    |> Enum.filter(fn arg -> arg != [] end)
    |> Enum.map(&trim_crlf/1)
    |> Enum.map(&:binary.list_to_bin/1)
    |> Enum.map(&:unicode.characters_to_binary/1)
    post_process(%{msg | args: args})
  end

  defp get_args([[?: | first_arg] | rest], msg) do
    args = (for arg <- [first_arg | rest], do: ' ' ++ trim_crlf(arg)) |> List.flatten
    case args do
      [_] ->
          get_args([], %{msg | args: msg.args})
      [_ | full_trail] ->
          get_args([], %{msg | args: [full_trail | msg.args]})
    end
  end

  defp get_args([arg | rest], msg) do
    get_args(rest, %{msg | args: [arg | msg.args]})
  end

  # This function allows us to handle special case messages which are not RFC
  # compliant, before passing it to the client.
  defp post_process(%IrcMessage{cmd: "332", args: [nick, channel]} = msg) do
    # Handle malformed RPL_TOPIC messages which contain no topic
    %{msg | :cmd => "331", :args => [channel, "No topic is set"], :nick => nick}
  end
  defp post_process(msg), do: msg

  ############################
  # Parse RPL_ISUPPORT (005)
  ############################

  @doc """
  Parse RPL_ISUPPORT message.

  If an empty list is provided, do nothing, otherwise parse CHANTYPES,
  NETWORK, and PREFIX parameters for relevant data.
  """
  @spec isup(parameters :: list(binary), state :: ExIrc.Client.ClientState.t) :: ExIrc.Client.ClientState.t
  def isup([], state), do: state
  def isup([param | rest], state) do
    try do
      isup(rest, isup_param(param, state))
    rescue
      _ -> isup(rest, state)
    end
  end

  defp isup_param("CHANTYPES=" <> channel_prefixes, state) do
    prefixes = channel_prefixes |> String.split("", trim: true)
    %{state | channel_prefixes: prefixes}
  end
  defp isup_param("NETWORK=" <> network, state) do
    %{state | network: network}
  end
  defp isup_param("PREFIX=" <> user_prefixes, state) do
    prefixes = Regex.run(~r/\((.*)\)(.*)/, user_prefixes, capture: :all_but_first)
               |> Enum.map(&String.to_char_list/1)
               |> List.zip
    %{state | user_prefixes: prefixes}
  end
  defp isup_param(_, state) do
    state
  end

  ###################
  # Helper Functions
  ###################

  @days_of_week   ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
  @months_of_year ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
  @doc """
  Get CTCP formatted time from a tuple representing the current calendar time:

  Example:

      iex> local_time = {{2013,12,6},{14,5,0}}
      {{2013,12,6},{14,5,0}}
      iex> ExIrc.Utils.ctcp_time local_time
      "Fri Dec 06 14:05:00 2013"
  """
  @spec ctcp_time(datetime :: {{integer, integer, integer}, {integer, integer, integer}}) :: binary
  def ctcp_time({{y, m, d}, {h, n, s}} = _datetime) do
    [:lists.nth(:calendar.day_of_the_week(y,m,d), @days_of_week),
     ' ',
     :lists.nth(m, @months_of_year),
     ' ',
     :io_lib.format("~2..0s", [Integer.to_char_list(d)]),
     ' ',
     :io_lib.format("~2..0s", [Integer.to_char_list(h)]),
     ':',
     :io_lib.format("~2..0s", [Integer.to_char_list(n)]),
     ':',
     :io_lib.format("~2..0s", [Integer.to_char_list(s)]),
     ' ',
     Integer.to_char_list(y)] |> List.flatten |> List.to_string
  end

  defp trim_crlf(charlist) do
    case Enum.reverse(charlist) do
      [?\n, ?\r | text] -> Enum.reverse(text)
      _ -> charlist
    end
  end

end