From b5121a346d5775bdac20aa6a52a96434073fb1b6 Mon Sep 17 00:00:00 2001 From: Evgeniy Khramtsov Date: Tue, 8 Mar 2016 20:04:29 +0300 Subject: [PATCH] Experimental MIX (XEP-0369) support --- include/ns.hrl | 7 + src/ejabberd_local.erl | 2 +- src/ejabberd_sm.erl | 1 + src/mod_mix.erl | 329 +++++++++++++++++++++++++++++++++++++++++ src/mod_pubsub.erl | 2 +- src/node_mix.erl | 167 +++++++++++++++++++++ 6 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 src/mod_mix.erl create mode 100644 src/node_mix.erl diff --git a/include/ns.hrl b/include/ns.hrl index 6934195d9..c7f556372 100644 --- a/include/ns.hrl +++ b/include/ns.hrl @@ -157,3 +157,10 @@ -define(NS_HTTP_UPLOAD_OLD, <<"eu:siacs:conversations:http:upload">>). -define(NS_THUMBS_1, <<"urn:xmpp:thumbs:1">>). -define(NS_NICK, <<"http://jabber.org/protocol/nick">>). +-define(NS_MIX_0, <<"urn:xmpp:mix:0">>). +-define(NS_MIX_SERVICEINFO_0, <<"urn:xmpp:mix:0#serviceinfo">>). +-define(NS_MIX_NODES_MESSAGES, <<"urn:xmpp:mix:nodes:messages">>). +-define(NS_MIX_NODES_PRESENCE, <<"urn:xmpp:mix:nodes:presence">>). +-define(NS_MIX_NODES_PARTICIPANTS, <<"urn:xmpp:mix:nodes:participants">>). +-define(NS_MIX_NODES_SUBJECT, <<"urn:xmpp:mix:nodes:subject">>). +-define(NS_MIX_NODES_CONFIG, <<"urn:xmpp:mix:nodes:config">>). diff --git a/src/ejabberd_local.erl b/src/ejabberd_local.erl index 1b7f93c77..292288a37 100644 --- a/src/ejabberd_local.erl +++ b/src/ejabberd_local.erl @@ -32,7 +32,7 @@ %% API -export([start_link/0]). --export([route/3, route_iq/4, route_iq/5, +-export([route/3, route_iq/4, route_iq/5, process_iq/3, process_iq_reply/3, register_iq_handler/4, register_iq_handler/5, register_iq_response_handler/4, register_iq_response_handler/5, unregister_iq_handler/2, diff --git a/src/ejabberd_sm.erl b/src/ejabberd_sm.erl index b2e5a21f3..218e657f3 100644 --- a/src/ejabberd_sm.erl +++ b/src/ejabberd_sm.erl @@ -35,6 +35,7 @@ -export([start/0, start_link/0, route/3, + process_iq/3, open_session/5, open_session/6, close_session/4, diff --git a/src/mod_mix.erl b/src/mod_mix.erl new file mode 100644 index 000000000..047ac8da1 --- /dev/null +++ b/src/mod_mix.erl @@ -0,0 +1,329 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 2 Mar 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(mod_mix). + +-behaviour(gen_server). +-behaviour(gen_mod). + +%% API +-export([start_link/2, start/2, stop/1, process_iq/3, + disco_items/5, disco_identity/5, disco_info/5, + disco_features/5]). + +%% gen_server callbacks +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-include("logger.hrl"). +-include("jlib.hrl"). +-include("pubsub.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]). + +-record(state, {server_host :: binary(), + 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]}, + temporary, 5000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +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]} + 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]} + end; + {error, Err} -> + IQ#iq{type = error, sub_el = [SubEl, Err]} + end; +process_iq(_From, _To, #iq{sub_el = SubEl} = IQ) -> + IQ#iq{type = error, sub_el = [SubEl, ?ERR_BAD_REQUEST]}. + +%%%=================================================================== +%%% 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), + {ok, #state{server_host = ServerHost, host = Host}}. + +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Msg, State) -> + {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 + end, + {noreply, State}; +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%%%=================================================================== +%%% 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 + 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 + end. + +is_item_not_found({error, ErrEl}) -> + case fxml:get_subtag_with_xmlns( + ErrEl, <<"item-not-found">>, ?NS_STANZAS) of + #xmlel{} -> true; + _ -> false + end. + +is_not_subscribed({error, ErrEl}) -> + case fxml:get_subtag_with_xmlns( + ErrEl, <<"not-subscribed">>, ?NS_PUBSUB_ERRORS) of + #xmlel{} -> true; + _ -> false + end. diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl index 61546353f..ed3debbf1 100644 --- a/src/mod_pubsub.erl +++ b/src/mod_pubsub.erl @@ -63,7 +63,7 @@ %% exports for console debug manual use -export([create_node/5, create_node/7, delete_node/3, subscribe_node/5, unsubscribe_node/5, publish_item/6, - delete_item/4, send_items/7, get_items/2, get_item/3, + delete_item/4, delete_item/5, send_items/7, get_items/2, get_item/3, get_cached_item/2, get_configure/5, set_configure/5, tree_action/3, node_action/4, node_call/4]). diff --git a/src/node_mix.erl b/src/node_mix.erl new file mode 100644 index 000000000..b0410a8c3 --- /dev/null +++ b/src/node_mix.erl @@ -0,0 +1,167 @@ +%%%------------------------------------------------------------------- +%%% @author Evgeny Khramtsov +%%% @copyright (C) 2016, Evgeny Khramtsov +%%% @doc +%%% +%%% @end +%%% Created : 8 Mar 2016 by Evgeny Khramtsov +%%%------------------------------------------------------------------- +-module(node_mix). + +-behaviour(gen_pubsub_node). + +%% API +-export([init/3, terminate/2, options/0, features/0, + create_node_permission/6, create_node/2, delete_node/1, + purge_node/2, subscribe_node/8, unsubscribe_node/4, + publish_item/6, delete_item/4, remove_extra_items/3, + get_entity_affiliations/2, get_node_affiliations/1, + get_affiliation/2, set_affiliation/3, + get_entity_subscriptions/2, get_node_subscriptions/1, + get_subscriptions/2, set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, + set_state/1, get_items/7, get_items/3, get_item/7, + get_item/2, set_item/1, get_item_name/3, node_to_path/1, + path_to_node/1]). + +-include("pubsub.hrl"). + +%%%=================================================================== +%%% API +%%%=================================================================== +init(Host, ServerHost, Opts) -> + node_flat:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_flat:terminate(Host, ServerHost). + +options() -> + [{deliver_payloads, true}, + {notify_config, false}, + {notify_delete, false}, + {notify_retract, true}, + {purge_offline, false}, + {persist_items, true}, + {max_items, ?MAXITEMS}, + {subscribe, true}, + {access_model, open}, + {roster_groups_allowed, []}, + {publish_model, open}, + {notification_type, headline}, + {max_payload_size, ?MAX_PAYLOAD_SIZE}, + {send_last_published_item, never}, + {deliver_notifications, true}, + {broadcast_all_resources, true}, + {presence_based_delivery, false}]. + +features() -> + [<<"create-nodes">>, + <<"delete-nodes">>, + <<"delete-items">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>]. + +create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access) -> + node_flat:create_node_permission(Host, ServerHost, Node, ParentNode, Owner, Access). + +create_node(Nidx, Owner) -> + node_flat:create_node(Nidx, Owner). + +delete_node(Removed) -> + node_flat:delete_node(Removed). + +subscribe_node(Nidx, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_flat:subscribe_node(Nidx, Sender, Subscriber, AccessModel, SendLast, + PresenceSubscription, RosterGroup, Options). + +unsubscribe_node(Nidx, Sender, Subscriber, SubId) -> + node_flat:unsubscribe_node(Nidx, Sender, Subscriber, SubId). + +publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload) -> + node_flat:publish_item(Nidx, Publisher, Model, MaxItems, ItemId, Payload). + +remove_extra_items(Nidx, MaxItems, ItemIds) -> + node_flat:remove_extra_items(Nidx, MaxItems, ItemIds). + +delete_item(Nidx, Publisher, PublishModel, ItemId) -> + node_flat:delete_item(Nidx, Publisher, PublishModel, ItemId). + +purge_node(Nidx, Owner) -> + node_flat:purge_node(Nidx, Owner). + +get_entity_affiliations(Host, Owner) -> + node_flat:get_entity_affiliations(Host, Owner). + +get_node_affiliations(Nidx) -> + node_flat:get_node_affiliations(Nidx). + +get_affiliation(Nidx, Owner) -> + node_flat:get_affiliation(Nidx, Owner). + +set_affiliation(Nidx, Owner, Affiliation) -> + node_flat:set_affiliation(Nidx, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_flat:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(Nidx) -> + node_flat:get_node_subscriptions(Nidx). + +get_subscriptions(Nidx, Owner) -> + node_flat:get_subscriptions(Nidx, Owner). + +set_subscriptions(Nidx, Owner, Subscription, SubId) -> + node_flat:set_subscriptions(Nidx, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_flat:get_pending_nodes(Host, Owner). + +get_states(Nidx) -> + node_flat:get_states(Nidx). + +get_state(Nidx, JID) -> + node_flat:get_state(Nidx, JID). + +set_state(State) -> + node_flat:set_state(State). + +get_items(Nidx, From, RSM) -> + node_flat:get_items(Nidx, From, RSM). + +get_items(Nidx, JID, AccessModel, PresenceSubscription, RosterGroup, SubId, RSM) -> + node_flat:get_items(Nidx, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId, RSM). + +get_item(Nidx, ItemId) -> + node_flat:get_item(Nidx, ItemId). + +get_item(Nidx, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) -> + node_flat:get_item(Nidx, ItemId, JID, AccessModel, + PresenceSubscription, RosterGroup, SubId). + +set_item(Item) -> + node_flat:set_item(Item). + +get_item_name(Host, Node, Id) -> + node_flat:get_item_name(Host, Node, Id). + +node_to_path(Node) -> + node_flat:node_to_path(Node). + +path_to_node(Path) -> + node_flat:path_to_node(Path). + +%%%=================================================================== +%%% Internal functions +%%%===================================================================