Prepared haircut_bot to gracefully restart on network problems.
[haircut.git] / src / haircut_bot.erl
1 %%%---------------------------------------------------------------------------
2 %%% @doc
3 %%%   IRC bot body.
4 %%% @end
5 %%%---------------------------------------------------------------------------
6
7 -module(haircut_bot).
8
9 -behaviour(gen_ealirc).
10
11 %%% public API
12 -export([start_link/0, start_link/6]).
13
14 %%% gen_ealirc callbacks
15 -export([init/1, terminate/2]).
16 -export([handle_call/3, handle_cast/2, handle_info/2, handle_message/4]).
17 -export([code_change/3]).
18
19 %%%---------------------------------------------------------------------------
20
21 -record(state, {nick}).
22
23 %%%---------------------------------------------------------------------------
24 %%% public API {{{
25
26 %% @doc Start haircut bot process, taking configuration from application
27 %%   environment.
28
29 -spec start_link() ->
30   {ok, pid()} | {error, term()}.
31
32 start_link() ->
33   {ok, Server} = application:get_env(server),
34   {ok, Port} = application:get_env(port),
35   {ok, Nick} = application:get_env(nick),
36   {ok, {User, FullName}} = application:get_env(user),
37   {ok, Channels} = application:get_env(channels),
38   start_link(Server, Port, Nick, User, FullName, Channels).
39
40 %% @doc Start haircut bot process.
41 %%
42 %%   When `User' is set to `env', the value of `os:getenv("USER")' is used
43 %%   here.
44 %%
45 %%   When `Nick' is set to `user', the value of `User' (after reading `$USER')
46 %%   is used as a nick.
47
48 -spec start_link(inet:hostname() | inet:ip_address(), integer(),
49                  user | string(), env | string(), string(),
50                  [ealirc:channel()]) ->
51   {ok, pid()} | {error, term()}.
52
53 start_link(Server, Port, Nick, env = _User, FullName, Channels) ->
54   EnvUser = os:getenv("USER"),
55   start_link(Server, Port, Nick, EnvUser, FullName, Channels);
56
57 start_link(Server, Port, user = _Nick, User, FullName, Channels)
58 when is_list(User) ->
59   start_link(Server, Port, User, User, FullName, Channels);
60
61 start_link(Server, Port, Nick, User, FullName, Channels) ->
62   Args = [Nick, User, FullName, Channels],
63   RegName = {local, ?MODULE},
64   case gen_ealirc:connect_link(Server, Port, RegName, ?MODULE, Args, []) of
65     % connected successfully
66     {ok, Pid} -> {ok, Pid};
67     % network error, to be restarted some other time
68     % TODO: log this event
69     {error, econnaborted} -> ignore();
70     {error, econnrefused} -> ignore();
71     {error, econnreset}   -> ignore();
72     {error, eintr}        -> ignore();
73     {error, enetdown}     -> ignore();
74     {error, enetunreach}  -> ignore();
75     {error, epipe}        -> ignore();
76     {error, erefused}     -> ignore();
77     {error, etimedout}    -> ignore();
78     {error, nxdomain}     -> ignore();
79     % non-network error, not a subject to restart
80     {error, Reason} -> {error, Reason}
81   end.
82
83 ignore() ->
84   Message = "network problem, leaving restart to restarter",
85   error_logger:warning_report(haircut, Message),
86   ignore.
87
88 %%% }}}
89 %%%---------------------------------------------------------------------------
90 %%% gen_ealirc callbacks
91
92 %%----------------------------------------------------------
93 %% initialization and cleanup {{{
94
95 %% @private
96 %% @doc Initialize {@link gen_ealirc} state.
97
98 init([Nick, User, FullName, Channels] = _Args) ->
99   gen_ealirc:nick(self(), Nick),
100   gen_ealirc:user(self(), User, none, FullName),
101   gen_ealirc:join(self(), Channels),
102   % TODO: `Nick' could be already in use
103   {ok, #state{nick = Nick}}.
104
105 %% @private
106 %% @doc Clean up {@link gen_ealirc} state.
107
108 terminate(_Reason, _State) ->
109   ok.
110
111 %% }}}
112 %%----------------------------------------------------------
113 %% communication {{{
114
115 %% @private
116 %% @doc Handle {@link gen_server:call/2}.
117
118 handle_call(_Request, _From, State) ->
119   {reply, {error, unknown}, State}.
120
121 %% @private
122 %% @doc Handle {@link gen_server:cast/2}.
123
124 handle_cast(_Request, State) ->
125   {noreply, State}.
126
127 %% @private
128 %% @doc Handle incoming messages.
129
130 handle_info(_Msg, State) ->
131   {noreply, State}.
132
133 %% @private
134 %% @doc Handle incoming IRC messages.
135
136 handle_message(Prefix, "PING" = _Command, Args, State = #state{nick = Nick}) ->
137   {ok, PongCmd} = ealirc_proto:pong(Nick),
138   gen_ealirc:quote(self(), PongCmd),
139   case {Prefix,Args} of
140     {none,[From | _]} ->
141       io:fwrite("<~s> PING from ~s~n", [Nick, From]);
142     {_,[From | _]} ->
143       io:fwrite("<~s> PING from ~s (~p)~n", [Nick, From, Prefix])
144   end,
145   {noreply, State};
146
147 handle_message({user, Nick, _, _} = _Prefix,
148                "NICK"             = _Command,
149                [NewNick]          = _Args,
150                State              = #state{nick = Nick}) ->
151   io:fwrite("<~s> Changing nickname from ~s to ~s~n", [Nick, Nick, NewNick]),
152   {noreply, State#state{nick = NewNick}};
153
154 handle_message({user, Nick, _, _} = _Prefix,
155                "PRIVMSG"          = _Command,
156                [MsgTarget, "!" ++ Request] = _Args,
157                State) ->
158   [ReqCmd | _] = string:tokens(Request, " "),
159   Reply = "sorry, command " ++ ReqCmd ++ " is not implemented yet",
160   case MsgTarget of
161     "#" ++ _ -> % other channel indicators: "+", "!", "&"
162       gen_ealirc:privmsg(self(), MsgTarget, Nick ++ ": " ++ Reply);
163     _ ->
164       gen_ealirc:privmsg(self(), Nick, Reply)
165   end,
166   {noreply, State};
167
168 handle_message({user, Nick, _, _} = _Prefix,
169                "MODE"             = _Command,
170                [Channel, "+o", SelfNick] = _Args,
171                State              = #state{nick = SelfNick}) ->
172   gen_ealirc:privmsg(self(), Channel, [Nick ++ ": thank you"]),
173   {noreply, State};
174
175 handle_message({user, Nick, _, _} = _Prefix,
176                "MODE"             = _Command,
177                [Channel, "-o", SelfNick] = _Args,
178                State              = #state{nick = SelfNick}) ->
179   gen_ealirc:privmsg(self(), Channel, [Nick ++ ": you bastard!"]),
180   {noreply, State};
181
182 handle_message(Prefix, Command, Args, State = #state{nick = Nick}) ->
183   io:fwrite("<~s> [~p] ~p ~1024p~n", [Nick, Prefix, Command, Args]),
184   {noreply, State}.
185
186 %% }}}
187 %%----------------------------------------------------------
188 %% code change {{{
189
190 %% @private
191 %% @doc Handle code change.
192
193 code_change(_OldVsn, State, _Extra) ->
194   {ok, State}.
195
196 %% }}}
197 %%----------------------------------------------------------
198
199 %%%---------------------------------------------------------------------------
200 %%% vim:ft=erlang:foldmethod=marker