diff options
Diffstat (limited to 'src/mod_mix.erl')
-rw-r--r-- | src/mod_mix.erl | 924 |
1 files changed, 621 insertions, 303 deletions
diff --git a/src/mod_mix.erl b/src/mod_mix.erl index a81efd5ce..8a55f1cbe 100644 --- a/src/mod_mix.erl +++ b/src/mod_mix.erl @@ -1,351 +1,669 @@ %%%------------------------------------------------------------------- -%%% @author Evgeny Khramtsov <ekhramtsov@process-one.net> -%%% @copyright (C) 2016, Evgeny Khramtsov -%%% @doc -%%% -%%% @end +%%% File : mod_mix.erl +%%% Author : Evgeny Khramtsov <ekhramtsov@process-one.net> %%% Created : 2 Mar 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net> -%%%------------------------------------------------------------------- +%%% +%%% +%%% ejabberd, Copyright (C) 2002-2018 ProcessOne +%%% +%%% This program is free software; you can redistribute it and/or +%%% modify it under the terms of the GNU General Public License as +%%% published by the Free Software Foundation; either version 2 of the +%%% License, or (at your option) any later version. +%%% +%%% This program is distributed in the hope that it will be useful, +%%% but WITHOUT ANY WARRANTY; without even the implied warranty of +%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%%% General Public License for more details. +%%% +%%% You should have received a copy of the GNU General Public License along +%%% with this program; if not, write to the Free Software Foundation, Inc., +%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +%%% +%%%---------------------------------------------------------------------- -module(mod_mix). - --behaviour(gen_server). -behaviour(gen_mod). +-behaviour(gen_server). +-protocol({xep, 369, '0.13.0'}). %% API --export([start_link/2, start/2, stop/1, process_iq/3, - disco_items/5, disco_identity/5, disco_info/5, - disco_features/5, mod_opt_type/1, depends/2]). - +-export([route/1]). +%% gen_mod callbacks +-export([start/2, stop/1, reload/3, depends/2, mod_opt_type/1, mod_options/1]). %% gen_server callbacks -export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). + terminate/2, code_change/3, format_status/2]). +%% Hooks +-export([process_disco_info/1, + process_disco_items/1, + process_mix_core/1, + process_mam_query/1, + process_pubsub_query/1]). +-include("xmpp.hrl"). -include("logger.hrl"). --include("jlib.hrl"). --include("pubsub.hrl"). +-include("translate.hrl"). +-include("ejabberd_stacktrace.hrl"). --define(PROCNAME, ejabberd_mod_mix). --define(NODES, [?NS_MIX_NODES_MESSAGES, - ?NS_MIX_NODES_PRESENCE, - ?NS_MIX_NODES_PARTICIPANTS, - ?NS_MIX_NODES_SUBJECT, - ?NS_MIX_NODES_CONFIG]). +-callback init(binary(), gen_mod:opts()) -> ok | {error, db_failure}. +-callback set_channel(binary(), binary(), binary(), + jid:jid(), boolean(), binary()) -> + ok | {error, db_failure}. +-callback get_channels(binary(), binary()) -> + {ok, [binary()]} | {error, db_failure}. +-callback get_channel(binary(), binary(), binary()) -> + {ok, {jid(), boolean(), binary()}} | + {error, notfound | db_failure}. +-callback set_participant(binary(), binary(), binary(), jid(), binary(), binary()) -> + ok | {error, db_failure}. +-callback get_participant(binary(), binary(), binary(), jid()) -> + {ok, {binary(), binary()}} | {error, notfound | db_failure}. --record(state, {server_host :: binary(), - host :: binary()}). +-record(state, {hosts :: [binary()], + server_host :: binary()}). %%%=================================================================== %%% API %%%=================================================================== -start_link(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - gen_server:start_link({local, Proc}, ?MODULE, [Host, Opts], []). - start(Host, Opts) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - ChildSpec = {Proc, {?MODULE, start_link, [Host, Opts]}, - transient, 5000, worker, [?MODULE]}, - supervisor:start_child(ejabberd_sup, ChildSpec). + gen_mod:start_child(?MODULE, Host, Opts). stop(Host) -> - Proc = gen_mod:get_module_proc(Host, ?PROCNAME), - supervisor:terminate_child(ejabberd_sup, Proc), - supervisor:delete_child(ejabberd_sup, Proc), - ok. - -disco_features(_Acc, _From, _To, _Node, _Lang) -> - {result, [?NS_MIX_0]}. - -disco_items(_Acc, _From, To, _Node, _Lang) when To#jid.luser /= <<"">> -> - To_s = jid:to_string(jid:remove_resource(To)), - {result, [#xmlel{name = <<"item">>, - attrs = [{<<"jid">>, To_s}, - {<<"node">>, Node}]} || Node <- ?NODES]}; -disco_items(_Acc, _From, _To, _Node, _Lang) -> - {result, []}. - -disco_identity(Acc, _From, To, _Node, _Lang) when To#jid.luser == <<"">> -> - Acc ++ [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"name">>, <<"MIX service">>}, - {<<"type">>, <<"text">>}]}]; -disco_identity(Acc, _From, _To, _Node, _Lang) -> - Acc ++ [#xmlel{name = <<"identity">>, - attrs = - [{<<"category">>, <<"conference">>}, - {<<"type">>, <<"mix">>}]}]. - -disco_info(_Acc, _From, To, _Node, _Lang) when is_atom(To) -> - [#xmlel{name = <<"x">>, - attrs = [{<<"xmlns">>, ?NS_XDATA}, - {<<"type">>, <<"result">>}], - children = [#xmlel{name = <<"field">>, - attrs = [{<<"var">>, <<"FORM_TYPE">>}, - {<<"type">>, <<"hidden">>}], - children = [#xmlel{name = <<"value">>, - children = [{xmlcdata, - ?NS_MIX_SERVICEINFO_0}]}]}]}]; -disco_info(Acc, _From, _To, _Node, _Lang) -> - Acc. - -process_iq(From, To, - #iq{type = set, sub_el = #xmlel{name = <<"join">>} = SubEl} = IQ) -> - Nodes = lists:flatmap( - fun(#xmlel{name = <<"subscribe">>, attrs = Attrs}) -> - Node = fxml:get_attr_s(<<"node">>, Attrs), - case lists:member(Node, ?NODES) of - true -> [Node]; - false -> [] - end; - (_) -> - [] - end, SubEl#xmlel.children), - case subscribe_nodes(From, To, Nodes) of - {result, _} -> - case publish_participant(From, To) of - {result, _} -> - LFrom_s = jid:to_string(jid:tolower(jid:remove_resource(From))), - Subscribe = [#xmlel{name = <<"subscribe">>, - attrs = [{<<"node">>, Node}]} || Node <- Nodes], - IQ#iq{type = result, - sub_el = [#xmlel{name = <<"join">>, - attrs = [{<<"jid">>, LFrom_s}, - {<<"xmlns">>, ?NS_MIX_0}], - children = Subscribe}]}; - {error, Err} -> - IQ#iq{type = error, sub_el = [SubEl, Err]} - end; - {error, Err} -> - IQ#iq{type = error, sub_el = [SubEl, Err]} + gen_mod:stop_child(?MODULE, Host). + +reload(Host, NewOpts, OldOpts) -> + Proc = gen_mod:get_module_proc(Host, ?MODULE), + gen_server:cast(Proc, {reload, Host, NewOpts, OldOpts}). + +depends(_Host, _Opts) -> + [{mod_mam, hard}]. + +mod_opt_type(access_create) -> + econf:acl(); +mod_opt_type(name) -> + econf:binary(); +mod_opt_type(host) -> + econf:host(); +mod_opt_type(hosts) -> + econf:hosts(); +mod_opt_type(db_type) -> + econf:db_type(?MODULE). + +mod_options(Host) -> + [{access_create, all}, + {host, <<"mix.", Host/binary>>}, + {hosts, []}, + {name, ?T("Channels")}, + {db_type, ejabberd_config:default_db(Host, ?MODULE)}]. + +-spec route(stanza()) -> ok. +route(#iq{} = IQ) -> + ejabberd_router:process_iq(IQ); +route(#message{type = groupchat, id = ID, lang = Lang, + to = #jid{luser = <<_, _/binary>>}} = Msg) -> + case ID of + <<>> -> + Txt = ?T("Attribute 'id' is mandatory for MIX messages"), + Err = xmpp:err_bad_request(Txt, Lang), + ejabberd_router:route_error(Msg, Err); + _ -> + process_mix_message(Msg) + end; +route(Pkt) -> + ?DEBUG("Dropping packet:~n~ts", [xmpp:pp(Pkt)]). + +-spec process_disco_info(iq()) -> iq(). +process_disco_info(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_info(#iq{type = get, to = #jid{luser = <<>>} = To, + from = _From, lang = Lang, + sub_els = [#disco_info{node = <<>>}]} = IQ) -> + ServerHost = ejabberd_router:host_of_route(To#jid.lserver), + X = ejabberd_hooks:run_fold(disco_info, ServerHost, [], + [ServerHost, ?MODULE, <<"">>, Lang]), + Name = mod_mix_opt:name(ServerHost), + Identity = #identity{category = <<"conference">>, + type = <<"text">>, + name = translate:translate(Lang, Name)}, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_MIX_CORE_0, ?NS_MIX_CORE_SEARCHABLE_0, + ?NS_MIX_CORE_CREATE_CHANNEL_0], + xmpp:make_iq_result( + IQ, #disco_info{features = Features, + identities = [Identity], + xdata = X}); +process_disco_info(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_info{node = Node}]} = IQ) + when Node == <<"mix">>; Node == <<>> -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + Identity = #identity{category = <<"conference">>, + type = <<"mix">>}, + Features = [?NS_DISCO_INFO, ?NS_DISCO_ITEMS, + ?NS_MIX_CORE_0, ?NS_MAM_2], + xmpp:make_iq_result( + IQ, #disco_info{node = Node, + features = Features, + identities = [Identity]}); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; -process_iq(From, To, - #iq{type = set, sub_el = #xmlel{name = <<"leave">>} = SubEl} = IQ) -> - case delete_participant(From, To) of - {result, _} -> - case unsubscribe_nodes(From, To, ?NODES) of - {result, _} -> - IQ#iq{type = result, sub_el = []}; - {error, Err} -> - IQ#iq{type = error, sub_el = [SubEl, Err]} +process_disco_info(#iq{type = get, sub_els = [#disco_info{node = Node}]} = IQ) -> + xmpp:make_iq_result(IQ, #disco_info{node = Node, features = [?NS_DISCO_INFO]}); +process_disco_info(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +-spec process_disco_items(iq()) -> iq(). +process_disco_items(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_disco_items(#iq{type = get, to = #jid{luser = <<>>} = To, + sub_els = [#disco_items{node = <<>>}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channels(ServerHost, Host) of + {ok, Channels} -> + Items = [#disco_item{jid = jid:make(Channel, Host)} + || Channel <- Channels], + xmpp:make_iq_result(IQ, #disco_items{items = Items}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_disco_items(#iq{type = get, to = #jid{luser = <<_, _/binary>>} = To, + sub_els = [#disco_items{node = Node}]} = IQ) + when Node == <<"mix">>; Node == <<>> -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BTo = jid:remove_resource(To), + Items = [#disco_item{jid = BTo, node = N} || N <- known_nodes()], + xmpp:make_iq_result(IQ, #disco_items{node = Node, items = Items}); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; +process_disco_items(#iq{type = get, sub_els = [#disco_items{node = Node}]} = IQ) -> + xmpp:make_iq_result(IQ, #disco_items{node = Node}); +process_disco_items(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +-spec process_mix_core(iq()) -> iq(). +process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, + sub_els = [#mix_create{}]} = IQ) -> + process_mix_create(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<>>}, + sub_els = [#mix_destroy{}]} = IQ) -> + process_mix_destroy(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_join{}]} = IQ) -> + process_mix_join(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_leave{}]} = IQ) -> + process_mix_leave(IQ); +process_mix_core(#iq{type = set, to = #jid{luser = <<_, _/binary>>}, + sub_els = [#mix_setnick{}]} = IQ) -> + process_mix_setnick(IQ); +process_mix_core(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +process_pubsub_query(#iq{type = get, + sub_els = [#pubsub{items = #ps_items{node = Node}}]} = IQ) + when Node == ?NS_MIX_NODES_PARTICIPANTS -> + process_participants_list(IQ); +process_pubsub_query(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). + +process_mam_query(#iq{from = From, to = To, type = T, + sub_els = [#mam_query{}]} = IQ) + when T == get; T == set -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + mod_mam:process_iq(ServerHost, IQ, mix); + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; - {error, Err} -> - IQ#iq{type = error, sub_el = [SubEl, Err]} + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end; -process_iq(_From, _To, #iq{sub_el = SubEl, lang = Lang} = IQ) -> - Txt = <<"Unsupported MIX query">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_BAD_REQUEST(Lang, Txt)]}. +process_mam_query(IQ) -> + xmpp:make_error(IQ, unsupported_error(IQ)). %%%=================================================================== %%% gen_server callbacks %%%=================================================================== -init([ServerHost, Opts]) -> - Host = gen_mod:get_opt_host(ServerHost, Opts, <<"mix.@HOST@">>), - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - ConfigTab = gen_mod:get_module_proc(Host, config), - ets:new(ConfigTab, [named_table]), - ets:insert(ConfigTab, {plugins, [<<"mix">>]}), - ejabberd_hooks:add(disco_local_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:add(disco_local_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:add(disco_local_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:add(disco_sm_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:add(disco_sm_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:add(disco_sm_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:add(disco_info, Host, ?MODULE, disco_info, 100), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_DISCO_INFO, mod_disco, - process_local_iq_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_ITEMS, mod_disco, - process_local_iq_items, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_DISCO_INFO, mod_disco, - process_local_iq_info, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_PUBSUB, mod_pubsub, iq_sm, IQDisc), - gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_MIX_0, ?MODULE, process_iq, IQDisc), - ejabberd_router:register_route(Host, ServerHost), - {ok, #state{server_host = ServerHost, host = Host}}. - -handle_call(_Request, _From, State) -> - Reply = ok, - {reply, Reply, State}. - -handle_cast(_Msg, State) -> +init([Host|_]) -> + process_flag(trap_exit, true), + Opts = gen_mod:get_module_opts(Host, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), + MyHosts = gen_mod:get_opt_hosts(Opts), + case Mod:init(Host, gen_mod:set_opt(hosts, MyHosts, Opts)) of + ok -> + lists:foreach( + fun(MyHost) -> + ejabberd_router:register_route( + MyHost, Host, {apply, ?MODULE, route}), + register_iq_handlers(MyHost) + end, MyHosts), + {ok, #state{hosts = MyHosts, server_host = Host}}; + {error, db_failure} -> + {stop, db_failure} + end. + +handle_call(Request, From, State) -> + ?WARNING_MSG("Unexpected call from ~p: ~p", [From, Request]), {noreply, State}. -handle_info({route, From, To, Packet}, State) -> - case catch do_route(State, From, To, Packet) of - {'EXIT', _} = Err -> - try - ?ERROR_MSG("failed to route packet ~p from '~s' to '~s': ~p", - [Packet, jid:to_string(From), jid:to_string(To), Err]), - ErrPkt = jlib:make_error_reply(Packet, ?ERR_INTERNAL_SERVER_ERROR), - ejabberd_router:route_error(To, From, ErrPkt, Packet) - catch _:_ -> - ok - end; - _ -> - ok +handle_cast(Request, State) -> + ?WARNING_MSG("Unexpected cast: ~p", [Request]), + {noreply, State}. + +handle_info({route, Packet}, State) -> + try route(Packet) + catch ?EX_RULE(Class, Reason, St) -> + StackTrace = ?EX_STACK(St), + ?ERROR_MSG("Failed to route packet:~n~ts~n** ~ts", + [xmpp:pp(Packet), + misc:format_exception(2, Class, Reason, StackTrace)]) end, {noreply, State}; -handle_info(_Info, State) -> +handle_info(Info, State) -> + ?WARNING_MSG("Unexpected info: ~p", [Info]), {noreply, State}. -terminate(_Reason, #state{host = Host}) -> - ejabberd_hooks:delete(disco_local_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_local_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_local_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:delete(disco_sm_items, Host, ?MODULE, disco_items, 100), - ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE, disco_features, 100), - ejabberd_hooks:delete(disco_sm_identity, Host, ?MODULE, disco_identity, 100), - ejabberd_hooks:delete(disco_info, Host, ?MODULE, disco_info, 100), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), - gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_0), - ejabberd_router:unregister_route(Host), - ok. +terminate(_Reason, State) -> + lists:foreach( + fun(MyHost) -> + unregister_iq_handlers(MyHost), + ejabberd_router:unregister_route(MyHost) + end, State#state.hosts). code_change(_OldVsn, State, _Extra) -> {ok, State}. +format_status(_Opt, Status) -> + Status. + %%%=================================================================== %%% Internal functions %%%=================================================================== -do_route(_State, From, To, #xmlel{name = <<"iq">>} = Packet) -> - if To#jid.luser == <<"">> -> - ejabberd_local:process_iq(From, To, Packet); - true -> - ejabberd_sm:process_iq(From, To, Packet) - end; -do_route(_State, From, To, #xmlel{name = <<"presence">>} = Packet) - when To#jid.luser /= <<"">> -> - case fxml:get_tag_attr_s(<<"type">>, Packet) of - <<"unavailable">> -> - delete_presence(From, To); - _ -> - ok - end; -do_route(_State, _From, _To, _Packet) -> - ok. - -subscribe_nodes(From, To, Nodes) -> - LTo = jid:tolower(jid:remove_resource(To)), - LFrom = jid:tolower(jid:remove_resource(From)), - From_s = jid:to_string(LFrom), - lists:foldl( - fun(_Node, {error, _} = Err) -> - Err; - (Node, {result, _}) -> - case mod_pubsub:subscribe_node(LTo, Node, From, From_s, []) of - {error, _} = Err -> - case is_item_not_found(Err) of - true -> - case mod_pubsub:create_node( - LTo, To#jid.lserver, Node, LFrom, <<"mix">>) of - {result, _} -> - mod_pubsub:subscribe_node(LTo, Node, From, From_s, []); - Error -> - Error - end; - false -> - Err - end; - {result, _} = Result -> - Result - end - end, {result, []}, Nodes). - -unsubscribe_nodes(From, To, Nodes) -> - LTo = jid:tolower(jid:remove_resource(To)), - LFrom = jid:tolower(jid:remove_resource(From)), - From_s = jid:to_string(LFrom), - lists:foldl( - fun(_Node, {error, _} = Err) -> - Err; - (Node, {result, _} = Result) -> - case mod_pubsub:unsubscribe_node(LTo, Node, From, From_s, <<"">>) of - {error, _} = Err -> - case is_not_subscribed(Err) of - true -> Result; - _ -> Err - end; - {result, _} = Res -> - Res - end - end, {result, []}, Nodes). - -publish_participant(From, To) -> - LFrom = jid:tolower(jid:remove_resource(From)), - LTo = jid:tolower(jid:remove_resource(To)), - Participant = #xmlel{name = <<"participant">>, - attrs = [{<<"xmlns">>, ?NS_MIX_0}, - {<<"jid">>, jid:to_string(LFrom)}]}, - ItemID = p1_sha:sha(jid:to_string(LFrom)), - mod_pubsub:publish_item( - LTo, To#jid.lserver, ?NS_MIX_NODES_PARTICIPANTS, - From, ItemID, [Participant]). - -delete_presence(From, To) -> - LFrom = jid:tolower(From), - LTo = jid:tolower(jid:remove_resource(To)), - case mod_pubsub:get_items(LTo, ?NS_MIX_NODES_PRESENCE) of - Items when is_list(Items) -> - lists:foreach( - fun(#pubsub_item{modification = {_, LJID}, - itemid = {ItemID, _}}) when LJID == LFrom -> - delete_item(From, To, ?NS_MIX_NODES_PRESENCE, ItemID); - (_) -> - ok - end, Items); - _ -> - ok +-spec process_mix_create(iq()) -> iq(). +process_mix_create(#iq{to = To, from = From, + sub_els = [#mix_create{channel = Chan}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + Creator = jid:remove_resource(From), + Chan1 = case Chan of + <<>> -> p1_rand:get_string(); + _ -> Chan + end, + Ret = case Mod:get_channel(ServerHost, Chan1, Host) of + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case {From#jid.luser, From#jid.lserver} of + {U, S} -> ok; + _ -> {error, conflict} + end; + {error, notfound} -> + Key = xmpp_util:hex(p1_rand:bytes(20)), + Mod:set_channel(ServerHost, Chan1, Host, + Creator, Chan == <<>>, Key); + {error, db_failure} = Err -> + Err + end, + case Ret of + ok -> + xmpp:make_iq_result(IQ, #mix_create{channel = Chan1}); + {error, conflict} -> + xmpp:make_error(IQ, channel_exists_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. -delete_participant(From, To) -> - LFrom = jid:tolower(jid:remove_resource(From)), - ItemID = p1_sha:sha(jid:to_string(LFrom)), - delete_presence(From, To), - delete_item(From, To, ?NS_MIX_NODES_PARTICIPANTS, ItemID). - -delete_item(From, To, Node, ItemID) -> - LTo = jid:tolower(jid:remove_resource(To)), - case mod_pubsub:delete_item( - LTo, Node, From, ItemID, true) of - {result, _} = Res -> - Res; - {error, _} = Err -> - case is_item_not_found(Err) of - true -> {result, []}; - false -> Err - end +-spec process_mix_destroy(iq()) -> iq(). +process_mix_destroy(#iq{to = To, + from = #jid{luser = U, lserver = S}, + sub_els = [#mix_destroy{channel = Chan}]} = IQ) -> + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, {#jid{luser = U, lserver = S}, _, _}} -> + case Mod:del_channel(ServerHost, Chan, Host) of + ok -> + xmpp:make_iq_result(IQ, #mix_destroy{channel = Chan}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {ok, _} -> + xmpp:make_error(IQ, ownership_error(IQ)); + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. -is_item_not_found({error, ErrEl}) -> - case fxml:get_subtag_with_xmlns( - ErrEl, <<"item-not-found">>, ?NS_STANZAS) of - #xmlel{} -> true; - _ -> false +-spec process_mix_join(iq()) -> iq(). +process_mix_join(#iq{to = To, from = From, + sub_els = [#mix_join{} = JoinReq]} = IQ) -> + Chan = To#jid.luser, + Host = To#jid.lserver, + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, {_, _, Key}} -> + ID = make_id(From, Key), + Nick = JoinReq#mix_join.nick, + BFrom = jid:remove_resource(From), + Nodes = filter_nodes(JoinReq#mix_join.subscribe), + try + ok = Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick), + ok = Mod:subscribe(ServerHost, Chan, Host, BFrom, Nodes), + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, #mix_join{id = ID, + subscribe = Nodes, + nick = Nick}) + catch _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. -is_not_subscribed({error, ErrEl}) -> - case fxml:get_subtag_with_xmlns( - ErrEl, <<"not-subscribed">>, ?NS_PUBSUB_ERRORS) of - #xmlel{} -> true; - _ -> false +-spec process_mix_leave(iq()) -> iq(). +process_mix_leave(#iq{to = To, from = From, + sub_els = [#mix_leave{}]} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + BFrom = jid:remove_resource(From), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {ID, _}} -> + try + ok = Mod:unsubscribe(ServerHost, Chan, Host, BFrom), + ok = Mod:del_participant(ServerHost, Chan, Host, BFrom), + notify_participant_left(Mod, ServerHost, To, ID), + xmpp:make_iq_result(IQ, #mix_leave{}) + catch _:{badmatch, {error, db_failure}} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_iq_result(IQ, #mix_leave{}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) end. -depends(_Host, _Opts) -> - [{mod_pubsub, hard}]. +-spec process_mix_setnick(iq()) -> iq(). +process_mix_setnick(#iq{to = To, from = From, + sub_els = [#mix_setnick{nick = Nick}]} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + BFrom = jid:remove_resource(From), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {_, Nick}} -> + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick}); + {ok, {ID, _}} -> + case Mod:set_participant(ServerHost, Chan, Host, BFrom, ID, Nick) of + ok -> + notify_participant_joined(Mod, ServerHost, To, From, ID, Nick), + xmpp:make_iq_result(IQ, #mix_setnick{nick = Nick}); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec process_mix_message(message()) -> ok. +process_mix_message(#message{from = From, to = To, + id = SubmissionID} = Msg) -> + {Chan, Host, _} = jid:tolower(To), + {FUser, FServer, _} = jid:tolower(From), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, {StableID, Nick}} -> + MamID = mod_mam:make_id(), + Msg1 = xmpp:set_subtag( + Msg#message{from = jid:replace_resource(To, StableID), + to = undefined, + id = integer_to_binary(MamID)}, + #mix{jid = BFrom, nick = Nick}), + Msg2 = xmpp:put_meta(Msg1, stanza_id, MamID), + case ejabberd_hooks:run_fold( + store_mam_message, ServerHost, Msg2, + [Chan, Host, BFrom, Nick, groupchat, recv]) of + #message{} -> + multicast(Mod, ServerHost, Chan, Host, + ?NS_MIX_NODES_MESSAGES, + fun(#jid{luser = U, lserver = S}) + when U == FUser, S == FServer -> + xmpp:set_subtag( + Msg1, #mix{jid = BFrom, + nick = Nick, + submission_id = SubmissionID}); + (_) -> + Msg1 + end); + _ -> + ok + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, not_joined_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) + end; + {error, notfound} -> + ejabberd_router:route_error(Msg, no_channel_error(Msg)); + {error, db_failure} -> + ejabberd_router:route_error(Msg, db_error(Msg)) + end. -mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; -mod_opt_type(host) -> fun iolist_to_binary/1; -mod_opt_type(_) -> [host, iqdisc]. +-spec process_participants_list(iq()) -> iq(). +process_participants_list(#iq{from = From, to = To} = IQ) -> + {Chan, Host, _} = jid:tolower(To), + ServerHost = ejabberd_router:host_of_route(Host), + Mod = gen_mod:db_mod(ServerHost, ?MODULE), + case Mod:get_channel(ServerHost, Chan, Host) of + {ok, _} -> + BFrom = jid:remove_resource(From), + case Mod:get_participant(ServerHost, Chan, Host, BFrom) of + {ok, _} -> + case Mod:get_participants(ServerHost, Chan, Host) of + {ok, Participants} -> + Items = items_of_participants(Participants), + Pubsub = #pubsub{ + items = #ps_items{ + node = ?NS_MIX_NODES_PARTICIPANTS, + items = Items}}, + xmpp:make_iq_result(IQ, Pubsub); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, not_joined_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end; + {error, notfound} -> + xmpp:make_error(IQ, no_channel_error(IQ)); + {error, db_failure} -> + xmpp:make_error(IQ, db_error(IQ)) + end. + +-spec items_of_participants([{jid(), binary(), binary()}]) -> [ps_item()]. +items_of_participants(Participants) -> + lists:map( + fun({JID, ID, Nick}) -> + Participant = #mix_participant{jid = JID, nick = Nick}, + #ps_item{id = ID, + sub_els = [xmpp:encode(Participant)]} + end, Participants). + +-spec known_nodes() -> [binary()]. +known_nodes() -> + [?NS_MIX_NODES_MESSAGES, + ?NS_MIX_NODES_PARTICIPANTS]. + +-spec filter_nodes([binary()]) -> [binary()]. +filter_nodes(Nodes) -> + lists:filter( + fun(Node) -> + lists:member(Node, Nodes) + end, known_nodes()). + +-spec multicast(module(), binary(), binary(), + binary(), binary(), fun((jid()) -> message())) -> ok. +multicast(Mod, LServer, Chan, Service, Node, F) -> + case Mod:get_subscribed(LServer, Chan, Service, Node) of + {ok, Subscribers} -> + lists:foreach( + fun(To) -> + Msg = xmpp:set_to(F(To), To), + ejabberd_router:route(Msg) + end, Subscribers); + {error, db_failure} -> + ok + end. + +-spec notify_participant_joined(module(), binary(), + jid(), jid(), binary(), binary()) -> ok. +notify_participant_joined(Mod, LServer, To, From, ID, Nick) -> + {Chan, Host, _} = jid:tolower(To), + Participant = #mix_participant{jid = jid:remove_resource(From), + nick = Nick}, + Item = #ps_item{id = ID, + sub_els = [xmpp:encode(Participant)]}, + Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, + items = [Item]}, + Event = #ps_event{items = Items}, + Msg = #message{from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event]}, + multicast(Mod, LServer, Chan, Host, + ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + +-spec notify_participant_left(module(), binary(), jid(), binary()) -> ok. +notify_participant_left(Mod, LServer, To, ID) -> + {Chan, Host, _} = jid:tolower(To), + Items = #ps_items{node = ?NS_MIX_NODES_PARTICIPANTS, + retract = ID}, + Event = #ps_event{items = Items}, + Msg = #message{from = jid:remove_resource(To), + id = p1_rand:get_string(), + sub_els = [Event]}, + multicast(Mod, LServer, Chan, Host, ?NS_MIX_NODES_PARTICIPANTS, + fun(_) -> Msg end). + +-spec make_id(jid(), binary()) -> binary(). +make_id(JID, Key) -> + Data = jid:encode(jid:tolower(jid:remove_resource(JID))), + xmpp_util:hex(crypto:hmac(sha256, Data, Key, 10)). + +%%%=================================================================== +%%% Error generators +%%%=================================================================== +-spec db_error(stanza()) -> stanza_error(). +db_error(Pkt) -> + Txt = ?T("Database failure"), + xmpp:err_internal_server_error(Txt, xmpp:get_lang(Pkt)). + +-spec channel_exists_error(stanza()) -> stanza_error(). +channel_exists_error(Pkt) -> + Txt = ?T("Channel already exists"), + xmpp:err_conflict(Txt, xmpp:get_lang(Pkt)). + +-spec no_channel_error(stanza()) -> stanza_error(). +no_channel_error(Pkt) -> + Txt = ?T("Channel does not exist"), + xmpp:err_item_not_found(Txt, xmpp:get_lang(Pkt)). + +-spec not_joined_error(stanza()) -> stanza_error(). +not_joined_error(Pkt) -> + Txt = ?T("You are not joined to the channel"), + xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + +-spec unsupported_error(stanza()) -> stanza_error(). +unsupported_error(Pkt) -> + Txt = ?T("No module is handling this query"), + xmpp:err_service_unavailable(Txt, xmpp:get_lang(Pkt)). + +-spec ownership_error(stanza()) -> stanza_error(). +ownership_error(Pkt) -> + Txt = ?T("Owner privileges required"), + xmpp:err_forbidden(Txt, xmpp:get_lang(Pkt)). + +%%%=================================================================== +%%% IQ handlers +%%%=================================================================== +-spec register_iq_handlers(binary()) -> ok. +register_iq_handlers(Host) -> + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO, + ?MODULE, process_disco_info), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS, + ?MODULE, process_disco_items), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0, + ?MODULE, process_mix_core), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB, + ?MODULE, process_pubsub_query), + gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2, + ?MODULE, process_mam_query). + +-spec unregister_iq_handlers(binary()) -> ok. +unregister_iq_handlers(Host) -> + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_INFO), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_DISCO_ITEMS), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MIX_CORE_0), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_PUBSUB), + gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2). |