ejabberd-contrib/ircd/src/ejabberd_ircd.erl

917 lines
29 KiB
Erlang

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