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".

5 comments:

  1. I saw it came up in reddit and I wanted to mention that this doesn't handle pings automatically, so if you connect to freenode and freenode pings the client, you should pong back else freenode will disconnect you. Anyway maybe some future version...

    ReplyDelete
  2. If you want something easily scriptable then I suggest you try this out: https://github.com/mazenharake/eirc

    I made it to be easy to create a simple bot. See this file which gives you a simple example of how to create a bot: https://github.com/mazenharake/eirc/blob/master/src/eirc_example_bot.erl

    If you have any comments or updates it is more than welcome :)

    ReplyDelete
  3. Did something like that, except also with gtk :) Erlang is nice for that.

    ReplyDelete
  4. You might want to post to erlang-questions mailing list for comments.

    ReplyDelete
  5. Thank you all your comments! Might post it in erlang-questions!

    ReplyDelete