From b704efd553d5d32d03d61854711465771706b95a Mon Sep 17 00:00:00 2001 From: Alexey Shchepin Date: Tue, 18 Feb 2003 20:33:10 +0000 Subject: [PATCH] * src/mod_irc/: Added support for private chats, nicks changes and error handling SVN Revision: 78 --- ChangeLog | 5 + src/mod_irc/mod_irc.erl | 28 ++- src/mod_irc/mod_irc_connection.erl | 280 +++++++++++++++++++++++------ 3 files changed, 253 insertions(+), 60 deletions(-) diff --git a/ChangeLog b/ChangeLog index 109db7eb6..3929108f2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2003-02-18 Alexey Shchepin + + * src/mod_irc/: Added support for private chats, nicks changes and + error handling + 2003-02-17 Alexey Shchepin * src/mod_irc/: Still not completed... diff --git a/src/mod_irc/mod_irc.erl b/src/mod_irc/mod_irc.erl index 3d3085235..a3c4d0225 100644 --- a/src/mod_irc/mod_irc.erl +++ b/src/mod_irc/mod_irc.erl @@ -91,21 +91,39 @@ do_route(Host, From, To, Packet) -> irc_connection, #irc_connection{userserver = {From, Server}, pid = Pid}), - mod_irc_connection:route( + mod_irc_connection:route_chan( Pid, Channel, Resource, Packet), ok; [R] -> Pid = R#irc_connection.pid, io:format("send to process ~p~n", [Pid]), - mod_irc_connection:route( + mod_irc_connection:route_chan( Pid, Channel, Resource, Packet), ok end; _ -> - Err = jlib:make_error_reply( - Packet, "406", "Not Acceptable"), - ejabberd_router:route(To, From, Err) + case string:tokens(ChanServ, "!") of + [[_ | _] = Nick, [_ | _] = Server] -> + case ets:lookup(irc_connection, {From, Server}) of + [] -> + Err = jlib:make_error_reply( + Packet, + "503", "Service Unavailable"), + ejabberd_router:route(To, From, Err); + [R] -> + Pid = R#irc_connection.pid, + io:format("send to process ~p~n", + [Pid]), + mod_irc_connection:route_nick( + Pid, Nick, Packet), + ok + end; + _ -> + Err = jlib:make_error_reply( + Packet, "406", "Not Acceptable"), + ejabberd_router:route(To, From, Err) + end end end. diff --git a/src/mod_irc/mod_irc_connection.erl b/src/mod_irc/mod_irc_connection.erl index 05a79165d..391d2ccde 100644 --- a/src/mod_irc/mod_irc_connection.erl +++ b/src/mod_irc/mod_irc_connection.erl @@ -13,7 +13,7 @@ -behaviour(gen_fsm). %% External exports --export([start/3, receiver/2, route/4]). +-export([start/3, receiver/2, route_chan/4, route_nick/3]). %% gen_fsm callbacks -export([init/1, @@ -33,7 +33,7 @@ -record(state, {socket, receiver, queue, user, myname, server, nick, - channels = ?SETS:new(), + channels = dict:new(), inbuf = "", outbuf = ""}). -define(IRC_ENCODING, "koi8-r"). @@ -110,17 +110,6 @@ open_socket(init, StateData) -> end. wait_for_registration(closed, StateData) -> - bounce_messages("Server Connect Failed"), - lists:foreach( - fun(Chan) -> - ejabberd_router:route( - {lists:concat([Chan, "%", StateData#state.server]), - StateData#state.myname, StateData#state.nick}, - StateData#state.user, - {xmlelement, "presence", [{"type", "error"}], - [{xmlelement, "error", [{"code", "502"}], - [{xmlcdata, "Server Connect Failed"}]}]}) - end, ?SETS:to_list(StateData#state.channels)), {stop, normal, StateData}. stream_established({xmlstreamend, Name}, StateData) -> @@ -187,31 +176,41 @@ code_change(OldVsn, StateName, StateData, Extra) -> %% {next_state, NextStateName, NextStateData, Timeout} | %% {stop, Reason, NewStateData} %%---------------------------------------------------------------------- -handle_info({route, Channel, Resource, {xmlelement, "presence", Attrs, Els}}, +handle_info({route_chan, Channel, Resource, + {xmlelement, "presence", Attrs, Els}}, StateName, StateData) -> NewStateData = case xml:get_attr_s("type", Attrs) of "unavailable" -> S1 = ?SEND(io_lib:format("PART #~s\r\n", [Channel])), S1#state{channels = - remove_element(Channel, S1#state.channels)}; + dict:erase(Channel, S1#state.channels)}; "subscribe" -> StateData; "subscribed" -> StateData; "unsubscribe" -> StateData; "unsubscribed" -> StateData; _ -> - S1 = ?SEND(io_lib:format("JOIN #~s\r\n", [Channel])), - S1#state{channels = - ?SETS:add_element(Channel, S1#state.channels)} + S1 = ?SEND(io_lib:format("NICK ~s\r\n" + "JOIN #~s\r\n", + [Resource, Channel])), + case dict:is_key(Channel, S1#state.channels) of + true -> + S1#state{nick = Resource}; + _ -> + S1#state{nick = Resource, + channels = + dict:store(Channel, ?SETS:new(), + S1#state.channels)} + end end, - case ?SETS:is_empty(NewStateData#state.channels) of - true -> + case length(dict:fetch_keys(NewStateData#state.channels)) of + 0 -> {stop, normal, NewStateData}; _ -> {next_state, StateName, NewStateData} end; -handle_info({route, Channel, Resource, +handle_info({route_chan, Channel, Resource, {xmlelement, "message", Attrs, Els} = El}, StateName, StateData) -> NewStateData = @@ -236,11 +235,58 @@ handle_info({route, Channel, Resource, [Channel, S]) end, Strings)), ?SEND(Res); - _ -> StateData + "chat" -> + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + Body1 = case Body of + [$/, $m, $e, $ | Rest] -> + "\001ACTION " ++ Rest ++ "\001"; + _ -> + Body + end, + Strings = string:tokens(Body1, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format("PRIVMSG ~s :~s\r\n", + [Resource, S]) + end, Strings)), + ?SEND(Res); + _ -> + StateData end, {next_state, StateName, NewStateData}; -handle_info({route, Channel, Resource, Packet}, StateName, StateData) -> +handle_info({route_chan, Channel, Resource, Packet}, StateName, StateData) -> + {next_state, StateName, StateData}; + + +handle_info({route_nick, Nick, + {xmlelement, "message", Attrs, Els} = El}, + StateName, StateData) -> + NewStateData = + case xml:get_attr_s("type", Attrs) of + "chat" -> + Body = xml:get_path_s(El, [{elem, "body"}, cdata]), + Body1 = case Body of + [$/, $m, $e, $ | Rest] -> + "\001ACTION " ++ Rest ++ "\001"; + _ -> + Body + end, + Strings = string:tokens(Body1, "\n"), + Res = lists:concat( + lists:map( + fun(S) -> + io_lib:format("PRIVMSG ~s :~s\r\n", + [Nick, S]) + end, Strings)), + ?SEND(Res); + _ -> + StateData + end, + {next_state, StateName, NewStateData}; + +handle_info({route_nick, Nick, Packet}, StateName, StateData) -> {next_state, StateName, StateData}; @@ -253,20 +299,20 @@ handle_info({ircstring, [$: | String]}, StateName, StateData) -> NewStateData = case Words of [_, "353" | Items] -> - process_channel_list(StateData, Items), - StateData; + process_channel_list(StateData, Items); [From, "PRIVMSG", [$# | Chan] | _] -> process_chanprivmsg(StateData, Chan, From, String), StateData; [From, "PRIVMSG", Nick, ":\001VERSION\001" | _] -> process_version(StateData, Nick, From), StateData; + [From, "PRIVMSG", Nick | _] -> + process_privmsg(StateData, Nick, From, String), + StateData; [From, "PART", [$# | Chan] | _] -> - process_part(StateData, Chan, From, String), - StateData; + process_part(StateData, Chan, From, String); [From, "JOIN", Chan | _] -> - process_join(StateData, Chan, From, String), - StateData; + process_join(StateData, Chan, From, String); [From, "MODE", [$# | Chan], "+o", Nick | _] -> process_mode_o(StateData, Chan, From, Nick, "admin", "moderator"), @@ -278,6 +324,8 @@ handle_info({ircstring, [$: | String]}, StateName, StateData) -> [From, "KICK", [$# | Chan], Nick | _] -> process_kick(StateData, Chan, From, Nick), StateData; + [From, "NICK", Nick | _] -> + process_nick(StateData, From, Nick); _ -> io:format("unknown irc command '~s'~n", [String]), StateData @@ -292,6 +340,11 @@ handle_info({ircstring, [$: | String]}, StateName, StateData) -> end, {next_state, stream_established, NewStateData1}; +handle_info({ircstring, [$E, $R, $R, $O, $R | _] = String}, + StateName, StateData) -> + process_error(StateData, String), + {next_state, StateName, StateData}; + handle_info({ircstring, String}, StateName, StateData) -> io:format("unknown irc command '~s'~n", [String]), @@ -331,6 +384,17 @@ handle_info({tcp_error, Socket, Reason}, StateName, StateData) -> terminate(Reason, StateName, StateData) -> mod_irc:closed_conection(StateData#state.user, StateData#state.server), + bounce_messages("Server Connect Failed"), + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + {lists:concat([Chan, "%", StateData#state.server]), + StateData#state.myname, StateData#state.nick}, + StateData#state.user, + {xmlelement, "presence", [{"type", "error"}], + [{xmlelement, "error", [{"code", "502"}], + [{xmlcdata, "Server Connect Failed"}]}]}) + end, dict:fetch_keys(StateData#state.channels)), case StateData#state.socket of undefined -> ok; @@ -395,8 +459,11 @@ bounce_messages(Reason) -> end. -route(Pid, Channel, Resource, Packet) -> - Pid ! {route, Channel, Resource, Packet}. +route_chan(Pid, Channel, Resource, Packet) -> + Pid ! {route_chan, Channel, Resource, Packet}. + +route_nick(Pid, Nick, Packet) -> + Pid ! {route_nick, Nick, Packet}. process_lines([S]) -> @@ -409,17 +476,17 @@ process_channel_list(StateData, Items) -> process_channel_list_find_chan(StateData, Items). process_channel_list_find_chan(StateData, []) -> - ok; + StateData; process_channel_list_find_chan(StateData, [[$# | Chan] | Items]) -> process_channel_list_users(StateData, Chan, Items); process_channel_list_find_chan(StateData, [_ | Items]) -> process_channel_list_find_chan(StateData, Items). process_channel_list_users(StateData, Chan, []) -> - ok; + StateData; process_channel_list_users(StateData, Chan, [User | Items]) -> - process_channel_list_user(StateData, Chan, User), - process_channel_list_users(StateData, Chan, Items). + NewStateData = process_channel_list_user(StateData, Chan, User), + process_channel_list_users(NewStateData, Chan, Items). process_channel_list_user(StateData, Chan, User) -> User1 = case User of @@ -439,13 +506,21 @@ process_channel_list_user(StateData, Chan, User) -> [{xmlelement, "item", [{"affiliation", Affiliation}, {"role", Role}], - []}]}]}). + []}]}]}), + case catch dict:update(Chan, + fun(Ps) -> + ?SETS:add_element(User2, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. process_chanprivmsg(StateData, Chan, From, String) -> [FromUser | _] = string:tokens(From, "!"), {ok, Msg, _} = regexp:sub(String, ".*PRIVMSG[^:]*:", ""), - %Msg = lists:last(string:tokens(String, ":")), Msg1 = case Msg of [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> "/me " ++ Rest; @@ -468,23 +543,44 @@ process_chanprivmsg(StateData, Chan, From, String) -> {xmlelement, "message", [{"type", "groupchat"}], [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). +process_privmsg(StateData, Nick, From, String) -> + [FromUser | _] = string:tokens(From, "!"), + {ok, Msg, _} = regexp:sub(String, ".*PRIVMSG[^:]*:", ""), + Msg1 = case Msg of + [1, $A, $C, $T, $I, $O, $N, $ | Rest] -> + "/me " ++ Rest; + _ -> + Msg + end, + Msg2 = lists:filter( + fun(C) -> + if (C < 32) and + (C /= 9) and + (C /= 10) and + (C /= 13) -> + false; + true -> true + end + end, Msg1), + ejabberd_router:route( + {lists:concat([FromUser, "!", StateData#state.server]), + StateData#state.myname, ""}, + StateData#state.user, + {xmlelement, "message", [{"type", "chat"}], + [{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). + process_version(StateData, Nick, From) -> - case StateData#state.nick of - Nick -> - [FromUser | _] = string:tokens(From, "!"), - send_text( - StateData#state.socket, - io_lib:format("NOTICE ~s :\001VERSION " - "ejabberd IRC transport ~s (c) Alexey Shchepin" - "\001\r\n", - [FromUser, ?VERSION]) ++ - io_lib:format("NOTICE ~s :\001VERSION " - "http://www.jabber.ru/projects/ejabberd/" - "\001\r\n", - [FromUser])); - _ -> - ok - end. + [FromUser | _] = string:tokens(From, "!"), + send_text( + StateData#state.socket, + io_lib:format("NOTICE ~s :\001VERSION " + "ejabberd IRC transport ~s (c) Alexey Shchepin" + "\001\r\n", + [FromUser, ?VERSION]) ++ + io_lib:format("NOTICE ~s :\001VERSION " + "http://www.jabber.ru/projects/ejabberd/" + "\001\r\n", + [FromUser])). process_part(StateData, Chan, From, String) -> @@ -498,7 +594,16 @@ process_part(StateData, Chan, From, String) -> [{xmlelement, "item", [{"affiliation", "member"}, {"role", "none"}], - []}]}]}). + []}]}]}), + case catch dict:update(Chan, + fun(Ps) -> + remove_element(FromUser, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. process_join(StateData, Channel, From, String) -> @@ -512,7 +617,17 @@ process_join(StateData, Channel, From, String) -> [{xmlelement, "item", [{"affiliation", "member"}, {"role", "participant"}], - []}]}]}). + []}]}]}), + case catch dict:update(Chan, + fun(Ps) -> + ?SETS:add_element(FromUser, Ps) + end, StateData#state.channels) of + {'EXIT', _} -> + StateData; + NS -> + StateData#state{channels = NS} + end. + process_mode_o(StateData, Chan, From, Nick, Affiliation, Role) -> @@ -541,6 +656,61 @@ process_kick(StateData, Chan, From, Nick) -> {xmlelement, "status", [{"code", "307"}], []} ]}]}). +process_nick(StateData, From, NewNick) -> + [FromUser | _] = string:tokens(From, "!"), + Nick = lists:subtract(NewNick, ":"), + NewChans = + dict:map( + fun(Chan, Ps) -> + case ?SETS:is_member(FromUser, Ps) of + true -> + ejabberd_router:route( + {lists:concat([Chan, "%", StateData#state.server]), + StateData#state.myname, FromUser}, + StateData#state.user, + {xmlelement, "presence", [{"type", "unavailable"}], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "participant"}, + {"nick", Nick}], + []}, + {xmlelement, "status", [{"code", "303"}], []} + ]}]}), + ejabberd_router:route( + {lists:concat([Chan, "%", StateData#state.server]), + StateData#state.myname, Nick}, + StateData#state.user, + {xmlelement, "presence", [], + [{xmlelement, "x", [{"xmlns", ?NS_MUC_USER}], + [{xmlelement, "item", + [{"affiliation", "member"}, + {"role", "participant"}], + []} + ]}]}), + ?SETS:add_element(Nick, + remove_element(FromUser, Ps)); + _ -> + Ps + end + end, StateData#state.channels), + StateData#state{channels = NewChans}. + + +process_error(StateData, String) -> + lists:foreach( + fun(Chan) -> + ejabberd_router:route( + {lists:concat([Chan, "%", StateData#state.server]), + StateData#state.myname, StateData#state.nick}, + StateData#state.user, + {xmlelement, "presence", [{"type", "error"}], + [{xmlelement, "error", [{"code", "502"}], + [{xmlcdata, String}]}]}) + end, dict:fetch_keys(StateData#state.channels)). + + + remove_element(E, Set) -> case ?SETS:is_element(E, Set) of