experimental patch including XEP-248 (thanks to Brian Cully)

SVN Revision: 2157
This commit is contained in:
Christophe Romain 2009-06-15 13:45:40 +00:00
parent 6f080f7fed
commit 50b73664e2
18 changed files with 640 additions and 103 deletions

View File

@ -4,7 +4,7 @@ include ..\Makefile.inc
EFLAGS = -I .. -pz ..
OUTDIR = ..
BEAMS = ..\gen_pubsub_node.beam ..\gen_pubsub_nodetree.beam ..\mod_pubsub.beam ..\nodetree_default.beam ..\nodetree_virtual.beam ..\node_buddy.beam ..\node_club.beam ..\node_default.beam ..\node_dispatch.beam ..\node_pep.beam ..\node_private.beam ..\node_public.beam
BEAMS = ..\gen_pubsub_node.beam ..\gen_pubsub_nodetree.beam ..\mod_pubsub.beam ..\nodetree_tree.beam ..\nodetree_virtual.beam ..\node_buddy.beam ..\node_club.beam ..\node_hometree.beam ..\node_dispatch.beam ..\node_pep.beam ..\node_private.beam ..\node_public.beam
ALL : $(BEAMS)
@ -20,8 +20,8 @@ $(OUTDIR)\gen_pubsub_nodetree.beam : gen_pubsub_nodetree.erl
$(OUTDIR)\mod_pubsub.beam : mod_pubsub.erl
erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub.erl
$(OUTDIR)\nodetree_default.beam : nodetree_default.erl
erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_default.erl
$(OUTDIR)\nodetree_tree.beam : nodetree_tree.erl
erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_tree.erl
$(OUTDIR)\nodetree_virtual.beam : nodetree_virtual.erl
erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_virtual.erl
@ -32,8 +32,8 @@ $(OUTDIR)\node_buddy.beam : node_buddy.erl
$(OUTDIR)\node_club.beam : node_club.erl
erlc -W $(EFLAGS) -o $(OUTDIR) node_club.erl
$(OUTDIR)\node_default.beam : node_default.erl
erlc -W $(EFLAGS) -o $(OUTDIR) node_default.erl
$(OUTDIR)\node_hometree.beam : node_hometree.erl
erlc -W $(EFLAGS) -o $(OUTDIR) node_hometree.erl
$(OUTDIR)\node_dispatch.beam : node_dispatch.erl
erlc -W $(EFLAGS) -o $(OUTDIR) node_dispatch.erl

View File

@ -47,6 +47,8 @@ behaviour_info(callbacks) ->
{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},

View File

@ -80,7 +80,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,
@ -304,7 +304,7 @@ update_node_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}
@ -334,12 +334,12 @@ update_node_database(Host, ServerHost) ->
#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}),
@ -371,6 +371,17 @@ update_node_database(Host, ServerHost) ->
?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.
@ -1443,6 +1454,12 @@ replace_subscription_helper(_, OldSub, Acc) ->
-define(STRINGXFIELD(Label, Var, Val),
?XFIELD("text-single", Label, Var, Val)).
-define(STRINGMXFIELD(Label, Var, Vals),
{xmlelement, "field", [{"type", "text-multi"},
{"label", translate:translate(Lang, Label)},
{"var", Var}],
[{xmlelement, "value", [], [{xmlcdata, V}]} || V <- Vals]}).
-define(XFIELDOPT(Type, Label, Var, Val, Opts),
{xmlelement, "field", [{"type", Type},
{"label", translate:translate(Lang, Label)},
@ -1601,10 +1618,14 @@ delete_node(_Host, [], _Owner) ->
{error, ?ERR_NOT_ALLOWED};
delete_node(Host, Node, Owner) ->
Action = fun(#pubsub_node{type = Type, id = NodeId}) ->
SubsByDepth = get_collection_subscriptions(Host, Node),
case node_call(Type, get_affiliation, [NodeId, Owner]) of
{result, owner} ->
Removed = tree_call(Host, delete_node, [Host, Node]),
node_call(Type, delete_node, [Removed]);
case node_call(Type, delete_node, [Removed]) of
{result, Res} -> {result, {SubsByDepth, Res}};
Error -> Error
end;
_ ->
%% Entity is not an owner
{error, ?ERR_FORBIDDEN}
@ -1612,27 +1633,26 @@ delete_node(Host, Node, Owner) ->
end,
Reply = [],
case transaction(Host, Node, Action, transaction) of
{result, {_, {Result, broadcast, Removed}}} ->
lists:foreach(fun({RNode, RSubscriptions}) ->
{result, {_, {SubsByDepth, {Result, broadcast, Removed}}}} ->
lists:foreach(fun({RNode, _RSubscriptions}) ->
{RH, RN} = RNode#pubsub_node.nodeid,
NodeId = RNode#pubsub_node.id,
Type = RNode#pubsub_node.type,
Options = RNode#pubsub_node.options,
broadcast_removed_node(RH, RN, NodeId, Type, Options, RSubscriptions),
unset_cached_item(RH, NodeId)
broadcast_removed_node(RH, RN, NodeId, Type, Options, SubsByDepth)
end, Removed),
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {_, {Result, _Removed}}} ->
{result, {_, {_, {Result, _Removed}}}} ->
case Result of
default -> {result, Reply};
_ -> {result, Result}
end;
{result, {_, default}} ->
{result, {_, {_, default}}} ->
{result, Reply};
{result, {_, Result}} ->
{result, {_, {_, Result}}} ->
{result, Result};
Error ->
Error
@ -2605,19 +2625,28 @@ service_jid(Host) ->
%% @spec (LJID, PresenceDelivery) -> boolean()
%% LJID = jid()
%% NotifyType = items | nodes
%% Depth = integer()
%% NodeOptions = [{atom(), term()}]
%% SubOptions = [{atom(), term()}]
%% @doc <p>Check if a notification must be delivered or not based on
%% node and subscription options.</p>
is_to_deliver(LJID, NodeOptions, SubOptions) ->
sub_to_deliver(LJID, SubOptions) andalso node_to_deliver(LJID, NodeOptions).
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, SubOptions) ->
lists:all(fun sub_option_can_deliver/1, SubOptions).
sub_to_deliver(_LJID, NotifyType, Depth, SubOptions) ->
lists:all(fun (Option) ->
sub_option_can_deliver(NotifyType, Depth, Option)
end, SubOptions).
sub_option_can_deliver({deliver, false}) -> false;
sub_option_can_deliver({expire, When}) -> now() < When;
sub_option_can_deliver(_) -> true.
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),
@ -2652,11 +2681,8 @@ event_stanza(Els) ->
broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, _From, Payload) ->
%broadcast(Host, Node, NodeId, NodeOptions, none, true, "items", ItemEls)
case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
{result, []} ->
{result, false};
{result, Subs} ->
SubOptions = get_options_for_subs(Host, Node, NodeId, Subs),
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Content = case get_option(NodeOptions, deliver_payloads) of
true -> Payload;
false -> []
@ -2665,7 +2691,7 @@ broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, _
[{xmlelement, "items", nodeAttr(Node),
[{xmlelement, "item", itemAttr(ItemId), Content}]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions, Stanza),
NodeOptions, SubsByDepth, items, Stanza),
case Removed of
[] ->
ok;
@ -2676,8 +2702,8 @@ broadcast_publish_item(Host, Node, NodeId, Type, NodeOptions, Removed, ItemId, _
[{xmlelement, "items", nodeAttr(Node),
[{xmlelement, "retract", itemAttr(RId), []} || RId <- Removed]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions,
RetractStanza);
NodeOptions, SubsByDepth,
items, RetractStanza);
_ ->
ok
end
@ -2695,16 +2721,13 @@ broadcast_retract_items(Host, Node, NodeId, Type, NodeOptions, ItemIds, ForceNot
%broadcast(Host, Node, NodeId, NodeOptions, notify_retract, ForceNotify, "retract", RetractEls)
case (get_option(NodeOptions, notify_retract) or ForceNotify) of
true ->
case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
{result, []} ->
{result, false};
{result, Subs} ->
SubOptions = get_options_for_subs(Host, Node, NodeId, Subs),
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Stanza = event_stanza(
[{xmlelement, "items", nodeAttr(Node),
[{xmlelement, "retract", itemAttr(ItemId), []} || ItemId <- ItemIds]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions, Stanza),
NodeOptions, SubsByDepth, items, Stanza),
{result, true};
_ ->
{result, false}
@ -2717,16 +2740,13 @@ broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) ->
%broadcast(Host, Node, NodeId, NodeOptions, notify_retract, false, "purge", [])
case get_option(NodeOptions, notify_retract) of
true ->
case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
{result, []} ->
{result, false};
{result, Subs} ->
SubOptions = get_options_for_subs(Host, Node, NodeId, Subs),
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Stanza = event_stanza(
[{xmlelement, "purge", nodeAttr(Node),
[]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions, Stanza),
NodeOptions, SubsByDepth, nodes, Stanza),
{result, true};
_ ->
{result, false}
@ -2735,20 +2755,19 @@ broadcast_purge_node(Host, Node, NodeId, Type, NodeOptions) ->
{result, false}
end.
broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, Subs) ->
broadcast_removed_node(Host, Node, NodeId, Type, NodeOptions, SubsByDepth) ->
%broadcast(Host, Node, NodeId, NodeOptions, notify_delete, false, "delete", [])
case get_option(NodeOptions, notify_delete) of
true ->
case Subs of
case SubsByDepth of
[] ->
{result, false};
_ ->
SubOptions = get_options_for_subs(Host, Node, NodeId, Subs),
Stanza = event_stanza(
[{xmlelement, "delete", nodeAttr(Node),
[]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions, Stanza),
NodeOptions, SubsByDepth, nodes, Stanza),
{result, true}
end;
_ ->
@ -2759,11 +2778,8 @@ broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
%broadcast(Host, Node, NodeId, NodeOptions, notify_config, false, "items", ConfigEls)
case get_option(NodeOptions, notify_config) of
true ->
case node_action(Host, Type, get_node_subscriptions, [NodeId]) of
{result, []} ->
{result, false};
{result, Subs} ->
SubOptions = get_options_for_subs(Host, Node, NodeId, Subs),
case get_collection_subscriptions(Host, Node) of
SubsByDepth when is_list(SubsByDepth) ->
Content = case get_option(NodeOptions, deliver_payloads) of
true ->
[{xmlelement, "x", [{"xmlns", ?NS_XDATA}, {"type", "form"}],
@ -2775,7 +2791,7 @@ broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
[{xmlelement, "items", nodeAttr(Node),
[{xmlelement, "item", itemAttr("configuration"), Content}]}]),
broadcast_stanza(Host, Node, NodeId, Type,
NodeOptions, SubOptions, Stanza),
NodeOptions, SubsByDepth, nodes, Stanza),
{result, true};
_ ->
{result, false}
@ -2784,6 +2800,28 @@ broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
{result, false}
end.
get_collection_subscriptions(Host, Node) ->
case mnesia:transaction(fun tree_call/3,
[Host, get_parentnodes_tree,
[Host, Node, service_jid(Host)]]) of
{atomic, NodesByDepth} when is_list(NodesByDepth) ->
lists:map(fun ({Depth, Nodes}) ->
{Depth, [{N, get_node_subs(N)} || N <- Nodes]}
end, NodesByDepth);
Other ->
Other
end.
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.
% TODO: merge broadcast code that way
%broadcast(Host, Node, NodeId, Type, NodeOptions, Feature, Force, ElName, SubEls) ->
% case (get_option(NodeOptions, Feature) or Force) of
@ -2802,15 +2840,14 @@ broadcast_config_notification(Host, Node, NodeId, Type, NodeOptions, Lang) ->
% {result, false}
% end
broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, Subs, Stanza) ->
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
DeliverSubs = lists:filter(fun({LJID, _Node, SubOptions}) ->
is_to_deliver(LJID, NodeOptions, SubOptions)
end, Subs),
lists:foreach(fun({LJID, _Node, _SubOptions}) ->
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,
@ -2818,10 +2855,11 @@ broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, Subs, Stanza) ->
false ->
[LJID]
end,
SHIMStanza = add_headers(Stanza, collection_shim(Node, Nodes)),
lists:foreach(fun(To) ->
ejabberd_router ! {route, From, jlib:make_jid(To), Stanza}
ejabberd_router ! {route, From, jlib:make_jid(To), SHIMStanza}
end, LJIDs)
end, DeliverSubs),
end, NodesByJID),
%% Handles implicit presence subscriptions
case Host of
{LUser, LServer, LResource} ->
@ -2869,6 +2907,37 @@ broadcast_stanza(Host, Node, _NodeId, _Type, NodeOptions, Subs, 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)
user_resources(User, Server) ->
@ -3000,6 +3069,10 @@ max_items(Options) ->
?LISTMXFIELD(Label, "pubsub#" ++ atom_to_list(Var),
get_option(Options, Var), Opts)).
-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),
?BOOL_CONFIG_FIELD("Deliver payloads with event notifications", deliver_payloads),
@ -3020,7 +3093,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)
].
%%<p>There are several reasons why the node configuration request might fail:</p>
@ -3052,8 +3126,10 @@ set_configure(Host, Node, From, Els, Lang) ->
end,
case set_xoption(XData, OldOpts) of
NewOpts when is_list(NewOpts) ->
tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]),
{result, ok};
case tree_call(Host, set_node, [N#pubsub_node{options = NewOpts}]) of
ok -> {result, ok};
Err -> Err
end;
Err ->
Err
end
@ -3166,6 +3242,9 @@ set_xoption([{"pubsub#type", Value} | Opts], NewOpts) ->
?SET_STRING_XOPT(type, Value);
set_xoption([{"pubsub#body_xslt", Value} | Opts], NewOpts) ->
?SET_STRING_XOPT(body_xslt, Value);
set_xoption([{"pubsub#collection", Value} | Opts], NewOpts) ->
NewValue = [string_to_node(V) || V <- Value],
?SET_LIST_XOPT(collection, NewValue);
set_xoption([_ | Opts], NewOpts) ->
% skip unknown field
set_xoption(Opts, NewOpts).
@ -3257,7 +3336,7 @@ features() ->
"member-affiliation", % RECOMMENDED
%TODO "meta-data", % RECOMMENDED
% see plugin "modify-affiliations", % OPTIONAL
%TODO "multi-collection", % OPTIONAL
% see plugin "multi-collection", % OPTIONAL
% see plugin "multi-subscribe", % OPTIONAL
% see plugin "outcast-affiliation", % RECOMMENDED
% see plugin "persistent-items", % RECOMMENDED
@ -3395,3 +3474,10 @@ itemsEls(Items) ->
lists:map(fun(#pubsub_item{itemid = {ItemId, _}, payload = Payload}) ->
{xmlelement, "item", itemAttr(ItemId), Payload}
end, Items).
add_headers({xmlelement, Name, Attrs, Els}, Headers) ->
{xmlelement, Name, Attrs, Els ++ Headers}.
collection_shim(Node, Nodes) ->
[{xmlelement, "header", [{"name", "Collection"}],
[{xmlcdata, node_to_string(N)}]} || N <- Nodes -- [Node]].

View File

@ -65,7 +65,8 @@
get_items/2,
get_item/7,
get_item/2,
set_item/1
set_item/1,
get_item_name/3
]).
@ -76,8 +77,7 @@ terminate(Host, ServerHost) ->
node_hometree:terminate(Host, ServerHost).
options() ->
[{node_type, __TO_BE_DEFINED__},
{deliver_payloads, true},
[{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
{notify_retract, true},
@ -118,8 +118,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,7 +158,7 @@ get_subscriptions(NodeId, Owner) ->
node_hometree:get_subscriptions(NodeId, Owner).
set_subscriptions(NodeId, Owner, Subscriptions) ->
node_hometree:set_subscriptions(NodeId, Owner, Subscription).
node_hometree:set_subscriptions(NodeId, Owner, Subscriptions).
get_states(NodeId) ->
node_hometree:get_states(NodeId).
@ -172,7 +172,7 @@ set_state(State) ->
get_items(NodeId, From) ->
node_hometree:get_items(NodeId, From).
get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId)
get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId) ->
node_hometree:get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, SubId).
get_item(NodeId, ItemId) ->
@ -183,3 +183,6 @@ get_item(NodeId, ItemId, JID, AccessModel, PresenceSubscription, RosterGroup, Su
set_item(Item) ->
node_hometree:set_item(Item).
get_item_name(Host, Node, Id) ->
node_hometree:get_item_name(Host, Node, Id).

View File

@ -78,8 +78,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},

View File

@ -78,8 +78,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},

154
src/mod_pubsub/node_dag.erl Normal file
View File

@ -0,0 +1,154 @@
%%% ====================================================================
%%% ``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 <bjc@kublai.com>
%%% @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/3,
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) ->
node_hometree:publish_item(NodeID, Publisher, Model, MaxItems,
ItemID, Payload).
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, Subscriptions) ->
node_hometree:set_subscriptions(NodeID, Owner, Subscriptions).
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).

View File

@ -76,8 +76,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},
@ -129,7 +128,7 @@ publish_item(NodeId, Publisher, Model, MaxItems, ItemId, Payload) ->
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, []}}.

View File

@ -69,8 +69,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},

View File

@ -132,8 +132,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},
@ -597,7 +596,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
@ -654,7 +653,7 @@ 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}, subscriptions = Ss}, Acc) ->
case NodeTree:get_node(N) of
@ -774,9 +773,10 @@ get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -
SubKey = jlib:jid_tolower(JID),
GenKey = jlib:jid_remove_resource(SubKey),
GenState = get_state(NodeId, GenKey),
SubState = get_state(NodeId, SubKey),
Affiliation = GenState#pubsub_state.affiliation,
Subscription = GenState#pubsub_state.subscriptions,
Whitelisted = can_fetch_item(Affiliation, Subscription),
Subscriptions = SubState#pubsub_state.subscriptions,
Whitelisted = can_fetch_item(Affiliation, Subscriptions),
if
%%SubID == "", ?? ->
%% Entity has multiple subscriptions to the node but does not specify a subscription ID
@ -796,7 +796,7 @@ get_items(NodeId, JID, AccessModel, PresenceSubscription, RosterGroup, _SubId) -
(AccessModel == whitelist) and (not Whitelisted) ->
%% Node has whitelist access model and entity lacks required affiliation
{error, ?ERR_EXTENDED(?ERR_NOT_ALLOWED, "closed-node")};
(AccessModel == authorize) -> % TODO: to be done
(AccessModel == authorize) and (not Whitelisted) ->
%% Node has authorize access model
{error, ?ERR_FORBIDDEN};
%%MustPay ->
@ -887,8 +887,10 @@ 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) ->
lists:any(fun ({subscribed, _SubID}) -> true;
(_) -> false
end, Subscriptions);
can_fetch_item(_Affiliation, _Subscription) -> false.
%% Returns the first item where Pred() is true in List

View File

@ -81,8 +81,7 @@ terminate(Host, ServerHost) ->
ok.
options() ->
[{node_type, pep},
{deliver_payloads, true},
[{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
{notify_retract, false},

View File

@ -76,8 +76,7 @@ terminate(Host, ServerHost) ->
ok.
options() ->
[{node_type, pep},
{deliver_payloads, true},
[{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
{notify_retract, false},
@ -174,7 +173,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
@ -206,7 +205,7 @@ 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}, subscriptions = Ss}, Acc) ->
case NodeTree:get_node(N) of

View File

@ -78,8 +78,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},

View File

@ -78,8 +78,7 @@ terminate(Host, ServerHost) ->
node_hometree:terminate(Host, ServerHost).
options() ->
[{node_type, public},
{deliver_payloads, true},
[{deliver_payloads, true},
{notify_config, false},
{notify_delete, false},
{notify_retract, true},

View File

@ -0,0 +1,246 @@
%%% ====================================================================
%%% ``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 <bjc@kublai.com>
%%% @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("jlib.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, ?ERR_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, ?ERR_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.
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, ?ERR_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, ?ERR_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, ?ERR_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, Host, [NodeID]).
%%====================================================================
%% 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, ?ERR_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, ?ERR_NOT_ALLOWED};
{_, collection} -> validate_parentage(Key, Owners, T)
end
end.
%% @spec (Host) -> jid()
%% Host = host()
%% @doc <p>Generate pubsub service JID.</p>
service_jid(Host) ->
case Host of
{U,S,_} -> {jid, U, S, "", U, S, ""};
_ -> {jid, "", Host, "", "", Host, ""}
end.

View File

@ -36,6 +36,8 @@
-module(nodetree_tree).
-author('christophe.romain@process-one.net').
-include_lib("stdlib/include/qlc.hrl").
-include("pubsub.hrl").
-include("jlib.hrl").
@ -50,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,
@ -124,6 +128,32 @@ 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 <p>Default node tree does not handle parents, return empty list.</p>
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 <p>Default node tree does not handle parents, return a list
%% containing just this node.</p>
get_parentnodes_tree(Host, Node, From) ->
case get_node(Host, Node, From) of
N when is_record(N, pubsub_node) ->
[{0, mnesia:read(pubsub_node, {Host, Node})}];
Error ->
Error
end.
%% @spec (Host, Node, From) -> [pubsubNode()] | {error, Reason}
%% Host = mod_pubsub:host()
%% Node = mod_pubsub:pubsubNode()
@ -131,7 +161,11 @@ 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) -> [pubsubNodeIdx()] | {error, Reason}
%% Host = mod_pubsub:host()
@ -180,7 +214,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}),

View File

@ -47,6 +47,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,
@ -104,6 +106,22 @@ get_nodes(Host, _From) ->
get_nodes(_Host) ->
[].
%% @spec (Host, Node, From) -> [pubsubNode()]
%% Host = mod_pubsub:host()
%% Node = mod_pubsub:pubsubNode()
%% From = mod_pubsub:jid()
%% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p>
get_parentnodes(_Host, _Node, _From) ->
[].
%% @spec (Host, Node, From) -> [pubsubNode()]
%% Host = mod_pubsub:host()
%% Node = mod_pubsub:pubsubNode()
%% From = mod_pubsub:jid()
%% @doc <p>Virtual node tree does not handle parent/child. Child list is empty.</p>
get_parentnodes_tree(_Host, _Node, _From) ->
[].
%% @spec (Host, Node, From) -> [pubsubNode()]
%% Host = mod_pubsub:host()
%% Node = mod_pubsub:pubsubNode()

View File

@ -91,7 +91,7 @@
%%% <p>The <tt>parentid</tt> and <tt>type</tt> fields are indexed.</p>
-record(pubsub_node, {nodeid,
id,
parent,
parents = [],
type = "flat",
owners = [],
options = []