From 938a4007b3775c91964c91f120b2ad627a891a44 Mon Sep 17 00:00:00 2001 From: Pablo Polvorin Date: Tue, 25 Aug 2009 17:14:30 +0000 Subject: [PATCH] Initial port of pubsub changes(up to r2444) to exmpp branch. pubsub odbc isn't ported yet. Not tested (only basic node creation and configuration), should still have losts of bugs to discover. SVN Revision: 2533 --- src/mod_pubsub/gen_pubsub_node.erl | 7 +- src/mod_pubsub/mod_pubsub.erl | 719 ++++++++++++++++++++----- src/mod_pubsub/node_buddy.erl | 26 +- src/mod_pubsub/node_club.erl | 28 +- src/mod_pubsub/node_dag.erl | 175 ++++++ src/mod_pubsub/node_dispatch.erl | 35 +- src/mod_pubsub/node_flat.erl | 28 +- src/mod_pubsub/node_hometree.erl | 320 ++++++++--- src/mod_pubsub/node_mb.erl | 25 +- src/mod_pubsub/node_pep.erl | 41 +- src/mod_pubsub/node_private.erl | 26 +- src/mod_pubsub/nodetree_dag.erl | 249 +++++++++ src/mod_pubsub/nodetree_tree.erl | 38 +- src/mod_pubsub/pubsub.hrl | 14 +- src/mod_pubsub/pubsub_subscription.erl | 333 ++++++++++++ 15 files changed, 1756 insertions(+), 308 deletions(-) create mode 100644 src/mod_pubsub/node_dag.erl create mode 100644 src/mod_pubsub/nodetree_dag.erl create mode 100644 src/mod_pubsub/pubsub_subscription.erl diff --git a/src/mod_pubsub/gen_pubsub_node.erl b/src/mod_pubsub/gen_pubsub_node.erl index 78256cd56..3762d4a44 100644 --- a/src/mod_pubsub/gen_pubsub_node.erl +++ b/src/mod_pubsub/gen_pubsub_node.erl @@ -46,7 +46,7 @@ behaviour_info(callbacks) -> {create_node, 2}, {delete_node, 1}, {purge_node, 2}, - {subscribe_node, 7}, + {subscribe_node, 8}, {unsubscribe_node, 4}, {publish_item, 6}, {delete_item, 4}, @@ -57,8 +57,9 @@ behaviour_info(callbacks) -> {set_affiliation, 3}, {get_node_subscriptions, 1}, {get_entity_subscriptions, 2}, - {get_subscription, 2}, - {set_subscription, 3}, + {get_subscriptions, 2}, + {set_subscriptions, 4}, + {get_pending_nodes, 2}, {get_states, 1}, {get_state, 2}, {set_state, 1}, diff --git a/src/mod_pubsub/mod_pubsub.erl b/src/mod_pubsub/mod_pubsub.erl index ead9e405f..dfe8ab5a2 100644 --- a/src/mod_pubsub/mod_pubsub.erl +++ b/src/mod_pubsub/mod_pubsub.erl @@ -33,6 +33,14 @@ %%% This module uses version 1.12 of the specification as a base. %%% Most of the specification is implemented. %%% Functions concerning configuration should be rewritten. +%%% +%%% Support for subscription-options and multi-subscribe features was +%%% added by Brian Cully . Subscriptions and options are +%%% stored in the pubsub_subscription table, with a link to them provided +%%% by the subscriptions field of pubsub_state. For information on +%%% subscription-options and mulit-subscribe see XEP-0060 sections 6.1.6, +%%% 6.2.3.1, 6.2.3.5, and 6.3. For information on subscription leases see +%%% XEP-0060 section 12.18. %%% TODO %%% plugin: generate Reply (do not use broadcast atom anymore) @@ -47,6 +55,7 @@ -include_lib("exmpp/include/exmpp.hrl"). -include("ejabberd.hrl"). +-include("adhoc.hrl"). -include("pubsub.hrl"). -define(STDTREE, "tree"). @@ -73,7 +82,7 @@ %% exports for console debug manual use -export([create_node/5, delete_node/3, - subscribe_node/4, + subscribe_node/5, unsubscribe_node/5, publish_item/6, delete_item/4, @@ -81,7 +90,7 @@ get_items/2, get_item/3, get_cached_item/2, - broadcast_stanza/7, + broadcast_stanza/8, get_configure/5, set_configure/5, tree_action/3, @@ -129,6 +138,12 @@ plugins = [?STDNODE], send_loop}). + +%%------------------- Ad hoc commands nodes -------------------------- +-define(NS_PUBSUB_GET_PENDING, "http://jabber.org/protocol/pubsub#get-pending"). +%%-------------------------------------------------------------------- + + %%==================================================================== %% API %%==================================================================== @@ -173,17 +188,17 @@ init([ServerHost, Opts]) -> ServerHostB = list_to_binary(ServerHost), pubsub_index:init(Host, ServerHost, Opts), ets:new(gen_mod:get_module_proc(Host, config), [set, named_table]), - ets:new(gen_mod:get_module_proc(ServerHostB, config), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, config), [set, named_table]), ets:new(gen_mod:get_module_proc(Host, last_items), [set, named_table]), - ets:new(gen_mod:get_module_proc(ServerHostB, last_items), [set, named_table]), + ets:new(gen_mod:get_module_proc(ServerHost, last_items), [set, named_table]), {Plugins, NodeTree, PepMapping} = init_plugins(Host, ServerHost, Opts), mod_disco:register_feature(ServerHost, ?NS_PUBSUB_s), ets:insert(gen_mod:get_module_proc(Host, config), {nodetree, NodeTree}), ets:insert(gen_mod:get_module_proc(Host, config), {plugins, Plugins}), ets:insert(gen_mod:get_module_proc(Host, config), {last_item_cache, LastItemCache}), - ets:insert(gen_mod:get_module_proc(ServerHostB, config), {nodetree, NodeTree}), - ets:insert(gen_mod:get_module_proc(ServerHostB, config), {plugins, Plugins}), - ets:insert(gen_mod:get_module_proc(ServerHostB, config), {pep_mapping, PepMapping}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {nodetree, NodeTree}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {plugins, Plugins}), + ets:insert(gen_mod:get_module_proc(ServerHost, config), {pep_mapping, PepMapping}), ejabberd_hooks:add(disco_sm_identity, ServerHostB, ?MODULE, disco_sm_identity, 75), ejabberd_hooks:add(disco_sm_features, ServerHostB, ?MODULE, disco_sm_features, 75), ejabberd_hooks:add(disco_sm_items, ServerHostB, ?MODULE, disco_sm_items, 75), @@ -205,7 +220,9 @@ init([ServerHost, Opts]) -> false -> ok end, - update_database(Host, ServerHost), + ejabberd_router:register_route(Host), + update_node_database(Host, ServerHost), + update_state_database(Host, ServerHost), init_nodes(Host, ServerHost), State = #state{host = Host, server_host = ServerHost, @@ -255,17 +272,17 @@ terminate_plugins(Host, ServerHost, Plugins, TreePlugin) -> TreePlugin:terminate(Host, ServerHost), ok. -init_nodes(Host, ServerHost) -> - create_node(Host, ServerHost, ["home"], service_jid(Host), ?STDNODE), - create_node(Host, ServerHost, ["home", ServerHost], service_jid(Host), ?STDNODE), +init_nodes(_Host, _ServerHost) -> + %create_node(Host, ServerHost, ["home"], service_jid(Host), "hometree"), + %create_node(Host, ServerHost, ["home", ServerHost], service_jid(Host), "hometree"), ok. -update_database(Host, ServerHost) -> +update_node_database(Host, ServerHost) -> mnesia:del_table_index(pubsub_node, type), mnesia:del_table_index(pubsub_node, parentid), case catch mnesia:table_info(pubsub_node, attributes) of [host_node, host_parent, info] -> - ?INFO_MSG("upgrade pubsub tables",[]), + ?INFO_MSG("upgrade node pubsub tables",[]), F = fun() -> lists:foldl( fun({pubsub_node, NodeId, ParentId, {nodeinfo, Items, Options, Entities}}, {RecList, NodeIdx}) -> @@ -292,11 +309,11 @@ update_database(Host, ServerHost) -> _ -> IAcc end end, [], ItemsList), - mnesia:write( - #pubsub_state{stateid = {JID, NodeIdx}, - items = UsrItems, - affiliation = Aff, - subscription = Sub}), + mnesia:write({pubsub_state, + {JID, NodeIdx}, + UsrItems, + Aff, + Sub}), case Aff of owner -> [JID | Acc]; _ -> Acc @@ -305,7 +322,7 @@ update_database(Host, ServerHost) -> mnesia:delete({pubsub_node, NodeId}), {[#pubsub_node{nodeid = NodeId, id = NodeIdx, - parent = element(2, ParentId), + parents = [element(2, ParentId)], owners = Owners, options = Options} | RecList], NodeIdx + 1} @@ -324,21 +341,23 @@ update_database(Host, ServerHost) -> end, case mnesia:transaction(FNew) of {atomic, Result} -> - ?INFO_MSG("Pubsub tables updated correctly: ~p", [Result]); + ?INFO_MSG("Pubsub node tables updated correctly: ~p", + [Result]); {aborted, Reason} -> - ?ERROR_MSG("Problem updating Pubsub tables:~n~p", [Reason]) + ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", + [Reason]) end; [nodeid, parentid, type, owners, options] -> F = fun({pubsub_node, NodeId, {_, Parent}, Type, Owners, Options}) -> #pubsub_node{ nodeid = NodeId, id = 0, - parent = Parent, + parents = [Parent], type = Type, owners = Owners, options = Options} end, - mnesia:transform_table(pubsub_node, F, [nodeid, id, parent, type, owners, options]), + mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]), FNew = fun() -> lists:foldl(fun(#pubsub_node{nodeid = NodeId} = PubsubNode, NodeIdx) -> mnesia:write(PubsubNode#pubsub_node{id = NodeIdx}), @@ -364,9 +383,61 @@ update_database(Host, ServerHost) -> end, case mnesia:transaction(FNew) of {atomic, Result} -> - ?INFO_MSG("Pubsub tables updated correctly: ~p", [Result]); + ?INFO_MSG("Pubsub node tables updated correctly: ~p", + [Result]); {aborted, Reason} -> - ?ERROR_MSG("Problem updating Pubsub tables:~n~p", [Reason]) + ?ERROR_MSG("Problem updating Pubsub node tables:~n~p", + [Reason]) + end; + [nodeid, id, parent, type, owners, options] -> + F = fun({pubsub_node, NodeId, Id, Parent, Type, Owners, Options}) -> + #pubsub_node{ + nodeid = NodeId, + id = Id, + parents = [Parent], + type = Type, + owners = Owners, + options = Options} + end, + mnesia:transform_table(pubsub_node, F, [nodeid, id, parents, type, owners, options]); + _ -> + ok + end. + +update_state_database(_Host, _ServerHost) -> + case catch mnesia:table_info(pubsub_state, attributes) of + [stateid, items, affiliation, subscription] -> + ?INFO_MSG("upgrade state pubsub tables", []), + F = fun ({pubsub_state, {JID, NodeID}, Items, Aff, Sub}, Acc) -> + Subs = case Sub of + none -> + []; + _ -> + {result, SubID} = pubsub_subscription:subscribe_node(JID, NodeID, []), + [{Sub, SubID}] + end, + NewState = #pubsub_state{stateid = {JID, NodeID}, + items = Items, + affiliation = Aff, + subscriptions = Subs}, + [NewState | Acc] + end, + {atomic, NewRecs} = mnesia:transaction(fun mnesia:foldl/3, + [F, [], pubsub_state]), + {atomic, ok} = mnesia:delete_table(pubsub_state), + {atomic, ok} = mnesia:create_table(pubsub_state, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_state)}]), + FNew = fun () -> + lists:foreach(fun mnesia:write/1, NewRecs) + end, + case mnesia:transaction(FNew) of + {atomic, Result} -> + ?INFO_MSG("Pubsub state tables updated correctly: ~p", + [Result]); + {aborted, Reason} -> + ?ERROR_MSG("Problem updating Pubsub state tables:~n~p", + [Reason]) end; _ -> ok @@ -625,7 +696,7 @@ disco_sm_items(Acc, From, To, NodeB, _Lang) -> presence_probe(JID, JID, Pid) -> {U, S, R} = jlib:short_prepd_jid(JID), - Host = S, % exmpp_jid:prep_domain_as_list(JID), + Host = exmpp_jid:prep_domain_as_list(JID), Proc = gen_mod:get_module_proc(Host, ?PROCNAME), gen_server:cast(Proc, {presence, JID, Pid}), gen_server:cast(Proc, {presence, U, S, [R], JID}); @@ -728,7 +799,7 @@ handle_cast({remove_user, LUser, LServer}, State) -> handle_cast({unsubscribe, Subscriber, Owner}, State) -> Host = State#state.host, - BJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + BJID = jlib:short_prepd_bare_jid(Owner), lists:foreach(fun(PType) -> {result, Subscriptions} = node_action(Host, PType, get_entity_subscriptions, [Host, Subscriber]), lists:foreach(fun @@ -840,7 +911,7 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> QAttrs = SubEl#xmlel.attrs, Node = exmpp_xml:get_attribute_from_list_as_list(QAttrs, 'node', ""), - ServerHostB = exmpp_jid:prep_domain(ServerHost), + ServerHostB = list_to_binary(ServerHost), Info = ejabberd_hooks:run_fold( disco_info, ServerHostB, [], [ServerHost, ?MODULE, <<>>, ""]), @@ -883,6 +954,8 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> lang = Lang, payload = SubEl} -> Res = case iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) of + {result, []} -> + exmpp_iq:result(Packet); {result, IQRes} -> exmpp_iq:result(Packet, IQRes); {error, Error} -> @@ -895,6 +968,15 @@ do_route(ServerHost, Access, Plugins, Host, From, To, Packet) -> children = iq_get_vcard(Lang)}, Res = exmpp_iq:result(Packet, VCard), ejabberd_router:route(To, From, Res); + #iq{type = set, ns = ?NS_ADHOC} = IQ -> + Res = case iq_command(Host, ServerHost, From, IQ, Access, Plugins) of + {error, Error} -> + exmpp_iq:error(Packet, Error); + {result, IQRes} -> + exmpp_iq:result(Packet, IQRes) + end, + ejabberd_router:route(To, From, Res); + #iq{} -> Err = exmpp_iq:error(Packet, 'feature-not-implemented'), @@ -1096,7 +1178,7 @@ iq_get_vcard(Lang) -> iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang) -> iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, all, plugins(ServerHost)). -iq_pubsub(Host, ServerHost, From, IQType, SubEl, _Lang, Access, Plugins) -> +iq_pubsub(Host, ServerHost, From, IQType, SubEl, Lang, Access, Plugins) -> WithoutCdata = exmpp_xml:remove_cdata_from_list(SubEl#xmlel.children), Configuration = lists:filter(fun(#xmlel{name = 'configure'}) -> true; (_) -> false @@ -1160,8 +1242,12 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, _Lang, Access, Plugins) -> "item-required")} end; {set, 'subscribe'} -> + Config = case Configuration of + [#xmlel{name = 'configure', children = C}] -> C; + _ -> [] + end, JID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'jid', ""), - subscribe_node(Host, Node, From, JID); + subscribe_node(Host, Node, From, JID, Config); {set, 'unsubscribe'} -> JID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'jid', ""), SubId = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'subid', ""), @@ -1184,9 +1270,13 @@ iq_pubsub(Host, ServerHost, From, IQType, SubEl, _Lang, Access, Plugins) -> {get, 'affiliations'} -> get_affiliations(Host, From, Plugins); {get, "options"} -> - {error, extended_error('feature-not-implemented', unsupported, "subscription-options")}; + SubID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'subid', ""), + JID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'jid', ""), + get_options(Host, Node, JID, SubID, Lang); {set, "options"} -> - {error, extended_error('feature-not-implemented', unsupported, "subscription-options")}; + SubID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'subid', ""), + JID = exmpp_xml:get_attribute_from_list_as_list(Attrs, 'jid', ""), + set_options(Host, Node, JID, SubID, Els); _ -> {error, 'feature-not-implemented'} end; @@ -1231,6 +1321,146 @@ iq_pubsub_owner(Host, ServerHost, From, IQType, SubEl, Lang) -> {error, 'bad-request'} end. +iq_command(Host, ServerHost, From, IQ, Access, Plugins) -> + case adhoc:parse_request(IQ) of + Req when is_record(Req, adhoc_request) -> + case adhoc_request(Host, ServerHost, From, Req, Access, Plugins) of + Resp when is_record(Resp, adhoc_response) -> + {result, [adhoc:produce_response(Req, Resp)]}; + Error -> + Error + end; + Err -> + Err + end. + +%% @doc

Processes an Ad Hoc Command.

+adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + lang = Lang, + action = "execute", + xdata = false}, + _Access, Plugins) -> + send_pending_node_form(Host, Owner, Lang, Plugins); +adhoc_request(Host, _ServerHost, Owner, + #adhoc_request{node = ?NS_PUBSUB_GET_PENDING, + action = "execute", + xdata = XData}, + _Access, _Plugins) -> + ParseOptions = case XData of + #xmlel{name = 'x'} = XEl -> + case jlib:parse_xdata_submit(XEl) of + invalid -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'bad-request')}; + XData2 -> + case set_xoption(XData2, []) of + NewOpts when is_list(NewOpts) -> + {result, NewOpts}; + Err -> + Err + end + end; + _ -> + ?INFO_MSG("Bad XForm: ~p", [XData]), + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'bad-request')} + end, + case ParseOptions of + {result, XForm} -> + case lists:keysearch(node, 1, XForm) of + {value, {_, Node}} -> + send_pending_auth_events(Host, Node, Owner); + false -> + {error, extended_error('bad-request', "bad-payload")} + end; + Error -> + Error + end; +adhoc_request(_Host, _ServerHost, _Owner, Other, _Access, _Plugins) -> + ?DEBUG("Couldn't process ad hoc command:~n~p", [Other]), + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}. + +%% @spec (Host, Owner, Lang, Plugins) -> iqRes() +%% @doc

Sends the process pending subscriptions XForm for Host to +%% Owner.

+send_pending_node_form(Host, Owner, _Lang, Plugins) -> + Filter = + fun (Plugin) -> + lists:member("get-pending", features(Plugin)) + end, + case lists:filter(Filter, Plugins) of + [] -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'feature-not-implemented')}; + Ps -> + XOpts = lists:map(fun (Node) -> + #xmlel{ns = ?NS_DATA_FORMS, name='option', + children = [ + #xmlel{ns = ?NS_DATA_FORMS, name = 'value', + children = [ + exmpp_xml:cdata(node_to_string(Node))]}]} + end, get_pending_nodes(Host, Owner, Ps)), + XForm = #xmlel{ns = ?NS_DATA_FORMS, name ='x', attrs = [?XMLATTR('type', <<"form">>)], + children = [ + #xmlel{ns = ?NS_DATA_FORMS, name = 'field', + attrs = [?XMLATTR('type', <<"list-single">>), + ?XMLATTR('var', <<"pubsub#node">>)], + children = lists:usort(XOpts)}]}, + #adhoc_response{status = executing, + defaultaction = "execute", + elements = [XForm]} + end. + +get_pending_nodes(Host, Owner, Plugins) -> + Tr = + fun (Type) -> + case node_call(Type, get_pending_nodes, [Host, Owner]) of + {result, Nodes} -> Nodes; + _ -> [] + end + end, + case transaction(fun () -> {result, lists:flatmap(Tr, Plugins)} end, + sync_dirty) of + {result, Res} -> Res; + Err -> Err + end. + +%% @spec (Host, Node, Owner) -> iqRes() +%% @doc

Send a subscription approval form to Owner for all pending +%% subscriptions on Host and Node.

+send_pending_auth_events(Host, Node, Owner) -> + ?DEBUG("Sending pending auth events for ~s on ~s:~s", + [exmpp_jid:jid_to_string(Owner), Host, node_to_string(Node)]), + Action = + fun (#pubsub_node{id = NodeID, type = Type} = N) -> + case lists:member("get-pending", features(Type)) of + true -> + case node_call(Type, get_affiliation, [NodeID, Owner]) of + {result, owner} -> + broadcast_pending_auth_events(N), + {result, ok}; + _ -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'forbidden')} + end; + false -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'feature-not-implemented')} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, _} -> + #adhoc_response{}; + Err -> + Err + end. + +broadcast_pending_auth_events(#pubsub_node{type = Type, id = NodeID} = Node) -> + {result, Subscriptions} = node_call(Type, get_node_subscriptions, [NodeID]), + lists:foreach(fun ({J, pending, _SubID}) -> + {U, S, R} = J, + send_authorization_request(Node, exmpp_jid:make(U,S,R)); + ({J, pending}) -> + {U, S, R} = J, + send_authorization_request(Node, exmpp_jid:make(U,S,R)) + end, Subscriptions). + %%% authorization handling send_authorization_request(#pubsub_node{owners = Owners, nodeid = {Host, Node}}, Subscriber) -> @@ -1261,7 +1491,8 @@ send_authorization_request(#pubsub_node{owners = Owners, nodeid = {Host, Node}}, ?XMLATTR('label', translate:translate(Lang, "Allow this Jabber ID to subscribe to this pubsub node?"))], children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'value', children = [#xmlcdata{cdata = <<"false">>}]}]}]}]}, lists:foreach(fun(Owner) -> - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(Owner), Stanza} + {U, S, R} = Owner, + ejabberd_router ! {route, service_jid(Host), exmpp_jid:make(U, S, R), Stanza} end, Owners). find_authorization_response(Packet) -> @@ -1322,19 +1553,14 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> end, Action = fun(#pubsub_node{type = Type, id = NodeId, owners = Owners}) -> IsApprover = lists:member(jlib:short_prepd_bare_jid(From), Owners), - {result, Subscription} = node_call(Type, get_subscription, [NodeId, Subscriber]), + {result, Subscriptions} = node_call(Type, get_subscriptions, [NodeId, Subscriber]), if not IsApprover -> {error, 'forbidden'}; - Subscription /= pending -> - {error, 'unexpected-request'}; true -> - NewSubscription = case Allow of - true -> subscribed; - false -> none - end, - send_authorization_approval(Host, Subscriber, SNode, NewSubscription), - node_call(Type, set_subscription, [NodeId, Subscriber, NewSubscription]) + update_auth(Host, SNode, Type, NodeId, + Subscriber, Allow, + Subscriptions) end end, case transaction(Host, Node, Action, sync_dirty) of @@ -1356,6 +1582,26 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> exmpp_stanza:reply_with_error(Packet, 'not-acceptable')) end. +update_auth(Host, Node, Type, NodeId, Subscriber, + Allow, Subscriptions) -> + Subscription = lists:filter(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), + case Subscription of + [{pending, SubID}] -> %% TODO does not work if several pending + NewSubscription = case Allow of + true -> subscribed; + false -> none + end, + node_call(Type, set_subscriptions, + [NodeId, Subscriber, NewSubscription, SubID]), + send_authorization_approval(Host, Subscriber, Node, + NewSubscription), + {result, ok}; + _ -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'unexpected-request')} + end. + -define(XFIELD(Type, Label, Var, Val), #xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR('type', Type), ?XMLATTR('label', translate:translate(Lang, Label)), @@ -1372,6 +1618,16 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> -define(STRINGXFIELD(Label, Var, Val), ?XFIELD("text-single", Label, Var, Val)). +-define(STRINGMXFIELD(Label, Var, Vals), + #xmlel{ns = ?NS_DATA_FORMS, + name = 'field', + attrs = [?XMLATTR('type', <<"text-multi">>), + ?XMLATTR('label', translate:translate(Lang, Label)), + ?XMLATTR('var', Var) + ], + children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'value', + children = [?XMLCDATA(V)]} || V <- Vals]}). + -define(XFIELDOPT(Type, Label, Var, Val, Opts), #xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR('type', Type), ?XMLATTR('label', translate:translate(Lang, Label)), @@ -1387,7 +1643,7 @@ handle_authorization_response(Host, From, To, Packet, XFields) -> ?XFIELDOPT("list-single", Label, Var, Val, Opts)). -define(LISTMXFIELD(Label, Var, Vals, Opts), - #xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR('type', Type), + #xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR('type', <<"list-multi">>), ?XMLATTR('label', translate:translate(Lang, Label)), ?XMLATTR('var', Var)], children = lists:map(fun(Opt) -> @@ -1484,8 +1740,8 @@ create_node(Host, ServerHost, Node, Owner, GivenType, Access, Configuration) -> {error, 'forbidden'} end end, - Reply = [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = - [#xmlel{ns = ?NS_PUBSUB, name = 'create', attrs = nodeAttr(Node)}]}], + Reply = #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + [#xmlel{ns = ?NS_PUBSUB, name = 'create', attrs = nodeAttr(Node)}]}, case transaction(CreateNode, transaction) of {result, {Result, broadcast}} -> %%Lang = "en", %% TODO: fix @@ -1567,7 +1823,7 @@ delete_node(Host, Node, Owner) -> Error end. -%% @spec (Host, Node, From, JID) -> +%% @spec (Host, Node, From, JID, Configuration) -> %% {error, Reason::stanzaError()} | %% {result, []} %% Host = host() @@ -1589,7 +1845,8 @@ delete_node(Host, Node, Owner) -> %%
  • The node does not support subscriptions.
  • %%
  • The node does not exist.
  • %% -subscribe_node(Host, Node, From, JID) -> +subscribe_node(Host, Node, From, JID, Configuration) -> + {result, SubOpts} = pubsub_subscription:parse_options_xform(Configuration), Subscriber = try jlib:short_prepd_jid(exmpp_jid:parse(JID)) catch @@ -1600,6 +1857,8 @@ subscribe_node(Host, Node, From, JID) -> Action = fun(#pubsub_node{options = Options, owners = [Owner|_], type = Type, id = NodeId}) -> Features = features(Type), SubscribeFeature = lists:member("subscribe", Features), + OptionsFeature = lists:member("subscription-options", Features), + HasOptions = not (SubOpts == []), SubscribeConfig = get_option(Options, subscribe), AccessModel = get_option(Options, access_model), SendLast = get_option(Options, send_last_published_item), @@ -1626,11 +1885,15 @@ subscribe_node(Host, Node, From, JID) -> not SubscribeConfig -> %% Node does not support subscriptions {error, extended_error('feature-not-implemented', unsupported, "subscribe")}; + HasOptions andalso not OptionsFeature -> + %% Node does not support subscription options + {error, extended_error('feature-not-implemented', unsupported, "subscription-options")}; true -> node_call(Type, subscribe_node, [NodeId, From, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup]) + PresenceSubscription, RosterGroup, + SubOpts]) end end, Reply = fun(Subscription) -> @@ -1639,12 +1902,12 @@ subscribe_node(Host, Node, From, JID) -> [?XMLATTR('node', node_to_string(Node)), ?XMLATTR('jid', exmpp_jid:to_binary(Subscriber)), ?XMLATTR('subscription', subscription_to_string(Subscription))], - [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'subscription', attrs = case Subscription of subscribed -> [?XMLATTR('subid', SubId)|Fields]; _ -> Fields - end}]}] + end}]} end, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, subscribed, send_last}}} -> @@ -1761,9 +2024,9 @@ publish_item(Host, ServerHost, Node, Publisher, ItemId, Payload) -> end end, ejabberd_hooks:run(pubsub_publish_item, ServerHost, [ServerHost, Node, Publisher, service_jid(Host), ItemId, Payload]), - Reply = [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + Reply = #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'publish', attrs = nodeAttr(Node), children = - [#xmlel{ns = ?NS_PUBSUB, name = 'item', attrs = itemAttr(ItemId)}]}]}], + [#xmlel{ns = ?NS_PUBSUB, name = 'item', attrs = itemAttr(ItemId)}]}]}, case transaction(Host, Node, Action, sync_dirty) of {result, {TNode, {Result, broadcast, Removed}}} -> NodeId = TNode#pubsub_node.id, @@ -1988,9 +2251,9 @@ get_items(Host, Node, From, SubId, SMaxItems, ItemIDs) -> end, %% Generate the XML response (Item list), limiting the %% number of items sent to MaxItems: - {result, [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'items', attrs = nodeAttr(Node), children = - itemsEls(lists:sublist(SendItems, MaxItems))}]}]}; + itemsEls(lists:sublist(SendItems, MaxItems))}]}}; Error -> Error end @@ -2027,9 +2290,10 @@ send_items(Host, Node, NodeId, Type, LJID, last) -> send_items(Host, Node, NodeId, Type, LJID, 1); LastItem -> Stanza = event_stanza( - [{xmlelement, "items", nodeAttr(Node), - itemsEls([LastItem])}]), - ejabberd_router ! {route, service_jid(Host), jlib:make_jid(LJID), Stanza} + [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'items', attrs = nodeAttr(Node), + children = itemsEls(LastItem)}]), + {U, S, R} = LJID, + ejabberd_router ! {route, service_jid(Host), exmpp_jid:make(U, S, R), Stanza} end; send_items(Host, Node, NodeId, Type, {LU, LS, LR} = LJID, Number) -> ToSend = case node_action(Host, Type, get_items, [NodeId, LJID]) of @@ -2078,9 +2342,9 @@ get_affiliations(Host, JID, Plugins) when is_list(Plugins) -> [?XMLATTR('node', node_to_string(Node)), ?XMLATTR('affiliation', affiliation_to_string(Affiliation))]}] end, lists:usort(lists:flatten(Affiliations))), - {result, [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'affiliations', children = - Entities}]}]}; + Entities}]}}; {Error, _} -> Error end; @@ -2111,9 +2375,9 @@ get_affiliations(Host, Node, JID) -> [?XMLATTR('jid', exmpp_jid:to_binary(AU, AS, AR)), ?XMLATTR('affiliation', affiliation_to_string(Affiliation))]}] end, Affiliations), - {result, [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'affiliations', attrs = nodeAttr(Node), children = - Entities}]}]}; + Entities}]}}; Error -> Error end. @@ -2186,6 +2450,106 @@ set_affiliations(Host, Node, From, EntitiesEls) -> end end. +get_options(Host, Node, JID, SubID, Lang) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + get_options_helper(JID, Lang, NodeID, SubID, Type); + false -> + {error, extended_error( + 'feature-not-implemented', + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, XForm}} -> {result, [XForm]}; + Error -> Error + end. + +get_options_helper(JID, Lang, NodeID, SubID, Type) -> + Subscriber = try exmpp_jid:parse(JID) of + J -> jlib:short_jid(J) + catch + _ -> + {"", "", ""} %%pablo TODO: "" or <<>> ?. short_jid uses exmpp_jid:node/1, etc. that returns binaries + end, + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error('not-acceptable', "not-subscribed")}; + {[], [SID]} -> + read_sub(Subscriber, NodeID, SID, Lang); + {[], _} -> + {error, extended_error('not-acceptable', "subid-required")}; + {_, _} -> + read_sub(Subscriber, NodeID, SubID, Lang) + end. + +read_sub(Subscriber, NodeID, SubID, Lang) -> + case pubsub_subscription:get_subscription(Subscriber, NodeID, SubID) of + {error, notfound} -> + {error, extended_error('not-acceptable', "invalid-subid")}; + {result, #pubsub_subscription{options = Options}} -> + pubsub_subscription:get_options_xform(Lang, Options) + end. + +set_options(Host, Node, JID, SubID, Configuration) -> + Action = fun(#pubsub_node{type = Type, id = NodeID}) -> + case lists:member("subscription-options", features(Type)) of + true -> + set_options_helper(Configuration, JID, NodeID, + SubID, Type); + false -> + {error, extended_error( + 'feature-not-implemented', + unsupported, "subscription-options")} + end + end, + case transaction(Host, Node, Action, sync_dirty) of + {result, {_Node, Result}} -> {result, Result}; + Error -> Error + end. + +set_options_helper(Configuration, JID, NodeID, SubID, Type) -> + Subscriber = try exmpp_jid:parse(JID) of + J -> jlib:short_jid(J) + catch + _ -> + {"", "", ""} %%pablo TODO: "" or <<>> ?. short_jid uses exmpp_jid:node/1, etc. that returns binaries + end, + {result, SubOpts} = pubsub_subscription:parse_options_xform(Configuration), + {result, Subs} = node_call(Type, get_subscriptions, + [NodeID, Subscriber]), + SubIDs = lists:foldl(fun({subscribed, SID}, Acc) -> + [SID | Acc]; + (_, Acc) -> + Acc + end, [], Subs), + case {SubID, SubIDs} of + {_, []} -> + {error, extended_error('not-acceptable', "not-subscribed")}; + {[], [SID]} -> + write_sub(Subscriber, NodeID, SID, SubOpts); + {[], _} -> + {error, extended_error('not-acceptable', "subid-required")}; + {_, _} -> + write_sub(Subscriber, NodeID, SubID, SubOpts) + end. + +write_sub(Subscriber, NodeID, SubID, Options) -> + case pubsub_subscription:set_subscription(Subscriber, NodeID, SubID, + Options) of + {error, notfound} -> + {error, extended_error('not-acceptable', "invalid-subid")}; + {result, _} -> + {result, []} + end. %% @spec (Host, Node, JID, Plugins) -> {error, Reason} | {result, Response} %% Host = host() @@ -2205,7 +2569,7 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> %% Service does not support retreive subscriptions {{error, extended_error('feature-not-implemented', unsupported, "retrieve-subscriptions")}, Acc}; true -> - Subscriber = jlib:jid_remove_resource(JID), + Subscriber = exmpp_jid:bare(JID), {result, Subscriptions} = node_action(Host, Type, get_entity_subscriptions, [Host, Subscriber]), {Status, [Subscriptions|Acc]} end @@ -2229,6 +2593,21 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> end; ({_, none, _}) -> []; + ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubID, SubJID}) -> + case Node of + [] -> + [#xmlel{ns = ?NS_PUBSUB, name='subscription', + attrs = [?XMLATTR('jid', exmpp_jid:jid_to_binary(SubJID)), + ?XMLATTR('subid', SubID), + ?XMLATTR('subscription', subscription_to_string(Subscription)) | nodeAttr(SubsNode)]}]; + SubsNode -> + [#xmlel{ns = ?NS_PUBSUB, name = 'subscription', + attrs = [?XMLATTR('jid', exmpp_jid:jid_to_binary(SubJID)), + ?XMLATTR('subid', SubID), + ?XMLATTR('subscription', subscription_to_string(Subscription))]}]; + _ -> + [] + end; ({#pubsub_node{nodeid = {_, SubsNode}}, Subscription, SubJID}) -> case Node of [] -> @@ -2244,9 +2623,9 @@ get_subscriptions(Host, Node, JID, Plugins) when is_list(Plugins) -> [] end end, lists:usort(lists:flatten(Subscriptions))), - {result, [#xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB, name = 'subscriptions', children = - Entities}]}]}; + Entities}]}}; {Error, _} -> Error end. @@ -2282,9 +2661,9 @@ get_subscriptions(Host, Node, JID) -> ?XMLATTR('subscription', subscription_to_string(Subscription)), ?XMLATTR('subid', SubId)]}] end, Subscriptions), - {result, [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'subscriptions', attrs = nodeAttr(Node), children = - Entities}]}]}; + Entities}]}}; Error -> Error end. @@ -2345,9 +2724,10 @@ set_subscriptions(Host, Node, From, EntitiesEls) -> %% @spec (OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, SubscriberResource}, AllowedGroups) %% -> {PresenceSubscription, RosterGroup} get_roster_info(OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, _}, AllowedGroups) -> + OwnerServerB = list_to_binary(OwnerServer), {Subscription, Groups} = ejabberd_hooks:run_fold( - roster_get_jid_info, OwnerServer, + roster_get_jid_info, OwnerServerB, {none, []}, [OwnerUser, OwnerServer, {SubscriberUser, SubscriberServer, undefined}]), PresenceSubscription = (Subscription == both) orelse (Subscription == from) @@ -2419,20 +2799,42 @@ service_jid(Host) -> _ -> exmpp_jid:make(Host) end. -%% @spec (LJID, Subscription, PresenceDelivery) -> boolean() +%% @spec (LJID, PresenceDelivery) -> boolean() %% LJID = jid() -%% Subscription = atom() -%% PresenceDelivery = boolean() -%% @doc

    Check if a notification must be delivered or not.

    -is_to_deliver(_, none, _) -> false; -is_to_deliver(_, pending, _) -> false; -is_to_deliver(_, _, false) -> true; -is_to_deliver({User, Server, _}, _, true) -> +%% NotifyType = items | nodes +%% Depth = integer() +%% NodeOptions = [{atom(), term()}] +%% SubOptions = [{atom(), term()}] +%% @doc

    Check if a notification must be delivered or not based on +%% node and subscription options.

    +is_to_deliver(LJID, NotifyType, Depth, NodeOptions, SubOptions) -> + sub_to_deliver(LJID, NotifyType, Depth, SubOptions) + andalso node_to_deliver(LJID, NodeOptions). + +sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) -> + lists:all(fun (Option) -> + sub_option_can_deliver(NotifyType, Depth, Option) + end, SubOptions). + +sub_option_can_deliver(items, _, {subscription_type, nodes}) -> false; +sub_option_can_deliver(nodes, _, {subscription_type, items}) -> false; +sub_option_can_deliver(_, _, {subscription_depth, all}) -> true; +sub_option_can_deliver(_, Depth, {subscription_depth, D}) -> Depth =< D; +sub_option_can_deliver(_, _, {deliver, false}) -> false; +sub_option_can_deliver(_, _, {expire, When}) -> now() < When; +sub_option_can_deliver(_, _, _) -> true. + +node_to_deliver(LJID, NodeOptions) -> + PresenceDelivery = get_option(NodeOptions, presence_based_delivery), + presence_can_deliver(LJID, PresenceDelivery). + +presence_can_deliver(_, false) -> true; +presence_can_deliver({User, Server, _}, true) -> case mnesia:dirty_match_object({session, '_', '_', {User, Server}, '_', '_'}) of [] -> false; Ss -> lists:foldl(fun({session, _, _, _, undefined, _}, Acc) -> Acc; - ({session, _, _, _, _Priority, _}, _Acc) -> true + ({session, _, _, _, _Priority, _}, _Acc) -> true end, false, Ss) end. @@ -2455,10 +2857,11 @@ event_stanza(Els) -> broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, _From, Payload) -> %broadcast(Host, Node, NodeId, Options, none, true, 'items', ItemEls) - case node_action(Host, Type, get_node_subscriptions, [NodeId]) of - {result, []} -> + case get_collection_subscriptions(Host, Node) of + [] -> {result, false}; - {result, Subs} -> + + SubsByDepth when is_list(SubsByDepth) -> Content = case get_option(Options, deliver_payloads) of true -> Payload; false -> [] @@ -2466,7 +2869,7 @@ broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, _From Stanza = event_stanza( [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'items', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'item', attrs = itemAttr(ItemId), children = Content}]}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, Stanza), + broadcast_stanza(Host, Node, NodeId, Type, Options, SubsByDepth, items, Stanza), case Removed of [] -> ok; @@ -2476,7 +2879,7 @@ broadcast_publish_item(Host, Node, NodeId, Type, Options, Removed, ItemId, _From RetractStanza = event_stanza( [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'items', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'retract', attrs = itemAttr(RId)} || RId <- Removed]}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, RetractStanza); + broadcast_stanza(Host, Node, NodeId, Type, Options, SubsByDepth, items, RetractStanza); _ -> ok end @@ -2494,14 +2897,15 @@ broadcast_retract_items(Host, Node, NodeId, Type, Options, ItemIds, ForceNotify) %broadcast(Host, Node, NodeId, Options, notify_retract, ForceNotify, 'retract', RetractEls) case (get_option(Options, notify_retract) or ForceNotify) of true -> - case node_action(Host, Type, get_node_subscriptions, [NodeId]) of - {result, []} -> + case get_collection_subscriptions(Host, Node) of + [] -> {result, false}; - {result, Subs} -> + + SubsByDepth when is_list(SubsByDepth)-> Stanza = event_stanza( [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'items', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'retract', attrs = itemAttr(ItemId)} || ItemId <- ItemIds]}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, Stanza), + broadcast_stanza(Host, Node, NodeId, Type, Options, SubsByDepth, items, Stanza), {result, true}; _ -> {result, false} @@ -2514,13 +2918,13 @@ broadcast_purge_node(Host, Node, NodeId, Type, Options) -> %broadcast(Host, Node, NodeId, Options, notify_retract, false, 'purge', []) case get_option(Options, notify_retract) of true -> - case node_action(Host, Type, get_node_subscriptions, [NodeId]) of - {result, []} -> + case get_collection_subscriptions(Host, Node) of + [] -> {result, false}; - {result, Subs} -> + SubsByDepth when is_list(SubsByDepth) -> Stanza = event_stanza( [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'purge', attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, Stanza), + broadcast_stanza(Host, Node, NodeId, Type, Options, SubsByDepth, nodes, Stanza), {result, true}; _ -> {result, false} @@ -2539,7 +2943,7 @@ broadcast_removed_node(Host, Node, NodeId, Type, Options, Subs) -> _ -> Stanza = event_stanza( [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'delete', attrs = nodeAttr(Node)}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, Stanza), + broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, nodes, Stanza), {result, true} end; _ -> @@ -2550,10 +2954,10 @@ broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang) -> %broadcast(Host, Node, NodeId, Options, notify_config, false, 'items', ConfigEls) case get_option(Options, notify_config) of true -> - case node_action(Host, Type, get_node_subscriptions, [NodeId]) of - {result, []} -> + case get_collection_subscriptions(Host, Node) of + [] -> {result, false}; - {result, Subs} -> + SubsByDepth when is_list(SubsByDepth) -> Content = case get_option(Options, deliver_payloads) of true -> [#xmlel{ns = ?NS_DATA_FORMS, name = 'x', attrs = [?XMLATTR('type', <<"form">>)], children = @@ -2565,7 +2969,7 @@ broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang) -> [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'items', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_PUBSUB_EVENT, name = 'item', attrs = [?XMLATTR('id', <<"configuration">>)], children = Content}]}]), - broadcast_stanza(Host, Node, NodeId, Type, Options, Subs, Stanza), + broadcast_stanza(Host, Node, NodeId, Type, Options, SubsByDepth, nodes, Stanza), {result, true}; _ -> {result, false} @@ -2574,6 +2978,28 @@ broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang) -> {result, false} end. +get_collection_subscriptions(Host, Node) -> + lists:map(fun ({Depth, Nodes}) -> + {Depth, [{N, get_node_subs(N)} || N <- Nodes]} + end, tree_action(Host, get_parentnodes_tree, [Host, Node, service_jid(Host)])). + +get_node_subs(#pubsub_node{type = Type, + nodeid = {Host, Node}, + id = NodeID}) -> + case node_action(Host, Type, get_node_subscriptions, [NodeID]) of + {result, Subs} -> + get_options_for_subs(Host, Node, NodeID, Subs); + Other -> + Other + end. + +get_options_for_subs(_Host, Node, NodeID, Subs) -> + lists:foldl(fun({JID, subscribed, SubID}, Acc) -> + {result, #pubsub_subscription{options = Options}} = pubsub_subscription:get_subscription(JID, NodeID, SubID), + [{JID, Node, Options} | Acc]; + (_, Acc) -> + Acc + end, [], Subs). % TODO: merge broadcast code that way %broadcast(Host, Node, NodeId, Type, Options, Feature, Force, ElName, SubEls) -> @@ -2593,29 +3019,27 @@ broadcast_config_notification(Host, Node, NodeId, Type, Options, Lang) -> % {result, false} % end -broadcast_stanza(Host, Node, _NodeId, _Type, Options, Subscriptions, Stanza) -> - %AccessModel = get_option(Options, access_model), - PresenceDelivery = get_option(Options, presence_based_delivery), - BroadcastAll = get_option(Options, broadcast_all_resources), %% XXX this is not standard, but usefull +broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, SubsByDepth, NotifyType, Stanza) -> + %AccessModel = get_option(NodeOptions, access_model), + BroadcastAll = get_option(NodeOptions, broadcast_all_resources), %% XXX this is not standard, but usefull From = service_jid(Host), %% Handles explicit subscriptions - lists:foreach(fun({LJID, Subscription}) -> - case is_to_deliver(LJID, Subscription, PresenceDelivery) of - true -> - LJIDs = case BroadcastAll of - true -> - {U, S, _} = LJID, - [{U, S, R} || R <- user_resources(U, S)]; - false -> - [LJID] - end, - lists:foreach(fun(To) -> - ejabberd_router ! {route, From, jlib:make_jid(To), Stanza} - end, LJIDs); - false -> - ok - end - end, Subscriptions), + FilteredSubsByDepth = depths_to_deliver(NotifyType, SubsByDepth), + NodesByJID = collate_subs_by_jid(FilteredSubsByDepth), + lists:foreach(fun ({LJID, Nodes}) -> + LJIDs = case BroadcastAll of + true -> + {U, S, _} = LJID, + [{U, S, R} || R <- user_resources(U, S)]; + false -> + [LJID] + end, + SHIMStanza = add_headers(Stanza, collection_shim(Node, Nodes)), + lists:foreach(fun(To) -> + {TU, TS, TR} = To, + ejabberd_router ! {route, From, exmpp_jid:make(TU, TS, TR), SHIMStanza} + end, LJIDs) + end, NodesByJID), %% Handles implicit presence subscriptions case Host of {LUser, LServer, LResource} -> @@ -2661,6 +3085,37 @@ broadcast_stanza(Host, Node, _NodeId, _Type, Options, Subscriptions, Stanza) -> _ -> ok end. + +depths_to_deliver(NotifyType, SubsByDepth) -> + NodesToDeliver = + fun (Depth, Node, Subs, Acc) -> + lists:foldl(fun ({LJID, _Node, SubOptions} = S, Acc2) -> + case is_to_deliver(LJID, NotifyType, Depth, + Node#pubsub_node.options, + SubOptions) of + true -> [S | Acc2]; + false -> Acc2 + end + end, Acc, Subs) + end, + + DepthsToDeliver = + fun ({Depth, SubsByNode}, Acc) -> + lists:foldl(fun ({Node, Subs}, Acc2) -> + NodesToDeliver(Depth, Node, Subs, Acc2) + end, Acc, SubsByNode) + end, + + lists:foldl(DepthsToDeliver, [], SubsByDepth). + +collate_subs_by_jid(SubsByDepth) -> + lists:foldl(fun ({JID, Node, _Options}, Acc) -> + OldNodes = case lists:keysearch(JID, 1, Acc) of + {value, {JID, Nodes}} -> Nodes; + false -> [] + end, + lists:keystore(JID, 1, Acc, {JID, [Node | OldNodes]}) + end, [], SubsByDepth). %% If we don't know the resource, just pick first if any %% If no resource available, check if caps anyway (remote online) @@ -2689,19 +3144,20 @@ is_caps_notify(Host, Node, LJID) -> %%
  • The service does not support retrieval of default node configuration.
  • %% get_configure(Host, ServerHost, Node, From, Lang) -> + ServerHostB = list_to_binary(ServerHost), Action = fun(#pubsub_node{options = Options, type = Type, id = NodeId}) -> case node_call(Type, get_affiliation, [NodeId, From]) of {result, owner} -> - Groups = ejabberd_hooks:run_fold(roster_groups, ServerHost, [], [ServerHost]), + Groups = ejabberd_hooks:run_fold(roster_groups, ServerHostB, [], [ServerHostB]), {result, - [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = + #xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'configure', attrs = nodeAttr(Node), children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'x', attrs = [?XMLATTR('type', <<"form">>)], children = get_configure_xfields(Type, Options, Lang, Groups) - }]}]}]}; + }]}]}}; _ -> {error, 'forbidden'} end @@ -2714,11 +3170,11 @@ get_configure(Host, ServerHost, Node, From, Lang) -> get_default(Host, Node, _From, Lang) -> Type = select_type(Host, Host, Node), Options = node_options(Type), - {result, [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = + {result, #xmlel{ns = ?NS_PUBSUB_OWNER, name = 'pubsub', children = [#xmlel{ns = ?NS_PUBSUB_OWNER, name = 'default', children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'x', attrs = [?XMLATTR('type', <<"form">>)], children = get_configure_xfields(Type, Options, Lang, []) - }]}]}]}. + }]}]}}. %% Get node option %% The result depend of the node type plugin system. @@ -2793,7 +3249,11 @@ max_items(Options) -> ?LISTMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), get_option(Options, Var), Opts)). -get_configure_xfields(Type, Options, Lang, Groups) -> +-define(NLIST_CONFIG_FIELD(Label, Var), + ?STRINGMXFIELD(Label, "pubsub#" ++ atom_to_list(Var), + [node_to_string(N) || N <- get_option(Options, Var, [])])). + +get_configure_xfields(_Type, Options, Lang, Groups) -> [?XFIELD("hidden", "", "FORM_TYPE", ?NS_PUBSUB_NODE_CONFIG_s), ?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads), ?BOOL_CONFIG_FIELD("Deliver event notifications", deliver_notifications), @@ -2813,7 +3273,8 @@ get_configure_xfields(Type, Options, Lang, Groups) -> ?INTEGER_CONFIG_FIELD("Max payload size in bytes", max_payload_size), ?ALIST_CONFIG_FIELD("When to send the last published item", send_last_published_item, [never, on_sub, on_sub_and_presence]), - ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery) + ?BOOL_CONFIG_FIELD("Only deliver notifications to available users", presence_based_delivery), + ?NLIST_CONFIG_FIELD("The collections with which a node is affiliated", collection) ]. %%

    There are several reasons why the node configuration request might fail:

    @@ -2991,8 +3452,9 @@ get_cached_item(Host, NodeId) -> end. %%%% plugin handling - -plugins(Host) -> +plugins(Host) when is_binary(Host) -> + plugins(binary_to_list(Host)); +plugins(Host) when is_list(Host) -> case ets:lookup(gen_mod:get_module_proc(Host, config), plugins) of [{plugins, []}] -> [?STDNODE]; [{plugins, PL}] -> PL; @@ -3180,3 +3642,12 @@ itemsEls(Items) -> lists:map(fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) -> #xmlel{ns = ?NS_PUBSUB, name = 'item', attrs = itemAttr(ItemId), children = Payload} end, Items). + +add_headers(#xmlel{} = El, Headers) -> + exmpp_xml:append_children(El, Headers). + +collection_shim(Node, Nodes) -> + [#xmlel{ns = ?NS_PUBSUB, name ='header', + attrs = [?XMLATTR('name', <<"Collection">>)], + children = [ ?XMLCDATA(node_to_string(N))] + } || N <- Nodes -- [Node]]. diff --git a/src/mod_pubsub/node_buddy.erl b/src/mod_pubsub/node_buddy.erl index 11d17e78a..2031fe9db 100644 --- a/src/mod_pubsub/node_buddy.erl +++ b/src/mod_pubsub/node_buddy.erl @@ -47,7 +47,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -58,8 +58,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -79,8 +80,7 @@ terminate(Host, ServerHost) -> node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, buddy}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -122,8 +122,8 @@ create_node(NodeId, Owner) -> delete_node(Removed) -> node_hometree:delete_node(Removed). -subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). @@ -158,11 +158,15 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> node_hometree:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_hometree:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). + +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). get_states(NodeId) -> node_hometree:get_states(NodeId). diff --git a/src/mod_pubsub/node_club.erl b/src/mod_pubsub/node_club.erl index 9d92365ee..20c4bdf6b 100644 --- a/src/mod_pubsub/node_club.erl +++ b/src/mod_pubsub/node_club.erl @@ -26,9 +26,8 @@ -module(node_club). -author('christophe.romain@process-one.net'). --include_lib("exmpp/include/exmpp.hrl"). - -include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). -behaviour(gen_pubsub_node). @@ -47,7 +46,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -58,8 +57,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -79,8 +79,7 @@ terminate(Host, ServerHost) -> node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, club}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -121,8 +120,8 @@ create_node(NodeId, Owner) -> delete_node(Removed) -> node_hometree:delete_node(Removed). -subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). @@ -157,11 +156,14 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> node_hometree:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_hometree:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_hometree:get_states(NodeId). diff --git a/src/mod_pubsub/node_dag.erl b/src/mod_pubsub/node_dag.erl new file mode 100644 index 000000000..df654433e --- /dev/null +++ b/src/mod_pubsub/node_dag.erl @@ -0,0 +1,175 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% @author Brian Cully +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(node_dag). +-author('bjc@kublai.com'). + +-include("pubsub.hrl"). +-include("jlib.hrl"). + +-behaviour(gen_pubsub_node). + +%% API definition +-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/6, + get_items/2, + get_item/7, + get_item/2, + set_item/1, + get_item_name/3]). + + +init(Host, ServerHost, Opts) -> + node_hometree:init(Host, ServerHost, Opts). + +terminate(Host, ServerHost) -> + node_hometree:terminate(Host, ServerHost). + +options() -> + [{node_type, leaf} | node_hometree:options()]. + +features() -> + ["multi-collection" | node_hometree:features()]. + +create_node_permission(_Host, _ServerHost, _Node, _ParentNode, + _Owner, _Access) -> + {result, true}. + +create_node(NodeID, Owner) -> + node_hometree:create_node(NodeID, Owner). + +delete_node(Removed) -> + node_hometree:delete_node(Removed). + +subscribe_node(NodeID, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeID, Sender, Subscriber, AccessModel, + SendLast, PresenceSubscription, RosterGroup, + Options). + +unsubscribe_node(NodeID, Sender, Subscriber, SubID) -> + node_hometree:unsubscribe_node(NodeID, Sender, Subscriber, SubID). + +publish_item(NodeID, Publisher, Model, MaxItems, ItemID, Payload) -> + %% TODO: should look up the NodeTree plugin here. There's no + %% access to the Host of the request at this level, so for now we + %% just use nodetree_dag. + case nodetree_dag:get_node(NodeID) of + #pubsub_node{options = Options} -> + case find_opt(node_type, Options) of + collection -> + {error, mod_pubsub:extended_error('not-allowed', "publish")}; + _ -> + node_hometree:publish_item(NodeID, Publisher, Model, + MaxItems, ItemID, Payload) + end; + Err -> + Err + end. + +find_opt(_, []) -> false; +find_opt(Option, [{Option, Value} | _]) -> Value; +find_opt(Option, [_ | T]) -> find_opt(Option, T). + +remove_extra_items(NodeID, MaxItems, ItemIDs) -> + node_hometree:remove_extra_items(NodeID, MaxItems, ItemIDs). + +delete_item(NodeID, Publisher, PublishModel, ItemID) -> + node_hometree:delete_item(NodeID, Publisher, PublishModel, ItemID). + +purge_node(NodeID, Owner) -> + node_hometree:purge_node(NodeID, Owner). + +get_entity_affiliations(Host, Owner) -> + node_hometree:get_entity_affiliations(Host, Owner). + +get_node_affiliations(NodeID) -> + node_hometree:get_node_affiliations(NodeID). + +get_affiliation(NodeID, Owner) -> + node_hometree:get_affiliation(NodeID, Owner). + +set_affiliation(NodeID, Owner, Affiliation) -> + node_hometree:set_affiliation(NodeID, Owner, Affiliation). + +get_entity_subscriptions(Host, Owner) -> + node_hometree:get_entity_subscriptions(Host, Owner). + +get_node_subscriptions(NodeID) -> + node_hometree:get_node_subscriptions(NodeID). + +get_subscriptions(NodeID, Owner) -> + node_hometree:get_subscriptions(NodeID, Owner). + +set_subscriptions(NodeID, Owner, Subscription, SubID) -> + node_hometree:set_subscriptions(NodeID, Owner, Subscription, SubID). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). + +get_states(NodeID) -> + node_hometree:get_states(NodeID). + +get_state(NodeID, JID) -> + node_hometree:get_state(NodeID, JID). + +set_state(State) -> + node_hometree:set_state(State). + +get_items(NodeID, From) -> + node_hometree:get_items(NodeID, From). + +get_items(NodeID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID) -> + node_hometree:get_items(NodeID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID). + +get_item(NodeID, ItemID) -> + node_hometree:get_item(NodeID, ItemID). + +get_item(NodeID, ItemID, JID, AccessModel, PresenceSubscription, + RosterGroup, SubID) -> + node_hometree:get_item(NodeID, ItemID, JID, AccessModel, + PresenceSubscription, RosterGroup, SubID). + +set_item(Item) -> + node_hometree:set_item(Item). + +get_item_name(Host, Node, ID) -> + node_hometree:get_item_name(Host, Node, ID). diff --git a/src/mod_pubsub/node_dispatch.erl b/src/mod_pubsub/node_dispatch.erl index 4de366a0d..76aee4891 100644 --- a/src/mod_pubsub/node_dispatch.erl +++ b/src/mod_pubsub/node_dispatch.erl @@ -26,9 +26,8 @@ -module(node_dispatch). -author('christophe.romain@process-one.net'). --include_lib("exmpp/include/exmpp.hrl"). - -include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). -behaviour(gen_pubsub_node). @@ -45,7 +44,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -56,8 +55,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -77,8 +77,7 @@ terminate(Host, ServerHost) -> node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, dispatch}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -119,27 +118,27 @@ delete_node(Removed) -> node_hometree:delete_node(Removed). subscribe_node(_NodeId, _Sender, _Subscriber, _AccessModel, - _SendLast, _PresenceSubscription, _RosterGroup) -> - {error, 'forbidden'}. + _SendLast, _PresenceSubscription, _RosterGroup, _Options) -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'forbidden')}. unsubscribe_node(_NodeId, _Sender, _Subscriber, _SubID) -> - {error, 'forbidden'}. + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'forbidden')}. publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) -> lists:foreach(fun(SubNode) -> node_hometree:publish_item( SubNode#pubsub_node.id, Publisher, Model, MaxItems, ItemId, Payload) - end, nodetree_default:get_subnodes(NodeId, Publisher)). + end, nodetree_tree:get_subnodes(NodeId, Publisher, Publisher)). remove_extra_items(_NodeId, _MaxItems, ItemIds) -> {result, {ItemIds, []}}. delete_item(_NodeId, _Publisher, _PublishModel, _ItemId) -> - {error, 'item-not-found'}. + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}. purge_node(_NodeId, _Owner) -> - {error, 'forbidden'}. + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'forbidden')}. get_entity_affiliations(_Host, _Owner) -> {result, []}. @@ -161,11 +160,14 @@ get_node_subscriptions(NodeId) -> %% DO NOT REMOVE node_hometree:get_node_subscriptions(NodeId). -get_subscription(_NodeId, _Owner) -> +get_subscriptions(_NodeId, _Owner) -> {result, []}. -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_hometree:get_states(NodeId). @@ -193,3 +195,4 @@ set_item(Item) -> get_item_name(Host, Node, Id) -> node_hometree:get_item_name(Host, Node, Id). + diff --git a/src/mod_pubsub/node_flat.erl b/src/mod_pubsub/node_flat.erl index acab96119..e0bac5ea1 100644 --- a/src/mod_pubsub/node_flat.erl +++ b/src/mod_pubsub/node_flat.erl @@ -25,9 +25,8 @@ -module(node_flat). -author('christophe.romain@process-one.net'). --include_lib("exmpp/include/exmpp.hrl"). - -include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). -behaviour(gen_pubsub_node). @@ -38,7 +37,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -49,8 +48,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -70,8 +70,7 @@ terminate(Host, ServerHost) -> node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, flat}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -109,8 +108,8 @@ create_node(NodeId, Owner) -> delete_node(Removed) -> node_hometree:delete_node(Removed). -subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup) -> - node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup). +subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options) -> + node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, PresenceSubscription, RosterGroup, Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). @@ -145,11 +144,14 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> node_hometree:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_hometree:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_hometree:get_states(NodeId). diff --git a/src/mod_pubsub/node_hometree.erl b/src/mod_pubsub/node_hometree.erl index ab6f2c1b5..4eb866cd0 100644 --- a/src/mod_pubsub/node_hometree.erl +++ b/src/mod_pubsub/node_hometree.erl @@ -41,9 +41,8 @@ -module(node_hometree). -author('christophe.romain@process-one.net'). --include_lib("exmpp/include/exmpp.hrl"). - -include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). -behaviour(gen_pubsub_node). @@ -54,7 +53,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -65,8 +64,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -92,15 +92,10 @@ %% plugin. It can be used for example by the developer to create the specific %% module database schema if it does not exists yet.

    init(_Host, _ServerHost, _Opts) -> + pubsub_subscription:init(), mnesia:create_table(pubsub_state, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_state)}]), - StatesFields = record_info(fields, pubsub_state), - case mnesia:table_info(pubsub_state, attributes) of - StatesFields -> ok; - _ -> - mnesia:transform_table(pubsub_state, ignore, StatesFields) - end, mnesia:create_table(pubsub_item, [{disc_only_copies, [node()]}, {attributes, record_info(fields, pubsub_item)}]), @@ -138,8 +133,7 @@ terminate(_Host, _ServerHost) -> %% {send_last_published_item, never}, %% {presence_based_delivery, false}]''' options() -> - [{node_type, default}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -159,11 +153,14 @@ options() -> features() -> ["create-nodes", "auto-create", + "access-authorize", "delete-nodes", "delete-items", + "get-pending", "instant-nodes", "manage-subscriptions", "modify-affiliations", + "multi-subscribe", "outcast-affiliation", "persistent-items", "publish", @@ -173,7 +170,8 @@ features() -> "retrieve-items", "retrieve-subscriptions", "subscribe", - "subscription-notifications" + "subscription-notifications", + "subscription-options" ]. %% @spec (Host, ServerHost, Node, ParentNode, Owner, Access) -> bool() @@ -229,8 +227,10 @@ create_node(NodeId, Owner) -> %% Removed = [mod_pubsub:pubsubNode()] %% @doc

    purge items of deleted nodes after effective deletion.

    delete_node(Removed) -> - Tr = fun(#pubsub_state{stateid = {J, _}, subscription = S}) -> - {J, S} + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Ss}) -> + lists:map(fun(S) -> + {J, S} + end, Ss) end, Reply = lists:map( fun(#pubsub_node{id = NodeId} = PubsubNode) -> @@ -240,7 +240,7 @@ delete_node(Removed) -> del_items(NodeId, Items), del_state(NodeId, LJID) end, States), - {PubsubNode, lists:map(Tr, States)} + {PubsubNode, lists:flatmap(Tr, States)} end, Removed), {result, {default, broadcast, Reply}}. @@ -278,7 +278,7 @@ delete_node(Removed) -> %%

    %%

    In the default plugin module, the record is unchanged.

    subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup) -> + SendLast, PresenceSubscription, RosterGroup, Options) -> SubKey = jlib:short_prepd_jid(Subscriber), GenKey = jlib:short_prepd_bare_jid(SubKey), Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), @@ -288,8 +288,11 @@ subscribe_node(NodeId, Sender, Subscriber, AccessModel, _ -> get_state(NodeId, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, - Subscription = SubState#pubsub_state.subscription, + Subscriptions = SubState#pubsub_state.subscriptions, Whitelisted = lists:member(Affiliation, [member, publisher, owner]), + PendingSubscription = lists:any(fun({pending, _}) -> true; + (_) -> false + end, Subscriptions), if not Authorized -> %% JIDs do not match @@ -297,7 +300,7 @@ subscribe_node(NodeId, Sender, Subscriber, AccessModel, Affiliation == outcast -> %% Requesting entity is blocked {error, 'forbidden'}; - Subscription == pending -> + PendingSubscription -> %% Requesting entity has pending subscription {error, ?ERR_EXTENDED('not-authorized', "pending-subscription")}; (AccessModel == presence) and (not PresenceSubscription) -> @@ -319,36 +322,35 @@ subscribe_node(NodeId, Sender, Subscriber, AccessModel, %% % Requesting entity is anonymous %% {error, 'forbidden'}; true -> - NewSubscription = - if - AccessModel == authorize -> - pending; - %%NeedConfiguration -> - %% unconfigured - true -> - subscribed - end, - set_state(SubState#pubsub_state{subscription = NewSubscription}), - case NewSubscription of - subscribed -> - case SendLast of - never -> {result, {default, NewSubscription}}; - _ -> {result, {default, NewSubscription, send_last}} + case pubsub_subscription:subscribe_node(Subscriber, NodeId, Options) of + {result, SubId} -> + NewSub = case AccessModel of + authorize -> pending; + _ -> subscribed + end, + set_state(SubState#pubsub_state{subscriptions = [{NewSub, SubId} | Subscriptions]}), + case {NewSub, SendLast} of + {subscribed, never} -> + {result, {default, subscribed, SubId}}; + {subscribed, _} -> + {result, {default, subscribed, SubId, send_last}}; + {_, _} -> + {result, {default, pending, SubId}} end; _ -> - {result, {default, NewSubscription}} + {error, 'internal-server-error'} end end. -%% @spec (NodeId, Sender, Subscriber, SubID) -> +%% @spec (NodeId, Sender, Subscriber, SubId) -> %% {error, Reason} | {result, []} %% NodeId = mod_pubsub:pubsubNodeId() %% Sender = mod_pubsub:jid() %% Subscriber = mod_pubsub:jid() -%% SubID = string() +%% SubId = mod_pubsub:subid() %% Reason = mod_pubsub:stanzaError() %% @doc

    Unsubscribe the Subscriber from the Node.

    -unsubscribe_node(NodeId, Sender, Subscriber, _SubId) -> +unsubscribe_node(NodeId, Sender, Subscriber, SubId) -> SubKey = jlib:short_prepd_jid(Subscriber), GenKey = jlib:short_prepd_bare_jid(SubKey), Authorized = (jlib:short_prepd_bare_jid(Sender) == GenKey), @@ -357,28 +359,67 @@ unsubscribe_node(NodeId, Sender, Subscriber, _SubId) -> GenKey -> GenState; _ -> get_state(NodeId, SubKey) end, + Subscriptions = lists:filter(fun({_Sub, _SubId}) -> true; + (_SubId) -> false + end, SubState#pubsub_state.subscriptions), + SubIdExists = case SubId of + [] -> false; + List when is_list(List) -> true; + _ -> false + end, if %% Requesting entity is prohibited from unsubscribing entity not Authorized -> {error, 'forbidden'}; - %% Entity did not specify SubID - %%SubID == "", ?? -> + %% Entity did not specify SubId + %%SubId == "", ?? -> %% {error, ?ERR_EXTENDED('bad-request', "subid-required")}; %% Invalid subscription identifier - %%InvalidSubID -> + %%InvalidSubId -> %% {error, ?ERR_EXTENDED('not-acceptable', "invalid-subid")}; %% Requesting entity is not a subscriber - SubState#pubsub_state.subscription == none -> + Subscriptions == [] -> {error, ?ERR_EXTENDED('unexpected-request', "not-subscribed")}; - %% Was just subscriber, remove the record - SubState#pubsub_state.affiliation == none -> - del_state(NodeId, SubKey), + %% Subid supplied, so use that. + SubIdExists -> + Sub = first_in_list(fun(S) -> + case S of + {_Sub, SubId} -> true; + _ -> false + end + end, SubState#pubsub_state.subscriptions), + case Sub of + {value, S} -> + delete_subscription(SubKey, NodeId, S, SubState), + {result, default}; + false -> + {error, ?ERR_EXTENDED('unexpected-request', + "not-subscribed")} + end; + %% No subid supplied, but there's only one matching + %% subscription, so use that. + length(Subscriptions) == 1 -> + delete_subscription(SubKey, NodeId, hd(Subscriptions), SubState), {result, default}; true -> - set_state(SubState#pubsub_state{subscription = none}), - {result, default} + {error, ?ERR_EXTENDED('bad-request', "subid-required")} end. +delete_subscription(SubKey, NodeID, {Subscription, SubId}, SubState) -> + Affiliation = SubState#pubsub_state.affiliation, + AllSubs = SubState#pubsub_state.subscriptions, + NewSubs = AllSubs -- [{Subscription, SubId}], + pubsub_subscription:unsubscribe_node(SubKey, NodeID, SubId), + case {Affiliation, NewSubs} of + {none, []} -> + % Just a regular subscriber, and this is final item, so + % delete the state. + del_state(NodeID, SubKey); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. + + %% @spec (NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> %% {true, PubsubItem} | {result, Reply} %% NodeId = mod_pubsub:pubsubNodeId() @@ -426,13 +467,15 @@ publish_item(NodeId, Publisher, PublishModel, MaxItems, ItemId, Payload) -> _ -> get_state(NodeId, SubKey) end, Affiliation = GenState#pubsub_state.affiliation, - Subscription = SubState#pubsub_state.subscription, + Subscribed = case PublishModel of + subscribers -> is_subscribed(SubState#pubsub_state.subscriptions); + _ -> undefined + end, if not ((PublishModel == open) or ((PublishModel == publishers) and ((Affiliation == owner) or (Affiliation == publisher))) - or ((PublishModel == subscribers) - and (Subscription == subscribed))) -> + or (Subscribed == true)) -> %% Entity does not have sufficient privileges to publish to node {error, 'forbidden'}; true -> @@ -554,7 +597,7 @@ get_entity_affiliations(Host, Owner) -> States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = case ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of [{nodetree, N}] -> N; - _ -> nodetree_default + _ -> nodetree_tree end, Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> case NodeTree:get_node(N) of @@ -566,8 +609,8 @@ get_entity_affiliations(Host, Owner) -> get_node_affiliations(NodeId) -> {result, States} = get_states(NodeId), - Tr = fun(#pubsub_state{stateid = {J, {_, _}}, affiliation = A}) -> - {J, A} + Tr = fun(#pubsub_state{stateid = {J, _}, affiliation = A}) -> + {J, A} end, {result, lists:map(Tr, States)}. @@ -579,7 +622,7 @@ get_affiliation(NodeId, Owner) -> set_affiliation(NodeId, Owner, Affiliation) -> GenKey = jlib:short_prepd_bare_jid(Owner), GenState = get_state(NodeId, GenKey), - case {Affiliation, GenState#pubsub_state.subscription} of + case {Affiliation, GenState#pubsub_state.subscriptions} of {none, none} -> del_state(NodeId, GenKey); _ -> @@ -598,7 +641,7 @@ set_affiliation(NodeId, Owner, Affiliation) -> %% pubsub_state table.

    get_entity_subscriptions(Host, Owner) -> {U, D, _} = SubKey = jlib:short_prepd_jid(Owner), - GenKey = jlib:short_prepd_bare_jid(SubKey), + GenKey = jlib:short_prepd_bare_jid(Owner), States = case SubKey of GenKey -> mnesia:match_object( #pubsub_state{stateid = {{U, D, '_'}, '_'}, _ = '_'}); @@ -609,11 +652,16 @@ get_entity_subscriptions(Host, Owner) -> end, NodeTree = case ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of [{nodetree, N}] -> N; - _ -> nodetree_default + _ -> nodetree_tree end, - Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscription = S}, Acc) -> + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> case NodeTree:get_node(N) of - #pubsub_node{nodeid = {Host, _}} = Node -> [{Node, S, J}|Acc]; + #pubsub_node{nodeid = {Host, _}} = Node -> + lists:foldl(fun({Sub, SubId}, Acc2) -> + [{Node, Sub, SubId, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, Acc, Ss); _ -> Acc end end, [], States), @@ -621,26 +669,115 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> {result, States} = get_states(NodeId), - Tr = fun(#pubsub_state{stateid = {J, _}, subscription = S}) -> - {J, S} + Tr = fun(#pubsub_state{stateid = {J, _}, subscriptions = Subscriptions}) -> + %% TODO: get rid of cases to handle non-list subscriptions + case Subscriptions of + [_|_] -> + lists:foldl(fun({S, SubId}, Acc) -> + [{J, S, SubId} | Acc]; + (S, Acc) -> + [{J, S} | Acc] + end, [], Subscriptions); + [] -> + []; + _ -> + [{J, none}] + end end, - {result, lists:map(Tr, States)}. + {result, lists:flatmap(Tr, States)}. -get_subscription(NodeId, Owner) -> +get_subscriptions(NodeId, Owner) -> SubKey = jlib:short_prepd_jid(Owner), SubState = get_state(NodeId, SubKey), - {result, SubState#pubsub_state.subscription}. + {result, SubState#pubsub_state.subscriptions}. -set_subscription(NodeId, Owner, Subscription) -> +set_subscriptions(NodeId, Owner, Subscription, SubId) -> SubKey = jlib:short_prepd_jid(Owner), SubState = get_state(NodeId, SubKey), - case {Subscription, SubState#pubsub_state.affiliation} of - {none, none} -> - del_state(NodeId, SubKey); + case {SubId, SubState#pubsub_state.subscriptions} of + {_, []} -> + {error, 'item-not-found'}; + {"", [{_, SID}]} -> + case Subscription of + none -> unsub_with_subid(NodeId, SID, SubState); + _ -> replace_subscription({Subscription, SID}, SubState) + end; + {"", [_|_]} -> + {error, ?ERR_EXTENDED('bad_request', "subid-required")}; _ -> - set_state(SubState#pubsub_state{subscription = Subscription}) - end, - ok. + case Subscription of + none -> unsub_with_subid(NodeId, SubId, SubState); + _ -> replace_subscription({Subscription, SubId}, SubState) + end + end. + +replace_subscription(NewSub, SubState) -> + NewSubs = replace_subscription(NewSub, + SubState#pubsub_state.subscriptions, []), + set_state(SubState#pubsub_state{subscriptions = NewSubs}). + +replace_subscription(_, [], Acc) -> + Acc; +replace_subscription({Sub, SubId}, [{_, SubID} | T], Acc) -> + replace_subscription({Sub, SubId}, T, [{Sub, SubID} | Acc]). + +unsub_with_subid(NodeId, SubId, SubState) -> + pubsub_subscription:unsubscribe_node(SubState#pubsub_state.stateid, + NodeId, SubId), + NewSubs = lists:filter(fun ({_, SID}) -> SubId =/= SID end, + SubState#pubsub_state.subscriptions), + case {NewSubs, SubState#pubsub_state.affiliation} of + {[], none} -> + del_state(NodeId, element(1, SubState#pubsub_state.stateid)); + _ -> + set_state(SubState#pubsub_state{subscriptions = NewSubs}) + end. +%% @spec (Host, Owner) -> {result, [Node]} | {error, Reason} +%% Host = host() +%% Owner = jid() +%% Node = pubsubNode() +%% @doc

    Returns a list of Owner's nodes on Host with pending +%% subscriptions.

    +get_pending_nodes(Host, Owner) -> + GenKey = jlib:jid_remove_resource(jlib:jid_tolower(Owner)), + States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, + affiliation = owner, + _ = '_'}), + NodeIDs = [ID || #pubsub_state{stateid = {_, ID}} <- States], + NodeTree = case ets:lookup(gen_mod:get_module_proc(Host, config), nodetree) of + [{nodetree, N}] -> N; + _ -> nodetree_tree + end, + Reply = mnesia:foldl(fun(#pubsub_state{stateid = {_, NID}} = S, Acc) -> + case lists:member(NID, NodeIDs) of + true -> + case get_nodes_helper(NodeTree, S) of + {value, Node} -> [Node | Acc]; + false -> Acc + end; + false -> + Acc + end + end, [], pubsub_state), + {result, Reply}. + +get_nodes_helper(NodeTree, + #pubsub_state{stateid = {_, N}, subscriptions = Subs}) -> + HasPending = fun ({pending, _}) -> true; + (pending) -> true; + (_) -> false + end, + case lists:any(HasPending, Subs) of + true -> + case NodeTree:get_node(N) of + #pubsub_node{nodeid = {_, Node}} -> + {value, Node}; + _ -> + false + end; + false -> + false + end. %% @spec (NodeId) -> [States] | [] %% NodeId = mod_pubsub:pubsubNodeId() @@ -655,8 +792,11 @@ set_subscription(NodeId, Owner, Subscription) -> %% ```get_states(NodeId) -> %% node_default:get_states(NodeId).'''

    get_states(NodeId) -> - States = mnesia:match_object( - #pubsub_state{stateid = {'_', NodeId}, _ = '_'}), + States = case catch mnesia:match_object( + #pubsub_state{stateid = {'_', NodeId}, _ = '_'}) of + List when is_list(List) -> List; + _ -> [] + end, {result, States}. %% @spec (NodeId, JID) -> [State] | [] @@ -666,7 +806,7 @@ get_states(NodeId) -> %% @doc

    Returns a state (one state list), given its reference.

    get_state(NodeId, JID) -> StateId = {JID, NodeId}, - case mnesia:read({pubsub_state, StateId}) of + case catch mnesia:read({pubsub_state, StateId}) of [State] when is_record(State, pubsub_state) -> State; _ -> #pubsub_state{stateid=StateId} end. @@ -702,16 +842,18 @@ get_items(NodeId, _From) -> Items = mnesia:match_object(#pubsub_item{itemid = {'_', NodeId}, _ = '_'}), {result, lists:reverse(lists:keysort(#pubsub_item.modification, Items))}. get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -> + SubKey = jlib:short_prepd_jid(JID), GenKey = jlib:short_prepd_bare_jid(JID), GenState = get_state(NodeId, GenKey), + SubState = get_state(NodeId, SubKey), Affiliation = GenState#pubsub_state.affiliation, - Subscription = GenState#pubsub_state.subscription, - Whitelisted = can_fetch_item(Affiliation, Subscription), + Subscriptions = SubState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), if - %%SubID == "", ?? -> + %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED('bad-request', "subid-required")}; - %%InvalidSubID -> + %%InvalidSubId -> %% Entity is subscribed but specifies an invalid subscription ID %{error, ?ERR_EXTENDED('not-acceptable', "invalid-subid")}; Affiliation == outcast -> @@ -752,10 +894,10 @@ get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, _S GenKey = jlib:short_prepd_bare_jid(JID), GenState = get_state(NodeId, GenKey), Affiliation = GenState#pubsub_state.affiliation, - Subscription = GenState#pubsub_state.subscription, - Whitelisted = can_fetch_item(Affiliation, Subscription), + Subscriptions = GenState#pubsub_state.subscriptions, + Whitelisted = can_fetch_item(Affiliation, Subscriptions), if - %%SubID == "", ?? -> + %%SubId == "", ?? -> %% Entity has multiple subscriptions to the node but does not specify a subscription ID %{error, ?ERR_EXTENDED('bad-request', "subid-required")}; %%InvalidSubID -> @@ -816,7 +958,19 @@ can_fetch_item(owner, _) -> true; can_fetch_item(member, _) -> true; can_fetch_item(publisher, _) -> true; can_fetch_item(outcast, _) -> false; -can_fetch_item(none, subscribed) -> true; -can_fetch_item(none, none) -> false; +can_fetch_item(none, Subscriptions) -> is_subscribed(Subscriptions); can_fetch_item(_Affiliation, _Subscription) -> false. +is_subscribed(Subscriptions) -> + lists:any(fun ({subscribed, _SubId}) -> true; + (_) -> false + end, Subscriptions). + +%% Returns the first item where Pred() is true in List +first_in_list(_Pred, []) -> + false; +first_in_list(Pred, [H | T]) -> + case Pred(H) of + true -> {value, H}; + _ -> first_in_list(Pred, T) + end. diff --git a/src/mod_pubsub/node_mb.erl b/src/mod_pubsub/node_mb.erl index 2dc785cfc..8debc41c1 100644 --- a/src/mod_pubsub/node_mb.erl +++ b/src/mod_pubsub/node_mb.erl @@ -50,7 +50,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -61,8 +61,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -82,8 +83,7 @@ terminate(Host, ServerHost) -> ok. options() -> - [{node_type, pep}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, false}, @@ -127,10 +127,10 @@ delete_node(Removed) -> node_pep:delete_node(Removed). subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup) -> + SendLast, PresenceSubscription, RosterGroup, Options) -> node_pep:subscribe_node( NodeId, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup). + PresenceSubscription, RosterGroup, Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> node_pep:unsubscribe_node(NodeId, Sender, Subscriber, SubID). @@ -165,11 +165,14 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> node_pep:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_pep:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_pep:get_subscriptions(NodeId, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_pep:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_pep:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_pep:get_states(NodeId). diff --git a/src/mod_pubsub/node_pep.erl b/src/mod_pubsub/node_pep.erl index c486ac2ac..64e21fac8 100644 --- a/src/mod_pubsub/node_pep.erl +++ b/src/mod_pubsub/node_pep.erl @@ -43,7 +43,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -54,8 +54,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -77,8 +78,7 @@ terminate(Host, ServerHost) -> ok. options() -> - [{node_type, pep}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, false}, @@ -146,10 +146,10 @@ delete_node(Removed) -> end. subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup) -> + SendLast, PresenceSubscription, RosterGroup, Options) -> node_hometree:subscribe_node( NodeId, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup). + PresenceSubscription, RosterGroup, Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> case node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID) of @@ -176,7 +176,7 @@ get_entity_affiliations(_Host, Owner) -> States = mnesia:match_object(#pubsub_state{stateid = {GenKey, '_'}, _ = '_'}), NodeTree = case ets:lookup(gen_mod:get_module_proc(D, config), nodetree) of [{nodetree, N}] -> N; - _ -> nodetree_default + _ -> nodetree_tree end, Reply = lists:foldl(fun(#pubsub_state{stateid = {_, N}, affiliation = A}, Acc) -> case NodeTree:get_node(N) of @@ -208,11 +208,18 @@ get_entity_subscriptions(_Host, Owner) -> end, NodeTree = case ets:lookup(gen_mod:get_module_proc(D, config), nodetree) of [{nodetree, N}] -> N; - _ -> nodetree_default + _ -> nodetree_tree end, - Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscription = S}, Acc) -> + Reply = lists:foldl(fun(#pubsub_state{stateid = {J, N}, subscriptions = Ss}, Acc) -> case NodeTree:get_node(N) of - #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> [{Node, S, J}|Acc]; + #pubsub_node{nodeid = {{_, D, _}, _}} = Node -> + lists:foldl(fun({subscribed, SubID}, Acc2) -> + [{Node, subscribed, SubID, J} | Acc2]; + ({pending, _SubID}, Acc2) -> + [{Node, pending, J} | Acc2]; + (S, Acc2) -> + [{Node, S, J} | Acc2] + end, Acc, Ss); _ -> Acc end end, [], States), @@ -226,11 +233,14 @@ get_node_subscriptions(NodeId) -> %% DO NOT REMOVE node_hometree:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_hometree:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_hometree:get_states(NodeId). @@ -279,4 +289,3 @@ complain_if_modcaps_disabled(ServerHost) -> _ -> ok end. - diff --git a/src/mod_pubsub/node_private.erl b/src/mod_pubsub/node_private.erl index d04cff38f..0170ab66a 100644 --- a/src/mod_pubsub/node_private.erl +++ b/src/mod_pubsub/node_private.erl @@ -47,7 +47,7 @@ create_node/2, delete_node/1, purge_node/2, - subscribe_node/7, + subscribe_node/8, unsubscribe_node/4, publish_item/6, delete_item/4, @@ -58,8 +58,9 @@ set_affiliation/3, get_entity_subscriptions/2, get_node_subscriptions/1, - get_subscription/2, - set_subscription/3, + get_subscriptions/2, + set_subscriptions/4, + get_pending_nodes/2, get_states/1, get_state/2, set_state/1, @@ -79,8 +80,7 @@ terminate(Host, ServerHost) -> node_hometree:terminate(Host, ServerHost). options() -> - [{node_type, private}, - {deliver_payloads, true}, + [{deliver_payloads, true}, {notify_config, false}, {notify_delete, false}, {notify_retract, true}, @@ -123,9 +123,10 @@ delete_node(Removed) -> node_hometree:delete_node(Removed). subscribe_node(NodeId, Sender, Subscriber, AccessModel, SendLast, - PresenceSubscription, RosterGroup) -> + PresenceSubscription, RosterGroup, Options) -> node_hometree:subscribe_node(NodeId, Sender, Subscriber, AccessModel, - SendLast, PresenceSubscription, RosterGroup). + SendLast, PresenceSubscription, RosterGroup, + Options). unsubscribe_node(NodeId, Sender, Subscriber, SubID) -> node_hometree:unsubscribe_node(NodeId, Sender, Subscriber, SubID). @@ -160,11 +161,14 @@ get_entity_subscriptions(Host, Owner) -> get_node_subscriptions(NodeId) -> node_hometree:get_node_subscriptions(NodeId). -get_subscription(NodeId, Owner) -> - node_hometree:get_subscription(NodeId, Owner). +get_subscriptions(NodeId, Owner) -> + node_hometree:get_subscriptions(NodeId, Owner). -set_subscription(NodeId, Owner, Subscription) -> - node_hometree:set_subscription(NodeId, Owner, Subscription). +set_subscriptions(NodeId, Owner, Subscription, SubId) -> + node_hometree:set_subscriptions(NodeId, Owner, Subscription, SubId). + +get_pending_nodes(Host, Owner) -> + node_hometree:get_pending_nodes(Host, Owner). get_states(NodeId) -> node_hometree:get_states(NodeId). diff --git a/src/mod_pubsub/nodetree_dag.erl b/src/mod_pubsub/nodetree_dag.erl new file mode 100644 index 000000000..64b4adde1 --- /dev/null +++ b/src/mod_pubsub/nodetree_dag.erl @@ -0,0 +1,249 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% @author Brian Cully +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(nodetree_dag). +-author('bjc@kublai.com'). + +%% API +-export([init/3, + terminate/2, + options/0, + set_node/1, + get_node/3, + get_node/2, + get_node/1, + get_nodes/2, + get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, + get_subnodes/3, + get_subnodes_tree/3, + create_node/5, + delete_node/2]). + +-include_lib("stdlib/include/qlc.hrl"). + +-include("ejabberd.hrl"). +-include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). + +-behaviour(gen_pubsub_nodetree). + +-define(DEFAULT_NODETYPE, leaf). +-define(DEFAULT_PARENTS, []). +-define(DEFAULT_CHILDREN, []). + +-compile(export_all). + +%%==================================================================== +%% API +%%==================================================================== +init(Host, ServerHost, Opts) -> + nodetree_tree:init(Host, ServerHost, Opts), + mnesia:transaction(fun create_node/5, + [Host, [], "default", service_jid(ServerHost), []]). + +terminate(Host, ServerHost) -> + nodetree_tree:terminate(Host, ServerHost). + +create_node(Key, NodeID, Type, Owner, Options) -> + OwnerJID = jlib:jid_tolower(jlib:jid_remove_resource(Owner)), + case find_node(Key, NodeID) of + false -> + ID = pubsub_index:new(node), + N = #pubsub_node{nodeid = oid(Key, NodeID), + id = ID, + type = Type, + owners = [OwnerJID], + options = Options}, + case set_node(N) of + ok -> {ok, ID}; + Other -> Other + end; + _ -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'conflict')} + end. + +set_node(#pubsub_node{nodeid = {Key, _}, + owners = Owners, + options = Options} = Node) -> + Parents = find_opt(collection, ?DEFAULT_PARENTS, Options), + case validate_parentage(Key, Owners, Parents) of + true -> + %% Update parents whenever the config changes. + mnesia:write(Node#pubsub_node{parents = Parents}); + Other -> + Other + end. + +delete_node(Key, NodeID) -> + case find_node(Key, NodeID) of + false -> + {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}; + Node -> + %% Find all of N's children, update their configs to + %% remove N from the collection setting. + lists:foreach(fun (#pubsub_node{options = Opts} = Child) -> + NewOpts = remove_config_parent(NodeID, Opts), + Parents = find_opt(collection, ?DEFAULT_PARENTS, NewOpts), + ok = mnesia:write(pubsub_node, + Child#pubsub_node{ + parents = Parents, + options = NewOpts}, + write) + end, get_subnodes(Key, NodeID)), + + %% Remove and return the requested node. + pubsub_index:free(node, Node#pubsub_node.id), + mnesia:delete_object(pubsub_node, Node, write), + [Node] + end. + +options() -> + nodetree_tree:options(). + +get_node(Host, NodeID, _From) -> + get_node(Host, NodeID). + +get_node(Host, NodeID) -> + case find_node(Host, NodeID) of + false -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}; + Node -> Node + end. + +get_node(NodeId) -> + nodetree_tree:get_node(NodeId). + +get_nodes(Key, From) -> + nodetree_tree:get_nodes(Key, From). + +get_nodes(Key) -> + nodetree_tree:get_nodes(Key). + +get_parentnodes(Host, NodeID, _From) -> + case find_node(Host, NodeID) of + false -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}; + #pubsub_node{parents = Parents} -> + Q = qlc:q([N || #pubsub_node{nodeid = {NHost, NNode}} = N <- mnesia:table(pubsub_node), + Parent <- Parents, + Host == NHost, + Parent == NNode]), + qlc:e(Q) + end. + +get_parentnodes_tree(Host, NodeID, _From) -> + Pred = fun (NID, #pubsub_node{nodeid = {_, NNodeID}}) -> + NID == NNodeID + end, + Tr = fun (#pubsub_node{parents = Parents}) -> Parents end, + traversal_helper(Pred, Tr, Host, [NodeID]). + +get_subnodes(Host, NodeID, _From) -> + get_subnodes(Host, NodeID). + +get_subnodes(Host, NodeID) -> + case find_node(Host, NodeID) of + false -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item-not-found')}; + _ -> + Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = Node <- mnesia:table(pubsub_node), + Host == NHost, + lists:member(NodeID, Parents)]), + qlc:e(Q) + end. + +get_subnodes_tree(Host, NodeID, From) -> + Pred = fun (NID, #pubsub_node{parents = Parents}) -> + lists:member(NID, Parents) + end, + Tr = fun (#pubsub_node{nodeid = {_, N}}) -> [N] end, + traversal_helper(Pred, Tr, 1, Host, [NodeID], + [{0, [get_node(Host, NodeID, From)]}]). + +%%==================================================================== +%% Internal functions +%%==================================================================== +oid(Key, Name) -> {Key, Name}. + +%% Key = jlib:jid() | host() +%% NodeID = string() +find_node(Key, NodeID) -> + case mnesia:read(pubsub_node, oid(Key, NodeID), read) of + [] -> false; + [Node] -> Node + end. + +%% Key = jlib:jid() | host() +%% Default = term() +%% Options = [{Key = atom(), Value = term()}] +find_opt(Key, Default, Options) -> + case lists:keysearch(Key, 1, Options) of + {value, {Key, Val}} -> Val; + _ -> Default + end. + +traversal_helper(Pred, Tr, Host, NodeIDs) -> + traversal_helper(Pred, Tr, 0, Host, NodeIDs, []). + +traversal_helper(_Pred, _Tr, _Depth, _Host, [], Acc) -> + Acc; +traversal_helper(Pred, Tr, Depth, Host, NodeIDs, Acc) -> + Q = qlc:q([Node || #pubsub_node{nodeid = {NHost, _}} = Node <- mnesia:table(pubsub_node), + NodeID <- NodeIDs, + Host == NHost, + Pred(NodeID, Node)]), + Nodes = qlc:e(Q), + IDs = lists:flatmap(Tr, Nodes), + traversal_helper(Pred, Tr, Depth + 1, Host, IDs, [{Depth, Nodes} | Acc]). + +remove_config_parent(NodeID, Options) -> + remove_config_parent(NodeID, Options, []). + +remove_config_parent(_NodeID, [], Acc) -> + lists:reverse(Acc); +remove_config_parent(NodeID, [{collection, Parents} | T], Acc) -> + remove_config_parent(NodeID, T, + [{collection, lists:delete(NodeID, Parents)} | Acc]); +remove_config_parent(NodeID, [H | T], Acc) -> + remove_config_parent(NodeID, T, [H | Acc]). + +validate_parentage(_Key, _Owners, []) -> + true; +validate_parentage(Key, Owners, [[] | T]) -> + validate_parentage(Key, Owners, T); +validate_parentage(Key, Owners, [ParentID | T]) -> + case find_node(Key, ParentID) of + false -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'item_not_found')}; + #pubsub_node{owners = POwners, options = POptions} -> + NodeType = find_opt(node_type, ?DEFAULT_NODETYPE, POptions), + MutualOwners = [O || O <- Owners, PO <- POwners, + O == PO], + case {MutualOwners, NodeType} of + {[], _} -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'forbidden')}; + {_, collection} -> validate_parentage(Key, Owners, T); + {_, _} -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'not-allowed')} + end + end. + +%% @spec (Host) -> jid() +%% Host = host() +%% @doc

    Generate pubsub service JID.

    +service_jid(Host) -> + case Host of + {U,S,_} -> exmpp_jid:make(U, S); + _ -> exmpp_jid:make(Host) + end. diff --git a/src/mod_pubsub/nodetree_tree.erl b/src/mod_pubsub/nodetree_tree.erl index 7600bfed3..012f3f866 100644 --- a/src/mod_pubsub/nodetree_tree.erl +++ b/src/mod_pubsub/nodetree_tree.erl @@ -36,6 +36,7 @@ -module(nodetree_tree). -author('christophe.romain@process-one.net'). +-include_lib("stdlib/include/qlc.hrl"). -include_lib("exmpp/include/exmpp.hrl"). -include("pubsub.hrl"). @@ -51,6 +52,8 @@ get_node/1, get_nodes/2, get_nodes/1, + get_parentnodes/3, + get_parentnodes_tree/3, get_subnodes/3, get_subnodes_tree/3, create_node/5, @@ -125,6 +128,29 @@ get_nodes(Host, _From) -> get_nodes(Host) -> mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, _ = '_'}). +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc

    Default node tree does not handle parents, return empty list.

    +get_parentnodes(_Host, _Node, _From) -> + []. + +%% @spec (Host, Node, From) -> [{Depth, Record}] | {error, Reason} +%% Host = mod_pubsub:host() | mod_pubsub:jid() +%% Node = mod_pubsub:pubsubNode() +%% From = mod_pubsub:jid() +%% Depth = integer() +%% Record = pubsubNode() +%% @doc

    Default node tree does not handle parents, return a list +%% containing just this node.

    +get_parentnodes_tree(Host, Node, From) -> + case get_node(Host, Node, From) of + N when is_record(N, pubsub_node) -> [{0, [N]}]; + Error -> Error + end. %% @spec (Host, Node, From) -> [pubsubNode()] | {error, Reason} %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() @@ -132,9 +158,13 @@ get_nodes(Host) -> get_subnodes(Host, Node, _From) -> get_subnodes(Host, Node). get_subnodes(Host, Node) -> - mnesia:match_object(#pubsub_node{nodeid = {Host, '_'}, parent = Node, _ = '_'}). + Q = qlc:q([N || #pubsub_node{nodeid = {NHost, _}, + parents = Parents} = N <- mnesia:table(pubsub_node), + Host == NHost, + lists:member(Node, Parents)]), + qlc:e(Q). -%% @spec (Host, Index) -> [pubsubNode()] | {error, Reason} +%% @spec (Host, Index) -> [pubsubNodeIdx()] | {error, Reason} %% Host = mod_pubsub:host() %% Node = mod_pubsub:pubsubNode() %% From = mod_pubsub:jid() @@ -172,7 +202,7 @@ create_node(Host, Node, Type, Owner, Options) -> _ -> case mnesia:read({pubsub_node, {Host, Parent}}) of [] -> {Parent, false}; - _ -> {Parent, true} + _ -> {Parent, lists:member(BJID, Parent#pubsub_node.owners)} end end end, @@ -181,7 +211,7 @@ create_node(Host, Node, Type, Owner, Options) -> NodeId = pubsub_index:new(node), mnesia:write(#pubsub_node{nodeid = {Host, Node}, id = NodeId, - parent = ParentNode, + parents = [ParentNode], type = Type, owners = [BJID], options = Options}), diff --git a/src/mod_pubsub/pubsub.hrl b/src/mod_pubsub/pubsub.hrl index b896d4614..3a018188a 100644 --- a/src/mod_pubsub/pubsub.hrl +++ b/src/mod_pubsub/pubsub.hrl @@ -91,7 +91,7 @@ %%%

    The parentid and type fields are indexed.

    -record(pubsub_node, {nodeid, id, - parent, + parents = [], type = "flat", owners = [], options = [] @@ -101,13 +101,13 @@ %%% stateid = {ljid(), nodeidx()}}, %%% items = [ItemId::string()], %%% affiliation = affiliation(), -%%% subscription = subscription()}. +%%% subscriptions = [subscription()]}. %%%

    This is the format of the affiliations table. The type of the %%% table is: set,ram/disc.

    -record(pubsub_state, {stateid, items = [], affiliation = none, - subscription = none + subscriptions = none }). %%% @type pubsubItem() = #pubsub_item{ @@ -123,3 +123,11 @@ payload = [] }). +%% @type pubsubSubscription() = #pubsub_subscription{ +%% subid = string(), +%% state_key = {ljid(), pubsubNodeId()}, +%% options = [{atom(), term()}] +%% }. +%%

    This is the format of the subscriptions table. The type of the +%% table is: set,ram/disc.

    +-record(pubsub_subscription, {subid, options}). diff --git a/src/mod_pubsub/pubsub_subscription.erl b/src/mod_pubsub/pubsub_subscription.erl new file mode 100644 index 000000000..e00dacd37 --- /dev/null +++ b/src/mod_pubsub/pubsub_subscription.erl @@ -0,0 +1,333 @@ +%%% ==================================================================== +%%% ``The contents of this file are subject to the Erlang Public License, +%%% Version 1.1, (the "License"); you may not use this file except in +%%% compliance with the License. You should have received a copy of the +%%% Erlang Public License along with this software. If not, it can be +%%% retrieved via the world wide web at http://www.erlang.org/. +%%% +%%% Software distributed under the License is distributed on an "AS IS" +%%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See +%%% the License for the specific language governing rights and limitations +%%% under the License. +%%% +%%% The Initial Developer of the Original Code is ProcessOne. +%%% Portions created by ProcessOne are Copyright 2006-2009, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2009, ProcessOne. +%%% +%%% @author Brian Cully +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +-module(pubsub_subscription). +-author("bjc@kublai.com"). + +%% API +-export([init/0, + subscribe_node/3, + unsubscribe_node/3, + get_subscription/3, + set_subscription/4, + get_options_xform/2, + parse_options_xform/1]). + +% Internal function also exported for use in transactional bloc from pubsub plugins +-export([add_subscription/3, + delete_subscription/3, + read_subscription/3, + write_subscription/4]). + +-include_lib("stdlib/include/qlc.hrl"). + +-include("pubsub.hrl"). +-include_lib("exmpp/include/exmpp.hrl"). + +-define(PUBSUB_DELIVER, "pubsub#deliver"). +-define(PUBSUB_DIGEST, "pubsub#digest"). +-define(PUBSUB_DIGEST_FREQUENCY, "pubsub#digest_frequency"). +-define(PUBSUB_EXPIRE, "pubsub#expire"). +-define(PUBSUB_INCLUDE_BODY, "pubsub#include_body"). +-define(PUBSUB_SHOW_VALUES, "pubsub#show-values"). +-define(PUBSUB_SUBSCRIPTION_TYPE, "pubsub#subscription_type"). +-define(PUBSUB_SUBSCRIPTION_DEPTH, "pubsub#subscription_depth"). + +-define(DELIVER_LABEL, + "Whether an entity wants to receive or disable notifications"). +-define(DIGEST_LABEL, + "Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually"). +-define(DIGEST_FREQUENCY_LABEL, + "The minimum number of milliseconds between sending any two notification digests"). +-define(EXPIRE_LABEL, + "The DateTime at which a leased subscription will end or has ended"). +-define(INCLUDE_BODY_LABEL, + "Whether an entity wants to receive an XMPP message body in addition to the payload format"). +-define(SHOW_VALUES_LABEL, + "The presence states for which an entity wants to receive notifications"). +-define(SUBSCRIPTION_TYPE_LABEL, + "Type of notification to receive"). +-define(SUBSCRIPTION_DEPTH_LABEL, + "Depth from subscription for which to receive notifications"). + +-define(SHOW_VALUE_AWAY_LABEL, "XMPP Show Value of Away"). +-define(SHOW_VALUE_CHAT_LABEL, "XMPP Show Value of Chat"). +-define(SHOW_VALUE_DND_LABEL, "XMPP Show Value of DND (Do Not Disturb)"). +-define(SHOW_VALUE_ONLINE_LABEL, "Mere Availability in XMPP (No Show Value)"). +-define(SHOW_VALUE_XA_LABEL, "XMPP Show Value of XA (Extended Away)"). + +-define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, + "Receive notification of new items only"). +-define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, + "Receive notification of new nodes only"). + +-define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, + "Receive notification from direct child nodes only"). +-define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, + "Receive notification from all descendent nodes"). + +%%==================================================================== +%% API +%%==================================================================== +init() -> + ok = create_table(). + +subscribe_node(JID, NodeID, Options) -> + try mnesia:sync_dirty(fun add_subscription/3, + [JID, NodeID, Options]) of + Result -> {result, Result} + catch + Error -> Error + end. + +unsubscribe_node(JID, NodeID, SubID) -> + try mnesia:sync_dirty(fun delete_subscription/3, + [JID, NodeID, SubID]) of + Result -> {result, Result} + catch + Error -> Error + end. + +get_subscription(JID, NodeID, SubID) -> + try mnesia:sync_dirty(fun read_subscription/3, + [JID, NodeID, SubID]) of + Result -> {result, Result} + catch + Error -> Error + end. + +set_subscription(JID, NodeID, SubID, Options) -> + try mnesia:sync_dirty(fun write_subscription/4, + [JID, NodeID, SubID, Options]) of + Result -> {result, Result} + catch + Error -> Error + end. + +get_options_xform(Lang, Options) -> + Keys = [deliver, show_values, subscription_type, subscription_depth], + XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], + + {result, #xmlel{ns = ?NS_DATA_FORMS, name = 'x', children = + [#xmlel{ns = ?NS_DATA_FORMS, + name = 'field', + attrs = [?XMLATTR('var', <<"FORM_TYPE">>), ?XMLATTR('type', <<"hidden">>)], + children = [#xmlel{ns = ?NS_DATA_FORMS, + name = 'value', + children = [?XMLCDATA(?NS_PUBSUB_SUBSCRIBE_OPTIONS_s)]}]}] ++ XFields}}. + +parse_options_xform(XFields) -> + case exmpp_xml:get_child_elements(XFields) of + [] -> {result, []}; + [#xmlel{name = 'x'} = XEl] -> + case jlib:parse_xdata_submit(XEl) of + XData when is_list(XData) -> + case set_xoption(XData, []) of + Opts when is_list(Opts) -> {result, Opts}; + Other -> Other + end; + Other -> + Other + end; + Other -> + Other + end. + +%%==================================================================== +%% Internal functions +%%==================================================================== +create_table() -> + case mnesia:create_table(pubsub_subscription, + [{disc_copies, [node()]}, + {attributes, record_info(fields, pubsub_subscription)}, + {type, set}]) of + {atomic, ok} -> ok; + {aborted, {already_exists, _}} -> ok; + Other -> Other + end. + +add_subscription(_JID, _NodeID, Options) -> + SubID = make_subid(), + Record = #pubsub_subscription{subid = SubID, options = Options}, + mnesia:write(Record), + SubID. + +delete_subscription(JID, NodeID, SubID) -> + Sub = read_subscription(JID, NodeID, SubID), + mnesia:delete({pubsub_subscription, SubID}), + Sub. + +read_subscription(_JID, _NodeID, SubID) -> + Q = qlc:q([Sub || Sub <- mnesia:table(pubsub_subscription), + Sub#pubsub_subscription.subid == SubID]), + case qlc:e(Q) of + [Sub] -> Sub; + [] -> mnesia:abort({error, notfound}) + end. + +write_subscription(JID, NodeID, SubID, Options) -> + Sub = read_subscription(JID, NodeID, SubID), + mnesia:write(Sub#pubsub_subscription{options = Options}). + +make_subid() -> + {T1, T2, T3} = now(), + lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3])). + +%% +%% Subscription XForm processing. +%% + +%% Return processed options, with types converted and so forth, using +%% Opts as defaults. +set_xoption([], Opts) -> + Opts; +set_xoption([{Var, Value} | T], Opts) -> + NewOpts = case var_xfield(Var) of + {error, _} -> + Opts; + Key -> + Val = val_xfield(Key, Value), + lists:keystore(Key, 1, Opts, {Key, Val}) + end, + set_xoption(T, NewOpts). + +%% Return the options list's key for an XForm var. +var_xfield(?PUBSUB_DELIVER) -> deliver; +var_xfield(?PUBSUB_DIGEST) -> digest; +var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> digest_frequency; +var_xfield(?PUBSUB_EXPIRE) -> expire; +var_xfield(?PUBSUB_INCLUDE_BODY) -> include_body; +var_xfield(?PUBSUB_SHOW_VALUES) -> show_values; +var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> subscription_type; +var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> subscription_depth; +var_xfield(_) -> {error, badarg}. + +%% Convert Values for option list's Key. +val_xfield(deliver, [Val]) -> xopt_to_bool(Val); +val_xfield(digest, [Val]) -> xopt_to_bool(Val); +val_xfield(digest_frequency, [Val]) -> list_to_integer(Val); +val_xfield(expire, [Val]) -> jlib:datetime_string_to_timestamp(Val); +val_xfield(include_body, [Val]) -> xopt_to_bool(Val); +val_xfield(show_values, Vals) -> Vals; +val_xfield(subscription_type, ["items"]) -> items; +val_xfield(subscription_type, ["nodes"]) -> nodes; +val_xfield(subscription_depth, ["all"]) -> all; +val_xfield(subscription_depth, [Depth]) -> + case catch list_to_integer(Depth) of + N when is_integer(N) -> N; + _ -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'not-acceptable')} + end. + +%% Convert XForm booleans to Erlang booleans. +xopt_to_bool("0") -> false; +xopt_to_bool("1") -> true; +xopt_to_bool("false") -> false; +xopt_to_bool("true") -> true; +xopt_to_bool(_) -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'not-acceptable')}. + +%% Return a field for an XForm for Key, with data filled in, if +%% applicable, from Options. +get_option_xfield(Lang, Key, Options) -> + Var = xfield_var(Key), + Label = xfield_label(Key), + {Type, OptEls} = type_and_options(xfield_type(Key), Lang), + Vals = case lists:keysearch(Key, 1, Options) of + {value, {_, Val}} -> + [tr_xfield_values(Vals) || Vals <- xfield_val(Key, Val)]; + false -> + [] + end, + #xmlel{ns = ?NS_DATA_FORMS, + name = 'field', + attrs = [?XMLATTR('var', Var), ?XMLATTR('type', Type), ?XMLATTR('label', translate:translate(Lang, Label))], + children = OptEls ++ Vals}. + +type_and_options({Type, Options}, Lang) -> + {Type, [tr_xfield_options(O, Lang) || O <- Options]}; +type_and_options(Type, _Lang) -> + {Type, []}. + +tr_xfield_options({Value, Label}, Lang) -> + #xmlel{ns = ?NS_DATA_FORMS, + name = 'option', + attrs = [?XMLATTR('label', transalte:translate(Lang, Label))], + children = [#xmlel{ns = ?NS_DATA_FORMS, + name = 'value', + children = [?XMLCDATA(Value)]}]}. + +tr_xfield_values(Value) -> + #xmlel{ns = ?NS_DATA_FORMS, name ='value', children = [?XMLCDATA(Value)]}. + +%% Return the XForm variable name for a subscription option key. +xfield_var(deliver) -> ?PUBSUB_DELIVER; +xfield_var(digest) -> ?PUBSUB_DIGEST; +xfield_var(digest_frequency) -> ?PUBSUB_DIGEST_FREQUENCY; +xfield_var(expire) -> ?PUBSUB_EXPIRE; +xfield_var(include_body) -> ?PUBSUB_INCLUDE_BODY; +xfield_var(show_values) -> ?PUBSUB_SHOW_VALUES; +xfield_var(subscription_type) -> ?PUBSUB_SUBSCRIPTION_TYPE; +xfield_var(subscription_depth) -> ?PUBSUB_SUBSCRIPTION_DEPTH. + +%% Return the XForm variable type for a subscription option key. +xfield_type(deliver) -> "boolean"; +xfield_type(digest) -> "boolean"; +xfield_type(digest_frequency) -> "text-single"; +xfield_type(expire) -> "text-single"; +xfield_type(include_body) -> "boolean"; +xfield_type(show_values) -> + {"list-multi", [{"away", ?SHOW_VALUE_AWAY_LABEL}, + {"chat", ?SHOW_VALUE_CHAT_LABEL}, + {"dnd", ?SHOW_VALUE_DND_LABEL}, + {"online", ?SHOW_VALUE_ONLINE_LABEL}, + {"xa", ?SHOW_VALUE_XA_LABEL}]}; +xfield_type(subscription_type) -> + {"list-single", [{"items", ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, + {"nodes", ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; +xfield_type(subscription_depth) -> + {"list-single", [{"1", ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, + {"all", ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. + +%% Return the XForm variable label for a subscription option key. +xfield_label(deliver) -> ?DELIVER_LABEL; +xfield_label(digest) -> ?DIGEST_LABEL; +xfield_label(digest_frequency) -> ?DIGEST_FREQUENCY_LABEL; +xfield_label(expire) -> ?EXPIRE_LABEL; +xfield_label(include_body) -> ?INCLUDE_BODY_LABEL; +xfield_label(show_values) -> ?SHOW_VALUES_LABEL; +xfield_label(subscription_type) -> ?SUBSCRIPTION_TYPE_LABEL; +xfield_label(subscription_depth) -> ?SUBSCRIPTION_DEPTH_LABEL. + +%% Return the XForm value for a subscription option key. +xfield_val(deliver, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest, Val) -> [bool_to_xopt(Val)]; +xfield_val(digest_frequency, Val) -> [integer_to_list(Val)]; +xfield_val(expire, Val) -> [jlib:now_to_utc_string(Val)]; +xfield_val(include_body, Val) -> [bool_to_xopt(Val)]; +xfield_val(show_values, Val) -> Val; +xfield_val(subscription_type, items) -> ["items"]; +xfield_val(subscription_type, nodes) -> ["nodes"]; +xfield_val(subscription_depth, all) -> ["all"]; +xfield_val(subscription_depth, N) -> [integer_to_list(N)]. + +%% Convert erlang booleans to XForms. +bool_to_xopt(false) -> "false"; +bool_to_xopt(true) -> "true".