-module(ejabberd_ircd). -author('henoch@dtek.chalmers.se'). -update_info({update, 0}). -behaviour(gen_fsm). %% External exports -export([start/2, start_link/2, socket_type/0]). %% gen_fsm callbacks -export([init/1, wait_for_nick/2, wait_for_cmd/2, handle_event/3, handle_sync_event/4, code_change/4, handle_info/3, terminate/3 ]). %-define(ejabberd_debug, true). -include("ejabberd.hrl"). -include("jlib.hrl"). -include("logger.hrl"). -define(DICT, dict). -record(state, {socket, sockmod, access, encoding, shaper, host, muc_host, sid = none, pass = "", nick = none, user = none, %% joining is a mapping from room JIDs to nicknames %% received but not yet forwarded joining = ?DICT:new(), joined = ?DICT:new(), %% mapping certain channels to certain rooms channels_to_jids = ?DICT:new(), jids_to_channels = ?DICT:new() }). -record(channel, {participants = [], topic = ""}). -record(line, {prefix, command, params}). %-define(DBGFSM, true). -ifdef(DBGFSM). -define(FSMOPTS, [{debug, [trace]}]). -else. -define(FSMOPTS, []). -endif. %%%---------------------------------------------------------------------- %%% API %%%---------------------------------------------------------------------- start(SockData, Opts) -> supervisor:start_child(ejabberd_ircd_sup, [SockData, Opts]). start_link(SockData, Opts) -> gen_fsm:start_link(ejabberd_ircd, [SockData, Opts], ?FSMOPTS). socket_type() -> raw. %%%---------------------------------------------------------------------- %%% Callback functions from gen_fsm %%%---------------------------------------------------------------------- %%---------------------------------------------------------------------- %% Func: init/1 %% Returns: {ok, StateName, StateData} | %% {ok, StateName, StateData, Timeout} | %% ignore | %% {stop, StopReason} %%---------------------------------------------------------------------- init([{SockMod, Socket}, Opts]) -> %iconv:start(), Access = case lists:keysearch(access, 1, Opts) of {value, {_, A}} -> A; _ -> all end, Shaper = case lists:keysearch(shaper, 1, Opts) of {value, {_, S}} -> S; _ -> none end, Host = case lists:keysearch(host, 1, Opts) of {value, {_, H}} -> H; _ -> ?MYNAME end, MucHost = case lists:keysearch(muc_host, 1, Opts) of {value, {_, M}} -> M; _ -> "conference." ++ ?MYNAME end, Encoding = case lists:keysearch(encoding, 1, Opts) of {value, {_, E}} -> E; _ -> "utf-8" end, ChannelMappings = case lists:keysearch(mappings, 1, Opts) of {value, {_, C}} -> C; _ -> [] end, {ChannelToJid, JidToChannel} = lists:foldl(fun({Channel, Room}, {CToJ, JToC}) -> RoomJID = jlib:string_to_jid(Room), BareChannel = case Channel of [$#|R] -> R; _ -> Channel end, {?DICT:store(BareChannel, RoomJID, CToJ), ?DICT:store(RoomJID, BareChannel, JToC)} end, {?DICT:new(), ?DICT:new()}, ChannelMappings), inet:setopts(Socket, [list, {packet, line}, {active, true}]), %%_ReceiverPid = start_ircd_receiver(Socket, SockMod), {ok, wait_for_nick, #state{socket = Socket, sockmod = SockMod, access = Access, encoding = Encoding, shaper = Shaper, host = Host, muc_host = MucHost, channels_to_jids = ChannelToJid, jids_to_channels = JidToChannel }}. handle_info({tcp, _Socket, Line}, StateName, StateData) -> %DecodedLine = iconv:convert(StateData#state.encoding, "utf-8", Line), DecodedLine = Line, Parsed = parse_line(DecodedLine), ?MODULE:StateName({line, Parsed}, StateData); handle_info({tcp_closed, _}, _StateName, StateData) -> {stop, normal, StateData}; handle_info({route, _, _, _} = Event, StateName, StateData) -> ?MODULE:StateName(Event, StateData); handle_info(Info, StateName, StateData) -> ?ERROR_MSG("Unexpected info: ~p", [Info]), {next_state, StateName, StateData}. handle_sync_event(Event, _From, StateName, StateData) -> ?ERROR_MSG("Unexpected sync event: ~p", [Event]), Reply = ok, {reply, Reply, StateName, StateData}. handle_event(_Event, StateName, StateData) -> {next_state, StateName, StateData}. code_change(_OldVsn, StateName, StateData, _Extra) -> {ok, StateName, StateData}. terminate(_Reason, _StateName, #state{socket = Socket, sockmod = SockMod, sid = SID, host = Host, nick = Nick, joined = JoinedDict} = State) -> ?INFO_MSG("closing IRC connection for ~p", [Nick]), case SID of none -> ok; _ -> Packet = {xmlel, <<"presence">>, [{<<"type">>, <<"unavailable">>}], []}, FromJID = user_jid(State), ?DICT:map(fun(ChannelJID, _ChannelData) -> ejabberd_router:route(FromJID, ChannelJID, Packet) end, JoinedDict), ejabberd_sm:close_session_unset_presence(SID, Nick, Host, "irc", "Logged out") end, gen_tcp = SockMod, ok = gen_tcp:close(Socket), ok. wait_for_nick({line, #line{command = "PASS", params = Params}}, State) -> ?DEBUG("in wait_for_nick", []), Pass = hd(Params), ?DEBUG("got password", []), {next_state, wait_for_nick, State#state{pass = Pass}}; wait_for_nick({line, #line{command = "NICK", params = Params}}, State) -> ?DEBUG("in wait_for_nick", []), Nick = hd(Params), Pass = State#state.pass, Server = State#state.host, ?DEBUG("user=~p server=~p", [Nick, Server]), JID = jlib:make_jid(list_to_binary(Nick), Server, <<"irc">>), ?DEBUG("JID=~p", [JID]), case JID of error -> ?DEBUG("invalid nick '~p'", [Nick]), send_reply('ERR_ERRONEUSNICKNAME', [Nick, "Erroneous nickname"], State), {next_state, wait_for_nick, State}; _ -> case acl:match_rule(Server, State#state.access, JID) of deny -> ?DEBUG("access denied for '~p'", [Nick]), send_reply('ERR_NICKCOLLISION', [Nick, "Nickname collision"], State), {next_state, wait_for_nick, State}; allow -> case ejabberd_auth:check_password(list_to_binary(Nick), Server, list_to_binary(Pass)) of false -> ?DEBUG("auth failed for '~p'", [Nick]), send_reply('ERR_NICKCOLLISION', [Nick, "Authentication failed"], State), {next_state, wait_for_nick, State}; true -> ?DEBUG("good nickname '~p'", [Nick]), SID = {now(), self()}, ejabberd_sm:open_session( SID, list_to_binary(Nick), Server, <<"irc">>, peerip(gen_tcp, State#state.socket)), ejabberd_sm:set_presence(SID, list_to_binary(Nick), Server, <<"irc">>, 3, "undefined", [{'ip', peerip(gen_tcp, State#state.socket)}, {'conn','c2s'}, {'state',"+"}]), send_text_command("", "001", [Nick, "IRC interface of ejabberd server "++Server], State), send_reply('RPL_MOTDSTART', [Nick, "- "++binary_to_list(Server)++" Message of the day - "], State), send_reply('RPL_MOTD', [Nick, "- This is the IRC interface of the ejabberd server "++binary_to_list(Server)++"."], State), send_reply('RPL_MOTD', [Nick, "- Your full JID is "++Nick++"@"++binary_to_list(Server)++"/irc."], State), send_reply('RPL_MOTD', [Nick, "- Channel #whatever corresponds to MUC room whatever@"++binary_to_list(State#state.muc_host)++"."], State), send_reply('RPL_MOTD', [Nick, "- This IRC interface is quite immature. You will probably find bugs."], State), send_reply('RPL_MOTD', [Nick, "- Have a good time!"], State), send_reply('RPL_ENDOFMOTD', [Nick, "End of /MOTD command"], State), {next_state, wait_for_cmd, State#state{nick = Nick, sid = SID, pass = ""}} end end end; wait_for_nick(Event, State) -> ?DEBUG("in wait_for_nick", []), ?INFO_MSG("unexpected event ~p", [Event]), {next_state, wait_for_nick, State}. peerip(SockMod, Socket) -> IP = case SockMod of gen_tcp -> inet:peername(Socket); _ -> SockMod:peername(Socket) end, case IP of {ok, IPOK} -> IPOK; _ -> undefined end. wait_for_cmd({line, #line{command = "USER", params = [_Username, _Hostname, _Servername, _Realname]}}, State) -> %% Yeah, like we care. {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "JOIN", params = Params}}, State) -> ?DEBUG("received JOIN ~p", [Params]), {ChannelsString, KeysString} = case Params of [C, K] -> {C, K}; [C] -> {C, []} end, Channels = string:tokens(ChannelsString, ","), Keys = string:tokens(KeysString, ","), ?DEBUG("joining channels ~p", [Channels]), NewState = join_channels(Channels, Keys, State), ?DEBUG("joined channels ~p", [Channels]), {next_state, wait_for_cmd, NewState}; %% USERHOST command wait_for_cmd({line, #line{command = "USERHOST", params = Params}}, State) -> case Params of [] -> send_reply('ERR_NEEDMOREPARAMS', ["USERHOST", "Not enough parameters"], State); UserParams -> Users = lists:sublist(string:tokens(UserParams, " "), 5), %% RFC 1459 specifies 5 items max lists:foreach( fun(UserSubList) -> User = lists:last(UserSubList), case ejabberd_sm:get_user_info(User, State#state.host, "irc") of offline -> send_reply('RPL_USERHOST',[State#state.nick, User++" offline"], State); [_Node, _Conn, Ip] -> {_,{{IP1,IP2,IP3,IP4}, _}} = Ip, send_reply('RPL_USERHOST',[State#state.nick, User ++ "=+" ++ integer_to_list(IP1) ++ "." ++ integer_to_list(IP2) ++ "." ++ integer_to_list(IP3) ++ "." ++ integer_to_list(IP4)], State) end end, Users) end, {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "PART", params = [ChannelsString | MaybeMessage]}}, State) -> Message = case MaybeMessage of [] -> nothing; [M] -> M end, Channels = string:tokens(ChannelsString, ","), NewState = part_channels(Channels, State, Message), {next_state, wait_for_cmd, NewState}; wait_for_cmd({line, #line{command = "PRIVMSG", params = [To, Text]}}, State) -> Recipients = string:tokens(To, ","), FromJID = user_jid(State), lists:foreach( fun(Rcpt) -> case Rcpt of [$# | Roomname] -> Packet = {xmlel, <<"message">>, [{<<"type">>, <<"groupchat">>}], [{xmlel, <<"body">>, [], filter_cdata(translate_action(Text))}]}, ToJID = channel_to_jid(Roomname, State), ejabberd_router:route(FromJID, ToJID, Packet); _ -> case string:tokens(Rcpt, "#") of [Nick, Channel] -> Packet = {xmlel, <<"message">>, [{<<"type">>, <<"chat">>}], [{xmlel, <<"body">>, [], filter_cdata(translate_action(Text))}]}, ToJID = channel_nick_to_jid(Nick, Channel, State), ejabberd_router:route(FromJID, ToJID, Packet); _ -> send_text_command(Rcpt, "NOTICE", [State#state.nick, "Your message to "++ Rcpt++ " was dropped. " "Try sending it to "++Rcpt++ "#somechannel."], State) end end end, Recipients), {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "PING", params = Params}}, State) -> {Token, Whom} = case Params of [A] -> {A, ""}; [A, B] -> {A, B} end, if Whom == ""; Whom == State#state.host -> %% Ping to us send_command("", "PONG", [State#state.host, Token], State); true -> %% Ping to someone else ?DEBUG("ignoring ping to ~s", [Whom]), ok end, {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "TOPIC", params = Params}}, State) -> case Params of [Channel] -> %% user asks for topic case ?DICT:find(channel_to_jid(Channel, State), State#state.joined) of {ok, #channel{topic = Topic}} -> case Topic of "" -> send_reply('RPL_NOTOPIC', ["No topic is set"], State); _ -> send_reply('RPL_TOPIC', [Topic], State) end; _ -> send_reply('ERR_NOTONCHANNEL', ["You're not on that channel"], State) end; [Channel, NewTopic] -> Packet = {xmlel, <<"message">>, [{<<"type">>, <<"groupchat">>}], [{xmlel, <<"subject">>, [], filter_cdata(NewTopic)}]}, FromJID = user_jid(State), ToJID = channel_to_jid(Channel, State), ejabberd_router:route(FromJID, ToJID, Packet) end, {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "MODE", params = [ModeOf | Params]}}, State) -> case ModeOf of [$# | Channel] -> ChannelJid = channel_to_jid(Channel, State), Joined = ?DICT:find(ChannelJid, State#state.joined), case Joined of {ok, _ChannelData} -> case Params of [] -> %% This is where we could mirror some advanced MUC %% properties. %%send_reply('RPL_CHANNELMODEIS', [Channel, Modes], State); send_reply('ERR_NOCHANMODES', [Channel], State); ["b"] -> send_reply('RPL_ENDOFBANLIST', [Channel, "Ban list not available"], State); _ -> send_reply('ERR_UNKNOWNCOMMAND', ["MODE", io_lib:format("MODE ~p not understood", [Params])], State) end; _ -> send_reply('ERR_NOTONCHANNEL', [Channel, "You're not on that channel"], State) end; Nick -> if Nick == State#state.nick -> case Params of [] -> send_reply('RPL_UMODEIS', [], State); [Flags|_] -> send_reply('ERR_UMODEUNKNOWNFLAG', [Flags, "No MODE flags supported"], State) end; true -> send_reply('ERR_USERSDONTMATCH', ["Can't change mode for other users"], State) end end, {next_state, wait_for_cmd, State}; wait_for_cmd({line, #line{command = "QUIT"}}, State) -> %% quit message is ignored for now {stop, normal, State}; wait_for_cmd({line, #line{command = Unknown, params = Params} = Line}, State) -> ?INFO_MSG("Unknown command: ~p", [Line]), send_reply('ERR_UNKNOWNCOMMAND', [Unknown, "Unknown command or arity: " ++ Unknown ++ "/" ++ integer_to_list(length(Params))], State), {next_state, wait_for_cmd, State}; wait_for_cmd({route, From, _To, {xmlel, <<"presence">>, Attrs, Els} = El}, State) -> ?DEBUG("Received a Presence ~p ~p ~p", [From, _To, El]), Type = xml:get_attr_s("type", Attrs), FromRoom = jlib:jid_remove_resource(From), FromNick = binary_to_list(From#jid.resource), Channel = jid_to_channel(From, State), MyNick = State#state.nick, IRCSender = make_irc_sender(FromNick, FromRoom, State), Joining = ?DICT:find(FromRoom, State#state.joining), Joined = ?DICT:find(FromRoom, State#state.joined), ?DEBUG("JoinState ~p ~p ~p", [Joining, Joined, Type]), case {Joining, Joined, Type} of {{ok, BufferedNicks}, _, <<"">>} -> ?DEBUG("BufferedNicks ~p", [BufferedNicks]), case BufferedNicks of [] -> %% If this is the first presence, tell the %% client that it's joining. ?DEBUG("Sending Command ~p ~p", [IRCSender, Channel]), send_command(IRCSender, "JOIN", [Channel], State), ?DEBUG("Command Sent", []); _ -> ok end, ?DEBUG("Getting NewRole", []), NewRole = case find_el("x", ?NS_MUC_USER, Els) of nothing -> ""; XMucEl -> xml:get_path_s(XMucEl, [{elem, "item"}, {attr, "role"}]) end, ?DEBUG("NewRole ~p", [NewRole]), NewBufferedNicks = [{FromNick, NewRole} | BufferedNicks], ?DEBUG("~s is present in ~s. we now have ~p.", [FromNick, Channel, NewBufferedNicks]), %% We receive our own presence last. XXX: there %% are some status codes here. See XEP-0045, %% section 7.1.3. NewState = case FromNick of MyNick -> send_reply('RPL_NAMREPLY', [MyNick, "=", Channel, lists:append( lists:map( fun({Nick, Role}) -> case Role of "moderator" -> "@"; "participant" -> "+"; _ -> "" end ++ Nick ++ " " end, NewBufferedNicks))], State), send_reply('RPL_ENDOFNAMES', [Channel, "End of /NAMES list"], State), NewJoiningDict = ?DICT:erase(FromRoom, State#state.joining), ChannelData = #channel{participants = NewBufferedNicks}, NewJoinedDict = ?DICT:store(FromRoom, ChannelData, State#state.joined), State#state{joining = NewJoiningDict, joined = NewJoinedDict}; _ -> NewJoining = ?DICT:store(FromRoom, NewBufferedNicks, State#state.joining), State#state{joining = NewJoining} end, {next_state, wait_for_cmd, NewState}; {{ok, _BufferedNicks}, _, <<"error">>} -> NewState = case FromNick of MyNick -> %% we couldn't join the room {ReplyCode, ErrorDescription} = case xml:get_subtag(El, "error") of {xmlel, _, _, _} = ErrorEl -> {ErrorName, ErrorText} = parse_error(ErrorEl), {case ErrorName of "forbidden" -> 'ERR_INVITEONLYCHAN'; _ -> 'ERR_NOSUCHCHANNEL' end, if is_list(ErrorText) -> ErrorName ++ ": " ++ ErrorText; true -> ErrorName end}; _ -> {'ERR_NOSUCHCHANNEL', "Unknown error"} end, send_reply(ReplyCode, [Channel, ErrorDescription], State), NewJoiningDict = ?DICT:erase(FromRoom, State#state.joining), State#state{joining = NewJoiningDict}; _ -> ?ERROR_MSG("ignoring presence of type ~s from ~s while joining room", [Type, jlib:jid_to_string(From)]), State end, {next_state, wait_for_cmd, NewState}; %% Presence in a channel we have already joined {_, {ok, _}, <<"">>} -> %% Someone enters send_command(IRCSender, "JOIN", [Channel], State), {next_state, wait_for_cmd, State}; {_, {ok, _}, _} -> %% Someone leaves send_command(IRCSender, "PART", [Channel], State), {next_state, wait_for_cmd, State}; _ -> ?INFO_MSG("unexpected presence from ~s", [jlib:jid_to_string(From)]), {next_state, wait_for_cmd, State} end; wait_for_cmd({route, From, _To, {xmlel, <<"message">>, Attrs, Els} = El}, State) -> ?DEBUG("Got a Message! ~p ~p ~p", [From, _To, El]), Type = xml:get_attr_s(<<"type">>, Attrs), case Type of <<"groupchat">> -> ?DEBUG("It's a groupchat", []), ChannelJID = jlib:jid_remove_resource(From), case ?DICT:find(ChannelJID, State#state.joined) of {ok, #channel{} = ChannelData} -> FromChannel = jid_to_channel(From, State), FromNick = binary_to_list(From#jid.resource), Subject = xml:get_path_s(El, [{elem, <<"subject">>}, cdata]), Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), ?DEBUG("Message Data ~p ~p", [Subject, Body]), XDelay = lists:any(fun({xmlel, <<"x">>, XAttrs, _}) -> xml:get_attr_s(<<"xmlns">>, XAttrs) == ?NS_DELAY; (_) -> false end, Els), ?DEBUG("XDelay ~p", [XDelay]), if Subject /= <<"">> -> ?DEBUG("Cleaning Subject!", []), CleanSubject = lists:map(fun($\n) -> $\ ; (C) -> C end, binary_to_list(Subject)), ?DEBUG("CleanSubject ~p", [CleanSubject]), IRCSender = make_irc_sender(From, State), ?DEBUG("IRCSender ~p", [IRCSender]), send_text_command(IRCSender, "TOPIC", [FromChannel, CleanSubject], State), NewChannelData = ChannelData#channel{topic = CleanSubject}, NewState = State#state{joined = ?DICT:store(jlib:jid_remove_resource(From), NewChannelData, State#state.joined)}, {next_state, wait_for_cmd, NewState}; not XDelay, FromNick == State#state.nick -> %% there is no message echo in IRC. %% we let the backlog through, though. ?DEBUG("Don't care about it", []), {next_state, wait_for_cmd, State}; true -> ?DEBUG("Send it to someone!", []), BodyLines = string:tokens(binary_to_list(Body), "\n"), lists:foreach( fun(Line) -> Line1 = case Line of [$/, $m, $e, $ | Action] -> [1]++"ACTION "++Action++[1]; _ -> Line end, send_text_command(make_irc_sender(From, State), "PRIVMSG", [FromChannel, Line1], State) end, BodyLines), {next_state, wait_for_cmd, State} end; error -> ?ERROR_MSG("got message from ~s without having joined it", [jlib:jid_to_string(ChannelJID)]), {next_state, wait_for_cmd, State} end; <<"error">> -> MucHost = State#state.muc_host, ErrorFrom = case From of #jid{lserver = MucHost, luser = Room, lresource = ""} -> [$#|Room]; #jid{lserver = MucHost, luser = Room, lresource = Nick} -> Nick++"#"++Room; #jid{} -> %% ??? jlib:jid_to_string(From) end, %% I think this should cover all possible combinations of %% XMPP and non-XMPP error messages... ErrorText = error_to_string(xml:get_subtag(El, <<"error">>)), send_text_command("", "NOTICE", [State#state.nick, "Message to "++ErrorFrom++" bounced: "++ ErrorText], State), {next_state, wait_for_cmd, State}; _ -> ChannelJID = jlib:jid_remove_resource(From), case ?DICT:find(ChannelJID, State#state.joined) of {ok, #channel{}} -> FromNick = binary_to_list(From#jid.lresource)++jid_to_channel(From, State), Body = xml:get_path_s(El, [{elem, <<"body">>}, cdata]), BodyLines = string:tokens(Body, "\n"), lists:foreach( fun(Line) -> Line1 = case Line of [$/, $m, $e, $ | Action] -> [1]++"ACTION "++Action++[1]; _ -> Line end, send_text_command(FromNick, "PRIVMSG", [State#state.nick, Line1], State) end, BodyLines), {next_state, wait_for_cmd, State}; _ -> ?INFO_MSG("unexpected message from ~s", [jlib:jid_to_string(From)]), {next_state, wait_for_cmd, State} end end; wait_for_cmd(Event, State) -> ?INFO_MSG("unexpected event ~p", [Event]), {next_state, wait_for_cmd, State}. join_channels([], _, State) -> State; join_channels(Channels, [], State) -> join_channels(Channels, [none], State); join_channels([Channel | Channels], [Key | Keys], #state{nick = Nick} = State) -> Packet = {xmlel, <<"presence">>, [], [{xmlel, <<"x">>, [{<<"xmlns">>, ?NS_MUC}], case Key of none -> []; _ -> [{xmlel, <<"password">>, [], filter_cdata(Key)}] end}]}, ?DEBUG("joining channel nick=~p channel=~p state=~p", [Nick, Channel, State]), From = user_jid(State), ?DEBUG("1 ~p", [From]), To = channel_nick_to_jid(Nick, Channel, State), ?DEBUG("2 ~p", [To]), Room = jlib:jid_remove_resource(To), ?DEBUG("3 ~p", [Room]), ejabberd_router:route(From, To, Packet), ?DEBUG("4", []), NewState = State#state{joining = ?DICT:store(Room, [], State#state.joining)}, ?DEBUG("5 ~p", [NewState]), join_channels(Channels, Keys, NewState). part_channels([], State, _Message) -> State; part_channels([Channel | Channels], State, Message) -> Packet = {xmlel, <<"presence">>, [{<<"type">>, <<"unavailable">>}], case Message of nothing -> []; _ -> [{xmlel, <<"status">>, [], [{xmlcdata, Message}]}] end}, From = user_jid(State), To = channel_nick_to_jid(State#state.nick, Channel, State), ejabberd_router:route(From, To, Packet), RoomJID = channel_to_jid(Channel, State), NewState = State#state{joined = ?DICT:erase(RoomJID, State#state.joined)}, part_channels(Channels, NewState, Message). parse_line(Line) -> {Line1, LastParam} = case string:str(Line, " :") of 0 -> {Line, []}; Index -> {string:substr(Line, 1, Index - 1), [string:substr(Line, Index + 2) -- "\r\n"]} end, Tokens = string:tokens(Line1, " \r\n"), {Prefix, Tokens1} = case Line1 of [$: | _] -> {hd(Tokens), tl(Tokens)}; _ -> {none, Tokens} end, [Command | Params] = Tokens1, UCCommand = upcase(Command), #line{prefix = Prefix, command = UCCommand, params = Params ++ LastParam}. upcase([]) -> []; upcase([C|String]) -> [if $a =< C, C =< $z -> C - ($a - $A); true -> C end | upcase(String)]. %% sender send_line(Line, #state{sockmod = SockMod, socket = Socket, encoding = Encoding}) -> ?DEBUG("sending ~s", [Line]), gen_tcp = SockMod, %EncodedLine = iconv:convert("utf-8", Encoding, Line), EncodedLine = Line, ok = gen_tcp:send(Socket, [EncodedLine, 13, 10]). send_command(Sender, Command, Params, State) -> send_command(Sender, Command, Params, State, false). %% Some IRC software require commands with text to have the text %% quoted, even it's not if not necessary. send_text_command(Sender, Command, Params, State) -> send_command(Sender, Command, Params, State, true). send_command(Sender, Command, Params, State, AlwaysQuote) -> ?DEBUG("SendCommand ~p ~p ~p", [Sender, Command, Params]), Prefix = case Sender of "" -> [$: | binary_to_list(State#state.host)]; _ -> [$: | Sender] end, ParamString = make_param_string(Params, AlwaysQuote), send_line(Prefix ++ " " ++ Command ++ ParamString, State). send_reply(Reply, Params, State) -> Number = case Reply of 'ERR_UNKNOWNCOMMAND' -> "421"; 'ERR_ERRONEUSNICKNAME' -> "432"; 'ERR_NICKCOLLISION' -> "436"; 'ERR_NOTONCHANNEL' -> "442"; 'ERR_NOCHANMODES' -> "477"; 'ERR_UMODEUNKNOWNFLAG' -> "501"; 'ERR_USERSDONTMATCH' -> "502"; 'RPL_UMODEIS' -> "221"; 'RPL_CHANNELMODEIS' -> "324"; 'RPL_NAMREPLY' -> "353"; 'RPL_ENDOFNAMES' -> "366"; 'RPL_BANLIST' -> "367"; 'RPL_ENDOFBANLIST' -> "368"; 'RPL_NOTOPIC' -> "331"; 'RPL_TOPIC' -> "332"; 'RPL_MOTD' -> "372"; 'RPL_MOTDSTART' -> "375"; 'RPL_ENDOFMOTD' -> "376" end, send_text_command("", Number, Params, State). make_param_string([], _) -> ""; make_param_string([LastParam], AlwaysQuote) -> case {AlwaysQuote, LastParam, lists:member($\ , LastParam)} of {true, _, _} -> " :" ++ LastParam; {_, _, true} -> " :" ++ LastParam; {_, [$:|_], _} -> " :" ++ LastParam; {_, _, _} -> " " ++ LastParam end; make_param_string([Param | Params], AlwaysQuote) -> case lists:member($\ , Param) of false -> " " ++ Param ++ make_param_string(Params, AlwaysQuote) end. find_el(Name, NS, [{xmlel, N, Attrs, _} = El|Els]) -> XMLNS = xml:get_attr_s("xmlns", Attrs), case {Name, NS} of {N, XMLNS} -> El; _ -> find_el(Name, NS, Els) end; find_el(_, _, []) -> nothing. channel_to_jid([$#|Channel], State) -> channel_to_jid(Channel, State); channel_to_jid(Channel, #state{muc_host = MucHost, channels_to_jids = ChannelsToJids}) -> case ?DICT:find(Channel, ChannelsToJids) of {ok, RoomJID} -> RoomJID; _ -> jlib:make_jid(list_to_binary(Channel), MucHost, <<"">>) end. channel_nick_to_jid(Nick, [$#|Channel], State) -> channel_nick_to_jid(Nick, Channel, State); channel_nick_to_jid(Nick, Channel, #state{muc_host = MucHost, channels_to_jids = ChannelsToJids}) -> case ?DICT:find(Channel, ChannelsToJids) of {ok, RoomJID} -> jlib:jid_replace_resource(RoomJID, list_to_binary(Nick)); _ -> jlib:make_jid(list_to_binary(Channel), MucHost, list_to_binary(Nick)) end. jid_to_channel(#jid{user = Room} = RoomJID, #state{jids_to_channels = JidsToChannels}) -> case ?DICT:find(jlib:jid_remove_resource(RoomJID), JidsToChannels) of {ok, Channel} -> [$#|binary_to_list(Channel)]; _ -> [$#|binary_to_list(Room)] end. make_irc_sender(Nick, #jid{luser = Room} = RoomJID, #state{jids_to_channels = JidsToChannels}) -> case ?DICT:find(jlib:jid_remove_resource(RoomJID), JidsToChannels) of {ok, Channel} -> Nick++"!"++Nick++"@"++binary_to_list(Channel); _ -> Nick++"!"++Nick++"@"++binary_to_list(Room) end. make_irc_sender(#jid{lresource = Nick} = JID, State) -> make_irc_sender(binary_to_list(Nick), JID, State). user_jid(#state{nick = Nick, host = Host}) -> jlib:make_jid(list_to_binary(Nick), Host, <<"irc">>). filter_cdata(Msg) -> [{xmlcdata, filter_message(Msg)}]. filter_message(Msg) -> lists:filter( fun(C) -> if (C < 32) and %% Add color support, but break XML: (see https://support.process-one.net/browse/EJAB-1097 ) %% (C /= 3) and (C /= 9) and (C /= 10) and (C /= 13) -> false; true -> true end end, Msg). translate_action(Msg) -> case Msg of [1, $A, $C, $T, $I, $O, $N, $ | Action] -> "/me "++Action; _ -> Msg end. parse_error({xmlel, "error", _ErrorAttrs, ErrorEls} = ErrorEl) -> ErrorTextEl = xml:get_subtag(ErrorEl, "text"), ErrorName = case ErrorEls -- [ErrorTextEl] of [{xmlel, ErrorReason, _, _}] -> ErrorReason; _ -> "unknown error" end, ErrorText = case ErrorTextEl of {xmlel, _, _, _} -> xml:get_tag_cdata(ErrorTextEl); _ -> nothing end, {ErrorName, ErrorText}. error_to_string({xmlel, "error", _ErrorAttrs, _ErrorEls} = ErrorEl) -> case parse_error(ErrorEl) of {ErrorName, ErrorText} when is_list(ErrorText) -> ErrorName ++ ": " ++ ErrorText; {ErrorName, _} -> ErrorName end; error_to_string(_) -> "unknown error".