Ad-hoc commands to join IRC channel, set nickname and encoding (thanks to Magnus Henoch)(EJAB-302)

SVN Revision: 2164
This commit is contained in:
Badlop 2009-06-15 18:56:52 +00:00
parent e0370d89b4
commit 83ffe1989a
5 changed files with 511 additions and 53 deletions

View File

@ -2023,6 +2023,8 @@ to the IRC transport instead of the Multi-User Chat service.
<TT>nickserver!irc.example.org@irc.jabberserver.org</TT>.
</LI><LI CLASS="li-itemize">Entering your password is possible by sending &#X2018;LOGIN nick password&#X2019;<BR>
to <TT>nickserver!irc.example.org@irc.jabberserver.org</TT>.
</LI><LI CLASS="li-itemize">The IRC transport provides Ad-Hoc Commands (<A HREF="http://www.xmpp.org/extensions/xep-0050.html">XEP-0050</A>)
to join a channel, and to set custom IRC username and encoding.
</LI><LI CLASS="li-itemize">When using a popular Jabber server, it can occur that no
connection can be achieved with some IRC servers because they limit the
number of conections from one IP.

View File

@ -2687,6 +2687,8 @@ End user information:
\jid{nickserver!irc.example.org@irc.jabberserver.org}.
\item Entering your password is possible by sending `LOGIN nick password' \\
to \jid{nickserver!irc.example.org@irc.jabberserver.org}.
\item The IRC transport provides Ad-Hoc Commands (\xepref{0050})
to join a channel, and to set custom IRC username and encoding.
\item When using a popular \Jabber{} server, it can occur that no
connection can be achieved with some IRC servers because they limit the
number of conections from one IP.

View File

@ -59,6 +59,7 @@ static int iconv_erl_control(ErlDrvData drv_data,
ErlDrvBinary *b;
char *from, *to, *string, *stmp, *rstring, *rtmp;
iconv_t cd;
int invalid_utf8_as_latin1 = 0;
ei_decode_version(buf, &index, &i);
ei_decode_tuple_header(buf, &index, &i);
@ -74,6 +75,15 @@ static int iconv_erl_control(ErlDrvData drv_data,
stmp = string = malloc(size + 1);
ei_decode_string(buf, &index, string);
/* Special mode: parse as UTF-8 if possible; otherwise assume it's
Latin-1. Makes no difference when encoding. */
if (strcmp(from, "utf-8+latin-1") == 0) {
from[5] = '\0';
invalid_utf8_as_latin1 = 1;
}
if (strcmp(to, "utf-8+latin-1") == 0) {
to[5] = '\0';
}
cd = iconv_open(to, from);
if (cd == (iconv_t) -1) {
@ -95,6 +105,12 @@ static int iconv_erl_control(ErlDrvData drv_data,
rtmp = rstring = malloc(avail);
while (inleft > 0) {
if (iconv(cd, &stmp, &inleft, &rtmp, &outleft) == (size_t) -1) {
if (invalid_utf8_as_latin1 && (*stmp & 0x80) && outleft >= 2) {
/* Encode one byte of (assumed) Latin-1 into two bytes of UTF-8 */
*rtmp++ = 0xc0 | ((*stmp & 0xc0) >> 6);
*rtmp++ = 0x80 | (*stmp & 0x3f);
outleft -= 2;
}
stmp++;
inleft--;
}

View File

@ -34,7 +34,8 @@
-export([start_link/2,
start/2,
stop/1,
closed_connection/3]).
closed_connection/3,
get_user_and_encoding/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@ -42,11 +43,15 @@
-include("ejabberd.hrl").
-include("jlib.hrl").
-include("adhoc.hrl").
-define(DEFAULT_IRC_ENCODING, "iso8859-1").
-define(POSSIBLE_ENCODINGS, ["koi8-r", "iso8859-1", "iso8859-2", "utf-8", "utf-8+latin-1"]).
-record(irc_connection, {jid_server_host, pid}).
-record(irc_custom, {us_host, data}).
-record(state, {host, server_host, default_encoding, access}).
-record(state, {host, server_host, access}).
-define(PROCNAME, ejabberd_mod_irc).
@ -98,14 +103,12 @@ init([Host, Opts]) ->
MyHost = gen_mod:get_opt_host(Host, Opts, "irc.@HOST@"),
update_table(MyHost),
Access = gen_mod:get_opt(access, Opts, all),
DefaultEncoding = gen_mod:get_opt(default_encoding, Opts, "koi8-r"),
catch ets:new(irc_connection, [named_table,
public,
{keypos, #irc_connection.jid_server_host}]),
ejabberd_router:register_route(MyHost),
{ok, #state{host = MyHost,
server_host = Host,
default_encoding = DefaultEncoding,
access = Access}}.
%%--------------------------------------------------------------------
@ -138,9 +141,8 @@ handle_cast(_Msg, State) ->
handle_info({route, From, To, Packet},
#state{host = Host,
server_host = ServerHost,
default_encoding = DefEnc,
access = Access} = State) ->
case catch do_route(Host, ServerHost, Access, From, To, Packet, DefEnc) of
case catch do_route(Host, ServerHost, Access, From, To, Packet) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
_ ->
@ -188,10 +190,10 @@ stop_supervisor(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_child(ejabberd_sup, Proc).
do_route(Host, ServerHost, Access, From, To, Packet, DefEnc) ->
do_route(Host, ServerHost, Access, From, To, Packet) ->
case acl:match_rule(ServerHost, Access, From) of
allow ->
do_route1(Host, ServerHost, From, To, Packet, DefEnc);
do_route1(Host, ServerHost, From, To, Packet);
_ ->
{xmlelement, _Name, Attrs, _Els} = Packet,
Lang = xml:get_attr_s("xml:lang", Attrs),
@ -201,7 +203,7 @@ do_route(Host, ServerHost, Access, From, To, Packet, DefEnc) ->
ejabberd_router:route(To, From, Err)
end.
do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
do_route1(Host, ServerHost, From, To, Packet) ->
#jid{user = ChanServ, resource = Resource} = To,
{xmlelement, _Name, _Attrs, _Els} = Packet,
case ChanServ of
@ -210,24 +212,64 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
"" ->
case jlib:iq_query_info(Packet) of
#iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
sub_el = _SubEl, lang = Lang} = IQ ->
Res = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
iq_disco(Lang)}]},
sub_el = SubEl, lang = Lang} = IQ ->
Node = xml:get_tag_attr_s("node", SubEl),
case iq_disco(Node, Lang) of
[] ->
Res = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
[]}]},
ejabberd_router:route(To,
From,
jlib:iq_to_xml(Res));
DiscoInfo ->
Res = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
DiscoInfo}]},
ejabberd_router:route(To,
From,
jlib:iq_to_xml(Res))
end;
#iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS,
sub_el = SubEl, lang = Lang} = IQ ->
Node = xml:get_tag_attr_s("node", SubEl),
case Node of
[] ->
ResIQ = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
[]}]},
Res = jlib:iq_to_xml(ResIQ);
"join" ->
ResIQ = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
[]}]},
Res = jlib:iq_to_xml(ResIQ);
"register" ->
ResIQ = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
[]}]},
Res = jlib:iq_to_xml(ResIQ);
?NS_COMMANDS ->
ResIQ = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS},
{"node", Node}],
command_items(Host, Lang)}]},
Res = jlib:iq_to_xml(ResIQ);
_ ->
Res = jlib:make_error_reply(
Packet, ?ERR_ITEM_NOT_FOUND)
end,
ejabberd_router:route(To,
From,
jlib:iq_to_xml(Res));
#iq{type = get, xmlns = ?NS_DISCO_ITEMS = XMLNS} = IQ ->
Res = IQ#iq{type = result,
sub_el = [{xmlelement, "query",
[{"xmlns", XMLNS}],
[]}]},
ejabberd_router:route(To,
From,
jlib:iq_to_xml(Res));
Res);
#iq{xmlns = ?NS_REGISTER} = IQ ->
process_register(Host, From, To, DefEnc, IQ);
process_register(Host, From, To, IQ);
#iq{type = get, xmlns = ?NS_VCARD = XMLNS,
lang = Lang} = IQ ->
Res = IQ#iq{type = result,
@ -238,6 +280,34 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
ejabberd_router:route(To,
From,
jlib:iq_to_xml(Res));
#iq{type = set, xmlns = ?NS_COMMANDS,
lang = _Lang, sub_el = SubEl} = IQ ->
Request = adhoc:parse_request(IQ),
case lists:keysearch(Request#adhoc_request.node, 1, commands()) of
{value, {_, _, Function}} ->
case catch Function(From, To, Request) of
{'EXIT', Reason} ->
?ERROR_MSG("~p~nfor ad-hoc handler of ~p",
[Reason, {From, To, IQ}]),
Res = IQ#iq{type = error, sub_el = [SubEl,
?ERR_INTERNAL_SERVER_ERROR]};
ignore ->
Res = ignore;
{error, Error} ->
Res = IQ#iq{type = error, sub_el = [SubEl, Error]};
Command ->
Res = IQ#iq{type = result, sub_el = [Command]}
end,
if Res /= ignore ->
ejabberd_router:route(To, From, jlib:iq_to_xml(Res));
true ->
ok
end;
_ ->
Err = jlib:make_error_reply(
Packet, ?ERR_ITEM_NOT_FOUND),
ejabberd_router:route(To, From, Err)
end;
#iq{} = _IQ ->
Err = jlib:make_error_reply(
Packet, ?ERR_FEATURE_NOT_IMPLEMENTED),
@ -256,10 +326,25 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
[] ->
?DEBUG("open new connection~n", []),
{Username, Encoding} = get_user_and_encoding(
Host, From, Server, DefEnc),
Host, From, Server),
ConnectionUsername =
case Packet of
%% If the user tries to join a
%% chatroom, the packet for sure
%% contains the desired username.
{xmlelement, "presence", _, _} ->
Resource;
%% Otherwise, there is no firm
%% conclusion from the packet.
%% Better to use the configured
%% username (which defaults to the
%% username part of the JID).
_ ->
Username
end,
{ok, Pid} = mod_irc_connection:start(
From, Host, ServerHost, Server,
Username, Encoding),
ConnectionUsername, Encoding),
ets:insert(
irc_connection,
#irc_connection{jid_server_host = {From, Server, Host},
@ -304,7 +389,7 @@ closed_connection(Host, From, Server) ->
ets:delete(irc_connection, {From, Server, Host}).
iq_disco(Lang) ->
iq_disco([], Lang) ->
[{xmlelement, "identity",
[{"category", "conference"},
{"type", "irc"},
@ -312,7 +397,22 @@ iq_disco(Lang) ->
{xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []},
{xmlelement, "feature", [{"var", ?NS_MUC}], []},
{xmlelement, "feature", [{"var", ?NS_REGISTER}], []},
{xmlelement, "feature", [{"var", ?NS_VCARD}], []}].
{xmlelement, "feature", [{"var", ?NS_VCARD}], []},
{xmlelement, "feature", [{"var", ?NS_COMMANDS}], []}];
iq_disco(Node, Lang) ->
case lists:keysearch(Node, 1, commands()) of
{value, {_, Name, _}} ->
[{xmlelement, "identity",
[{"category", "automation"},
{"type", "command-node"},
{"name", translate:translate(Lang, Name)}], []},
{xmlelement, "feature",
[{"var", ?NS_COMMANDS}], []},
{xmlelement, "feature",
[{"var", ?NS_XDATA}], []}];
_ ->
[]
end.
iq_get_vcard(Lang) ->
[{xmlelement, "FN", [],
@ -320,11 +420,23 @@ iq_get_vcard(Lang) ->
{xmlelement, "URL", [],
[{xmlcdata, ?EJABBERD_URI}]},
{xmlelement, "DESC", [],
[{xmlcdata, translate:translate(Lang, "ejabberd IRC module") ++
[{xmlcdata, translate:translate(Lang, "ejabberd IRC module") ++
"\nCopyright (c) 2003-2009 Alexey Shchepin"}]}].
process_register(Host, From, To, DefEnc, #iq{} = IQ) ->
case catch process_irc_register(Host, From, To, DefEnc, IQ) of
command_items(Host, Lang) ->
lists:map(fun({Node, Name, _Function})
-> {xmlelement, "item",
[{"jid", Host},
{"node", Node},
{"name", translate:translate(Lang, Name)}], []}
end, commands()).
commands() ->
[{"join", "Join channel", fun adhoc_join/3},
{"register", "Configure username and encoding", fun adhoc_register/3}].
process_register(Host, From, To, #iq{} = IQ) ->
case catch process_irc_register(Host, From, To, IQ) of
{'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]);
ResIQ ->
@ -354,7 +466,7 @@ find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) ->
find_xdata_el1([_ | Els]) ->
find_xdata_el1(Els).
process_irc_register(Host, From, _To, DefEnc,
process_irc_register(Host, From, _To,
#iq{type = Type, xmlns = XMLNS,
lang = Lang, sub_el = SubEl} = IQ) ->
case Type of
@ -400,7 +512,7 @@ process_irc_register(Host, From, _To, DefEnc,
get ->
Node =
string:tokens(xml:get_tag_attr_s("node", SubEl), "/"),
case get_form(Host, From, Node, Lang ,DefEnc) of
case get_form(Host, From, Node, Lang) of
{result, Res} ->
IQ#iq{type = result,
sub_el = [{xmlelement, "query",
@ -415,7 +527,7 @@ process_irc_register(Host, From, _To, DefEnc,
get_form(Host, From, [], Lang, DefEnc) ->
get_form(Host, From, [], Lang) ->
#jid{user = User, server = Server,
luser = LUser, lserver = LServer} = From,
US = {LUser, LServer},
@ -469,7 +581,7 @@ get_form(Host, From, [], Lang, DefEnc) ->
"for IRC servers, fill this list with values "
"in format '{\"irc server\", \"encoding\"}'. "
"By default this service use \"~s\" encoding."),
[DefEnc]))}]}]},
[?DEFAULT_IRC_ENCODING]))}]}]},
{xmlelement, "field", [{"type", "fixed"}],
[{xmlelement, "value", [],
[{xmlcdata,
@ -494,7 +606,7 @@ get_form(Host, From, [], Lang, DefEnc) ->
]}]}
end;
get_form(_Host, _, _, _Lang, _) ->
get_form(_Host, _, _, _Lang) ->
{error, ?ERR_SERVICE_UNAVAILABLE}.
@ -544,23 +656,244 @@ set_form(_Host, _, _, _Lang, _XData) ->
{error, ?ERR_SERVICE_UNAVAILABLE}.
get_user_and_encoding(Host, From, IRCServer, DefEnc) ->
get_user_and_encoding(Host, From, IRCServer) ->
#jid{user = User, server = _Server,
luser = LUser, lserver = LServer} = From,
US = {LUser, LServer},
case catch mnesia:dirty_read({irc_custom, {US, Host}}) of
{'EXIT', _Reason} ->
{User, DefEnc};
{User, ?DEFAULT_IRC_ENCODING};
[] ->
{User, DefEnc};
{User, ?DEFAULT_IRC_ENCODING};
[#irc_custom{data = Data}] ->
{xml:get_attr_s(username, Data),
case xml:get_attr_s(IRCServer, xml:get_attr_s(encodings, Data)) of
"" -> DefEnc;
"" -> ?DEFAULT_IRC_ENCODING;
E -> E
end}
end.
adhoc_join(_From, _To, #adhoc_request{action = "cancel"} = Request) ->
adhoc:produce_response(Request,
#adhoc_response{status = canceled});
adhoc_join(From, To, #adhoc_request{lang = Lang,
node = _Node,
action = _Action,
xdata = XData} = Request) ->
%% Access control has already been taken care of in do_route.
if XData == false ->
Form =
{xmlelement, "x",
[{"xmlns", ?NS_XDATA},
{"type", "form"}],
[{xmlelement, "title", [], [{xmlcdata, translate:translate(Lang, "Join IRC channel")}]},
{xmlelement, "field",
[{"var", "channel"},
{"type", "text-single"},
{"label", translate:translate(Lang, "Channel to join (without leading #)")}],
[{xmlelement, "required", [], []}]},
{xmlelement, "field",
[{"var", "server"},
{"type", "text-single"},
{"label", translate:translate(Lang, "Server")}],
[{xmlelement, "required", [], []}]}]},
adhoc:produce_response(Request,
#adhoc_response{status = executing,
elements = [Form]});
true ->
case jlib:parse_xdata_submit(XData) of
invalid ->
{error, ?ERR_BAD_REQUEST};
Fields ->
Channel = case lists:keysearch("channel", 1, Fields) of
{value, {"channel", C}} ->
C;
_ ->
false
end,
Server = case lists:keysearch("server", 1, Fields) of
{value, {"server", S}} ->
S;
_ ->
false
end,
if Channel /= false,
Server /= false ->
RoomJID = Channel ++ "%" ++ Server ++ "@" ++ To#jid.server,
Invite = {xmlelement, "message", [],
[{xmlelement, "x",
[{"xmlns", ?NS_MUC_USER}],
[{xmlelement, "invite",
[{"from", jlib:jid_to_string(From)}],
[{xmlelement, "reason", [],
[{xmlcdata,
translate:translate(Lang,
"Join the IRC channel here.")}]}]}]},
{xmlelement, "x",
[{"xmlns", ?NS_XCONFERENCE}],
[{xmlcdata, translate:translate(Lang,
"Join the IRC channel here.")}]},
{xmlelement, "body", [],
[{xmlcdata, io_lib:format(
translate:translate(Lang,
"Find the IRC channel at JID ~s"),
[RoomJID])}]}]},
ejabberd_router:route(jlib:string_to_jid(RoomJID), From, Invite),
adhoc:produce_response(Request, #adhoc_response{status = completed});
true ->
{error, ?ERR_BAD_REQUEST}
end
end
end.
adhoc_register(_From, _To, #adhoc_request{action = "cancel"} = Request) ->
adhoc:produce_response(Request,
#adhoc_response{status = canceled});
adhoc_register(From, To, #adhoc_request{lang = Lang,
node = _Node,
xdata = XData,
action = Action} = Request) ->
#jid{user = User, luser = LUser, lserver = LServer} = From,
#jid{lserver = Host} = To,
US = {LUser, LServer},
%% Generate form for setting username and encodings. If the user
%% hasn't begun to fill out the form, generate an initial form
%% based on current values.
if XData == false ->
case catch mnesia:dirty_read({irc_custom, {US, Host}}) of
{'EXIT', _Reason} ->
Username = User,
Encodings = [];
[] ->
Username = User,
Encodings = [];
[#irc_custom{data = Data}] ->
Username = xml:get_attr_s(username, Data),
Encodings = xml:get_attr_s(encodings, Data)
end,
Error = false;
true ->
case jlib:parse_xdata_submit(XData) of
invalid ->
Error = {error, ?ERR_BAD_REQUEST},
Username = false,
Encodings = false;
Fields ->
Username = case lists:keysearch("username", 1, Fields) of
{value, {"username", U}} ->
U;
_ ->
User
end,
Encodings = parse_encodings(Fields),
Error = false
end
end,
if Error /= false ->
Error;
Action == "complete" ->
case mnesia:transaction(
fun () ->
mnesia:write(
#irc_custom{us_host =
{US, Host},
data =
[{username,
Username},
{encodings,
Encodings}]})
end) of
{atomic, _} ->
adhoc:produce_response(Request, #adhoc_response{status = completed});
_ ->
{error, ?ERR_INTERNAL_SERVER_ERROR}
end;
true ->
Form = generate_adhoc_register_form(Lang, Username, Encodings),
adhoc:produce_response(Request,
#adhoc_response{status = executing,
elements = [Form],
actions = ["next", "complete"]})
end.
generate_adhoc_register_form(Lang, Username, Encodings) ->
{xmlelement, "x",
[{"xmlns", ?NS_XDATA},
{"type", "form"}],
[{xmlelement, "title", [], [{xmlcdata, translate:translate(Lang, "IRC settings")}]},
{xmlelement, "instructions", [],
[{xmlcdata,
translate:translate(
Lang,
"Enter username and encodings you wish to use for "
"connecting to IRC servers. Press 'Next' to get more fields "
"to fill in. Press 'Complete' to save settings.")}]},
{xmlelement, "field",
[{"var", "username"},
{"type", "text-single"},
{"label", translate:translate(Lang, "IRC username")}],
[{xmlelement, "required", [], []},
{xmlelement, "value", [], [{xmlcdata, Username}]}]}] ++
generate_encoding_fields(Lang, Encodings, 1, [])}.
generate_encoding_fields(Lang, [], Number, Acc) ->
Field = generate_encoding_field(Lang, "", "", Number),
lists:reverse(Field ++ Acc);
generate_encoding_fields(Lang, [{Server, Encoding} | Encodings], Number, Acc) ->
Field = generate_encoding_field(Lang, Server, Encoding, Number),
generate_encoding_fields(Lang, Encodings, Number + 1, Field ++ Acc).
generate_encoding_field(Lang, Server, Encoding, Number) ->
EncodingUsed = case Encoding of
[] ->
?DEFAULT_IRC_ENCODING;
_ ->
Encoding
end,
%% Fields are in reverse order, as they will be reversed again later.
[{xmlelement, "field",
[{"var", "encoding" ++ io_lib:format("~b", [Number])},
{"type", "list-single"},
{"label", io_lib:format(translate:translate(Lang, "Encoding for server ~b"), [Number])}],
[{xmlelement, "value", [], [{xmlcdata, EncodingUsed}]} |
lists:map(fun(E) ->
{xmlelement, "option", [{"label", E}],
[{xmlelement, "value", [], [{xmlcdata, E}]}]}
end, ?POSSIBLE_ENCODINGS)]},
{xmlelement, "field",
[{"var", "server" ++ io_lib:format("~b", [Number])},
{"type", "text-single"},
{"label", io_lib:format(translate:translate(Lang, "Server ~b"), [Number])}],
[{xmlelement, "value", [], [{xmlcdata, Server}]}]}].
parse_encodings(Fields) ->
%% Find all fields staring with serverN and encodingN, for any values
%% of N, and generate lists of {"N", Value}.
Servers = lists:sort(
[{lists:nthtail(6, Var), lists:flatten(Value)} || {Var, Value} <- Fields,
lists:prefix("server", Var)]),
Encodings = lists:sort(
[{lists:nthtail(8, Var), lists:flatten(Value)} || {Var, Value} <- Fields,
lists:prefix("encoding", Var)]),
%% Now sort the lists, and find the corresponding pairs.
parse_encodings(Servers, Encodings).
parse_encodings([{ServerN, Server} | Servers], [{EncodingN, Encoding} | Encodings]) ->
%% Try to match pairs of servers and encodings, no matter what fields
%% the client might have left out.
if ServerN == EncodingN ->
[{Server, Encoding} | parse_encodings(Servers, Encodings)];
ServerN < EncodingN ->
parse_encodings(Servers, [{EncodingN, Encoding} | Encodings]);
ServerN > EncodingN ->
parse_encodings([{ServerN, Server} | Servers], Encodings)
end;
parse_encodings([], _) ->
[];
parse_encodings(_, []) ->
[].
update_table(Host) ->
Fields = record_info(fields, irc_custom),

View File

@ -1,7 +1,7 @@
%%%----------------------------------------------------------------------
%%% File : mod_irc_connection.erl
%%% Author : Alexey Shchepin <alexey@process-one.net>
%%% Purpose :
%%% Purpose :
%%% Created : 15 Feb 2003 by Alexey Shchepin <alexey@process-one.net>
%%%
%%%
@ -51,6 +51,7 @@
-record(state, {socket, encoding, queue,
user, host, server, nick,
channels = dict:new(),
nickchannel,
inbuf = "", outbuf = ""}).
%-define(DBGFSM, true).
@ -217,15 +218,21 @@ handle_info({route_chan, Channel, Resource,
_ ->
Resource
end,
S1 = ?SEND(io_lib:format("NICK ~s\r\n"
"JOIN #~s\r\n",
[Nick, Channel])),
S1 = if
Nick /= StateData#state.nick ->
S11 = ?SEND(io_lib:format("NICK ~s\r\n", [Nick])),
% The server reply will change the copy of the
% nick in the state (or indicate a clash).
S11#state{nickchannel = Channel};
true ->
StateData
end,
case dict:is_key(Channel, S1#state.channels) of
true ->
S1#state{nick = Nick};
S1;
_ ->
S1#state{nick = Nick,
channels =
S2 = ?SEND(io_lib:format("JOIN #~s\r\n", [Channel])),
S2#state{channels =
dict:store(Channel, ?SETS:new(),
S1#state.channels)}
end
@ -471,6 +478,35 @@ handle_info({ircstring, [$P, $I, $N, $G, $ | ID]}, StateName, StateData) ->
send_text(StateData, "PONG " ++ ID ++ "\r\n"),
{next_state, StateName, StateData};
handle_info({ircstring, [$: | String]}, wait_for_registration, StateData) ->
Words = string:tokens(String, " "),
{NewState, NewStateData} =
case Words of
[_, "001" | _] ->
{stream_established, StateData};
[_, "433" | _] ->
{error,
{error, error_nick_in_use(StateData, String), StateData}};
[_, [$4, _, _] | _] ->
{error,
{error, error_unknown_num(StateData, String, "cancel"),
StateData}};
[_, [$5, _, _] | _] ->
{error,
{error, error_unknown_num(StateData, String, "cancel"),
StateData}};
_ ->
?DEBUG("unknown irc command '~s'~n", [String]),
{wait_for_registration, StateData}
end,
% Note that we don't send any data at this stage.
if
NewState == error ->
{stop, normal, NewStateData};
true ->
{next_state, NewState, NewStateData}
end;
handle_info({ircstring, [$: | String]}, _StateName, StateData) ->
Words = string:tokens(String, " "),
NewStateData =
@ -495,6 +531,15 @@ handle_info({ircstring, [$: | String]}, _StateName, StateData) ->
[_, "319", _, Nick | _ ] ->
process_whois319(StateData, String, Nick),
StateData;
[_, "433" | _] ->
process_nick_in_use(StateData, String);
% CODEPAGE isn't standard, so don't complain if it's not there.
[_, "421", _, "CODEPAGE" | _] ->
StateData;
[_, [$4, _, _] | _] ->
process_num_error(StateData, String);
[_, [$5, _, _] | _] ->
process_num_error(StateData, String);
[From, "PRIVMSG", [$# | Chan] | _] ->
process_chanprivmsg(StateData, Chan, From, String),
StateData;
@ -581,7 +626,16 @@ handle_info({tcp_error, _Socket, _Reason}, StateName, StateData) ->
%% Purpose: Shutdown the fsm
%% Returns: any
%%----------------------------------------------------------------------
terminate(_Reason, _StateName, StateData) ->
terminate(_Reason, _StateName, FullStateData) ->
% Extract error message if there was one.
{Error, StateData} = case FullStateData of
{error, SError, SStateData} ->
{SError, SStateData};
_ ->
{{xmlelement, "error", [{"code", "502"}],
[{xmlcdata, "Server Connect Failed"}]},
FullStateData}
end,
mod_irc:closed_connection(StateData#state.host,
StateData#state.user,
StateData#state.server),
@ -594,8 +648,7 @@ terminate(_Reason, _StateName, StateData) ->
StateData#state.host, StateData#state.nick),
StateData#state.user,
{xmlelement, "presence", [{"type", "error"}],
[{xmlelement, "error", [{"code", "502"}],
[{xmlcdata, "Server Connect Failed"}]}]})
[Error]})
end, dict:fetch_keys(StateData#state.channels)),
case StateData#state.socket of
undefined ->
@ -738,7 +791,6 @@ process_channel_topic_who(StateData, Chan, String) ->
String
end,
Msg2 = filter_message(Msg1),
ejabberd_router:route(
jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]),
StateData#state.host, ""),
@ -747,6 +799,45 @@ process_channel_topic_who(StateData, Chan, String) ->
[{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}).
error_nick_in_use(_StateData, String) ->
{ok, Msg, _} = regexp:sub(String, ".*433 +[^ ]* +", ""),
Msg1 = filter_message(Msg),
{xmlelement, "error", [{"code", "409"}, {"type", "cancel"}],
[{xmlelement, "conflict", [{"xmlns", ?NS_STANZAS}], []},
{xmlelement, "text", [{"xmlns", ?NS_STANZAS}],
[{xmlcdata, Msg1}]}]}.
process_nick_in_use(StateData, String) ->
% We can't use the jlib macro because we don't know the language of the
% message.
Error = error_nick_in_use(StateData, String),
case StateData#state.nickchannel of
undefined ->
% Shouldn't happen with a well behaved server
StateData;
Chan ->
ejabberd_router:route(
jlib:make_jid(
lists:concat([Chan, "%", StateData#state.server]),
StateData#state.host, StateData#state.nick),
StateData#state.user,
{xmlelement, "presence", [{"type", "error"}], [Error]}),
StateData#state{nickchannel = undefined}
end.
process_num_error(StateData, String) ->
Error = error_unknown_num(StateData, String, "continue"),
lists:foreach(
fun(Chan) ->
ejabberd_router:route(
jlib:make_jid(
lists:concat([Chan, "%", StateData#state.server]),
StateData#state.host, StateData#state.nick),
StateData#state.user,
{xmlelement, "message", [{"type", "error"}],
[Error]})
end, dict:fetch_keys(StateData#state.channels)),
StateData.
process_endofwhois(StateData, _String, Nick) ->
ejabberd_router:route(
@ -876,7 +967,7 @@ process_version(StateData, _Nick, From) ->
"\001\r\n",
[FromUser, ?VERSION]) ++
io_lib:format("NOTICE ~s :\001VERSION "
?EJABBERD_URI
"http://ejabberd.jabberstudio.org/"
"\001\r\n",
[FromUser])).
@ -1069,7 +1160,14 @@ process_nick(StateData, From, NewNick) ->
Ps
end
end, StateData#state.channels),
StateData#state{channels = NewChans}.
if
FromUser == StateData#state.nick ->
StateData#state{nick = Nick,
nickchannel = undefined,
channels = NewChans};
true ->
StateData#state{channels = NewChans}
end.
process_error(StateData, String) ->
@ -1085,6 +1183,13 @@ process_error(StateData, String) ->
[{xmlcdata, String}]}]})
end, dict:fetch_keys(StateData#state.channels)).
error_unknown_num(_StateData, String, Type) ->
{ok, Msg, _} = regexp:sub(String, ".*[45][0-9][0-9] +[^ ]* +", ""),
Msg1 = filter_message(Msg),
{xmlelement, "error", [{"code", "500"}, {"type", Type}],
[{xmlelement, "undefined-condition", [{"xmlns", ?NS_STANZAS}], []},
{xmlelement, "text", [{"xmlns", ?NS_STANZAS}],
[{xmlcdata, Msg1}]}]}.