25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-12-24 17:29:28 +01:00

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
This commit is contained in:
Pablo Polvorin 2009-08-25 17:14:30 +00:00
parent 6470e6cc25
commit 938a4007b3
15 changed files with 1756 additions and 308 deletions

View File

@ -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},

File diff suppressed because it is too large Load Diff

View File

@ -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).

View File

@ -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).

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

@ -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 <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/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).

View File

@ -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).

View File

@ -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).

View File

@ -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.</p>
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 <p>purge items of deleted nodes after effective deletion.</p>
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) ->
%% </p>
%% <p>In the default plugin module, the record is unchanged.</p>
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 <p>Unsubscribe the <tt>Subscriber</tt> from the <tt>Node</tt>.</p>
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) ->
%% <tt>pubsub_state</tt> table.</p>
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 <p>Returns a list of Owner's nodes on Host with pending
%% subscriptions.</p>
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).'''</p>
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 <p>Returns a state (one state list), given its reference.</p>
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.

View File

@ -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).

View File

@ -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.

View File

@ -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).

View File

@ -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 <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_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 <p>Generate pubsub service JID.</p>
service_jid(Host) ->
case Host of
{U,S,_} -> exmpp_jid:make(U, S);
_ -> exmpp_jid:make(Host)
end.

View File

@ -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 <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, [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}),

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 = []
@ -101,13 +101,13 @@
%%% stateid = {ljid(), nodeidx()}},
%%% items = [ItemId::string()],
%%% affiliation = affiliation(),
%%% subscription = subscription()}.
%%% subscriptions = [subscription()]}.
%%% <p>This is the format of the <tt>affiliations</tt> table. The type of the
%%% table is: <tt>set</tt>,<tt>ram/disc</tt>.</p>
-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()}]
%% }.
%% <p>This is the format of the <tt>subscriptions</tt> table. The type of the
%% table is: <tt>set</tt>,<tt>ram/disc</tt>.</p>
-record(pubsub_subscription, {subid, options}).

View File

@ -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 <bjc@kublai.com>
%%% @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".