summaryrefslogtreecommitdiff
path: root/lib/irc/base_client.ex
blob: 8da45be6636ea053fffaff2af2273c92168006a2 (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
defmodule Irc.BaseClient do
  @behaviour :gen_statem
  require Logger
  alias Irc.Parser.Line
  alias Irc.Connection
  require Irc.Parser.Numeric
  import Irc.Parser.Numeric

  @moduledoc """
  Extensible fully-featured IRC client.

  ## Behaviour

  The BaseClient requires a callback module. You can check out `Irc.Client`.

  ## Capabs

  IRCv3 Capabs are handled by separates modules. 
  """

  @internal {:next_event, :internal, nil}

  defstruct [:conn, :conn_mon, :module, :modstate, :args, :nick, :modes, :info, :error]

  def callback_mode, do: [:state_functions, :state_enter]

  @type event ::event_connected | event_nick | event_modes | event_line | event_down

  @type event_connected :: {:connected, Connection.Info.t}
  @type event_nick :: {:nick, prev_nick :: String.t, new_nick :: String.t}
  @type event_modes :: {:modes, changes :: String.t, prev :: list, modes :: list}
  @type event_line :: {:line, line :: Irc.Parser.Line.t}
  @type event_down :: {:down, Connection.error, delay :: integer}

  @type modstate :: any
  @type handle_return :: {:ok, modstate()}
  @type handle_call_return :: {:reply, any(), modstate()} | {:noreply,  modstate()}

  @callback init(Keyword.t) :: {:ok, modstate} | any
  @callback handle_event(event(), modstate()) :: handle_return()
  @callback handle_info(any(), modstate()) :: handle_return()
  @callback handle_cast(any(), modstate()) :: handle_return()
  @callback handle_call(any(), reference(), modstate()) :: handle_call_return()
  @callback stop(Connection.error, modstate) :: any()

  @type start_opt :: Connection.start_opt
                  | {:module, module()}
                  | {:conn_statem_opts, [:gen_statem.start_opt]}
  @type start_ret :: {:ok, pid} | {:error, Connection.error} | :gen_statem.start_ret

  @spec start(Connection.nick, Connection.host, [start_opt], [:gen_statem.start_opt]) :: start_ret
  def start(nick, host, opts \\ [], start_opts \\ []) do
    :gen_statem.start(__MODULE__, [nick, host, prepare_args(opts)], start_opts)
  end

  @spec start_link(Connection.nick, Connection.host, [start_opt], [:gen_statem.start_opt]) :: start_ret
  def start_link(nick, host, opts \\ [], start_opts \\ []) do
    :gen_statem.start_link(__MODULE__, [nick, host, prepare_args(opts)], start_opts)
  end


  @doc false
  def init([args]) do
    capabs = ["account-notify", "away-notify"]
    args = args
           |> Keyword.put(:capabs, capabs)
    with \
         {:ok, conn} <- Connection.start(Keyword.get(args, :nick), Keyword.get(args, :host), args, Keyword.get(args, :conn_statem_opts, [])),
         conn_mon <- Process.monitor(conn),
         {:ok, module} <- Keyword.fetch(args, :module),
         {:ok, modstate} <- module.init(args)
    do
      data = %__MODULE__{
        conn: conn, conn_mon: conn_mon,
        module: module, modstate: modstate,
        args: args
      }
      {:ok, :disconnected, data}
    else
      error -> {:stop, error}
    end
  end

  def disconnected(:enter, :connected, data) do
    Logger.debug "#{inspect data} disconnected: #{inspect data.error}"
    :keep_state_and_data
  end

  def disconnected(:info, msg = {:irc_conn_up, _, info}, data) do
    data = run_handler_event({:connected, info}, data)
    {:next_state, :connected, %__MODULE__{data | nick: info.nick, info: info, modes: info.modes, error: nil}}
  end

  def disconnected(type, content, data) do
    handle_common(:disconnected, type, content, data)
  end

  def connected(:enter, _, data) do
    Logger.debug "#{inspect data} UP"
    :keep_state_and_data
  end

  def connected(:info, msg = {:irc_conn_nick, _, prev_nick, new_nick}, data) do
    data = run_handler_event({:nick, prev_nick, new_nick}, data)
    {:keep_state, %__MODULE__{data | nick: new_nick}}
  end

  def connected(:info, msg = {:irc_conn_modes, _, changes, prev, modes}, data) do
    data = run_handler_event({:modes, changes, prev, modes}, data)
    {:keep_state, %__MODULE__{data | modes: modes}}
  end

  def connected(:info, {:irc_conn_down, _, reason, delay}, data) do
    data = run_handler_event({:down, reason, delay}, data)
    {:next_state, :disconnected, %__MODULE__{data | error: reason}}
  end

  # TODO: REMOVE?
  def connected(:info, {:irc_conn_line, _, line}, data) do
    data = run_handler_event({:line, line}, data)
    :keep_state_and_data
  end

  def connected(type, content, data) do
    handle_common(:connected, type, content, data)
  end

  # TODO: Callback stop?
  def error(:internal, reason, data) do
    Logger.error "#{inspect data}: #{inspect reason}"
    data.module.stop(reason, data.modstate)
    {:stop, :normal}
  end

  def handle_common(_, :info, {:irc_conn_error, _, reason}, data) do
    {:next_state, :error, data, {:next_event, :internal, reason}}
  end

  def handle_common(_, :enter, _, _) do
    :keep_state_and_data
  end

  def terminate(reason, state, data) do
    Logger.error("#{inspect(data)} terminating in #{inspect(state)}: #{inspect reason}")
  end

  def code_change(_old_vsn, old_state, old_data, _extra) do
    {:ok, old_state, old_data}
  end

  defp prepare_args(args) do
    args
  end

  defp run_handler_event(event, data) do
    case data.module.handle_event(event, data.modstate) do
      {:ok, modstate} -> %__MODULE__{data | modstate: modstate}
      :ok -> data
    end
  end

  defimpl Inspect, for: __MODULE__ do
    import Inspect.Algebra
    def inspect(struct, _opts) do
      concat(["#Irc.Client<", inspect(self()), ">"])
    end
  end


end