24
1
mirror of https://github.com/processone/ejabberd.git synced 2024-06-18 22:15:20 +02:00

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>. <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> </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>. 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 </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 connection can be achieved with some IRC servers because they limit the
number of conections from one IP. number of conections from one IP.

View File

@ -2687,6 +2687,8 @@ End user information:
\jid{nickserver!irc.example.org@irc.jabberserver.org}. \jid{nickserver!irc.example.org@irc.jabberserver.org}.
\item Entering your password is possible by sending `LOGIN nick password' \\ \item Entering your password is possible by sending `LOGIN nick password' \\
to \jid{nickserver!irc.example.org@irc.jabberserver.org}. 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 \item When using a popular \Jabber{} server, it can occur that no
connection can be achieved with some IRC servers because they limit the connection can be achieved with some IRC servers because they limit the
number of conections from one IP. number of conections from one IP.

View File

@ -59,6 +59,7 @@ static int iconv_erl_control(ErlDrvData drv_data,
ErlDrvBinary *b; ErlDrvBinary *b;
char *from, *to, *string, *stmp, *rstring, *rtmp; char *from, *to, *string, *stmp, *rstring, *rtmp;
iconv_t cd; iconv_t cd;
int invalid_utf8_as_latin1 = 0;
ei_decode_version(buf, &index, &i); ei_decode_version(buf, &index, &i);
ei_decode_tuple_header(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); stmp = string = malloc(size + 1);
ei_decode_string(buf, &index, string); 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); cd = iconv_open(to, from);
if (cd == (iconv_t) -1) { if (cd == (iconv_t) -1) {
@ -95,6 +105,12 @@ static int iconv_erl_control(ErlDrvData drv_data,
rtmp = rstring = malloc(avail); rtmp = rstring = malloc(avail);
while (inleft > 0) { while (inleft > 0) {
if (iconv(cd, &stmp, &inleft, &rtmp, &outleft) == (size_t) -1) { 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++; stmp++;
inleft--; inleft--;
} }

View File

@ -34,7 +34,8 @@
-export([start_link/2, -export([start_link/2,
start/2, start/2,
stop/1, stop/1,
closed_connection/3]). closed_connection/3,
get_user_and_encoding/3]).
%% gen_server callbacks %% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
@ -42,11 +43,15 @@
-include("ejabberd.hrl"). -include("ejabberd.hrl").
-include("jlib.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_connection, {jid_server_host, pid}).
-record(irc_custom, {us_host, data}). -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). -define(PROCNAME, ejabberd_mod_irc).
@ -98,14 +103,12 @@ init([Host, Opts]) ->
MyHost = gen_mod:get_opt_host(Host, Opts, "irc.@HOST@"), MyHost = gen_mod:get_opt_host(Host, Opts, "irc.@HOST@"),
update_table(MyHost), update_table(MyHost),
Access = gen_mod:get_opt(access, Opts, all), 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, catch ets:new(irc_connection, [named_table,
public, public,
{keypos, #irc_connection.jid_server_host}]), {keypos, #irc_connection.jid_server_host}]),
ejabberd_router:register_route(MyHost), ejabberd_router:register_route(MyHost),
{ok, #state{host = MyHost, {ok, #state{host = MyHost,
server_host = Host, server_host = Host,
default_encoding = DefaultEncoding,
access = Access}}. access = Access}}.
%%-------------------------------------------------------------------- %%--------------------------------------------------------------------
@ -138,9 +141,8 @@ handle_cast(_Msg, State) ->
handle_info({route, From, To, Packet}, handle_info({route, From, To, Packet},
#state{host = Host, #state{host = Host,
server_host = ServerHost, server_host = ServerHost,
default_encoding = DefEnc,
access = Access} = State) -> 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} -> {'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]); ?ERROR_MSG("~p", [Reason]);
_ -> _ ->
@ -188,10 +190,10 @@ stop_supervisor(Host) ->
supervisor:terminate_child(ejabberd_sup, Proc), supervisor:terminate_child(ejabberd_sup, Proc),
supervisor:delete_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 case acl:match_rule(ServerHost, Access, From) of
allow -> allow ->
do_route1(Host, ServerHost, From, To, Packet, DefEnc); do_route1(Host, ServerHost, From, To, Packet);
_ -> _ ->
{xmlelement, _Name, Attrs, _Els} = Packet, {xmlelement, _Name, Attrs, _Els} = Packet,
Lang = xml:get_attr_s("xml:lang", Attrs), 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) ejabberd_router:route(To, From, Err)
end. end.
do_route1(Host, ServerHost, From, To, Packet, DefEnc) -> do_route1(Host, ServerHost, From, To, Packet) ->
#jid{user = ChanServ, resource = Resource} = To, #jid{user = ChanServ, resource = Resource} = To,
{xmlelement, _Name, _Attrs, _Els} = Packet, {xmlelement, _Name, _Attrs, _Els} = Packet,
case ChanServ of case ChanServ of
@ -210,24 +212,64 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
"" -> "" ->
case jlib:iq_query_info(Packet) of case jlib:iq_query_info(Packet) of
#iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS, #iq{type = get, xmlns = ?NS_DISCO_INFO = XMLNS,
sub_el = _SubEl, lang = Lang} = IQ -> sub_el = SubEl, lang = Lang} = IQ ->
Res = IQ#iq{type = result, Node = xml:get_tag_attr_s("node", SubEl),
sub_el = [{xmlelement, "query", case iq_disco(Node, Lang) of
[{"xmlns", XMLNS}], [] ->
iq_disco(Lang)}]}, 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, ejabberd_router:route(To,
From, From,
jlib:iq_to_xml(Res)); 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));
#iq{xmlns = ?NS_REGISTER} = IQ -> #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, #iq{type = get, xmlns = ?NS_VCARD = XMLNS,
lang = Lang} = IQ -> lang = Lang} = IQ ->
Res = IQ#iq{type = result, Res = IQ#iq{type = result,
@ -238,6 +280,34 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
ejabberd_router:route(To, ejabberd_router:route(To,
From, From,
jlib:iq_to_xml(Res)); 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 -> #iq{} = _IQ ->
Err = jlib:make_error_reply( Err = jlib:make_error_reply(
Packet, ?ERR_FEATURE_NOT_IMPLEMENTED), Packet, ?ERR_FEATURE_NOT_IMPLEMENTED),
@ -256,10 +326,25 @@ do_route1(Host, ServerHost, From, To, Packet, DefEnc) ->
[] -> [] ->
?DEBUG("open new connection~n", []), ?DEBUG("open new connection~n", []),
{Username, Encoding} = get_user_and_encoding( {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( {ok, Pid} = mod_irc_connection:start(
From, Host, ServerHost, Server, From, Host, ServerHost, Server,
Username, Encoding), ConnectionUsername, Encoding),
ets:insert( ets:insert(
irc_connection, irc_connection,
#irc_connection{jid_server_host = {From, Server, Host}, #irc_connection{jid_server_host = {From, Server, Host},
@ -304,7 +389,7 @@ closed_connection(Host, From, Server) ->
ets:delete(irc_connection, {From, Server, Host}). ets:delete(irc_connection, {From, Server, Host}).
iq_disco(Lang) -> iq_disco([], Lang) ->
[{xmlelement, "identity", [{xmlelement, "identity",
[{"category", "conference"}, [{"category", "conference"},
{"type", "irc"}, {"type", "irc"},
@ -312,7 +397,22 @@ iq_disco(Lang) ->
{xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []}, {xmlelement, "feature", [{"var", ?NS_DISCO_INFO}], []},
{xmlelement, "feature", [{"var", ?NS_MUC}], []}, {xmlelement, "feature", [{"var", ?NS_MUC}], []},
{xmlelement, "feature", [{"var", ?NS_REGISTER}], []}, {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) -> iq_get_vcard(Lang) ->
[{xmlelement, "FN", [], [{xmlelement, "FN", [],
@ -323,8 +423,20 @@ iq_get_vcard(Lang) ->
[{xmlcdata, translate:translate(Lang, "ejabberd IRC module") ++ [{xmlcdata, translate:translate(Lang, "ejabberd IRC module") ++
"\nCopyright (c) 2003-2009 Alexey Shchepin"}]}]. "\nCopyright (c) 2003-2009 Alexey Shchepin"}]}].
process_register(Host, From, To, DefEnc, #iq{} = IQ) -> command_items(Host, Lang) ->
case catch process_irc_register(Host, From, To, DefEnc, IQ) of 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} -> {'EXIT', Reason} ->
?ERROR_MSG("~p", [Reason]); ?ERROR_MSG("~p", [Reason]);
ResIQ -> ResIQ ->
@ -354,7 +466,7 @@ find_xdata_el1([{xmlelement, Name, Attrs, SubEls} | Els]) ->
find_xdata_el1([_ | Els]) -> find_xdata_el1([_ | 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, #iq{type = Type, xmlns = XMLNS,
lang = Lang, sub_el = SubEl} = IQ) -> lang = Lang, sub_el = SubEl} = IQ) ->
case Type of case Type of
@ -400,7 +512,7 @@ process_irc_register(Host, From, _To, DefEnc,
get -> get ->
Node = Node =
string:tokens(xml:get_tag_attr_s("node", SubEl), "/"), 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} -> {result, Res} ->
IQ#iq{type = result, IQ#iq{type = result,
sub_el = [{xmlelement, "query", 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, #jid{user = User, server = Server,
luser = LUser, lserver = LServer} = From, luser = LUser, lserver = LServer} = From,
US = {LUser, LServer}, US = {LUser, LServer},
@ -469,7 +581,7 @@ get_form(Host, From, [], Lang, DefEnc) ->
"for IRC servers, fill this list with values " "for IRC servers, fill this list with values "
"in format '{\"irc server\", \"encoding\"}'. " "in format '{\"irc server\", \"encoding\"}'. "
"By default this service use \"~s\" encoding."), "By default this service use \"~s\" encoding."),
[DefEnc]))}]}]}, [?DEFAULT_IRC_ENCODING]))}]}]},
{xmlelement, "field", [{"type", "fixed"}], {xmlelement, "field", [{"type", "fixed"}],
[{xmlelement, "value", [], [{xmlelement, "value", [],
[{xmlcdata, [{xmlcdata,
@ -494,7 +606,7 @@ get_form(Host, From, [], Lang, DefEnc) ->
]}]} ]}]}
end; end;
get_form(_Host, _, _, _Lang, _) -> get_form(_Host, _, _, _Lang) ->
{error, ?ERR_SERVICE_UNAVAILABLE}. {error, ?ERR_SERVICE_UNAVAILABLE}.
@ -544,23 +656,244 @@ set_form(_Host, _, _, _Lang, _XData) ->
{error, ?ERR_SERVICE_UNAVAILABLE}. {error, ?ERR_SERVICE_UNAVAILABLE}.
get_user_and_encoding(Host, From, IRCServer, DefEnc) -> get_user_and_encoding(Host, From, IRCServer) ->
#jid{user = User, server = _Server, #jid{user = User, server = _Server,
luser = LUser, lserver = LServer} = From, luser = LUser, lserver = LServer} = From,
US = {LUser, LServer}, US = {LUser, LServer},
case catch mnesia:dirty_read({irc_custom, {US, Host}}) of case catch mnesia:dirty_read({irc_custom, {US, Host}}) of
{'EXIT', _Reason} -> {'EXIT', _Reason} ->
{User, DefEnc}; {User, ?DEFAULT_IRC_ENCODING};
[] -> [] ->
{User, DefEnc}; {User, ?DEFAULT_IRC_ENCODING};
[#irc_custom{data = Data}] -> [#irc_custom{data = Data}] ->
{xml:get_attr_s(username, Data), {xml:get_attr_s(username, Data),
case xml:get_attr_s(IRCServer, xml:get_attr_s(encodings, Data)) of case xml:get_attr_s(IRCServer, xml:get_attr_s(encodings, Data)) of
"" -> DefEnc; "" -> ?DEFAULT_IRC_ENCODING;
E -> E E -> E
end} end}
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) -> update_table(Host) ->
Fields = record_info(fields, irc_custom), Fields = record_info(fields, irc_custom),

View File

@ -51,6 +51,7 @@
-record(state, {socket, encoding, queue, -record(state, {socket, encoding, queue,
user, host, server, nick, user, host, server, nick,
channels = dict:new(), channels = dict:new(),
nickchannel,
inbuf = "", outbuf = ""}). inbuf = "", outbuf = ""}).
%-define(DBGFSM, true). %-define(DBGFSM, true).
@ -217,15 +218,21 @@ handle_info({route_chan, Channel, Resource,
_ -> _ ->
Resource Resource
end, end,
S1 = ?SEND(io_lib:format("NICK ~s\r\n" S1 = if
"JOIN #~s\r\n", Nick /= StateData#state.nick ->
[Nick, Channel])), 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 case dict:is_key(Channel, S1#state.channels) of
true -> true ->
S1#state{nick = Nick}; S1;
_ -> _ ->
S1#state{nick = Nick, S2 = ?SEND(io_lib:format("JOIN #~s\r\n", [Channel])),
channels = S2#state{channels =
dict:store(Channel, ?SETS:new(), dict:store(Channel, ?SETS:new(),
S1#state.channels)} S1#state.channels)}
end end
@ -471,6 +478,35 @@ handle_info({ircstring, [$P, $I, $N, $G, $ | ID]}, StateName, StateData) ->
send_text(StateData, "PONG " ++ ID ++ "\r\n"), send_text(StateData, "PONG " ++ ID ++ "\r\n"),
{next_state, StateName, StateData}; {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) -> handle_info({ircstring, [$: | String]}, _StateName, StateData) ->
Words = string:tokens(String, " "), Words = string:tokens(String, " "),
NewStateData = NewStateData =
@ -495,6 +531,15 @@ handle_info({ircstring, [$: | String]}, _StateName, StateData) ->
[_, "319", _, Nick | _ ] -> [_, "319", _, Nick | _ ] ->
process_whois319(StateData, String, Nick), process_whois319(StateData, String, Nick),
StateData; 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] | _] -> [From, "PRIVMSG", [$# | Chan] | _] ->
process_chanprivmsg(StateData, Chan, From, String), process_chanprivmsg(StateData, Chan, From, String),
StateData; StateData;
@ -581,7 +626,16 @@ handle_info({tcp_error, _Socket, _Reason}, StateName, StateData) ->
%% Purpose: Shutdown the fsm %% Purpose: Shutdown the fsm
%% Returns: any %% 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, mod_irc:closed_connection(StateData#state.host,
StateData#state.user, StateData#state.user,
StateData#state.server), StateData#state.server),
@ -594,8 +648,7 @@ terminate(_Reason, _StateName, StateData) ->
StateData#state.host, StateData#state.nick), StateData#state.host, StateData#state.nick),
StateData#state.user, StateData#state.user,
{xmlelement, "presence", [{"type", "error"}], {xmlelement, "presence", [{"type", "error"}],
[{xmlelement, "error", [{"code", "502"}], [Error]})
[{xmlcdata, "Server Connect Failed"}]}]})
end, dict:fetch_keys(StateData#state.channels)), end, dict:fetch_keys(StateData#state.channels)),
case StateData#state.socket of case StateData#state.socket of
undefined -> undefined ->
@ -738,7 +791,6 @@ process_channel_topic_who(StateData, Chan, String) ->
String String
end, end,
Msg2 = filter_message(Msg1), Msg2 = filter_message(Msg1),
ejabberd_router:route( ejabberd_router:route(
jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]), jlib:make_jid(lists:concat([Chan, "%", StateData#state.server]),
StateData#state.host, ""), StateData#state.host, ""),
@ -747,6 +799,45 @@ process_channel_topic_who(StateData, Chan, String) ->
[{xmlelement, "body", [], [{xmlcdata, Msg2}]}]}). [{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) -> process_endofwhois(StateData, _String, Nick) ->
ejabberd_router:route( ejabberd_router:route(
@ -876,7 +967,7 @@ process_version(StateData, _Nick, From) ->
"\001\r\n", "\001\r\n",
[FromUser, ?VERSION]) ++ [FromUser, ?VERSION]) ++
io_lib:format("NOTICE ~s :\001VERSION " io_lib:format("NOTICE ~s :\001VERSION "
?EJABBERD_URI "http://ejabberd.jabberstudio.org/"
"\001\r\n", "\001\r\n",
[FromUser])). [FromUser])).
@ -1069,7 +1160,14 @@ process_nick(StateData, From, NewNick) ->
Ps Ps
end end
end, StateData#state.channels), 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) -> process_error(StateData, String) ->
@ -1085,6 +1183,13 @@ process_error(StateData, String) ->
[{xmlcdata, String}]}]}) [{xmlcdata, String}]}]})
end, dict:fetch_keys(StateData#state.channels)). 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}]}]}.