Sunday, December 19, 2010

Poor Man's Erlang IRC Client

I need comments on this.


-module(conn).
-import(lists, [concat/1, keysearch/3]).
-export([start/2, stop/0]).
-export([init/1, terminate/2, handle_event/2, handle_call/2, handle_info/2]).
-export([repl/0, conn/3]).
-behaviour(gen_event).

conn(ClientPid, Host, PortNo) ->
Result = gen_tcp:connect(Host, PortNo, [binary,
{active, true},
{packet, line},
{keepalive, true},
{nodelay, true}]),
case Result of
{ok, Socket} ->
Pid = spawn(fun() -> loop(ClientPid, Socket) end),
gen_tcp:controlling_process(Socket, Pid),
Pid;
{error, Reason} ->
io:format("Error, reason: ~p~n", [Reason]),
error
end.

loop(ClientPid, Socket) ->
receive
{tcp, _Socket, Data} ->
gen_event:notify(?MODULE, {recv, Data}),
loop(ClientPid, Socket);
{tcp_closed, _Socket} ->
io:format("[SOCKET] Connection Closed.~n"),
ok;
{tcp_error, _Socket, Reason} ->
io:format("[SOCKET] Error, reason: ~p~n", [Reason]),
ok;
{send, ClientPid, Data} ->
gen_tcp:send(Socket, Data),
loop(ClientPid, Socket);
{close, ClientPid} ->
gen_tcp:close(Socket),
io:format("[SOCKET] Closing connection...~n"),
ok;
Msg ->
io:format("[SOCKET] Recv: ~p~n", [Msg])
end.

%
% exported functions
%
start(Host, PortNo) ->
gen_event:start({local, ?MODULE}),
gen_event:add_handler(?MODULE, ?MODULE, [{host, Host}, {port, PortNo}]).

stop() ->
gen_event:stop(?MODULE).


%
% helper
%
irc_msg(Args) ->
lists:concat(Args) ++ "\r\n".

%
% gen_event behaviour
%
init(Args) ->
io:format("*** initiating gen_event ***~n"),
{value, {host, Host}} = keysearch(host, 1, Args),
{value, {port, PortNo}} = keysearch(port, 1, Args),
Pid = conn(self(), Host, PortNo),
State = {disconnected, Pid},
{ok, State}.

handle_event({recv, Line}, State) ->
io:format("~s", [binary_to_list(Line)]),
{ok, State};
handle_event({send, Line}, {_, Pid}=State) ->
Pid ! {send, self(), Line ++ "\r\n"},
{ok, State};
handle_event({pass, Password}, {disconnected, Pid}) ->
Pid ! {send, self(), irc_msg(["PASS ", Password])},
{ok, {nick, Pid}};
handle_event({nick, Nickname}, {nick, Pid}) ->
Pid ! {send, self(), irc_msg(["NICK ", Nickname])},
{ok, {user, Pid}};
handle_event({user, Nickname, Realname}, {user, Pid}) ->
Pid ! {send, self(), irc_msg(["USER ", Nickname, " 0 * :", Realname])},
{ok, {connected, Pid}};
handle_event({privmsg, To, Msg}, {connected, Pid}) ->
Pid ! {send, self(), irc_msg(["PRIVMSG ", To, " :", Msg])},
{ok, {connected, Pid}};
handle_event({join, Channel}, {connected, Pid}) ->
Pid ! {send, self(), irc_msg(["JOIN ", Channel])},
{ok, {connected, Pid}};
handle_event(quit, {connected, Pid}) ->
Pid ! {send, self(), irc_msg(["QUIT"])},
{ok, {disconnected, Pid}};
handle_event(Event, State) ->
io:format("*** Error Unknow Event: ~p ***~n", [Event]),
{ok, State}.

handle_call(_Request, State) ->
io:format("~p~n", [State]),
{ok, State, State}.

handle_info(_Info, State) ->
io:format("~p, my pid: ~n", [State]),
{ok, State}.

terminate({stop, Reason}, {_, Pid}) ->
io:format("Stoping: ~p~n", [Reason]),
Pid ! {close, self()},
ok;
terminate(stop, {_, Pid}) ->
io:format("Stoping.. ~n"),
Pid ! {close, self()},
ok.

%
%
%

repl() ->
io:format("*** Registering ***~n"),
gen_event:notify(?MODULE, {pass, "some string"}),
gen_event:notify(?MODULE, {nick, "mynickisdick"}),
gen_event:notify(?MODULE, {user, "mynickisdick", "My Real Name"}),
repl_loop("me:").

repl_loop(Prompt) ->
case io:get_line(Prompt) of
eof ->
ok;
{error, Reason} ->
io:format("error: ~w~n", [Reason]);
Data ->
CleanedData = string:strip(Data, both, $\n),
Op = getops(CleanedData),
case Op of
{join, _Channel} ->
gen_event:notify(?MODULE, Op),
repl_loop(Prompt);
{privmsg, _To, _Msg} ->
gen_event:notify(?MODULE, Op),
repl_loop(Prompt);
{quit} ->
gen_event:notify(?MODULE, quit);
_ ->
io:format("Error, Op was: ~p~n", [Op]),
repl_loop(Prompt)
end
end.

getops(String) ->
AvailOps = [{"/join", join, 1}, {"/quit", quit, 0}, {"/msg", privmsg, 2}],
Msg = string:tokens(String, "\t\n "),
Key = lists:nth(1, Msg),
case keysearch(Key, 1, AvailOps) of
{value, {"/msg", privmsg, 2}} ->
{privmsg, lists:nth(2, Msg),
string:join(lists:nthtail(2, Msg), " ")};
{value, {Key, Op, Len}} ->
list_to_tuple([Op] ++ lists:sublist(Msg, 2, Len));
_ ->
Msg
end.


How to use it?


$ erl
Erlang R13B01 (erts-5.7.2) [source] [smp:2:2] [rq:2] [async-threads:0] [kernel-poll:false]

Eshell V5.7.2 (abort with ^G)
1> c(conn).
./conn.erl:6: Warning: undefined callback function code_change/3 (behaviour 'gen_event')
{ok,conn}
2> conn:start("irc.freenode.org", 6667).
Initiating gen_event...
ok
3> conn:repl().
...


You got "/join #channel", "/msg #channel text to say" or "/msg nick test to say" and of course "/quit".