aboutsummaryrefslogtreecommitdiff
path: root/src/mod_mix.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mod_mix.erl')
-rw-r--r--src/mod_mix.erl924
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).