diff options
Diffstat (limited to 'src/mod_pubsub_ng')
-rw-r--r-- | src/mod_pubsub_ng/Makefile.in | 62 | ||||
-rw-r--r-- | src/mod_pubsub_ng/Makefile.win32 | 88 | ||||
-rw-r--r-- | src/mod_pubsub_ng/exmpp_pubsub.erl | 685 | ||||
-rw-r--r-- | src/mod_pubsub_ng/mod_pubsub_dev.erl | 863 | ||||
-rw-r--r-- | src/mod_pubsub_ng/node_flat_dev.erl | 310 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_api.hrl | 101 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_broadcast.erl | 1240 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_core.erl | 1707 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_db.erl | 1575 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_db_mnesia.erl | 2129 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_dev.hrl | 146 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_disco.erl | 1437 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_groups.erl | 904 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_hooks.erl | 570 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_index_dev.erl | 113 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_options.erl | 3369 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_parser.erl | 1442 | ||||
-rw-r--r-- | src/mod_pubsub_ng/pubsub_tools.erl | 685 | ||||
-rw-r--r-- | src/mod_pubsub_ng/xmpp_xdata.erl | 108 |
19 files changed, 17534 insertions, 0 deletions
diff --git a/src/mod_pubsub_ng/Makefile.in b/src/mod_pubsub_ng/Makefile.in new file mode 100644 index 000000000..9a114a1a3 --- /dev/null +++ b/src/mod_pubsub_ng/Makefile.in @@ -0,0 +1,62 @@ +CC = @CC@ +CFLAGS = @CFLAGS@ +CPPFLAGS = @CPPFLAGS@ +LDFLAGS = @LDFLAGS@ +LIBS = @LIBS@ + +ERLANG_CFLAGS = @ERLANG_CFLAGS@ +ERLANG_LIBS = @ERLANG_LIBS@ + +EFLAGS += -I .. +EFLAGS += -pz .. + +# make debug=true to compile Erlang module with debug informations. +ifdef debug + EFLAGS+=+debug_info +endif + +OUTDIR = .. +ERLBEHAVS = gen_pubsub_node.erl gen_pubsub_nodetree.erl +SOURCES_ALL = $(wildcard *.erl) +SOURCES = $(filter-out $(ERLBEHAVS),$(SOURCES_ALL)) +ERLBEHAVBEAMS = $(addprefix $(OUTDIR)/,$(ERLBEHAVS:.erl=.beam)) +BEAMS = $(addprefix $(OUTDIR)/,$(SOURCES:.erl=.beam)) + + +all: $(ERLBEHAVBEAMS) $(BEAMS) + +$(BEAMS): $(ERLBEHAVBEAMS) + +$(OUTDIR)/%.beam: %.erl + @ERLC@ -W $(EFLAGS) -o $(OUTDIR) $< + +clean: + rm -f $(BEAMS) + +distclean: clean + rm -f Makefile + + + +TAGS: + etags *.erl + + +DEVDOCDIR=../../doc/devdoc +DDTDIR=.. +HTMLS = $(SOURCES:%.erl=../../doc/devdoc/%.html) +ERLHTMLS = $(SOURCES:%.erl=../../doc/devdoc/%.erl.html) +SVGS = $(SOURCES:%.erl=../../doc/devdoc/%.svg) + +devdoc: $(HTMLS) $(ERLHTMLS) $(SVGS) + +$(DEVDOCDIR)/%.erl.html: %.erl + @ERL@ -noshell -pa $(DEVDOCDIR) -run escobar_run file $< $(DDTDIR) -s init stop + +$(DEVDOCDIR)/%.html: %.erl + @ERL@ -noshell -run edoc_run file $< \ + '[{dir,"$(DDTDIR)"},{packages,false},{todo,true},{private,true},{def,{vsn,"$(VSN)"}},{stylesheet,"process-one.css"},{overview,"overview.edoc"}]' -s init stop + +$(DEVDOCDIR)/%.svg: %.erl + @ERL@ -noshell -pa $(DEVDOCDIR) -run funrelg dir $< $(DDTDIR) -s init stop + diff --git a/src/mod_pubsub_ng/Makefile.win32 b/src/mod_pubsub_ng/Makefile.win32 new file mode 100644 index 000000000..6d5707b23 --- /dev/null +++ b/src/mod_pubsub_ng/Makefile.win32 @@ -0,0 +1,88 @@ + +include ..\Makefile.inc + +EFLAGS = -I .. -pz .. + +OUTDIR = .. +BEAMS = ..\exmpp_pubsub.beam ..\pubsub_disco.beam ..\mod_pubsub_dev.beam ..\pubsub_hooks.beam ..\mod_pubsub_odbc.beam ..\pubsub_index_dev.beam ..\pubsub_broadcast.beam ..\pubsub_options.beam ..\pubsub_core.beam ..\pubsub_parser.beam ..\pubsub_db.beam ..\xmpp_xdata.beam ..\pubsub_db_mnesia.beam + + +ALL : $(BEAMS) + +CLEAN : + -@erase $(BEAMS) + +$(OUTDIR)\gen_pubsub_node.beam : gen_pubsub_node.erl + erlc -W $(EFLAGS) -o $(OUTDIR) gen_pubsub_node.erl + +$(OUTDIR)\gen_pubsub_nodetree.beam : gen_pubsub_nodetree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) gen_pubsub_nodetree.erl + +$(OUTDIR)\mod_pubsub.beam : mod_pubsub.erl + erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub.erl + +$(OUTDIR)\mod_pubsub_odbc.beam : mod_pubsub_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) mod_pubsub_odbc.erl + +$(OUTDIR)\node_buddy.beam : node_buddy.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_buddy.erl + +$(OUTDIR)\node_club.beam : node_club.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_club.erl + +$(OUTDIR)\node_dag.beam : node_dag.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_dag.erl + +$(OUTDIR)\node_dispatch.beam : node_dispatch.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_dispatch.erl + +$(OUTDIR)\node_flat.beam : node_flat.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_flat.erl + +$(OUTDIR)\node_flat_odbc.beam : node_flat_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_flat_odbc.erl + +$(OUTDIR)\node_hometree.beam : node_hometree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_hometree.erl + +$(OUTDIR)\node_hometree_odbc.beam : node_hometree_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_hometree_odbc.erl + +$(OUTDIR)\node_mb.beam : node_mb.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_mb.erl + +$(OUTDIR)\node_pep.beam : node_pep.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_pep.erl + +$(OUTDIR)\node_pep_odbc.beam : node_pep_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_pep_odbc.erl + +$(OUTDIR)\node_private.beam : node_private.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_private.erl + +$(OUTDIR)\node_public.beam : node_public.erl + erlc -W $(EFLAGS) -o $(OUTDIR) node_public.erl + +$(OUTDIR)\nodetree_dag.beam : nodetree_dag.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_dag.erl + +$(OUTDIR)\nodetree_tree.beam : nodetree_tree.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_tree.erl + +$(OUTDIR)\nodetree_tree_odbc.beam : nodetree_tree_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_tree_odbc.erl + +$(OUTDIR)\nodetree_virtual.beam : nodetree_virtual.erl + erlc -W $(EFLAGS) -o $(OUTDIR) nodetree_virtual.erl + +$(OUTDIR)\pubsub_db_odbc.beam : pubsub_db_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_db_odbc.erl + +$(OUTDIR)\pubsub_index.beam : pubsub_index.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_index.erl + +$(OUTDIR)\pubsub_subscription.beam : pubsub_subscription.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_subscription.erl + +$(OUTDIR)\pubsub_subscription_odbc.beam : pubsub_subscription_odbc.erl + erlc -W $(EFLAGS) -o $(OUTDIR) pubsub_subscription_odbc.erl diff --git a/src/mod_pubsub_ng/exmpp_pubsub.erl b/src/mod_pubsub_ng/exmpp_pubsub.erl new file mode 100644 index 000000000..19bc1d76d --- /dev/null +++ b/src/mod_pubsub_ng/exmpp_pubsub.erl @@ -0,0 +1,685 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(exmpp_pubsub). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + + +%% -------------------------------------------------------------------- +%% Documentation / type definitions. +%% -------------------------------------------------------------------- + + + +%-- Affiliations --% +-export_type([ + affiliation/0, + %% + affiliation_owner/0, + affiliation_publisher/0, + affiliation_publish_only/0, + affiliation_member/0, + affiliation_none/0, + affiliation_outcast/0 +]). + +%% @type affiliation_owner() = 'owner'. +-type(affiliation_owner() :: 'owner'). + +%% @type affiliation_publisher() = 'publisher'. +-type(affiliation_publisher() :: 'publisher'). + +%% @type affiliation_publish_only() = 'publish-only'. +-type(affiliation_publish_only() :: 'publish-only'). + +%% @type affiliation_member() = 'member'. +-type(affiliation_member() :: 'member'). + +%% @type affiliation_none() = 'none'. +-type(affiliation_none() :: 'none'). + +%% @type affiliation_outcast() = 'outcast'. +-type(affiliation_outcast() :: 'outcast'). + +%% @type affiliation() = Owner | Publisher | Publish_Only | Member | None | Outcast +%% Owner = exmpp_pubsub:affiliation_owner() +%% Publisher = exmpp_pubsub:affiliation_publisher() +%% Publish_Only = exmpp_pubsub:affiliation_publish_only() +%% Member = exmpp_pubsub:affiliation_member() +%% None = exmpp_pubsub:affiliation_none() +%% Outcast = exmpp_pubsub:affiliation_outcast(). +-type(affiliation() :: exmpp_pubsub:affiliation_owner() + | exmpp_pubsub:affiliation_publisher() + | exmpp_pubsub:affiliation_publish_only() + | exmpp_pubsub:affiliation_member() + | exmpp_pubsub:affiliation_none() + | exmpp_pubsub:affiliation_outcast() +). + + +%% + +%-- Things identifiers --% +-export_type([ + index/0, + itemId/0, + itemIds/0, + level/0, + nodeId/0, + nodeIdx/0, + subId/0, + plugin/0 +]). + +%% @type index() = 'node'. +-type(index() :: 'node' ). + +%% @type itemId() = binary(). +-type(itemId() :: binary()). + +%% @type itemIds() = [ItemId::exmpp_pubsub:itemId(),...]. +-type(itemIds() :: [ItemId::exmpp_pubsub:itemId(),...]). + +%% @type level() = non_neg_integer(). +-type(level() :: non_neg_integer()). + +%% @type nodeId() = binary(). +-type(nodeId() :: binary()). + +%% @type nodeIdx() = non_neg_integer(). +-type(nodeIdx() :: non_neg_integer()). + +%% @type plugin() = module(). +-type(plugin() :: module()). + +%% @type subId() = binary(). +-type(subId() :: binary()). + + +-export_type([ + t_now/0 +]). + +%% Temporary -type, until calendar.erl -type is exported +-type(t_now() + :: {MegaSec::non_neg_integer(), + Sec::non_neg_integer(), + MilliSec::non_neg_integer()} +). + + +%% +-export_type([ + feature/0, + features/0, + pubsub_feature/0, + pubsub_features/0 +]). + +-type(feature() :: binary()). + +-type(features() :: Features::[Feature::exmpp_pubsub:feature(),...]). + +-type(pubsub_feature() :: exmpp_pubsub:feature()). + +-type(pubsub_features() :: Pubsub_Features::[Pubsub_Feature::exmpp_pubsub:feature(),...]). + +%% + +%-- Payload types --% +-export_type([ + payload/0, + %% + payload_empty/0, + payload_full/0 +]). + +%% @type payload_empty() = []. +-type(payload_empty() :: []). + +%% @type payload_full() = #xmlel{}. +-type(payload_full() :: xmlel()). + +%% @type payload() = Empty_Payload | Full_Payload +%% Empty_Payload = exmpp_pubsub:payload_empty() +%% Full_Payload = exmpp_pubsub:payload_full(). +-type(payload() :: exmpp_pubsub:payload_empty() + | exmpp_pubsub:payload_full() +). + + +%% + +%-- Pusbub Host --% +-export_type([ + host/0, + %% + host_pubsub/0, + host_pep/0 +]). + +%% @type host_pubsub() = xmpp_jid:domain_jid(). +%% ```<<"pubsub.shakespeare.lit">>''' +%% Identifier type of the Pubsub service +-type(host_pubsub() :: binary()). + +%% @type host_pep() = xmpp_jid:usr_contact_bare(). +%% ```{<<"juliet">>, <<"capulet.lit">>, undefined}''' +%% Identifier type of the Pubsub-On-Jid service +-type(host_pep() :: xmpp_jid:usr_contact_bare()). + +%% @type host() = Pubsub_Host | Pubsub_On_Jid_Host +%% Pubsub_Host = exmpp_pubsub:host_pubsub() +%% Pubsub_On_Jid_Host = exmpp_pubsub:host_pep(). +%% Identifier types of the Pubsub and PEP (Pubsub-On-Jid) services +%% ```* Pubsub_Host : <<"pubsub.shakespeare.lit">> +%% * Pubsub_PEP : {<<"juliet">>, <<"capulet.lit">>, undefined}''' +-type(host() :: exmpp_pubsub:host_pubsub() + | exmpp_pubsub:host_pep() +). + + +%% + +%-- Subscription States --% +-export_type([ + subscription_state/0, + %% + subscription_state_none/0, + subscription_state_pending/0, + subscription_state_unconfigured/0, + subscription_state_subscribed/0 +]). + +%% @type subscription_state_none() = 'none'. +-type(subscription_state_none() :: 'none'). + +%% @type subscription_state_pending() = 'pending'. +-type(subscription_state_pending() :: 'pending'). + +%% @type subscription_state_unconfigured() = 'unconfigured'. +-type(subscription_state_unconfigured() :: 'unconfigured'). + +%% @type subscription_state_subscribed() = 'subscribed'. +-type(subscription_state_subscribed() :: 'subscribed'). + +%% @type subscription_state() = None | Pending | Unconfigured | Subscribed +%% None = exmpp_pubsub:subscription_state_none() +%% Pending = exmpp_pubsub:subscription_state_pending() +%% Unconfigured = exmpp_pubsub:subscription_state_unconfigured() +%% Subscribed = exmpp_pubsub:subscription_state_subscribed() +-type(subscription_state() :: exmpp_pubsub:subscription_state_pending() + | exmpp_pubsub:subscription_state_unconfigured() + | exmpp_pubsub:subscription_state_subscribed() +). + +%% + +%-- Subscription -- %% +-export_type([ + subscriptions/0, + subscription/0, + %% + subscription_subscribed/0, + subscription_unconfigured/0, + subscription_pending/0 +]). + +-type(subscription_subscribed() + :: {Subscription_State :: exmpp_pubsub:subscription_state_subscribed(), + SubId :: exmpp_pubsub:subId(), + Resource :: undefined | xmpp_jid:resource_jid() | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: [] | pubsub_options:options_subscription()} +). + +-type(subscription_unconfigured() + :: {Subscription_State :: exmpp_pubsub:subscription_state_unconfigured(), + SubId :: exmpp_pubsub:subId(), + Resource :: undefined | xmpp_jid:resource_jid() | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: []} +). + +-type(subscription_pending() + :: {Subscription_State :: exmpp_pubsub:subscription_state_pending(), + SubId :: exmpp_pubsub:subId(), + Resource :: undefined | xmpp_jid:resource_jid() | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: []} +). + +-type(subscription() + :: {Subscription_State :: exmpp_pubsub:subscription_state(), + SubId :: exmpp_pubsub:subId(), + Resource :: undefined | xmpp_jid:resource_jid() | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: [] | pubsub_options:options_subscription()} +%% :: exmpp_pubsub:subscription_subscribed() +%% | exmpp_pubsub:subscription_pending() +%% | exmpp_pubsub:subscription_unconfigured() +). + +-type(subscriptions() + :: [exmpp_pubsub:subscription(),...] +%% :: [exmpp_pubsub:subscription_subscribed(),...] +%% | [exmpp_pubsub:subscription_unconfigured(),...] +%% | [exmpp_pubsub:subscription_pending(),...] +). + +%% TODO : commented -type unions return the following dialyzer error (bug ?) : +%% ... +%% Adding information from /home/meta/otp-exmpp.plt to ejabberd.plt... +%%=ERROR REPORT==== 29-Jul-2011::15:14:42 === +%Error in process <0.2604.0> with exit value: {function_clause,[{erl_types,inf_tuples_in_sets,4},{erl_types,inf_tuple_sets,4},{erl_types,inf_tuple_sets,3},{erl_types,t_inf,3},{erl_types,t_inf_lists_strict,4},{erl_types,t_inf,3},{dialyzer_typesig,solve_one_c... +%% +%% +%%dialyzer: Analysis failed with error: {function_clause,[{erl_types,inf_tuples_in_sets,4}, +%% {erl_types,inf_tuple_sets,4}, +%% {erl_types,inf_tuple_sets,3}, +%% {erl_types,t_inf,3}, +%% {erl_types,t_inf_lists_strict,4}, +%% {erl_types,t_inf,3}, +%% {dialyzer_typesig,solve_one_c,...}, +%% {dialyzer_typesig,...}]} +%%Last messages in the log cache: +%% Typesig analysis for SCC: [{ejabberd_web_admin,make_xhtml,5}] +%% Typesig analysis for SCC: [{ejabberd_web_admin,make_xhtml,4}] +%% Typesig analysis for SCC: [{ejabberd_web_admin,process_admin,2}] +%% Typesig analysis for SCC: [{ejabberd_web_admin,process,2}] +%% Typesig analysis for SCC: [{mod_stats,get_local_stat,3}] +%% Typesig analysis for SCC: [{mod_stats,get_local_stats,3}] +%% Typesig analysis for SCC: [{mod_stats,process_local_iq,3}] +%% Typesig analysis for SCC: [{mod_register_web,send_registration_notifications,2}] +%% Typesig analysis for SCC: [{mod_register_web,process,2}] +%% Typesig analysis for SCC: [{pubsub_db,create_node,5}] + +%% + + +%% Pubsub #xmlel{} templates + + +%% -------------------------------------------------------------------- +%% Functions. +%% -------------------------------------------------------------------- + + + + +%% @spec nodeId() -> NodeId::exmpp_pubsub:nodeId() +-spec(nodeId/0 :: () -> NodeId::exmpp_pubsub:nodeId()). + +nodeId() -> + randoms:get_string(). + +%% @spec subId() -> SubId::exmpp_pubsub:subId() +-spec(subId/0 :: () -> SubId::exmpp_pubsub:subId()). + +subId() -> + {T1, T2, T3} = now(), + list_to_binary(lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3]))). + +%% @spec itemId() -> ItemId::exmpp_pubsub:itemId() +-spec(itemId/0 :: () -> ItemId::exmpp_pubsub:itemId()). + +itemId() -> + {T1, T2, T3} = now(), + list_to_binary(lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3]))). + +id() -> + {T1, T2, T3} = now(), + list_to_binary(lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3]))). + + + +%% +-spec(subscription_subscribed/2 :: +( + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: [] | pubsub_options:options_subscription()) + -> Subscription_Subscribed::exmpp_pubsub:subscription_subscribed() +). + +subscription_subscribed(Resource, Subscription_Options) -> + {'subscribed', subId(), Resource, Subscription_Options}. + +%% +-spec(subscription_pending/2 :: +( + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: [] | pubsub_options:options_subscription()) + -> Subscription_Pending::exmpp_pubsub:subscription_pending() +). + +subscription_pending(Resource, Subscription_Options) -> + {'pending', subId(), Resource, Subscription_Options}. + + + +%% Pubsub Xmlel templates + +xmlcdata(CData) -> + {xmlcdata, CData}. + +xmlattr(Name, Value) -> + {Name, Value}. + + +xmlel('disco#info', Name, Attrs, Children) -> + xmlel(?NS_DISCO_INFO, Name, Attrs, Children); +xmlel('disco#items', Name, Attrs, Children) -> + xmlel(?NS_DISCO_ITEMS, Name, Attrs, Children); +xmlel('jabber:client', Name, Attrs, Children) -> + xmlel(?NS_JABBER_CLIENT, Name, Attrs, Children); +xmlel('pubsub', Name, Attrs, Children) -> + xmlel(?NS_PUBSUB, Name, Attrs, Children); +xmlel('pubsub#event', Name, Attrs, Children) -> + xmlel(?NS_PUBSUB_EVENT, Name, Attrs, Children); +xmlel('pubsub#owner', Name, Attrs, Children) -> + xmlel(?NS_PUBSUB_OWNER, Name, Attrs, Children); +xmlel('shim', Name, Attrs, Children) -> + xmlel(?NS_SHIM, Name, Attrs, Children); +xmlel(NS, Name, Attrs, Children) -> + #xmlel{name = Name, attrs = [{<<"xmlns">>, NS} | Attrs], children = Children}. +%% +xmlattr_affiliation(Affiliation) -> + xmlattr(<<"affiliation">>, list_to_binary(atom_to_list(Affiliation))). +%% +xmlattr_category(Category) -> + xmlattr(<<"category">>, Category). +%% +xmlattr_id(ItemId) -> + xmlattr(<<"id">>, ItemId). +%% +xmlattr_jid(Jid) -> + xmlattr(<<"jid">>, Jid). +%% +xmlattr_name(Name) -> + xmlattr(<<"name">>, Name). +%% +xmlattr_node(NodeId) -> + xmlattr(<<"node">>, NodeId). +%% +xmlattr_publisher(Publisher) -> + xmlattr(<<"publisher">>, Publisher). +%% +xmlattr_subid(SubId) -> + xmlattr(<<"subid">>, SubId). +%% +xmlattr_subscription(Subscription_State) -> + xmlattr(<<"subscription">>, list_to_binary(atom_to_list(Subscription_State))). +%% +xmlattr_type(Type) when is_atom(Type)-> + xmlattr(<<"type">>, list_to_binary(atom_to_list(Type))); +%% +xmlattr_type(Type) -> + xmlattr(<<"type">>, Type). +%% +xmlattr_uri(URI) -> + xmlattr(<<"uri">>, URI). +%% +xmlattr_var(Var) -> + xmlattr(<<"var">>, Var). + +%% +xmlel_affiliation('pubsub', NodeId, Affiliation) -> + xmlel('pubsub', <<"affiliation">>, + [xmlattr_node(NodeId), + xmlattr_affiliation(Affiliation)], + []); +xmlel_affiliation('pubsub#owner', Jid, Affiliation) -> + xmlel('pubsub#owner', <<"affiliation">>, + [xmlattr_jid(Jid), + xmlattr_affiliation(Affiliation)], + []). +%% +xmlel_affiliations('pubsub', Xmlels) -> + xmlel('pubsub', <<"affiliations">>, [], Xmlels). + +xmlel_affiliations('pubsub#owner', NodeId, Xmlels) -> + xmlel('pubsub#owner', <<"affiliations">>, [xmlattr_node(NodeId)], Xmlels). +%% +xmlel_create(NodeId) -> + xmlel('pubsub', <<"create">>, [xmlattr_node(NodeId)], []). + +%% +xmlel_configure('pubsub#owner', NodeId, Xmlels) -> + xmlel('pubsub#owner', <<"configure">>, [xmlattr_node(NodeId)], Xmlels). + +%% +xmlel_default('pubsub', Xmlels) -> + xmlel('pubsub', <<"default">>, [], Xmlels); +%% +xmlel_default('pubsub#owner', Xmlels) -> + xmlel('pubsub#owner', <<"default">>, [], Xmlels). + +%% +xmlel_default('pubsub', NodeId, Xmlels) -> + xmlel('pubsub', <<"default">>, [xmlattr_node(NodeId)], Xmlels). + +%% +xmlel_delete(NodeId, RedirectURI) -> + xmlel('pubsub#event', <<"delete">>, + [xmlattr_node(NodeId)], + case RedirectURI of + undefined -> []; + _RedirectURI -> [xmlel_redirect(RedirectURI)] + end). +%% +xmlel_event(Xmlels) -> + xmlel('pubsub#event', <<"event">>, [], Xmlels). +%% +xmlel_feature('disco#info', Feature) -> + xmlel('disco#info', <<"feature">>, [xmlattr_var(Feature)], []). +%% +xmlel_header(SubId) -> + xmlel('shim', <<"header">>, [xmlattr_name(<<"SubID">>)], [xmlcdata(SubId)]). +%% +xmlel_headers(Xmlels_Header) -> + xmlel('shim', <<"headers">>, [], Xmlels_Header). + + +xmlel_identity('disco#info', Category, Type, Name) -> + xmlel('disco#info', <<"identity">>, + case Name of + undefined -> + [xmlattr_category(Category), + xmlattr_type(Type)]; + _ -> + [xmlattr_category(Category), + xmlattr_name(Name), + xmlattr_type(Type)] + end, + []). + +%% +xmlel_item('pubsub', ItemId) -> + xmlel('pubsub', <<"item">>, [xmlattr_id(ItemId)], []). + +%% +xmlel_item('disco#items', ItemId, Jid) -> + xmlel('disco#items', <<"item">>, + [xmlattr_name(ItemId), + xmlattr_jid(Jid)], + []); +xmlel_item('pubsub', ItemId, Payload) -> + xmlel('pubsub', <<"item">>, [xmlattr_id(ItemId)], + case Payload of + [] -> []; + _Payload -> [Payload] + end). + +xmlel_item('disco#items', NodeId, Jid, Name) -> + xmlel('disco#items', <<"item">>, + case Name of + undefined -> + [xmlattr_jid(Jid), + xmlattr_node(NodeId)]; + _ -> + [xmlattr_jid(Jid), + xmlattr_node(NodeId), + xmlattr_name(Name)] + end, + []); +%% +xmlel_item('pubsub#event', ItemId, Publisher, Payload) -> + xmlel('pubsub#event', <<"item">>, + case Publisher of + undefined -> + [xmlattr_id(ItemId)]; + _Publisher -> + [xmlattr_id(ItemId), + xmlattr_publisher(Publisher)] + end, + case Payload of + [] -> []; + _Payload -> [Payload] + end). +%% +xmlel_items('pubsub', NodeId, Xmlels) -> + xmlel('pubsub', <<"items">>, [xmlattr_node(NodeId)], Xmlels); +%% +xmlel_items('pubsub#event', undefined = _NodeId, Xmlels) -> + xmlel('pubsub#event', <<"items">>, [], Xmlels); +%% +xmlel_items('pubsub#event', NodeId, Xmlels) -> + xmlel('pubsub#event', <<"items">>, [xmlattr_node(NodeId)], Xmlels). +%% +xmlel_items('pubsub#event', NodeId, undefined = _ItemId, _Publisher, _Payload) -> + xmlel('pubsub#event', <<"items">>, [xmlattr_node(NodeId)], []); +xmlel_items('pubsub#event', NodeId, ItemId, Publisher, Payload) -> + xmlel('pubsub#event', <<"items">>, + [xmlattr_node(NodeId)], + [xmlel_item('pubsub#event', ItemId, Publisher, Payload)]). +%% +xmlel_message(undefined = _Type, Xmlels) -> + xmlel_message('normal', Xmlels); +xmlel_message(Type, Xmlels) -> + xmlel('jabber:client', <<"message">>, + [xmlattr_type(Type), + xmlattr_id(_Id = id())], + Xmlels). +%% +xmlel_options('pubsub', NodeId, Jid, SubId, Xmlels) -> + xmlel('pubsub', <<"options">>, + case SubId of + undefined -> + [xmlattr_node(NodeId), + xmlattr_jid(Jid)]; + _ -> + [xmlattr_node(NodeId), + xmlattr_jid(Jid), + xmlattr_subid(SubId)] + end, + Xmlels). +%% +xmlel_publish('pubsub', NodeId, Xmlels) -> + xmlel('pubsub', <<"publish">>, [xmlattr_node(NodeId)], Xmlels). +%% +xmlel_purge(NodeId) -> + xmlel('pubsub#event', <<"purge">>, [xmlattr_node(NodeId)], []). + +%% +xmlel_query('disco#info', Xmlels) -> + xmlel('disco#info', <<"query">>, [], Xmlels); +%% +xmlel_query('disco#items', Xmlels) -> + xmlel('disco#items', <<"query">>, [], Xmlels). + +%% +xmlel_query('disco#info', NodeId, Xmlels) -> + xmlel('disco#info', <<"query">>, [xmlattr_node(NodeId)], Xmlels); +xmlel_query('disco#items', NodeId, Xmlels) -> + xmlel('disco#items', 'query', [xmlattr_node(NodeId)], Xmlels). + +%% +xmlel_redirect(RedirectURI) -> + xmlel('pubsub#event', <<"redirect">>, [xmlattr_uri(RedirectURI)], []). +%% +xmlel_retract(undefined = _ItemId) -> + xmlel('pubsub#event', <<"retract">>, [], []); +xmlel_retract(ItemId) -> + xmlel('pubsub#event', <<"retract">>, [xmlattr_id(ItemId)], []). +%% +xmlel_subscription('pubsub', NodeId, Jid, SubId, Subscription_State) -> + xmlel('pubsub', <<"subscription">>, + [xmlattr_node(NodeId), + xmlattr_jid(Jid), + xmlattr_subid(SubId), + xmlattr_subscription(Subscription_State)], + []); +xmlel_subscription('pubsub#event', NodeId, Jid, SubId, Subscription_State) -> + xmlel('pubsub#event', <<"subscription">>, + case SubId of + undefined -> + [xmlattr_node(NodeId), + xmlattr_jid(Jid), + xmlattr_subscription(Subscription_State)]; + _SubId -> + [xmlattr_node(NodeId), + xmlattr_jid(Jid), + xmlattr_subid(SubId), + xmlattr_subscription(Subscription_State)] + end, + []). +%% +xmlel_subscription('pubsub#owner', Jid, SubId, Subscription_State) -> + xmlel('pubsub#owner', <<"subscription">>, + case SubId of + undefined -> + [xmlattr_jid(Jid), + xmlattr_subscription(Subscription_State)]; + _SubId -> + [xmlattr_jid(Jid), + xmlattr_subid(SubId), + xmlattr_subscription(Subscription_State)] + end, + []). +%% +xmlel_subscriptions('pubsub', Xmlels) -> + xmlel('pubsub', <<"subscriptions">>, [], Xmlels). + +xmlel_subscriptions('pubsub#owner', NodeId, Xmlels) -> + xmlel('pubsub#owner', <<"subscriptions">>, [xmlattr_node(NodeId)], Xmlels). +%% +xmlel_publish(NodeId, undefined = _ItemId) -> + xmlel('pubsub', <<"publish">>, [xmlattr_node(NodeId)], []); +xmlel_publish(NodeId, ItemId) -> + xmlel('pubsub', <<"publish">>, + [xmlattr_node(NodeId)], + [xmlel_item('pubsub', ItemId)]). +%% +xmlel_pubsub('pubsub', Children) -> + xmlel('pubsub', <<"pubsub">>, [], Children); +%% +xmlel_pubsub('pubsub#owner', Children) -> + xmlel('pubsub#owner', <<"pubsub">>, [], Children). + diff --git a/src/mod_pubsub_ng/mod_pubsub_dev.erl b/src/mod_pubsub_ng/mod_pubsub_dev.erl new file mode 100644 index 000000000..9faca6552 --- /dev/null +++ b/src/mod_pubsub_ng/mod_pubsub_dev.erl @@ -0,0 +1,863 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(mod_pubsub_dev). +-author('christophe.romain@process-one.net'). +-author('karim.gemayel@process-one.net'). +-version('1.13-0'). + +-behaviour(gen_server). +-behaviour(gen_mod). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + +-compile(export_all). +%%% Export Functions + +%% API and gen_server callbacks +-export([ + start_link/2, + start/2, + stop/1, + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3 +]). + +%% +-export_type([ + resource_show/0, + presence_cache/0 +]). + +-type(resource_show() + :: {Resource :: xmpp_jid:resource_jid(), + Show :: 'away' | 'chat' | 'dnd' | 'online' | 'xa' } +). + +-type(presence_cache() :: [Resource_Show::resource_show()]). + +%% +-export_type([ + resource_subids/0, + resources_subids/0 +]). + +-type(resource_subids() + :: {Resource :: xmpp_jid:resource_jid(), + SubIds :: [SubId::exmpp_pubsub:subId(),...]} +). + +-type(resources_subids() :: [Resource_SubIds::resource_subids(),...]). + +%%% +-export_type([ + entity/0, + n0de/0, + cache/0, + subids/0, + event/0 +]). + +-type(entity() :: #entity{}). +% id :: xmpp_jid:usr_bare(), +% affiliation :: 'member' | 'owner' | 'publisher', +% subscriptions :: []%[exmpp_pubsub:subscription(),...] +%}). + +%%% +-type(n0de() :: #node{}). +% id :: exmpp_pubsub:nodeId(), +% owners :: [Node_Owner::xmpp_jid:usr_bare(),...], +% access_model :: pubsub_options:access_model(), +% itemreply :: 'owner' | 'publisher', +% notification_type :: 'headline' | 'normal', +% presence_based_delivery :: boolean(), +% rosters_groups_allowed :: [] | pubsub_options:rosters_groups_allowed() +%}). + +%%% +-type(cache() :: #cache{}). +% presence :: undefined | mod_pubsub_dev:presence_cache(), +% presence_subscriptions :: undefined | boolean(), +% rosters_groups :: undefined | boolean() +%}). + +%%% +-type(subids() :: #subids{}). +% presence :: undefined | [] | mod_pubsub_dev:resources_subids(), +% no_presence :: undefined | [] | mod_pubsub_dev:resources_subids() +%}). + +%% +-type(event() :: #event{ +% host :: xmpp_jid:raw_jid_component_bare(), +% component :: xmpp_jid:component_bare(), +% entity :: mod_pubsub_dev:entity(), +% node :: mod_pubsub_dev:n0de(), +% cache :: mod_pubsub_dev:cache(), +% subids :: mod_pubsub_dev:subids() +}). + +%%% +-export_type([ + item/0, + published_item/0, + retracted_item/0 +]). + +-type(published_item() :: #item{ + access_model :: undefined | pubsub_options:access_model(), + presence_based_delivery :: undefined | boolean(), + rosters_groups_allowed :: [] | pubsub_options:rosters_groups_allowed(), + stanza :: xmlel() +}). + +-type(retracted_item() :: #item{ + access_model :: undefined | pubsub_options:access_model(), + presence_based_delivery :: undefined | boolean(), + rosters_groups_allowed :: [] | pubsub_options:rosters_groups_allowed(), + stanza :: xmlel() +}). + +-type(item() :: published_item() | retracted_item()). + + +%% + +-export_type([ + pubsub_itemId/0, + pubsub_nodeId/0, + pubsub_stateId/0, + item_owners/0, + node_owners/0, + item_creation/0, + item_modification/0, + node_creation/0 +]). + +-type(pubsub_itemId() + :: {ItemId::exmpp_pubsub:itemId(), NodeIdx::exmpp_pubsub:nodeIdx()} +). + +-type(pubsub_nodeId() + :: {Pubsub_Host :: exmpp_pubsub:host(), NodeId :: exmpp_pubsub:nodeId()} +). + +-type(pubsub_stateId() + :: {Entity::xmpp_jid:usr_bare(), NodeIdx::exmpp_pubsub:nodeIdx()} +). + +-type(item_owners() :: [Entity::xmpp_jid:usr_bare(),...]). +-type(node_owners() :: [Entity::xmpp_jid:usr_bare(),...]). + +-type(item_creation() + :: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()} +). + +-type(item_modification() + :: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()} +). + +-type(node_creation() + :: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()} +). + + + +%% +-export_type([ + pubsub_state/0, + pubsub_states/0, + % + pubsub_state_owner/0, + pubsub_state_publisher/0, + pubsub_state_publish_only/0, + pubsub_state_member/0, + pubsub_state_none/0, + pubsub_state_outcast/0 +]). + +-type(pubsub_state_owner() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'owner', + access :: undefined | 'presence' | 'roster', + %subscriptions :: [exmpp_pubsub:subscription_subscribed() | + % exmpp_pubsub:subscription_unconfigured()], + subscriptions :: [exmpp_pubsub:subscription()], + itemids :: [] | exmpp_pubsub:itemIds() + } +). + +%% +-type(pubsub_state_publisher() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'publisher', + access :: undefined | 'presence' | 'roster', + %subscriptions :: [exmpp_pubsub:subscription_subscribed() | + % exmpp_pubsub:subscription_unconfigured()], + subscriptions :: [exmpp_pubsub:subscription()], + itemids :: [] | exmpp_pubsub:itemIds() + } +). + +%% +-type(pubsub_state_publish_only() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'publish-only', + access :: undefined | 'presence' | 'roster', + subscriptions :: [], + itemids :: [] | exmpp_pubsub:itemIds() + } +). + +%% +-type(pubsub_state_member() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'member', + access :: undefined | 'pending' | 'presence' | 'roster', + subscriptions :: [], + itemids :: [] | exmpp_pubsub:itemIds() + } + %% + | #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'member', + access :: undefined | 'pending' | 'presence' | 'roster', + %subscriptions :: [exmpp_pubsub:subscription_pending(),...], + subscriptions :: [exmpp_pubsub:subscription()], + itemids :: [] | exmpp_pubsub:itemIds() + } + %% + | #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'member', + subscriptions :: [exmpp_pubsub:subscription()], + %subscriptions :: [exmpp_pubsub:subscription_pending() | + % exmpp_pubsub:subscription_subscribed() | + % exmpp_pubsub:subscription_unconfigured()], + itemids :: [] | exmpp_pubsub:itemIds() + } +). + +%% +-type(pubsub_state_none() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'none', + access :: undefined | 'presence' | 'roster', + subscriptions :: [], + itemids :: [] + } + %% + | #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'none', + access :: undefined | 'presence' | 'roster', + subscriptions :: [], + itemids :: exmpp_pubsub:itemIds() + } + %% + | #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'none', + access :: undefined | 'presence' | 'roster', + subscriptions :: [], + itemids :: exmpp_pubsub:itemIds() + } +). + +%% +-type(pubsub_state_outcast() + :: #pubsub_state_dev{ + id :: mod_pubsub_dev:pubsub_stateId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + affiliation :: 'outcast', + access :: undefined | 'presence' | 'roster', + subscriptions :: [exmpp_pubsub:subscription()], + %subscriptions :: [exmpp_pubsub:subscription_pending() | + % exmpp_pubsub:subscription_subscribed() | + % exmpp_pubsub:subscription_unconfigured()], + itemids :: [] | exmpp_pubsub:itemIds() + } +). + + +%% +-type(pubsub_state() :: mod_pubsub_dev:pubsub_state_owner() + | mod_pubsub_dev:pubsub_state_publisher() + | mod_pubsub_dev:pubsub_state_publish_only() + | mod_pubsub_dev:pubsub_state_member() + | mod_pubsub_dev:pubsub_state_none() + | mod_pubsub_dev:pubsub_state_outcast() +). + +-type(pubsub_states() :: [Pubsub_State::mod_pubsub_dev:pubsub_state(),...]). + + +%% +-export_type([ + pubsub_node/0, + pubsub_nodes/0 +]). + +-type(pubsub_node() + :: #pubsub_node_dev{ + id :: mod_pubsub_dev:pubsub_nodeId(), + idx :: exmpp_pubsub:nodeIdx(), + creation :: mod_pubsub_dev:node_creation(), + level :: exmpp_pubsub:level(), + owners :: mod_pubsub_dev:node_owners(), + itemids :: [] | exmpp_pubsub:itemIds(), + options :: pubsub_options:options_node() + } +). + +-type(pubsub_nodes() :: [Pubsub_Node::mod_pubsub_dev:pubsub_node(),...]). + + +%% +-export_type([ + pubsub_item/0, + pubsub_items/0 +]). + +-type(pubsub_item() + :: #pubsub_item_dev{ + id :: mod_pubsub_dev:pubsub_itemId(), + nodeidx :: exmpp_pubsub:nodeIdx(), + owners :: mod_pubsub_dev:item_owners(), + creation :: mod_pubsub_dev:item_creation(), + modification :: mod_pubsub_dev:item_modification(), + payload :: exmpp_pubsub:payload(), + options :: [] | pubsub_options:options_item() + } +). + +-type(pubsub_items() :: [Pubsub_Item::mod_pubsub_dev:pubsub_item(),...]). + +%% +-export_type([ + pubsub_last_item/0 +]). + +-type(pubsub_last_item() + :: #pubsub_last_item_dev{ + nodeidx :: exmpp_pubsub:nodeIdx(), + id :: exmpp_pubsub:itemId(), + owners :: mod_pubsub_dev:item_owners(), + creation :: mod_pubsub_dev:item_creation(), + payload :: exmpp_pubsub:payload(), + options :: [] | pubsub_options:options_item() + } +). + +%% +-export_type([ + pubsub_subscription_pending/0 +]). + +-type(pubsub_subscription_pending() + :: #pubsub_subscription_pending{ + id :: {Entity::xmpp_jid:usr_bare(), NodeIdx::exmpp_pubsub:nodeIdx()}, + nodeidx :: exmpp_pubsub:nodeIdx(), + subids :: [SubId::exmpp_pubsub:subId(),...] + } +). + + + + + +-define(PROCNAME, ejabberd_mod_pubsub_dev). + + + + + + + +-record(state, +{ + host :: xmpp_jid:raw_jid_component_bare(), + pubsub_host :: xmpp_jid:raw_jid_component_bare(), + pubsub :: #capabilities{}, + pubsub_on_jid :: #capabilities{} +}). + +-type(state() + :: #state{ + host :: xmpp_jid:raw_jid_component_bare(), + pubsub_host :: xmpp_jid:raw_jid_component_bare(), + pubsub :: #capabilities{}, + pubsub_on_jid :: #capabilities{} + } +). + + +%%==================================================================== +%% API +%%==================================================================== +%%-------------------------------------------------------------------- +%% Function: start_link() -> {ok,Pid} | ignore | {error,Error} +%% Description: Starts the server +%%-------------------------------------------------------------------- + +%% @spec start_link(Host, Options) -> {'ok', pid()} | 'ignore' | {'error', atom()} +%% Host = string() +%% Options = {Option::atom(), Value::term()} +-spec(start_link/2 :: +( + Host :: string(), + Options :: [Option::{Key::atom(), Value::term()}]) + -> {'ok', pid()} | 'ignore' | {'error', _} +). + +start_link(Host, Options) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:start_link({local, Proc}, ?MODULE, [Host, Options], []). + +%% @spec start(Host, Options) -> {'error',atom()} | {'ok','undefined' | pid()} | {'ok','undefined' | pid(),any()} +%% Host = string() +%% Options = {Option::atom(), Value::term()} +-spec(start/2 :: +( + Host :: string(), + Options :: [Option::{Key::atom(), Value::term()}]) + -> {'error',_} | {'ok','undefined' | pid()} | {'ok','undefined' | pid(),_} +). + +start(Host, Options) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + ChildSpec = {Proc, + {?MODULE, start_link, [Host, Options]}, + transient, 1000, worker, [?MODULE]}, + supervisor:start_child(ejabberd_sup, ChildSpec). + +%% @spec stop(Host) -> 'ok' | {'error','not_found' | 'running' | 'simple_one_for_one'} +%% Host = string() +-spec(stop/1 :: +( + Host :: string()) + -> 'ok' | {'error','not_found' | 'running' | 'simple_one_for_one'} +). + +stop(Host) -> + Proc = gen_mod:get_module_proc(Host, ?PROCNAME), + gen_server:call(Proc, stop), + supervisor:delete_child(ejabberd_sup, Proc). + + + + +%%==================================================================== +%% gen_server callbacks +%%==================================================================== + +%%-------------------------------------------------------------------- +%% Function: init(Args) -> {ok, State} | +%% {ok, State, Timeout} | +%% ignore | +%% {stop, Reason} +%% Description: Initiates the server +%%-------------------------------------------------------------------- +-spec(init/1 :: +( + Args :: [binary() | [{Option::atom(), Value::term()}]]) + -> {'ok', State::state()} +). + +init([Host, Options] = _Args) -> + ?DEBUG("pubsub init ~p ~p",[Host, Options]), + Pubsub_Host = gen_mod:expand_host_name(Host, Options, <<"pubsub">>), + IQDisc = gen_mod:get_opt('iqdisc', Options, 'one_queue'), + + % mod_disco:register_feature(Server_HostB, ?NS_PUBSUB_s), + %% + %% Pubsub Hooks + %% - Service disco hooks + %% * disco_local_identity + %% * disco_local_features + %% * disco_local_items + %% * disco_local_forms + %% - Connectivity hooks + %% * presence_probe_hook + %% * sm_remove_connection_hook + %% - Roster hooks + %% * roster_in_subscription + %% * roster_out_subscription + %% - User managment hooks + %% * remove_user + %% * anonymous_purge_hook + %% + %% Pubsub On Jid hooks + %% - Service disco hooks + %% * disco_sm_identity + %% * disco_sm_features + %% * disco_sm_items + %% * disco_sm_forms + %% - Connectitivy + %% * caps_update + + + Pubsub_Features = node_flat_dev:pubsub_features(), + % mod_disco:register_feature(Server_HostB, ?NS_PUBSUB_s), + pubsub_db:init('mnesia', 'dev'), + %% Register hooks according to available Pubsub Features + register_hooks(Host, Pubsub_Features, pubsub_hooks), + %% + ejabberd_router:register_route(Pubsub_Host), + {ok, + #state{ + host = Host, + pubsub_host = Pubsub_Host, + pubsub = node_flat_dev:capabilities() + } + }. + +%%-------------------------------------------------------------------- +%% Function: +%% handle_call(Request, From, State) -> {reply, Reply, State} | +%% {reply, Reply, State, Timeout} | +%% {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, Reply, State} | +%% {stop, Reason, State} +%% Description: Handling call messages +%%-------------------------------------------------------------------- +%% @private + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_cast(Msg, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling cast messages +%%-------------------------------------------------------------------- +%% @private +handle_cast(_Msg, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: handle_info(Info, State) -> {noreply, State} | +%% {noreply, State, Timeout} | +%% {stop, Reason, State} +%% Description: Handling all non call/cast messages +%%-------------------------------------------------------------------- +%% @private + +handle_info({route, From, #jid{resource = undefined} = To, Stanza}, + #state{host = Host, pubsub = Capabilities} = State) -> + do_route(Host, To, From, Stanza, Capabilities), + {noreply, State}; +%% +handle_info(_Info, State) -> + {noreply, State}. + +%%-------------------------------------------------------------------- +%% Function: terminate(Reason, State) -> void() +%% Description: This function is called by a gen_server when it is about to +%% terminate. It should be the opposite of Module:init/1 and do any necessary +%% cleaning up. When it returns, the gen_server terminates with Reason. +%% The return value is ignored. +%%-------------------------------------------------------------------- +%% @private +%% @todo : -spec +-spec(terminate/2 :: +( + Reason :: _, + State :: state()) + -> 'ok' +). + +terminate(_Reason, #state{host = Host, pubsub_host = Pubsub_Host}) -> + ejabberd_router:unregister_route(Pubsub_Host), + ok. + +%%-------------------------------------------------------------------- +%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} +%% Description: Convert process state when code is changed +%%-------------------------------------------------------------------- +%% @private +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + + +%% +-define(Is_Stanza_IQ(Stanza), +( + Stanza#xmlel.name == <<"iq">> +)). + +-define(Is_Stanza_Message(Stanza), +( + Stanza#xmlel.name == <<"message">> +)). + +-define(Is_Stanza_Presence(Stanza), +( + Stanza#xmlel.name == <<"presence">> +)). + +-define(Is_IQ_Ignore(IQ), +( + IQ#iq.type == 'result' + orelse + IQ#iq.type == 'error' +)). + +-define(Is_IQ_Pubsub(IQ), +( + (IQ#iq.type == 'set' + orelse + IQ#iq.type == 'get') + andalso + (IQ#iq.xmlns == ?NS_PUBSUB + orelse + IQ#iq.xmlns == ?NS_PUBSUB_OWNER) + andalso + IQ#iq.sub_el#xmlel.name == <<"pubsub">> +)). + +-define(Is_IQ_Get_Disco_Info(IQ), +( + IQ#iq.type == 'get' + andalso + IQ#iq.xmlns == ?NS_DISCO_INFO + andalso + IQ#iq.sub_el#xmlel.name == <<"query">> +% andalso +% IQ#iq.payload#xmlel.children == [] +)). + +-define(Is_IQ_Get_Disco_Items(IQ), +( + IQ#iq.type == 'get' + andalso + IQ#iq.xmlns == ?NS_DISCO_ITEMS + andalso + IQ#iq.sub_el#xmlel.name == <<"query">> +% andalso +% IQ#iq.payload#xmlel.children == [] +)). + +%% +do_route(Host, + #jid{lserver = Pubsub_Host} = Pubsub_Component, + #jid{luser = U, lserver = S, lresource = R} = Entity, Stanza_IQ, + #capabilities{privacy = Privacy, plugin = Plugin} = Capabilities) + when ?Is_Stanza_IQ(Stanza_IQ) -> + case IQ = jlib:iq_query_or_response_info(Stanza_IQ) of + IQ when ?Is_IQ_Ignore(IQ) -> + ok; + IQ when ?Is_IQ_Get_Disco_Info(IQ) -> + case + pubsub_disco:iq_disco_info('dev', Host, Pubsub_Host, + Privacy, {U,S,undefined}, + case S of + Host -> 'local'; + _ -> 'remote' + end, + Plugin, + case xml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el) of + <<>> -> undefined; + NodeId -> NodeId + end, + xml:remove_cdata(IQ#iq.sub_el#xmlel.children)) + of + {result, Xmlel_Query} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:iq_to_xml(IQ#iq{ + type = result, + sub_el = [Xmlel_Query] + })); + {error, Error} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:make_error_reply(Stanza_IQ, Error)) + end; + IQ when ?Is_IQ_Get_Disco_Items(IQ) -> + case + pubsub_disco:iq_disco_items('dev', Host, Pubsub_Host, + Privacy, {U,S,undefined}, + case S of + Host -> 'local'; + _ -> 'remote' + end, + Plugin, + case xml:get_tag_attr_s(<<"node">>, IQ#iq.sub_el) of + <<>> -> undefined; + NodeId -> NodeId + end, + xml:remove_cdata(IQ#iq.sub_el#xmlel.children)) + of + {result, Xmlel_Query} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:iq_to_xml(IQ#iq{ + type = result, + sub_el = [Xmlel_Query] + })); + {error, Error} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:make_error_reply(Stanza_IQ, Error)) + end; + IQ when ?Is_IQ_Pubsub(IQ) -> + case + iq_pubsub(Host, Pubsub_Host, {U,S,R}, IQ#iq.type, + _Rsm = lists:member(?NS_RSM, Plugin:features()), + xml:remove_cdata(IQ#iq.sub_el#xmlel.children), + IQ#iq.lang, Capabilities) + of + {result, []} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:iq_to_xml(IQ#iq{ + type = result, + sub_el = [] + })); + {result, Xmlels} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:iq_to_xml(IQ#iq{ + type = result, + sub_el = Xmlels + })); + {error, Error} -> + ejabberd_router:route(Pubsub_Component, Entity, + jlib:make_error_reply(Stanza_IQ, Error)) + end + end; +do_route(Host, + #jid{lserver = Pubsub_Host} = Pubsub_Component, + #jid{luser = U, lserver = S, lresource = R} = Entity, Stanza_Message, + Capabilities) + when ?Is_Stanza_Message(Stanza_Message) -> + ok; +do_route(Host, + #jid{lserver = Pubsub_Host} = Pubsub_Component, + #jid{luser = U, lserver = S, lresource = R} = Entity, Stanza_Presence, + Capabilities) + when ?Is_Stanza_Presence(Stanza_Presence) -> + ok. + + +iq_pubsub(Host, Pubsub_Host, Entity, IQ_Type, Rsm, Xmlels, _Lang, + #capabilities{api = #api{parser = Parser, core = API_Core}} = Capabilities) -> + case Parser:parse(IQ_Type, Xmlels, Rsm, API_Core) of + {result, Module, Function, Parameters} -> + Module:Function(Host, Pubsub_Host, Entity, Parameters, Capabilities); + Error -> + Error + end. + + + +register_hooks(Host, Pubsub_Features, Module) -> + register_hooks('roster', Host, Pubsub_Features, Module), + register_hooks('presence', Host, Pubsub_Features, Module). + + %ejabberd_hooks:add(roster_in_subscription, Host, pubsub_hooks, roster_in_subscription, 50), + %ejabberd_hooks:add(roster_out_subscription, Host, ?MODULE, roster_out_subscription, 50), + %ejabberd_hooks:add(roster_get, Host, ?MODULE, roster_get, 50), + %ejabberd_hooks:add(roster_get_jid_info, Host, ?MODULE, roster_get_jid_info, 50), + %ejabberd_hooks:add(roster_get_subscription_lists, Host, ?MODULE, roster_get_subscription_lists, 50), + %%ejabberd_hooks:add(roster_process_item, Host, pubsub_hooks, roster_process_item, 50). + + +register_hooks('roster', Host, Pubsub_Features, Module) -> + case lists:member("access-roster", Pubsub_Features) of + true -> + ejabberd_hooks:add(roster_process_item, Host, Module, + roster_process_item, 50), + ok; + false -> + ok + end; +%% +register_hooks('presence', Host, Pubsub_Features, Module) -> + case + lists:member(<<"access-presence">>, Pubsub_Features) + orelse + lists:member(<<"last-published">>, Pubsub_Features) + orelse + lists:member(<<"leased-subscription">>, Pubsub_Features) + orelse + lists:member(<<"presence-notifications">>, Pubsub_Features) + of + true -> + ejabberd_hooks:add(presence_probe_hook, Host, Module, + presence_online, 80), + ejabberd_hooks:add(sm_remove_connection_hook, Host, Module, + presence_offline, 80), + ok; + false -> + ok + end. + + +roster_get(Acc, {User, Server}) -> + ?INFO_MSG("ROSTER_GET Acc ~p ~nUser ~p ~nServer ~p ~n", + [Acc, User, Server]). + +roster_get_jid_info(Acc, User, Server, JID) -> + ?INFO_MSG("ROSTER_GET_JID_INFO Acc ~p ~nUser ~p ~nServer ~p ~nJID ~p ~n", + [Acc, User, Server, JID]). + +roster_get_subscription_lists(Acc, User, Server) -> + ?INFO_MSG("ROSTER_GET_SUBSCRIPTION_LISTS Acc ~p ~nUser ~p ~nServer ~p ~n", + [Acc, User, Server]). + +roster_in_subscription(User, Server, JID, SubscriptionType, Reason, A) -> + ?INFO_MSG("ROSTER_IN_SUBSCRITION User ~p ~n Server ~p ~n JID ~p ~n SubscriptionType ~p ~n, Reason ~p ~nA ~p ~n", + [User, Server, JID, SubscriptionType, Reason, A]). + +roster_out_subscription(User, Server, JID, SubscriptionType) -> + ?INFO_MSG("ROSTER_OUT_SUBSCRITION User ~p ~n Server ~p ~n JID ~p ~n SubscriptionType ~p ~n", + [User, Server, JID, SubscriptionType]). + +roster_process_item(RosterItem, Server) -> + ?INFO_MSG("ROSTER_PROCESS_ITEM RosterItem ~p ~nServer ~p ~n", + [RosterItem, Server]), + RosterItem. + diff --git a/src/mod_pubsub_ng/node_flat_dev.erl b/src/mod_pubsub_ng/node_flat_dev.erl new file mode 100644 index 000000000..9de5e5a1f --- /dev/null +++ b/src/mod_pubsub_ng/node_flat_dev.erl @@ -0,0 +1,310 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(node_flat_dev). +-author('karim.gemayel@process-one.net'). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + +-compile(export_all). + + + +%% +-spec(capabilities/0 :: () -> #capabilities{}). + +capabilities() -> + #capabilities{plugin = ?MODULE}. + + +%% +-spec(pubsub_features/0 :: () -> Pubsub_Features::exmpp_pubsub:pubsub_features()). + +pubsub_features() -> + [<<"access-authorize">>, + <<"access-open">>, + <<"access-presence">>, + <<"access-roster">>, + <<"access-whitelist">>, + <<"auto-create">>, + <<"auto-subscribe">>, + %<<"collections">>, + <<"config-node">>, + <<"create-and-configure">>, + <<"create-nodes">>, + <<"delete-items">>, + <<"delete-nodes">>, + %<<"filtered-notifications">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"last-published">>, + <<"leased-subscription">>, + <<"manage-subscriptions">>, + <<"member-affiliation">>, + <<"meta-data">>, + <<"modify-affiliations">>, + %<<"multi-collections">>, + <<"multi-subscribe">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"presence-notifications">>, + %"presence-subscribe">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"publish-options">>, + <<"publisher-affiliation">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-default">>, + <<"retrieve-default-sub">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>, + <<"subscription-options">>]. + +%% +-spec(pubsub_features/1 :: +( + Entity_Type :: 'local' | 'remote') + -> Pubsub_Features::exmpp_pubsub:pubsub_features() +). + +pubsub_features('local') -> + [<<"access-authorize">>, + <<"access-open">>, + <<"access-presence">>, + <<"access-roster">>, + <<"access-whitelist">>, + <<"auto-create">>, + <<"auto-subscribe">>, + %<<"collections">>, + <<"config-node">>, + <<"create-and-configure">>, + <<"create-nodes">>, + <<"delete-items">>, + <<"delete-nodes">>, + %<<"filtered-notifications">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"last-published">>, + <<"leased-subscription">>, + <<"manage-subscriptions">>, + <<"member-affiliation">>, + <<"meta-data">>, + <<"modify-affiliations">>, + %<<"multi-collections">>, + <<"multi-subscribe">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"presence-notifications">>, + %<<"presence-subscribe">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"publish-options">>, + <<"publisher-affiliation">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-default">>, + <<"retrieve-default-sub">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>, + <<"subscription-options">>]; +%% +pubsub_features('remote') -> + [<<"access-authorize">>, + <<"access-open">>, + <<"access-presence">>, + <<"access-roster">>, + <<"access-whitelist">>, + <<"auto-create">>, + <<"auto-subscribe">>, + %<<"collections">>, + <<"config-node">>, + <<"create-and-configure">>, + <<"create-nodes">>, + <<"delete-items">>, + <<"delete-nodes">>, + %<<"filtered-notifications">>, + <<"get-pending">>, + <<"instant-nodes">>, + <<"item-ids">>, + <<"last-published">>, + %<<"leased-subscription">>, + <<"manage-subscriptions">>, + <<"member-affiliation">>, + <<"meta-data">>, + <<"modify-affiliations">>, + %<<"multi-collections">>, + <<"multi-subscribe">>, + <<"outcast-affiliation">>, + <<"persistent-items">>, + <<"publish">>, + <<"publish-only-affiliation">>, + <<"publish-options">>, + <<"publisher-affiliation">>, + <<"purge-nodes">>, + <<"retract-items">>, + <<"retrieve-affiliations">>, + <<"retrieve-default">>, + <<"retrieve-default-sub">>, + <<"retrieve-items">>, + <<"retrieve-subscriptions">>, + <<"subscribe">>, + <<"subscription-notifications">>, + <<"subscription-options">>]. + +%% +%% +-spec(node_options/1 :: +( + Node_Type :: 'leaf' | 'collection') + -> Node_Options :: pubsub_options:options_node_leaf() + | pubsub_options:options_node_collection() +). + +node_options('leaf') -> + [{'access_model', open}, + {'contact', []}, + %{'collection', []}, + {'deliver_notifications', true}, + {'deliver_payloads', true}, + {'description', undefined}, + {'item_expire', undefined}, + {'itemreply', publisher}, + {'language', <<"en">>}, + {'max_items', 2}, + {'max_payload_size', undefined}, + {'node_type', leaf}, + {'notification_type', normal}, + {'notify_config', true}, + {'notify_delete', true}, + {'notify_retract', true}, + {'notify_sub', true}, + {'persist_items', true}, + {'presence_based_delivery', true}, + {'publish_model', open}, + {'purge_offline', false}, + {'roster_groups_allowed', []}, + {'send_last_published_item', on_sub_and_presence}, + {'subscribe', true}, + {'tempsub', false}, + {'title', undefined}, + {'type', undefined}]; +%% +node_options('collection') -> + []. + +item_options() -> + [{'access_model', open}, + {'deliver_notifications', true}, + {'deliver_payloads', true}, + {'item_expire', undefined}, + {'itemreply', publisher}, +% {'max_payload_size', undefined}, + {'notification_type', normal}, + {'notify_config', true}, + {'notify_retract', true}, + {'persist_items', true}, + {'presence_based_delivery', true}, +% {'publish_model', open}, + {'purge_offline', false}, + {'roster_groups_allowed', []}, + {'send_last_published_item', on_sub_and_presence}, + {'type', undefined}]. + +%% +-spec(subscription_options/2 :: +( + Entity_Type :: 'local' | 'remote', + Node_Type :: 'leaf' | 'collection') + -> Subscription_Options :: [] | pubsub_options:options_subscription() +). + +subscription_options('local', 'leaf') -> + []; +subscription_options('remote', 'leaf') -> + []; +subscription_options('local', 'collection') -> + []; +subscription_options('remote', 'collection') -> + []. + +%% +-spec(default_subscription_options/2 :: +( + Entity_Type :: 'local' | 'remote', + Node_Type :: 'leaf')%| 'collection') + -> Subscription_Options :: pubsub_options:options_subscription_leaf() +). + +default_subscription_options('local', 'leaf') -> + [{'deliver', true}, + {'expire', undefined}, + %{'include_body', undefined}, + {'show-values', ['away', 'chat', 'dnd', 'online', 'xa']}]; +%% +default_subscription_options('remote', 'leaf') -> + [{'deliver', true} + %{'include_body', undefined} + ]. +%% +%%default_subscription_options('local', 'collection') -> +%% [{'deliver', true}, +%% %{'expire', undefined}, +%% %{'include_body', undefined}, +%% {'show-values', ['away', 'chat', 'dnd', 'online', 'xa']}, +%% {'subscription_type', 'all'}, +%% {'subscription_depth', 1}]; +%% +%%default_subscription_options('remote', 'collection') -> +%% [{'deliver', true}, +%% %{'include_body', undefined}, +%% {'show-values', ['away', 'chat', 'dnd', 'online', 'xa']}, +%% {'subscription_type', 'all'}, +%% {'subscription_depth', 1}]. + +%% +-spec(features/0 :: () -> Features::exmpp_pubsub:features()). + +features() -> + [%?NS_ADDRESS, + ?NS_DISCO_INFO, + ?NS_DISCO_ITEMS, + %?NS_RSM, + ?NS_VCARD]. + +%% +identity() -> + [{<<"pubsub">>, <<"Publish-Subscribe">>, <<"service">>}, + {<<"pubsub">>, <<"ejabberd/mod_pubsub/flat">>, <<"service">>}]. diff --git a/src/mod_pubsub_ng/pubsub_api.hrl b/src/mod_pubsub_ng/pubsub_api.hrl new file mode 100644 index 000000000..a948d3575 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_api.hrl @@ -0,0 +1,101 @@ +%% API function definition + +-record(api_core, +{ + create_node = 'pubsub_core' :: module(), + delete_node = 'pubsub_core' :: module(), + purge_node = 'pubsub_core' :: module(), + get_configure_node = 'pubsub_core' :: module(), + get_configure_node_default = 'pubsub_core' :: module(), + %% + publish_item = 'pubsub_core' :: module(), + retract_item = 'pubsub_core' :: module(), + %% + subscribe_node = 'pubsub_core' :: module(), + unsubscribe_node = 'pubsub_core' :: module(), + set_configure_subscription = 'pubsub_core' :: module(), + get_configure_subscription = 'pubsub_core' :: module(), + get_configure_subscription_default = 'pubsub_core' :: module(), + get_items = 'pubsub_core' :: module(), + %% + get_entity_affiliations = 'pubsub_core' :: module(), + get_entity_subscriptions = 'pubsub_core' :: module(), + %% + get_node_affiliations = 'pubsub_core' :: module(), + get_node_subscriptions = 'pubsub_core' :: module() +}). + +-record(api_db, +{ + create_node = 'pubsub_db' :: module(), + delete_node = 'pubsub_db' :: module(), + purge_node = 'pubsub_db' :: module(), + get_configure_node = 'pubsub_db' :: module(), + %get_configure_node_default = 'pubsub_db' :: module(), + %% + publish_item = 'pubsub_db' :: module(), + retract_item = 'pubsub_db' :: module(), + %% + subscribe_node = 'pubsub_db' :: module(), + unsubscribe_node = 'pubsub_db' :: module(), + set_configure_subscription = 'pubsub_db' :: module(), + get_configure_subscription = 'pubsub_db' :: module(), + get_configure_subscription_default = 'pubsub_db' :: module(), + get_items = 'pubsub_db' :: module(), + %% + get_entity_affiliations = 'pubsub_db' :: module(), + get_entity_subscriptions = 'pubsub_db' :: module(), + %% + get_node_affiliations = 'pubsub_db' :: module(), + get_node_subscriptions = 'pubsub_db' :: module() +}). + +-record(api_broadcast, +{ + broadcast_publish = 'pubsub_broadcast' :: module(), + broadcast_publish_last = 'pubsub_broadcast' :: module(), + notify_create = 'pubsub_broadcast' :: module(), + notify_delete = 'pubsub_broadcast' :: module(), + notify_publish = 'pubsub_broadcast' :: module(), + notify_purge = 'pubsub_broadcast' :: module(), + notify_retract = 'pubsub_broadcast' :: module(), + notify_subscription = 'pubsub_broadcast' :: module(), + notify_subscriptions = 'pubsub_broadcast' :: module() +}). + +-record(api, +{ + core = #api_core{} :: #api_core{}, + db = #api_db{} :: #api_db{}, + broadcast = #api_broadcast{} :: #api_broadcast{}, + parser = 'pubsub_parser' :: module(), + options = 'pubsub_options' :: module() +}). + +-record(capabilities, +{ + plugin :: exmpp_pubsub:plugin(), + privacy = false :: boolean(), + api = #api{} :: #api{} +}). + +-record(api2, +{ + func, + core :: 'core', + db :: 'db', + bkd :: 'mnesia', + rtr :: 'router' +}). + +-record(mod_pubsub, +{ + server, + component, + plugin, + entity, + parameters, + features, + parser, + options +}). diff --git a/src/mod_pubsub_ng/pubsub_broadcast.erl b/src/mod_pubsub_ng/pubsub_broadcast.erl new file mode 100644 index 000000000..33dce8a49 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_broadcast.erl @@ -0,0 +1,1240 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_broadcast). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1, + get_resources/2, + get_resources_show/2, + rosters_groups_allowed_cache/2 +]). + + +-spec(notify_subscriptions/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Notification_Type :: pubsub_options:notification_type(), + Entities_Subscriptions :: [Entity_Subscriptions :: { + Entity :: xmpp_jid:usr_entity(), + Subscriptions :: _%sub:subscriptions() + }]) + -> 'ok' +). + +notify_subscriptions(_Host, Pubsub_Host, NodeId, Notification_Type, + Entities_Subscriptions) -> + Pubsub_Component_Jid = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + lists:foreach(fun + (_Entity = {{U,S,_R}, Subscriptions}) -> + lists:foreach(fun + ({Subscription_State, SubId, Resource, _Subscription_Options}) -> + Recipient_Jid = jlib:make_jid(U,S,Resource), + ejabberd_router:route(_From = Pubsub_Component_Jid, + _To = Recipient_Jid, + _Stanza_Message = exmpp_pubsub:xmlel_message( + Notification_Type, + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_subscription('pubsub#event', + NodeId, + _Jid = pubsub_tools:jid_to_string(Recipient_Jid), + SubId, + Subscription_State + ) + ])] + ) + ) + + end, Subscriptions) + end, Entities_Subscriptions). + +%% +-spec(notify_subscription/6 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + NodeId :: exmpp_pubsub:nodeId(), + Pubsub_Component_Jid :: xmpp_jid:component_bare(), + Recipient :: xmpp_jid:entity_bare() | [xmpp_jid:entity_bare(),...], + Notification_Type :: pubsub_options:notification_type(), + Subscription :: { + Subscriber :: xmpp_jid:raw_jid_entity(), + Subscription_State :: exmpp_pubsub:subscription_state(), + SubId :: exmpp_pubsub:subId() + }) + -> 'ok' +). + +notify_subscription(_Host, NodeId, Pubsub_Component_Jid, Recipients, + Notification_Type, {Subscriber, Subscription_State, SubId}) + when is_list(Recipients) -> + Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_subscription('pubsub#event', + NodeId, Subscriber, SubId, Subscription_State) + ]) + ]), + lists:foreach(fun + (Recipient) -> + ejabberd_router:route(_From = Pubsub_Component_Jid, + _To = pubsub_tools:make_jid(Recipient), Stanza_Message) + end, Recipients); +%% +notify_subscription(_Host, NodeId, Pubsub_Component_Jid, Recipient, + Notification_Type, {Subscriber, Subscription_State, SubId}) -> + Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_subscription('pubsub#event', + NodeId, Subscriber, SubId, Subscription_State) + ]) + ]), + ejabberd_router:route(_From = Pubsub_Component_Jid, + _To = Recipient, Stanza_Message). + +%% +-spec(notify_delete/6 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Notification_Type :: pubsub_options:notification_type(), + RedirectURI :: undefined | binary(), + Recipients :: [Recipient::xmpp_jid:usr_bare(),...]) + -> 'ok' +). + +notify_delete(_Host, Pubsub_Host, NodeId, Notification_Type, RedirectURI, + Recipients) -> + Pubsub_Component_Jid = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_delete(NodeId, RedirectURI)])] + ), + lists:foreach(fun + (Recipient) -> + ejabberd_router:route(_To = Pubsub_Component_Jid, + _From = pubsub_tools:make_jid(Recipient), + Stanza_Message + ) + end, Recipients). + +%% +-spec(notify_purge/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Notification_Type :: pubsub_options:notification_type(), + Recipients :: [Recipient::xmpp_jid:usr_bare(),...]) + -> 'ok' +). + +notify_purge(_Host, Pubsub_Host, NodeId, Notification_Type, Recipients) -> + Pubsub_Component_Jid = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_purge(NodeId)])]), + lists:foreach(fun + (Recipient) -> + ejabberd_router:route(_To = Pubsub_Component_Jid, + _From = pubsub_tools:make_jid(Recipient), + Stanza_Message + ) + end, Recipients). + +%% +-spec(notify_publish/8 :: +( + From :: xmpp_jid:component_bare(), + To :: xmpp_jid:entity(), + Notification_Type :: 'headline' | 'normal', + NodeId :: exmpp_pubsub:nodeId(), + ItemId :: exmpp_pubsub:itemId(), + Publisher :: undefined | xmpp_jid:raw_jid_entity(), + Payload :: exmpp_pubsub:payload(), + SubIds :: [SubId::exmpp_pubsub:subId(),...]) + -> 'ok' +). + +notify_publish(From, To, Notification_Type, NodeId, ItemId, Publisher, Payload, + SubIds) -> + ejabberd_router:route(From, To, + _Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + case SubIds of + [_SubId] -> + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_items('pubsub#event', NodeId, + [exmpp_pubsub:xmlel_item('pubsub#event', ItemId, + Publisher, Payload) + ]) + ]) + ]; + _SubIds -> + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_items('pubsub#event', NodeId, + [exmpp_pubsub:xmlel_item('pubsub#event', ItemId, + Publisher, Payload) + ]) + ]), + exmpp_pubsub:xmlel_headers( + [exmpp_pubsub:xmlel_header(SubId) || SubId <- SubIds]) + ] + end) + ). + + +-define(Xmlel(NS, Name, Attrs, Children), +( + #xmlel{ + name = Name, + attrs = [{<<"xmlns">>, NS} | Attrs], + children = Children + } +)). + +-define(CData(Data), +( + {xmlcdata, Data} +)). + +-define(Stanza_Message(Type, Id, Children), +( + ?Xmlel(?NS_JABBER_CLIENT, <<"message">>, [{<<"type">>, Type}, {<<"id">>, Id}], + Children) +)). + +%% +-spec(stanza_notify_publish/5 :: +( + Notification_Type :: pubsub_options:notification_type(), + NodeId :: exmpp_pubsub:nodeId(), + ItemId :: exmpp_pubsub:itemId(), + Publisher :: undefined | xmpp_jid:usr_entity() | xmpp_jid:raw_jid_entity(), + Payload :: exmpp_pubsub:payload()) + -> Stanza_Notify_Publish::xmlel() +). + +stanza_notify_publish(Notification_Type, NodeId, ItemId, Publisher, Payload) + when is_tuple(Publisher) -> + stanza_notify_publish(Notification_Type, NodeId, ItemId, + pubsub_tools:jid_to_string(Publisher), Payload); + +stanza_notify_publish(Notification_Type, NodeId, ItemId, Publisher, Payload) -> + ?Stanza_Message(list_to_binary(atom_to_list(Notification_Type)), exmpp_pubsub:id(),[ + ?Xmlel(?NS_PUBSUB_EVENT, <<"event">>, [], [ + ?Xmlel(?NS_PUBSUB_EVENT, <<"items">>, [{<<"node">>, NodeId}], [ + ?Xmlel(?NS_PUBSUB_EVENT, <<"item">>, + case Publisher of + undefined -> + [{<<"id">>, ItemId}]; + _ -> + [{<<"id">>, ItemId}, {<<"publisher">>, Publisher}] + end, + case Payload of + [] -> []; + _ -> [Payload] + end) + ]) + ]) + ]). + + +%% +-spec(stanza_notify_retract/3 :: +( + Notification_Type :: pubsub_options:notification_type(), + NodeId :: exmpp_pubsub:nodeId(), + ItemId :: exmpp_pubsub:itemId()) + -> Stanza_Notify_Retract::xmlel() +). + +stanza_notify_retract(Notification_Type, NodeId, ItemId) -> + ?Stanza_Message(list_to_binary(atom_to_list(Notification_Type)), exmpp_pubsub:id(),[ + ?Xmlel(?NS_PUBSUB_EVENT, <<"event">>, [], [ + ?Xmlel(?NS_PUBSUB_EVENT, <<"items">>, [{<<"node">>, NodeId}], [ + ?Xmlel(?NS_PUBSUB_EVENT, <<"retract">>, [{<<"id">>, ItemId}], []) + ]) + ]) + ]). + + +%% +-spec(notify_retract/6 :: +( + From :: xmpp_jid:component_bare(), + To :: xmpp_jid:entity(), + Notification_Type :: 'headline' | 'normal', + NodeId :: exmpp_pubsub:nodeId(), + ItemId :: exmpp_pubsub:itemId(), + SubIds :: [SubId::exmpp_pubsub:subId(),...]) + -> none +). + +notify_retract(From, To, Notification_Type, NodeId, ItemId, SubIds) -> + ejabberd_router:route(From, To, + _Stanza_Message = exmpp_pubsub:xmlel_message(Notification_Type, + case SubIds of + [_SubId] -> + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_items('pubsub#event', NodeId, + [exmpp_pubsub:xmlel_retract(ItemId)]) + ]) + ]; + _SubIds -> + [exmpp_pubsub:xmlel_event([ + exmpp_pubsub:xmlel_items('pubsub#event', NodeId, + [exmpp_pubsub:xmlel_retract(ItemId)]) + ]), + exmpp_pubsub:xmlel_headers( + [exmpp_pubsub:xmlel_header(SubId) || SubId <- SubIds]) + ] + end) + ). + + +broadcast_item(From, To, Stanza, SubIds) -> + ejabberd_router:route(From, To, + case SubIds of + [_] -> Stanza; + _ -> exmpp_xml:append_child(Stanza, shim_headers(SubIds)) + end). + + +shim_headers(SubIds) -> + ?Xmlel(?NS_SHIM, <<"headers">>, [], + [?Xmlel(?NS_SHIM, <<"header">>, [{<<"name">>, <<"SubID">>}], [?CData(SubId)]) + || SubId <- SubIds] + ). + +%% +xmlel_delay(From, DateTime) -> + {T_string, Tz_string} = jlib:timestamp_to_iso( + calendar:now_to_datetime(DateTime), 'utc'), + ?Xmlel(?NS_DELAY, <<"delay">>, + [{<<"from">>, pubsub_tools:jid_to_string(From)}, + {<<"stamp">>, list_to_binary(T_string ++ Tz_string)}], + []). + +%% +-spec(broadcast_publish/8 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Broadcasted_Items :: { + Published_Items :: [{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item(), + Payload :: exmpp_pubsub:payload(), + Publisher :: xmpp_jid:usr_entity() + }], + % + Retracted_Items :: [{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item() + }] + }, + Subscription :: undefined + | {Subscriber :: xmpp_jid:raw_jid_entity(), + Subscription_State :: _,%exmpp_pubsub:subscription_state(), + SubId :: exmpp_pubsub:subId()}, + Pubsub_States :: [Pubsub_State::mod_pubsub_dev:pubsub_state(),...]) + -> 'ok' +). + +broadcast_publish(Host, Pubsub_Host, NodeId, Node_Options, Node_Owners, + {Published_Items, Retracted_Items}, Subscription, Pubsub_States) -> + Event = #event{ + host = Host, + component = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + node = #node{ + id = NodeId, + owners = Node_Owners, + access_model = get_value(Node_Options, 'access_model'), + rosters_groups_allowed = get_value(Node_Options, 'roster_groups_allowed') + } + }, + Broadcasted_Items = lists:map(fun + %% + ({ItemId, _Item_Options = []}) -> + #item{ + presence_based_delivery = get_value(Node_Options, + 'presence_based_delivery'), + stanza = stanza_notify_retract( + get_value(Node_Options, 'notification_type', 'headline'), + NodeId, ItemId) + }; + %% + ({ItemId, Item_Options}) -> + #item{ + access_model = get_value(Item_Options, 'access_model', undefined), + presence_based_delivery = get_value({Node_Options, Item_Options}, + 'presence_based_delivery'), + rosters_groups_allowed = get_value(Item_Options, + 'roster_groups_allowed', []), + stanza = stanza_notify_retract( + get_value({Node_Options, Item_Options}, 'notification_type', + 'headline'), + NodeId, ItemId) + }; + %% + ({ItemId, _Item_Options = [], Payload, Publisher}) -> + #item{ + presence_based_delivery = get_value(Node_Options, + 'presence_based_delivery'), + stanza = stanza_notify_publish( + get_value(Node_Options, 'notification_type', 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value(Node_Options, 'itemreply', 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload) + }; + %% + ({ItemId, Item_Options, Payload, Publisher}) -> + #item{ + access_model = get_value(Item_Options, 'access_model'), + presence_based_delivery = get_value({Node_Options, Item_Options}, + 'presence_based_delivery'), + rosters_groups_allowed = get_value(Item_Options, + 'roster_groups_allowed', []), + stanza = stanza_notify_publish( + get_value({Node_Options, Item_Options}, 'notification_type', + 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value({Node_Options, Item_Options}, 'itemreply', + 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload) + } + % + end, lists:append(Retracted_Items, Published_Items)), + % + lists:foreach(fun + %% + (#pubsub_state_dev{id = {{U,S,R} = _Entity, _NodeIdx}, + affiliation = Affiliation, subscriptions = Subscriptions}) + when (Affiliation == 'member' + orelse Affiliation == 'owner' + orelse Affiliation == 'publisher') + andalso Subscriptions =/= [] -> + case Subscription =/= undefined andalso Affiliation == 'owner' of + true -> + notify_subscription(Host, NodeId, + _Pubsub_Component_Jid = Event#event.component, + _Recipient = jlib:make_jid(U,S,<<>>), + _Notification_Type = get_value(Node_Options, + 'notification_type'), + Subscription); + false -> + ok + end, + lists:foldl(fun broadcast_item/2, + Event#event{ + entity = #entity{ + id = {U,S,R}, + local = S == Event#event.host, + affiliation = Affiliation, + subscriptions = Subscriptions + }, + cache = #cache{}, + subids = #subids{} + }, + Broadcasted_Items); + %% + (_Pubsub_State) -> + ok + % + end, Pubsub_States). + +%% + +-spec(broadcast_publish_last/9 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Published_Items :: [Published_Item :: { + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item(), + Payload :: exmpp_pubsub:payload(), + Publisher :: xmpp_jid:usr_entity(), + DateTime :: erlang:timestamp() + },...], + Entity :: xmpp_jid:usr_entity(), + Affiliation :: 'member' | 'owner' | 'publisher', + Subscription :: exmpp_pubsub:subscription()) + -> none() +). + +broadcast_publish_last(Host, Pubsub_Host, NodeId, Node_Options, Node_Owners, + Published_Items, {U,S,R} = Entity, Affiliation, Subscription) -> + Event = #event{ + host = Host, + component = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + node = #node{ + id = NodeId, + owners = Node_Owners, + access_model = get_value(Node_Options, 'access_model'), + rosters_groups_allowed = get_value(Node_Options, 'roster_groups_allowed') + } + }, + Broadcasted_Items = lists:map(fun + %% + ({ItemId, _Item_Options = [], Payload, Publisher, DateTime}) -> + #item{ + presence_based_delivery = get_value(Node_Options, + 'presence_based_delivery'), + stanza = exmpp_xml:append_child(stanza_notify_publish( + get_value(Node_Options, 'notification_type', 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value(Node_Options, 'itemreply', 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload), + xmlel_delay(Publisher, DateTime)) + }; + %% + ({ItemId, Item_Options, Payload, Publisher, DateTime}) -> + #item{ + access_model = get_value(Item_Options, 'access_model'), + presence_based_delivery = get_value({Node_Options, Item_Options}, + 'presence_based_delivery'), + rosters_groups_allowed = get_value(Item_Options, + 'roster_groups_allowed', []), + stanza = exmpp_xml:append_child(stanza_notify_publish( + get_value({Node_Options, Item_Options}, 'notification_type', + 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value({Node_Options, Item_Options}, 'itemreply', + 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload), + xmlel_delay(Publisher, DateTime)) + } + % + end, Published_Items), + case get_value(Node_Options, 'notify_sub', false) of + true -> + {Subscription_State, SubId, Resource, _} = Subscription, + Subscriber = pubsub_tools:jid_to_string({U,S,Resource}), + Notification_Type = get_value(Node_Options, 'notification_type', + 'headline'), + lists:foreach(fun + (Node_Owner) -> + notify_subscription(Host, NodeId, + _Pubsub_Component_Jid = Event#event.component, + _Recipient = pubsub_tools:make_jid(Node_Owner), + Notification_Type, + {Subscriber, Subscription_State, SubId}) + + end, Node_Owners); + false -> + ok + end, + %% + lists:foldl(fun broadcast_item/2, + Event#event{ + entity = #entity{ + id = {U,S,R}, + local = S == Event#event.host, + affiliation = Affiliation, + subscriptions = [Subscription] + }, + cache = #cache{}, + subids = #subids{} + }, + Broadcasted_Items). + + +-spec(broadcast_publish_last2/9 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Published_Items :: [Published_Item :: { + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item(), + Payload :: exmpp_pubsub:payload(), + Publisher :: xmpp_jid:usr_entity(), + DateTime :: erlang:timestamp() + },...], + Entity :: xmpp_jid:usr_entity(), + Affiliation :: 'member' | 'owner' | 'publisher', + Subscriptions :: exmpp_pubsub:subscription()) + -> none() +). + +broadcast_publish_last2(Host, Pubsub_Host, NodeId, Node_Options, Node_Owners, + Published_Items, {U,S,R} = Entity, Affiliation, Subscriptions) -> + Event = #event{ + host = Host, + component = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + node = #node{ + id = NodeId, + owners = Node_Owners, + access_model = get_value(Node_Options, 'access_model'), + rosters_groups_allowed = get_value(Node_Options, 'roster_groups_allowed') + } + }, + Broadcasted_Items = lists:map(fun + %% + ({ItemId, _Item_Options = [], Payload, Publisher, DateTime}) -> + #item{ + presence_based_delivery = get_value(Node_Options, + 'presence_based_delivery'), + stanza = exmpp_xml:append_child(stanza_notify_publish( + get_value(Node_Options, 'notification_type', 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value(Node_Options, 'itemreply', 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload), + xmlel_delay(Publisher, DateTime)) + }; + %% + ({ItemId, Item_Options, Payload, Publisher, DateTime}) -> + #item{ + access_model = get_value(Item_Options, 'access_model'), + presence_based_delivery = get_value({Node_Options, Item_Options}, + 'presence_based_delivery'), + rosters_groups_allowed = get_value(Item_Options, + 'roster_groups_allowed', []), + stanza = exmpp_xml:append_child(stanza_notify_publish( + get_value({Node_Options, Item_Options}, 'notification_type', + 'headline'), + NodeId, + ItemId, + _Publisher = case + get_value({Node_Options, Item_Options}, 'itemreply', + 'owner') + of + 'publisher' -> pubsub_tools:jid_to_string(Publisher); + _Owner -> undefined + end, + Payload), + xmlel_delay(Publisher, DateTime)) + } + % + end, Published_Items), + %% + lists:foldl(fun broadcast_item/2, + Event#event{ + entity = #entity{ + id = {U,S,R}, + local = S == Event#event.host, + affiliation = Affiliation, + subscriptions = Subscriptions + }, + cache = #cache{}, + subids = #subids{} + }, + Broadcasted_Items). + +%% +-define(Resources_Subids(Item, Event), + (Item#item.presence_based_delivery == false + andalso + Event#event.subids#subids.no_presence =/= undefined + andalso + Event#event.subids#subids.no_presence =/= []) + orelse + (Item#item.presence_based_delivery == true + andalso + Event#event.subids#subids.presence =/= undefined + andalso + Event#event.subids#subids.presence =/= []) +). + +-define(Item_Access_Model(Item, Event), + Item#item.access_model == undefined + orelse + Item#item.access_model == 'open' + orelse + Item#item.access_model == 'authorize' + orelse + Item#item.access_model == 'whitelist' + orelse + (Item#item.access_model == Event#event.node#node.access_model + andalso + Item#item.access_model =/= 'roster') + orelse + (Item#item.access_model == 'presence' + andalso + (Event#event.entity#entity.affiliation == 'owner' + orelse +% Event#event.entity#entity.affiliation == 'publisher' +% orelse + Event#event.node#node.access_model == 'presence' + orelse + Event#event.node#node.access_model == 'roster' + orelse + Event#event.cache#cache.presence_subscriptions == true) + ) + orelse + (Item#item.access_model == 'roster' + andalso + (Event#event.entity#entity.affiliation == 'owner' + orelse + Event#event.entity#entity.affiliation == 'publisher' + orelse + Event#event.cache#cache.rosters_groups == true) + ) +). + + +-define(Broadcast_Event(Item, Event), + (?Resources_Subids(Item, Event)) and (?Item_Access_Model(Item, Event)) +). + +-define(Presence_Based_Delivery(Item, Event), + Item#item.presence_based_delivery == true + andalso + Event#event.subids#subids.presence == undefined +). + +-define(No_Presence_Based_Delivery(Item, Event), + Item#item.presence_based_delivery == false + andalso + Event#event.subids#subids.no_presence == undefined +). + +-define(Presence_Based_Delivery_Cache(Item, Event), + Item#item.presence_based_delivery == true + andalso + Event#event.cache#cache.presence == undefined +). + +-define(Item_Access_Model_Presence(Item, Event), + Item#item.access_model == 'presence' + andalso + Event#event.cache#cache.presence_subscriptions == undefined +). + +-define(Item_Access_Model_Roster(Item, Event), + Item#item.access_model == 'roster' + andalso + Event#event.cache#cache.rosters_groups == undefined +). + + +-spec(broadcast_item/2 :: +( + Item :: mod_pubsub_dev:item(), + Event :: mod_pubsub_dev:event()) + -> Event::mod_pubsub_dev:event() +% Event::#event{cache::#cache{rosters_groups :: undefined}} +). + + + +%% +broadcast_item(#item{stanza = Stanza} = Item, + #event{entity = #entity{id = {U,S,_R}}} = Event) + when ?Broadcast_Event(Item, Event) -> + lists:foreach(fun + ({Resource, SubIds}) -> + broadcast_item(_From = Event#event.component, + _To = jlib:make_jid(U,S,Resource), + Stanza, + SubIds) + end, + _Resources_SubIds = case Item#item.presence_based_delivery of + true -> Event#event.subids#subids.presence; + false -> Event#event.subids#subids.no_presence + end), + Event#event{cache = Event#event.cache#cache{rosters_groups = undefined}}; +%% +broadcast_item(Item, Event) + when ?No_Presence_Based_Delivery(Item, Event) -> + {Presence_Cache, Resources_SubIds} = resources_subids( + _Entity = Event#event.entity#entity.id, + _Local_Entity = Event#event.entity#entity.local, + _Presence_Based_Delivery = false, + _Subscriptions = Event#event.entity#entity.subscriptions, + {Event#event.cache#cache.presence, []}), + case Resources_SubIds of + [] -> + Event#event{ + cache = Event#event.cache#cache{ + presence = Presence_Cache, + rosters_groups = undefined + }, + subids = Event#event.subids#subids{ + no_presence = [] + } + }; + _Resources_SubIds -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + presence = Presence_Cache + }, + subids = Event#event.subids#subids{ + no_presence = Resources_SubIds + } + } + ) + end; +%% +broadcast_item(Item, #event{entity = #entity{id = {U,S,_R}}} = Event) + when ?Presence_Based_Delivery_Cache(Item, Event) -> + case get_resources_show(U,S) of + [] -> + Event#event{ + cache = Event#event.cache#cache{ + presence = [], + rosters_groups = undefined + } + }; + Resources_Show -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + presence = Resources_Show + } + }) + end; +%% +broadcast_item(Item, Event) + when ?Presence_Based_Delivery(Item, Event) -> + {Presence_Cache, Resources_SubIds} = resources_subids( + _Entity = Event#event.entity#entity.id, + _Local_Entity = Event#event.entity#entity.local, + _Presence_Based_Delivery = true, + _Subscriptions = Event#event.entity#entity.subscriptions, + {Event#event.cache#cache.presence, []}), + case Resources_SubIds of + [] -> + Event#event{ + cache = Event#event.cache#cache{ + presence = Presence_Cache, + rosters_groups = undefined + }, + subids = Event#event.subids#subids{ + presence = []} + }; + _Resources_SubIds -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + presence = Presence_Cache + }, + subids = Event#event.subids#subids{ + presence = Resources_SubIds + } + }) + end; +%% +broadcast_item(Item, Event) + when ?Item_Access_Model_Presence(Item, Event) -> + case + is_contact_subscribed_to_node_owners( + _Host = Event#event.host, + _Entity = Event#event.entity#entity.id, + _Node_Owners = Event#event.node#node.owners + ) + of + false -> + Event#event{ + cache = Event#event.cache#cache{ + presence_subscriptions = false, + rosters_groups = undefined + } + }; + _Node_Owner -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + presence_subscriptions = true + } + } + ) + end; +%% +broadcast_item(Item, Event) + when ?Item_Access_Model_Roster(Item, Event) -> + case Event#event.node#node.access_model of + 'roster' -> + case Item#item.rosters_groups_allowed of + %% + [] -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + rosters_groups = true + } + } + ); + %% + Item_Rosters_Groups_Allowed -> + case + is_contact_in_allowed_roster_groups( + _Contact = Event#event.entity#entity.id, + _Rosters_Groups_Allowed = Item_Rosters_Groups_Allowed + ) + of + false -> + Event; + {_Node_Owner, _Roster_Group_Allowed} -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{ + rosters_groups = true + } + }) + end + end; + _Node_Access_Model -> + case + is_contact_in_allowed_roster_groups( + _Contact = Event#event.entity#entity.id, + _Rosters_Groups_Allowed = case + Item#item.rosters_groups_allowed + of + [] -> + Event#event.node#node.rosters_groups_allowed; + Item_Rosters_Groups_Allowed -> + Item_Rosters_Groups_Allowed + end + ) + of + false -> + Event; + {_Node_Owner, _Roster_Group_Allowed} -> + broadcast_item(Item, + Event#event{ + cache = Event#event.cache#cache{rosters_groups = true} + }) + end + end; +%% +broadcast_item(_Item, Event) -> + Event#event{cache = Event#event.cache#cache{rosters_groups = undefined}}. + + +%% +-spec(resources_subids/5 :: +( + Entity :: xmpp_jid:usr_bare(), + Local_Entity :: boolean(), + Presence_Based_Delivery :: boolean(), + Subscriptions :: _,%exmpp_pubsub:subscriptions() | [], + Cache :: { + Presence_Cache :: undefined | [] | mod_pubsub_dev:presence_cache(), + Resources_SubIds :: undefined | [] | mod_pubsub_dev:resources_subids() + }) + -> {Presence_Cache :: undefined | [] | mod_pubsub_dev:presence_cache(), + Resources_SubIds :: [] | mod_pubsub_dev:resources_subids()} +). + +resources_subids(_Entity, _Local_Entity, _Presence_Based_Delivery, + [] = _Subscriptions, {Presence_Cache, Resources_SubIds}) -> + {Presence_Cache, Resources_SubIds}; +%% +resources_subids(Entity, Local_Entity, Presence_Based_Delivery, + [{'pending', _SubId, _Resource, _Subscription_Options} | Subscriptions], + {Presence_Cache, Resources_SubIds}) -> + resources_subids(Entity, Local_Entity, Presence_Based_Delivery, Subscriptions, + {Presence_Cache, Resources_SubIds}); +%% +resources_subids(Entity, Local_Entity, Presence_Based_Delivery, + [{Subscription_State, SubId, {caps, Resource}, Subscription_Options} + | Subscriptions], {Presence_Cache, Resources_SubIds}) -> + resources_subids(Entity, Local_Entity, Presence_Based_Delivery, + [{Subscription_State, SubId, Resource, Subscription_Options} + | Subscriptions], + {Presence_Cache, Resources_SubIds}); +%% Presence_Based_Delivery = false +%% Local_Entity = boolean() +%% Subscription_Options = [] +resources_subids(Entity, Local_Entity, false = Presence_Based_Delivery, + [{_Subscription_State, SubId, Resource, [] = _Subscription_Options} + | Subscriptions], {Presence_Cache, Resources_SubIds}) -> + resources_subids(Entity, Local_Entity, Presence_Based_Delivery, Subscriptions, + {Presence_Cache, + _Resources_SubIds = resources_subids(Resource, SubId, + Resources_SubIds)}); +%% Presence_Based_Delivery = false +%% Local_Entity = true +%% Resource = undefined | xmpp_jid:resource_jid() +%% Subscription_Options =/= [] +resources_subids({U,S,R} = _Entity, true = Local_Entity, false = Presence_Based_Delivery, + [{_Subscription_State, SubId, Resource, Subscription_Options} | Subscriptions], + {Presence_Cache, Resources_SubIds}) -> + resources_subids({U,S,R}, Local_Entity, Presence_Based_Delivery, + Subscriptions, + case get_value(Subscription_Options, 'deliver', true) of + true -> + case get_value(Subscription_Options, 'show-values') of + 'none' -> %% 'show-values' NA + {Presence_Cache, + _Resources_SubIds = resources_subids(Resource, SubId, + Resources_SubIds)}; + %% + Show_Values + when Show_Values == [] + orelse Presence_Cache == [] -> + {Presence_Cache, Resources_SubIds}; + %% + Show_Values + when Presence_Cache == undefined -> + case get_resources_show(U, S) of + %%%% + [] -> + {_Presence_Cache = [], Resources_SubIds}; + %%%% + Resources_Show -> + {_Presence_Cache = Resources_Show, + _Resources_SubIds = lists:foldl(fun + (Resource_To_Deliver, Resources_SubIds_Bis) -> + resources_subids(Resource_To_Deliver, + SubId, Resources_SubIds_Bis) + end, + Resources_SubIds, + _Resources_To_Deliver = resources_to_deliver( + Resource, Show_Values, Resources_Show, + []))} + end; + %% + Show_Values -> + {Presence_Cache, + _Resources_SubIds = lists:foldl(fun + (Resource_To_Deliver, Resources_SubIds_Bis) -> + resources_subids(Resource_To_Deliver, + SubId, Resources_SubIds_Bis) + end, + Resources_SubIds, + _Resources_To_Deliver = resources_to_deliver( + Resource, Show_Values, Presence_Cache, []))} + end; + false -> + {Presence_Cache, Resources_SubIds} + end); +%% Presence_Based_Delivery = false +%% Local_Entity = false +%% Subscription_Options =/= [] +resources_subids(Entity, false = Local_Entity, false = Presence_Based_Delivery, + [{_Subscription_State, SubId, Resource, Subscription_Options} | Subscriptions], + {Presence_Cache, Resources_SubIds}) -> + resources_subids(Entity, Local_Entity, Presence_Based_Delivery, Subscriptions, + case get_value(Subscription_Options, 'deliver', true) of + true -> + {Presence_Cache, + _Resources_SubIds = resources_subids(Resource, SubId, + Resources_SubIds)}; + false -> + {Presence_Cache, Resources_SubIds} + end); +%% Presence_Based_Delivery = true +%% Local_Entity = true +%% Subscription_Options : NA +%% Presence_Cache = 'none' +resources_subids({U,S,R} = _Entity, true = _Local_Entity, + true = _Presence_Based_Delivery, Subscriptions, + {undefined = _Presence_Cache, Resources_SubIds}) -> + case get_resources_show(U, S) of + [] -> + {[], Resources_SubIds}; + _Resources_Show = Presence_Cache -> + resources_subids({U,S,R}, true, true, Subscriptions, + {Presence_Cache, Resources_SubIds}) + end; +%% Presence_Based_Delivery = true +%% Local_Entity = true +%% Subscription_Options : NA +%% Presence_Cache = [] +resources_subids(_Entity, true = _Local_Entity, true = _Presence_Based_Delivery, + _Subscriptions, {[] = Presence_Cache, Resources_SubIds}) -> + {Presence_Cache, Resources_SubIds}; +%% Presence_Based_Delivery = true +%% Local_Entity = true +%% Resource = undefined | xmpp_jid:resource_jid() +%% Subscription_Options =/= [] +%% Presence_Cache =/= [] +resources_subids(Entity, true = _Local_Entity, true = Presence_Based_Delivery, + [{_Subscription_State, SubId, Resource, Subscription_Options} | Subscriptions], + {Presence_Cache, Resources_SubIds}) -> + resources_subids(Entity, true, Presence_Based_Delivery, Subscriptions, + case + get_value(Subscription_Options, 'deliver', true) == true + andalso + (Resource == undefined + orelse lists:keymember(Resource, 1, Presence_Cache)) + of + true -> + {Presence_Cache, + _Resources_SubIds = case + get_value(Subscription_Options, 'show-values') + of + 'none' when Resource == undefined -> %% 'show-values' NA + lists:foldl(fun + ({Resource_To_Deliver, _Show}, Resources_SubIds_Bis) -> + resources_subids(Resource_To_Deliver, + SubId, Resources_SubIds_Bis) + end, Resources_SubIds, Presence_Cache); + 'none' -> %% 'show-values' NA + resources_subids(Resource, SubId, Resources_SubIds); + [] -> + Resources_SubIds; + Show_Values -> + lists:foldl(fun + (Resource_To_Deliver, Resources_SubIds_Bis) -> + resources_subids(Resource_To_Deliver, + SubId, Resources_SubIds_Bis) + end, + Resources_SubIds, + _Resources_To_Deliver = resources_to_deliver( + Resource, Show_Values, Presence_Cache, [])) + end}; + false -> + {Presence_Cache, Resources_SubIds} + end); +%% Presence_Based_Delivery = true +%% Local_Entity = false +%% Subscription_Options : NA +resources_subids(_Entity, false = _Local_Entity, true = _Presence_Based_Delivery, + _Subscriptions, {Presence_Cache, Resources_SubIds}) -> + {Presence_Cache, Resources_SubIds}. + +%% +-spec(resources_subids/3 :: +( + Resource :: xmpp_jid:resource_jid(), + SubId :: exmpp_pubsub:subId(), + Resources_SubIds :: [] | mod_pubsub_dev:resources_subids()) + -> %% + Resources_SubIds::mod_pubsub_dev:resources_subids() +). + +resources_subids(Resource, SubId, Resources_SubIds) -> + _Resources_SubIds = case lists:keyfind(Resource, 1, Resources_SubIds) of + {_Resource, SubIds} -> + lists:keyreplace(Resource, 1, Resources_SubIds, + {Resource, [SubId | SubIds]}); + false -> + [{Resource, [SubId]} | Resources_SubIds] + end. + +%% +-spec(resources_to_deliver/4 :: +( + Resource :: undefined | xmpp_jid:resource_jid(), + Show_Values :: ['away' | 'chat' | 'dnd' | 'online' | 'xa',...], + Presence_Cache :: [] | mod_pubsub_dev:presence_cache(), + Resources_To_Deliver :: [Resource::xmpp_jid:resource_jid()]) + -> %% + Resources_To_Deliver::[Resource::xmpp_jid:resource_jid()] +). + +resources_to_deliver(_Resource, _Show_Values, [] = _Presence_Cache, + Resources_To_Deliver) -> + Resources_To_Deliver; +%% +resources_to_deliver(Resource, Show_Values, [Resource_Show | Presence_Cache], + Resources_To_Deliver) -> + resources_to_deliver(Resource, Show_Values, Presence_Cache, + _Resources_To_Deliver = resource_to_deliver(Resource, Show_Values, + Resource_Show, Resources_To_Deliver)). + +%% +-spec(resource_to_deliver/4 :: +( + Resource :: undefined | xmpp_jid:resource_jid(), + Show_Values :: ['away' | 'chat' | 'dnd' | 'online' | 'xa',...], + Resource_Show :: mod_pubsub_dev:resource_show(), + Resources_To_Deliver :: [Resource::xmpp_jid:resource_jid()]) + -> %% + Resources_To_Deliver::[Resource::xmpp_jid:resource_jid()] +). + +resource_to_deliver(_Resource, [] = _Show_Values, _Resource_Show, + Resources_To_Deliver) -> + Resources_To_Deliver; +%% +resource_to_deliver(undefined = _Resource, [_Show_Value = Show | Show_Values], + {Resource, Show}, Resources_To_Deliver) -> + resource_to_deliver(_Resource = undefined, Show_Values, {Resource, Show}, + [_Resource_To_Deliver = Resource | Resources_To_Deliver]); +%% +resource_to_deliver(Resource, [_Show_Value = Show | Show_Values], + {Resource, Show}, Resources_To_Deliver) -> + resource_to_deliver(Resource, Show_Values, {Resource, Show}, + [_Resource_To_Deliver = Resource | Resources_To_Deliver]); +%% +resource_to_deliver(Resource, [_Show_Value | Show_Values], Resource_Show, + Resources_To_Deliver) -> + resource_to_deliver(Resource, Show_Values, Resource_Show, + Resources_To_Deliver). + diff --git a/src/mod_pubsub_ng/pubsub_core.erl b/src/mod_pubsub_ng/pubsub_core.erl new file mode 100644 index 000000000..8914987f9 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_core.erl @@ -0,0 +1,1707 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_core). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + +-import(pubsub_tools, +[ + is_jid/1, + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1 +]). + + + +-spec(features/3 :: +( + Function :: 'create_node' + | 'purge_node' + | 'publish_item' + | 'retract_item' + | 'get_items' + | 'subscribe_node' + | 'get_configure_subscription_default' + | 'get_configure_node_default', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Criteria :: %-- Create_Node --% + {NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Config :: [] | [Xmlel::xmlel(),...]} + %% %-- Purge_Node --% + | 'none' + %% %-- Publish_Item --% + | {NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Config :: [] | [Xmlel::xmlel(),...], + Publish_Options :: [] | [Xmlel::xmlel(),...]} + %% %-- Retract_Item --% + | 'none' + %% %-- Get_Items --% + | 'none' + %% %-- Subscribe_Node --% + | {Subscribe_Options :: [Xmlel::xmlel()]} + %% %-- Get_Configure_Subscription_Default --% + | 'none' + %% %-- Get_Configure_Node_Default --% + | 'none' + ) + -> ok + %%% + %-- Create_Node --% + | {error, 'feature-not-implemented', 'create-nodes'} + | {error, 'feature-not-implemented', 'create-and-configure'} + % + | {error, 'not-acceptable', 'nodeid-required'} + %-- Purge_Node --% + | {error, 'feature-not-implemented', 'purge-nodes'} + | {error, 'feature-not-implemented', 'persistent-items'} + %-- Publish_Item --% + | {error, 'feature-not-implemented', 'publish'} + | {error, 'feature-not-implemented', 'auto-create'} + | {error, 'feature-not-implemented', 'config-node'} + | {error, 'feature-not-implemented', 'publish-options'} + %-- Retract_Item --% + | {error, 'feature-not-implemented', 'delete-items'} + | {error, 'feature-not-implemented', 'persistent-items'} + %-- Get_Items --% + | {error, 'feature-not-implemented', 'retrieve-items'} + | {error, 'feature-not-implemented', 'persistent-items'} + %-- Subscribe_Node --% + | {error, 'feature-not-implemented', 'subscribe'} + | {error, 'feature-not-implemented', 'subscription-options'} + %-- Get_Configure_Subscription_Default --% + | {error, 'feature-not-implemented', 'subscription-options'} + | {error, 'feature-not-implemented', 'retrieve-default-sub'} + %-- Get_Configure_Node_Default --% + | {error, 'feature-not-implemented', 'config-node'} + | {error, 'feature-not-implemented', 'retrieve-default'} +). + +%-- Create_Node --% +features('create_node', Pubsub_Features, {NodeId, [] = _Node_Config}) -> + case lists:member(<<"create-nodes">>, Pubsub_Features) of + true when NodeId =/= undefined -> + ok; + true -> + case lists:member(<<"instant-nodes">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'not-acceptable', 'nodeid-required'} + end; + false -> + {error, 'feature-not-implemented', 'create-nodes'} + end; +% +features('create_node', Pubsub_Features, {NodeId, _Node_Config}) -> + case features('create_node', Pubsub_Features, {NodeId, []}) of + ok -> + case lists:member(<<"create-and-configure">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'create-and-configure'} + end; + Error -> + Error + end; +%-- Purge_Node --% +features('purge_node', Pubsub_Features, none) -> + case lists:member(<<"purge-nodes">>, Pubsub_Features) of + true -> + case lists:member(<<"persistent-items">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'persistent-items'} + end; + false -> + {error, 'feature-not-implemented', 'purge-nodes'} + end; +%-- Publish_Item --% +features('publish_item', Pubsub_Features, + {NodeId, [] = _Node_Config, [] = _Publish_Options}) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true when NodeId =/= undefined -> + ok; + true -> + case lists:member(<<"auto-create">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'auto-create'} + end; + false -> + {error, 'feature-not-implemented', 'publish'} + end; +%% +features('publish_item', Pubsub_Features, + {NodeId, _Node_Config, [] = _Publish_Options}) -> + case features('publish_item', Pubsub_Features, {NodeId, [], []}) of + ok -> + case lists:member(<<"config-node">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'config-node'} + end; + Error -> + Error + end; +% +features('publish_item', Pubsub_Features, + {NodeId, Node_Config, _Publish_Options}) -> + case features('publish_item', Pubsub_Features, {NodeId, Node_Config, []}) of + ok -> + case lists:member(<<"publish-options">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'publish-options'} + end; + Error -> + Error + end; +%-- Purge_Node --% +features('retract_item', Pubsub_Features, none) -> + case lists:member(<<"delete-items">>, Pubsub_Features) of + true -> + case lists:member(<<"persistent-items">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'persistent-items'} + end; + false -> + {error, 'feature-not-implemented', 'delete-items'} + end; +%-- Get_Items --% +features('get_items', Pubsub_Features, none) -> + case lists:member(<<"retrieve-items">>, Pubsub_Features) of + true -> + case lists:member(<<"persistent-items">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'persistent-items'} + end; + false -> + {error, 'feature-not-implemented', 'retrieve-items'} + end; +%-- Subscribe_Node --% +features('subscribe_node', Pubsub_Features, {[] = _Subscribe_Options}) -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'retrieve-items'} + end; +%% +features('subscribe_node', Pubsub_Features, {_Subscribe_Options}) -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> + case lists:member(<<"subscription-options">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'subscription-options'} + end; + false -> + {error, 'feature-not-implemented', 'retrieve-items'} + end; +%-- Get_Configure_Subscription_Default --% +features('get_configure_subscription_default', Pubsub_Features, 'none') -> + case lists:member(<<"subscription-options">>, Pubsub_Features) of + true -> + case lists:member(<<"retrieve-default-sub">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'retrieve-default-sub'} + end; + false -> + {error, 'feature-not-implemented', 'subscription-options'} + end; +%-- Get_Configure_Node_Default --% +features('get_configure_node_default', Pubsub_Features, 'none') -> + case lists:member(<<"config-node">>, Pubsub_Features) of + true -> + case lists:member(<<"retrieve-default">>, Pubsub_Features) of + true -> + ok; + false -> + {error, 'feature-not-implemented', 'retrieve-default'} + end; + false -> + {error, 'feature-not-implemented', 'config-node'} + end. + + +%-- Create_Node --% +-spec(create_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Config :: [] | [Xmlel::xmlel(),...]}, + Capabilities :: #capabilities{}) + -> %% + {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'create-nodes'} + | {error, 'feature-not-implemented', 'create-and-configure'} + % + | {error, 'not-acceptable', 'nodeid-required'} + % + | {error, 'forbidden'} + | {error, 'conflict'} + % + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} + % + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + + +create_node(Host, Pubsub_Host, {U,S,R} = Entity, + {NodeId, Node_Config} = _Parameters, + #capabilities{ + plugin = Plugin, + api = #api{db = API_DB, broadcast = API_Broadcast, options = Options_Mod}} + = _Capabilities) -> + case + checks_on_create_node(Host, {U,S,R}, Plugin, {NodeId, Node_Config}, + Options_Mod) + of + {ok, Pubsub_Features, Node_Options} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.create_node, + 'create_node', + [Host, Pubsub_Host, {U,S,undefined}, Pubsub_Features, + {NodeId, Node_Options}] + ) + of + {result, NodeId_Bis, _Actions} -> + case + (_Actions =/= []) + andalso + lists:member(<<"subscription-notifications">>, + Pubsub_Features) + of + true -> + [{Notification_Type, Subscription}] = _Actions, + spawn(API_Broadcast + #api_broadcast.notify_subscriptions, + 'notify_subscriptions', + [Host, Pubsub_Host, NodeId_Bis, Notification_Type, + [{Entity, [Subscription]}]]); + false -> + ok + end, + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_create(NodeId_Bis)])]}; + Error -> + Error + end; + Error -> + Error + end. + + +checks_on_create_node(Host, {U,S,R} = _Entity, Plugin, {NodeId, Node_Config}, + Options_Mod) -> + case + features('create_node', Pubsub_Features = Plugin:pubsub_features(), + {NodeId, Node_Config}) + of + ok -> + case + acl:match_rule(Host, pubsub_createnode, + jlib:jid_to_string({U, S, <<>>})) + of + allow -> + case + Options_Mod:parse_xmlel_x(Plugin, Pubsub_Features, + {U,S,R}, 'node_config', Node_Config) + of + {ok, Node_Options} -> + {ok, Pubsub_Features, Node_Options}; + Error -> + Error + end; + _Deny -> + {error, 'forbidden'} + end; + Error -> + Error + end. + +%-- Delete_Node --% +-spec(delete_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + RedirectURI :: undefined | binary()}, + Capabilities :: #capabilities{}) + -> {result, []} + %%% + | {error, 'feature-not-implemented', 'delete-nodes'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +delete_node(Host, Pubsub_Host, {U,S,_R} = _Entity, + _Parameters = {NodeId, RedirectURI}, + #capabilities{plugin = Plugin, api = #api{db = API_DB, broadcast = API_Broadcast}} + = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case lists:member(<<"delete-nodes">>, Pubsub_Features) of + true -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.delete_node, + 'delete_node', + [Pubsub_Host, {U,S,undefined}, {NodeId}]) + of + {result, _Actions = []} -> + {result, []}; + {result, + _Actions = [{Notification_Type, Recipients}]} -> + spawn(API_Broadcast#api_broadcast.notify_delete, + 'notify_delete', + [Host, Pubsub_Host, NodeId, Notification_Type, + RedirectURI, Recipients]), + {result, []}; + Error -> + Error + end; + false -> + {error, 'feature-not-implemented', 'delete-nodes'} + end. + +%-- Purge_Node --% +-spec(purge_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, []} + %%% + | {error, 'feature-not-implemented', 'purge-nodes'} + | {error, 'feature-not-implemented', 'persistent-items'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +purge_node(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB, broadcast = API_Broadcast}} + = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('purge_node', Pubsub_Features, none) of + ok -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.purge_node, + 'purge_node', + [Pubsub_Host, {U,S,undefined}, {NodeId}]) + of + {result, []} -> + {result, []}; + {result, + _Actions = [{Notification_Type, Recipients}]} -> + spawn(API_Broadcast#api_broadcast.notify_purge, + 'notify_purge', + [Host, Pubsub_Host, NodeId, Notification_Type, Recipients]), + {result, []}; + Error -> + Error + end; + Error -> + Error + end. + +%% +-spec(publish_item/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Item :: 0 | 1, + ItemId :: undefined | exmpp_pubsub:itemId(), + Payload :: exmpp_pubsub:payload() | [Xmlel::xmlel()], + Node_Config :: [], %| [Xmlel::xmlel(),...], + Publish_Options :: [Xmlel::xmlel()] + }, + Capabilities :: #capabilities{}) + -> %% + {result, Xmlel_Pubsub::[Xmlel::xmlel()]} + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} + %%% + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} + % + | {error, 'bad-request', 'invalid-payload'} + | {error, 'bad-request', 'item-required'} + | {error, 'bad-request', 'item-forbidden'} + | {error, 'not-acceptable', 'payload-too-big'} +). +publish_item(Host, Pubsub_Host, {U,S,R} = _Entity, + {_NodeId, Item, _ItemId, Payload, [] = Node_Config, Publish_Options}, + #capabilities{plugin = Plugin, + api = #api{options = Options_Module, db = API_DB, + broadcast = API_Broadcast}} = _Capabilities) -> + case + checks_on_publish_item(Host, {U,S,R}, Plugin, Options_Module, + {_NodeId, Node_Config, Publish_Options}) + of + {ok, Pubsub_Features, _Node_Options, Item_Options} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.publish_item, + 'publish_item', + [Host, Pubsub_Host, {U,S,R}, Plugin, Pubsub_Features, + {_NodeId, Item, _ItemId, Payload, {Node_Config, Item_Options}}]) + of + {result, NodeId, ItemId, Node_Options, Node_Owners, SubId, + Subscription, Broadcasted_Items, Pubsub_States} -> + case Pubsub_States of + [] -> + ok; + _Pubsub_States -> + spawn(API_Broadcast#api_broadcast.broadcast_publish, + broadcast_publish, + [Host, Pubsub_Host, NodeId, Node_Options, + Node_Owners, Broadcasted_Items, + case SubId of + undefined -> + undefined; + _SubId -> + {_Subscriber = jlib:jid_to_string({U, S, <<>>}), + _Subscription_State = 'subscribed', + SubId} + end, + Pubsub_States]) + end, + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_publish('pubsub', NodeId, + [exmpp_pubsub:xmlel_item('pubsub', ItemId)])] + )] + }; + Error -> + Error + end; + Error -> + Error + end. + +%% +-spec(checks_on_publish_item/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_entity(), + Plugin :: exmpp_pubsub:plugin(), + Options_Module :: module(), + Criteria :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Config :: [Xmlel_X::xmlel(),...] + | [Xmlel::xmlel()], + Publish_Options :: [Xmlel_X::xmlel(),...] + | [Xmlel::xmlel()] + }) + -> {ok, + Pubsub_Features ::exmpp_pubsub:pubsub_features(), + Node_Options :: [] | pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()} + %%% + | {error, 'forbidden'} + %%% + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + +checks_on_publish_item(Host, {U,S,R} = _Entity, Plugin, Options_Module, + {undefined = NodeId, Node_Config, Publish_Options}) -> + case + features('publish_item', Pubsub_Features = Plugin:pubsub_features(), + {NodeId, Node_Config, Publish_Options}) + of + ok -> + case + acl:match_rule(Host, pubsub_createnode, + jlib:jid_to_string({U, S, <<>>})) + of + allow -> + case + {Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, + {U,S,R}, 'node_config', Node_Config), + Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, + {U,S,R}, 'publish-options', Publish_Options)} + of + {{ok, Node_Options}, {ok, Item_Options}} -> + {ok, Pubsub_Features, Node_Options, Item_Options}; + {Error, {ok, _Item_Options}} -> + Error; + {{ok, _Node_Options}, Error} -> + Error; + {Error, _Error} -> + Error + end; + _Deny -> + {error, 'forbidden'} + end; + Error -> + Error + end; +%% +checks_on_publish_item(_Host, Entity, Plugin, Options_Module, + {NodeId, Node_Config, Publish_Options}) -> + case + features('publish_item', Pubsub_Features = Plugin:pubsub_features(), + {NodeId, Node_Config, Publish_Options}) + of + ok -> + case + {Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, Entity, + 'node_config', Node_Config), + Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, Entity, + 'publish-options', Publish_Options)} + of + {{ok, Node_Options}, {ok, Item_Options}} -> + {ok, Pubsub_Features, Node_Options, Item_Options}; + {Error, {ok, _Item_Options}} -> + Error; + {{ok, _Node_Options}, Error} -> + Error; + {Error, _Error} -> + Error + end; + Error -> + Error + end. + +%-- Retract_Items --% +-spec(retract_item/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + ItemId :: undefined | exmpp_pubsub:itemId(), + Adhoc_Notify_Retract :: undefined | boolean()}, + Capabilities :: #capabilities{}) + -> {result, []} + %%% + | {error, 'feature-not-implemented', 'delete-items'} + | {error, 'feature-not-implemented', 'persistent-items'} + % + | {error, 'bad-request', 'nodeid-required'} + | {error, 'bad-request', 'item-required'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +retract_item(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId, ItemId, Adhoc_Notify_Retract} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB, broadcast = API_Broadcast}} + = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('retract_item', Pubsub_Features, none) of + ok -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.retract_item, + 'retract_item', + [Pubsub_Host, {U,S,undefined}, + {NodeId, ItemId, Adhoc_Notify_Retract}]) + of + {result, []} -> + {result, []}; + {result, + [_Notify_Retract = { + Node_Owners, Node_Options, Item_Options, Pubsub_States + }]} -> + spawn(API_Broadcast#api_broadcast.broadcast_publish, + broadcast_publish, + [Host, Pubsub_Host, NodeId, Node_Options, Node_Owners, + {[{ItemId, Item_Options}], []}, undefined, Pubsub_States]), + {result, []}; + Error -> + Error + end; + Error -> + Error + end. + +%-- Subscribe_Node --% +-spec(subscribe_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + Subscribe_Options :: [Xmlel::xmlel(),...] + | [Xmlel::xmlel()]}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'subscribe'} + | {error, 'feature-not-implemented', 'subscription-options'} + % + | {error, 'bad-request', 'invalid-jid'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} + % + | {error, 'invalid-options'} + | {error, 'not-acceptable'} + % + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-authorized', 'pending-subscription'} + | {error, 'not-allowed', 'closed-node'} +). + +subscribe_node(Host, Pubsub_Host, {U,S,_} = Entity, + {NodeId, Jid, Subscribe_Options} = _Parameters, + #capabilities{plugin = Plugin, + api = #api{options = Options_Module, db = API_DB, broadcast = API_Broadcast}} + = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('subscribe_node', Pubsub_Features, {Subscribe_Options}) of + ok -> + case is_jid(Jid) of + #jid{luser = U, lserver = S, lresource = Resource} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.subscribe_node, + 'subscribe_node', + [Host, Pubsub_Host, {U,S,undefined}, Plugin, Options_Module, + Pubsub_Features, + {NodeId, Resource, Subscribe_Options}]) + of + {result, + {Subscription_State, SubId, _Resource, _Subscription_Options} + = Subscription, Affiliation, + Node_Owners, Node_Options, Pubsub_Last_Item} -> + case Pubsub_Last_Item of + #pubsub_last_item_dev{ + id = ItemId, + creation = {DateTime, Creator}, + payload = Payload, + options = Item_Options + } -> + spawn(API_Broadcast#api_broadcast.broadcast_publish_last, + broadcast_publish_last, + [Host, Pubsub_Host, NodeId, Node_Options, + Node_Owners, + [{ItemId, Item_Options, Payload, Creator, DateTime}], + Entity, Affiliation, Subscription]); + _ -> + ok + end, + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_subscription('pubsub', + NodeId, Jid, SubId, Subscription_State)])]}; + Error -> + Error + end; + _ -> + {error, 'bad-request', 'invalid-jid'} + end; + Error -> + Error + end. + +%-- Unubscribe_Node --% +-spec(unsubscribe_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + _SubId :: undefined | exmpp_pubsub:subId()}, + Capabilities :: #capabilities{}) + -> {result, []} + %%% + | {error, 'feature-not-implemented', 'subscribe'} + | {error, 'forbidden'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} + | {error, 'unexpected', 'not-subscribed'} + % + | {error, 'unexpected', 'not-subscribed'} + | {error, 'not-acceptable', 'invalid-subid'} + | {error, 'bad-request', 'subid-required'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +unsubscribe_node(Host, Pubsub_Host, {U,S,_} = Entity, + {NodeId, Jid, _SubId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB, broadcast = API_Broadcast}} + = _Capabilities) -> + case lists:member(<<"subscribe">>, Pubsub_Features = Plugin:pubsub_features()) of + true -> + case is_jid(Jid) of + #jid{luser = U, lserver = S, lresource = Resource} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.unsubscribe_node, + 'unsubscribe_node', + [Host, Pubsub_Host, {U,S,undefined}, + {NodeId, Resource, _SubId}]) + of + {result, SubId} -> + {result, []}; + {result, SubId, Node_Owners, Notification_Type} -> + spawn(API_Broadcast#api_broadcast.notify_subscription, + notify_subscription, + [Host, NodeId, jlib:jid_to_string({<<>>, Host, <<>>}), + Node_Owners, Notification_Type, + {Jid, 'none', SubId}]), + {result, []}; + Error -> + Error + end; + _ -> + {error, 'forbidden'} + end; + false -> + {error, 'feature-not-implemented', 'subscribe'} + end. + +%-- Get_Items --% +-spec(get_items/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + _SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: non_neg_integer(), + ItemIds :: undefined} + | {NodeId :: undefined | exmpp_pubsub:nodeId(), + _SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: undefined, + ItemIds :: [] | exmpp_pubsub:itemIds()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'retrieve-items'} + | {error, 'feature-not-implemented', 'persistent-items'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-allowed', 'closed-node'} +). + +get_items(Host, Pubsub_Host, {U,S,_} = _Entity, + {NodeId, SubId, Max_Items, ItemIds}, + #capabilities{plugin = Plugin, api = #api{db = API_DB}} = _Capabilities) -> + case features('get_items', Plugin:pubsub_features(), none) of + ok -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_items, + 'get_items', + [Host, Pubsub_Host, {U,S,undefined}, + {NodeId, SubId, Max_Items, ItemIds}]) + of + {result, Pubsub_Items, Cache, Node_Owners, Node_Access_Model, + Node_Groups, Affiliation, Access} -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_items('pubsub', NodeId, + result_get_items(Host, Pubsub_Items, + {{U,S,undefined}, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + Cache, []))])]}; + Error -> + Error + end; + Error -> + Error + end. + +%% +-spec(result_get_items/6 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Entity_Parameters :: {Entity :: xmpp_jid:usr_bare(), + Affiliation :: 'member' | 'none' | 'owner' | 'publisher', + Access :: undefined | 'presence' | 'roster' | 'authorize'}, + Node_Parameters :: {Node_Owners :: mod_pubsub_dev:node_owners(), + Node_Access_Model :: pubsub_options:access_model(), + Node_Groups :: [Group::binary()]}, + Cache :: {Presence_Cache :: undefined | boolean(), + Groups_Cache :: undefined + | [{Entity :: xmpp_jid:usr_bare(), + Groups :: [Group::binary()]}]}, + Xmlels_Item :: [Xmlel_Item::#xmlel{name::binary(), children::[]}]) + -> Xmlels_Item :: [Xmlel_Item::#xmlel{name::binary(), children::[]}] +). + +result_get_items(_Host, [] = _Pubsub_Items, _Entity_Parameters, _Node_Parameters, + _Cache, Xmlels_Item) -> + Xmlels_Item; +%% +result_get_items(Host, + [#pubsub_item_dev{id = {ItemId, _NodeIdx}, payload = Payload, options = Item_Options} + | Pubsub_Items], + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + Cache, Xmlels_Item) + when Item_Options == [] + orelse Affiliation == 'owner' -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + Cache, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]); +%% +result_get_items(Host, + [#pubsub_item_dev{id = {ItemId, _NodeIdx}, payload = Payload, options = Item_Options} | Pubsub_Items], + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache} = _Cache, Xmlels_Item) -> + Item_Groups = get_value(Item_Options, 'roster_groups_allowed', []), + case get_value(Item_Options, 'access_model', undefined) of + %% + Item_Access_Model + when Item_Access_Model == undefined + orelse Item_Access_Model == 'open' + orelse Item_Access_Model == 'authorize' + orelse Item_Access_Model == 'whitelist' + % + orelse (Item_Access_Model == Node_Access_Model + andalso + Item_Access_Model =/= 'roster') + % + orelse (Item_Access_Model == 'presence' + andalso + (Access == 'presence' + orelse + Access == 'roster' + orelse + Presence_Cache == true)) + % + orelse (Item_Access_Model == 'roster' + andalso + Node_Access_Model == 'roster' + andalso + Item_Groups == []) + % + orelse ((Item_Access_Model == 'authorize' + orelse + Item_Access_Model == 'whitelist') + andalso + (Affiliation == 'member' + orelse + Affiliation == 'publisher')) -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache}, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]); + %% + Item_Access_Model + when Item_Access_Model == 'presence' + andalso Presence_Cache == undefined -> + case is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) of + false -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {_Presence_Cache = false, Groups_Cache}, + Xmlels_Item); + _Node_Owner -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {_Presence_Cache = true, Groups_Cache}, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]) + end; + Item_Access_Model + when Item_Access_Model == 'roster' + andalso Groups_Cache == undefined -> + case is_contact_in_allowed_roster_groups(Entity, Item_Groups) of + {Node_Owner, Roster_Group} -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, _Groups_Cache = [{Node_Owner, [Roster_Group]}]}, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]); + false -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache}, + Xmlels_Item) + end; + Item_Access_Model + when Item_Access_Model == 'roster' -> + case + lists:any(fun + ({Owner, Groups}) -> + case lists:keyfind(Owner, 1, Item_Groups) of + {_Owner, Owner_Groups} -> + lists:any(fun + (Group) -> + lists:member(Group, Owner_Groups) + end, Groups); + false -> + false + end + end, Groups_Cache) + of + true -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache}, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]); + false -> + case is_contact_in_allowed_roster_groups(Entity, Node_Groups) of + {Node_Owner, Roster_Group} -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, + _Groups_Cache = case + lists:keyfind(Node_Owner, 1, Groups_Cache) + of + {_Node_Owner, Node_Owner_Groups} -> + lists:keyreplace(Node_Owner, 1, Groups_Cache, + {Node_Owner, + [Roster_Group + | lists:delete(Roster_Group, Node_Owner_Groups)]}); + false -> + [{Node_Owner, [Roster_Group]} | Groups_Cache] + end}, + [exmpp_pubsub:xmlel_item('pubsub', ItemId, Payload) + | Xmlels_Item]); + false -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache}, + Xmlels_Item) + end + end; + _ -> + result_get_items(Host, Pubsub_Items, + {Entity, Affiliation, Access}, + {Node_Owners, Node_Access_Model, Node_Groups}, + {Presence_Cache, Groups_Cache}, + Xmlels_Item) + end. + +%-- Set_Configure_Subscription --% +-spec(set_configure_subscription/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + SubId :: undefined | exmpp_pubsub:subId(), + Subscribe_Options :: [Xmlel::xmlel(),...] + | [Xmlel::xmlel()]}, + Capabilities :: #capabilities{}) + -> {result, []} + %%% + | {error, 'feature-not-implemented', 'subscription-options'} + % + | {error, 'bad-request', 'invalid-options'} + | {error, 'bad-request', 'jid-required'} + | {error, 'bad-request', 'invalid-jid'} + % + | {error, 'item-not-found'} + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'forbidden'} + | {error, 'bad-request', 'invalid-options'} + % + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +set_configure_subscription(Host, Pubsub_Host, {U,S,_} = Entity, + {NodeId, Jid, SubId, Subscribe_Options}, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, db = API_DB}} + = _Capabilities) -> + case + checks_on_set_configure_subscription(Entity, Jid, Plugin, + Subscribe_Options) + of + {ok, Pubsub_Features, Resource} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.set_configure_subscription, + 'set_configure_subscription', + [Host, Pubsub_Host, Entity, Plugin, Options_Module, + Pubsub_Features, {NodeId, SubId, Resource, Subscribe_Options}]) + of + {result, []} -> + {result, []}; + Error -> + Error + end; + Error -> + Error + end. + +%% +-spec(checks_on_set_configure_subscription/4 :: +( + Entity :: xmpp_jid:usr_entity(), + Subscriber_Jid :: binary() | undefined, + Plugin :: exmpp_pubsub:plugin(), + Subscribe_Options :: [Xmlel::xmlel(),...] + | [Xmlel::xmlel()]) + -> {ok, + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Resource :: undefined | xmpp_jid:resource_jid()} + %%% + | {error, 'feature-not-implemented', 'subscription-options'} + % + | {error, 'bad-request', 'invalid-options'} + | {error, 'bad-request', 'jid-required'} + | {error, 'bad-request', 'invalid-jid'} +). + +checks_on_set_configure_subscription({U,S,_} = _Entity, Subscriber_Jid, Plugin, + Subscribe_Options) -> + Pubsub_Features = Plugin:pubsub_features(), + case lists:member(<<"subscription-options">>, Pubsub_Features) of + true -> + case Subscribe_Options of + [] -> + {error, 'bad-request', 'invalid-options'}; + _ -> + case Subscriber_Jid of + undefined -> + {error, 'bad-request', 'jid-required'}; + _ -> + case is_jid(Subscriber_Jid) of + #jid{luser = U, lserver = S, lresource = Resource} -> + {ok, Pubsub_Features, Resource}; + _ -> + {error, 'bad-request', 'invalid-jid'} + end + end + end; + false -> + {error, 'feature-not-implemented', 'subscription-options'} + end. + +%-- Get_Entity_Affiliations --% +-spec(get_entity_affiliations/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'retrieve-affiliations'} +). + +get_entity_affiliations(_Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB}} = _Capabilities) -> + case + lists:member(<<"retrieve-affiliations">>, + _Pubsub_Features = Plugin:pubsub_features()) + of + true -> + {result, + result_get_entity_affiliations( + _Entity_Affiliations = pubsub_db:transaction('mnesia', + API_DB#api_db.get_entity_affiliations, + 'get_entity_affiliations', + [Pubsub_Host, {U,S,undefined}, {NodeId}]))}; + false -> + {error, 'feature-not-implemented', 'retrieve-affiliations'} + end. + +%% +-spec(result_get_entity_affiliations/1 :: +( + Entity_Affiliations :: [Entity_Affiliation :: { + NodeId :: exmpp_pubsub:nodeId(), + Affiliation :: exmpp_pubsub:affiliation() + }]) + -> [Xmlel_Pubsub::xmlel(),...] +). + +result_get_entity_affiliations(Entity_Affiliations) -> + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_affiliations('pubsub', + lists:map(fun({NodeId, Affiliation}) -> + exmpp_pubsub:xmlel_affiliation('pubsub', NodeId, Affiliation) + end, Entity_Affiliations) + )] + )]. + + +%-- Get_Entity_Subscriptions --% +-spec(get_entity_subscriptions/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'retrieve-subscriptions'} +). + +get_entity_subscriptions(_Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB}} = _Capabilities) -> + case + lists:member(<<"retrieve-subscriptions">>, + _Pubsub_Features = Plugin:pubsub_features()) + of + true -> + {result, + result_get_entity_subscriptions({U,S,undefined}, + _Entity_Subscriptions = pubsub_db:transaction('mnesia', + API_DB#api_db.get_entity_subscriptions, + 'get_entity_subscriptions', + [Pubsub_Host, {U,S,undefined}, {NodeId}])) + }; + false -> + {error, 'feature-not-implemented', 'retrieve-subscriptions'} + end. + +%% +-spec(result_get_entity_subscriptions/2 :: +( + Entity :: xmpp_jid:usr_bare(), + Entity_Subscriptions :: [{NodeId::exmpp_pubsub:nodeId(), + Subscriptions::exmpp_pubsub:subscriptions()}]) + -> [Xmlel_Pubsub::xmlel(),...] +). + +result_get_entity_subscriptions({U,S,_R} = _Entity, Entity_Subscriptions) -> + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_subscriptions('pubsub', + lists:map(fun + ({NodeId, Subscriptions}) -> + lists:map(fun + %% + ({Subscription_State, SubId, {caps, Resource}, + _Subscription_Options}) -> + exmpp_pubsub:xmlel_subscription('pubsub', + NodeId, + _Jid = jlib:jid_to_string({U, S, <<>>}), + SubId, + Subscription_State); + %% + ({Subscription_State, SubId, Resource, + _Subscription_Options}) -> + exmpp_pubsub:xmlel_subscription('pubsub', + NodeId, + _Jid = jlib:jid_to_string({U, S, Resource}), + SubId, + Subscription_State) + % + end, Subscriptions) + end, Entity_Subscriptions) + )] + )]. + + +%-- Get_Node_Affiliations --% +-spec(get_node_affiliations/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'modify-affiliations'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +get_node_affiliations(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB}} = _Capabilities) -> + case + lists:member(<<"modify-affiliations">>, + _Pubsub_Features = Plugin:pubsub_features()) + of + true -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_node_affiliations, + 'get_node_affiliations', + [Host, Pubsub_Host, {U,S,undefined}, {NodeId}]) + of + {result, Pubsub_States} -> + {result, result_get_node_affiliations(NodeId, Pubsub_States)}; + Error -> + Error + end; + false -> + {error, 'feature-not-implemented', 'modify-affiliations'} + end. + +%% +-spec(result_get_node_affiliations/2 :: +( + NodeId :: exmpp_pubsub:nodeId(), + Pubsub_States :: [Pusbub_State::#pubsub_state_dev{},...]) + -> [Xmlel_Pubsub::xmlel(),...] +). + +result_get_node_affiliations(NodeId, Pubsub_States) -> + [exmpp_pubsub:xmlel_pubsub('pubsub#owner', + [exmpp_pubsub:xmlel_affiliations('pubsub#owner', NodeId, + lists:map(fun + (#pubsub_state_dev{id = {Entity, _NodeIdx}, + affiliation = Affiliation}) -> + exmpp_pubsub:xmlel_affiliation('pubsub#owner', + _Jid = pubsub_tools:jid_to_string(Entity), + Affiliation) + end, Pubsub_States) + )] + )]. + + +%-- Get_Node_Subscriptions --% +-spec(get_node_subscriptions/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'manage-subscriptions'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +get_node_subscriptions(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{db = API_DB}} = _Capabilities) -> + case + lists:member(<<"manage-subscriptions">>, + _Pubsub_Features = Plugin:pubsub_features()) + of + true -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_node_subscriptions, + 'get_node_subscriptions', + [Host, Pubsub_Host, {U,S,undefined}, {NodeId}]) + of + {result, Pubsub_States} -> + {result, result_get_node_subscriptions(NodeId, Pubsub_States)}; + Error -> + Error + end; + false -> + {error, 'feature-not-implemented', 'manage-subscriptions'} + end. + +%% +-spec(result_get_node_subscriptions/2 :: +( + NodeId :: exmpp_pubsub:nodeId(), + Pubsub_States :: [Pubsub_State::#pubsub_state_dev{}]) + -> [Xmlel_Pubsub::xmlel(),...] +). + +result_get_node_subscriptions(NodeId, Pubsub_States) -> + [exmpp_pubsub:xmlel_pubsub('pubsub#owner', + [exmpp_pubsub:xmlel_subscriptions('pubsub#owner', NodeId, + lists:foldl(fun + %% + (#pubsub_state_dev{affiliation = 'none'}, Xmlels_Subscription) -> + Xmlels_Subscription; + %% + (#pubsub_state_dev{id = {{U, S, _}, _NodeIdx}, + subscriptions = Subscriptions}, Xmlels_Subscription) -> + Xmlels_Subscription + ++ + lists:map(fun + %% + ({Subscription_State, SubId, {caps, Resource}, + _Subscription_Options}) -> + exmpp_pubsub:xmlel_subscription('pubsub#owner', + _Jid = pubsub_tools:jid_to_string({U, S, Resource}), + SubId, + Subscription_State); + %% + ({Subscription_State, SubId, Resource, + _Subscription_Options}) -> + exmpp_pubsub:xmlel_subscription('pubsub#owner', + _Jid = pubsub_tools:jid_to_string({U, S, Resource}), + SubId, + Subscription_State) + end, Subscriptions) + % + end, [], Pubsub_States) + )] + )]. + +%% Get_Configure_Subscription_Default +-spec(get_configure_subscription_default/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'subscription-options'} + | {error, 'feature-not-implemented', 'retrieve-default-sub'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +get_configure_subscription_default(Host, Pubsub_Host, {U,S,_R} = _Entity, + {undefined = _NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, + db = API_DB}} = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('get_configure_subscription_default', Pubsub_Features, none) of + ok -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_default('pubsub', + [Options_Module:xdata_x('subscribe_options', 'result', + Pubsub_Features, Host, {U,S,undefined}, + _Subscription_Options = Plugin:default_subscription_options( + case Host of + S -> 'local'; + _ -> 'remote' + end, + 'leaf') + )] + )] + )] + }; + Error -> + Error + end; +%% +get_configure_subscription_default(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, + db = API_DB}} = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('get_configure_subscription_default', Pubsub_Features, none) of + ok -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_configure_subscription_default, + 'get_configure_subscription_default', + [Host, Pubsub_Host, {U,S,undefined}, NodeId]) + of + ok -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_default('pubsub', NodeId, + [Options_Module:xdata_x('subscribe_options', 'result', + Pubsub_Features, Host, {U,S,undefined}, + _Subscription_Options = Plugin:default_subscription_options( + case Host of + S -> 'local'; + _ -> 'remote' + end, + 'leaf') + )] + )] + )] + }; + Error -> + Error + end; + Error -> + Error + end. + +%% Get_Configure_Subscription +-spec(get_configure_subscription/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + SubId :: undefined | exmpp_pubsub:subId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'subscription-options'} + % + | {error, 'item-not-found'} + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'forbidden'} + % + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +get_configure_subscription(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId, Jid, SubId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, + db = API_DB}} = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case checks_on_get_configure_subscription({U,S,undefined}, Jid, Plugin) of + {ok, Pubsub_Features, Resource} -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_configure_subscription, + 'get_configure_subscription', + [Host, Pubsub_Host, {U,S,undefined}, Plugin, + Pubsub_Features, {NodeId, SubId, Resource}]) + of + {result, {_, _, _, Subscription_Options} = Subscription} -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub', + [exmpp_pubsub:xmlel_options('pubsub', NodeId, Jid, SubId, + [Options_Module:xdata_x('subscribe_options', 'form', + Pubsub_Features, Host, {U,S,undefined},Subscription_Options, + _Default_Subscription_Options = Plugin:default_subscription_options( + case Host of + S -> 'local'; + _ -> 'remote' + end, + 'leaf') + )] + )] + )] + }; + Error -> + Error + end; + Error -> + Error + end. + +%% +-spec(checks_on_get_configure_subscription/3 :: +( + Entity :: xmpp_jid:usr_entity(), + Subscriber_Jid :: binary() | undefined, + Plugin :: exmpp_pubsub:plugin()) + -> {ok, + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Resource :: undefined | xmpp_jid:resource_jid()} + %%% + | {error, 'feature-not-implemented', 'subscription-options'} + % + | {error, 'bad-request', 'jid-required'} + | {error, 'bad-request', 'invalid-jid'} +). + +checks_on_get_configure_subscription({U,S,_} = _Entity, Subscriber_Jid, Plugin) -> + Pubsub_Features = Plugin:pubsub_features(), + case lists:member(<<"subscription-options">>, Pubsub_Features) of + true -> + case Subscriber_Jid of + undefined -> + {error, 'bad-request', 'jid-required'}; + _ -> + case is_jid(Subscriber_Jid) of + #jid{luser = U, lserver = S, lresource = Resource} -> + {ok, Pubsub_Features, Resource}; + _ -> + {error, 'bad-request', 'invalid-jid'} + end + end; + false -> + {error, 'feature-not-implemented', 'subscription-options'} + end. + +%% Get_Configure_Node_Default +-spec(get_configure_node_default/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'config-node'} + | {error, 'feature-not-implemented', 'retrieve-default'} +). + +get_configure_node_default(Host, Pubsub_Host, {U,S,_R} = _Entity, + {undefined = _NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, + db = API_DB}} = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case features('get_configure_node_default', Pubsub_Features, none) of + ok -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub#owner', + [exmpp_pubsub:xmlel_default('pubsub#owner', + [Options_Module:xdata_x('node_config', 'form', + Pubsub_Features, Host, {U,S,undefined}, + _Node_Options = Plugin:node_options('leaf') + )] + )] + )] + }; + Error -> + Error + end. + +%% Get_Configure_Node +-spec(get_configure_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}, + Capabilities :: #capabilities{}) + -> {result, [Xmlel_Pubsub::xmlel(),...]} + %%% + | {error, 'feature-not-implemented', 'config-node'} + % + | {error, 'bad-request', 'nodeid-required'} + | {error, 'item-not-found'} + | {error, 'forbidden'} + +). + +get_configure_node(Host, Pubsub_Host, {U,S,_R} = _Entity, + {NodeId} = _Parameters, + #capabilities{plugin = Plugin, api = #api{options = Options_Module, + db = API_DB}} = _Capabilities) -> + Pubsub_Features = Plugin:pubsub_features(), + case lists:member(<<"config-node">>, Pubsub_Features) of + true -> + case + pubsub_db:transaction('mnesia', + API_DB#api_db.get_configure_node, + 'get_configure_node', + [Host, Pubsub_Host, {U,S,undefined}, {NodeId}]) + of + {result, Node_Options} -> + {result, + [exmpp_pubsub:xmlel_pubsub('pubsub#owner', + [exmpp_pubsub:xmlel_configure('pubsub#owner', NodeId, + [Options_Module:xdata_x('node_config', 'form', + Pubsub_Features, Host, {U,S,undefined}, Node_Options + )] + )] + )] + }; + Error -> + Error + end; + false -> + {error, 'feature-not-implemented', 'config-node'} + end. + diff --git a/src/mod_pubsub_ng/pubsub_db.erl b/src/mod_pubsub_ng/pubsub_db.erl new file mode 100644 index 000000000..b08fe611b --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_db.erl @@ -0,0 +1,1575 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http:%%www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_db). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1 +]). + + + +%% @doc Intialize mnesia + +-spec(init/2 :: +( + Backend :: 'mnesia' | 'odbc', + Suffix :: atom()) + -> 'ok' +). + +init('mnesia', Suffix) -> + pubsub_db_mnesia:init(Suffix); +init('odbc', _Suffix) -> + ok. + + +transaction('mnesia', Module, Function, Arguments) -> + pubsub_db_mnesia:transaction(Module, Function, Arguments). + + +%% +db_transaction(Module, Function, Arguments) -> + case mnesia:transaction( + fun() -> + apply(Module, Function, Arguments) + end) + of + {atomic, Result} -> + Result; + {aborted, Reason} -> + ?INFO_MSG("DB TRANSACTION ERROR ~p", [Reason]), + {error, 'internal-server-error'} + end. + +%-- Create_Node --% +-spec(create_node/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Entity :: xmpp_jid:usr_bare(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Options :: pubsub_options:options_node()}) + -> %% + {result, + NodeId :: exmpp_pubsub:nodeId(), + Notify :: [] + | [Notify_Subscription :: { + Notification_Type :: pubsub_options:notification_type(), + Subscription :: exmpp_pubsub:subscription_subscribed() + },...] + } + % + %%% + | {error, 'conflict'} +). + +create_node(Host, Pubsub_Host, {_U,S,_R} = Entity, Pubsub_Features, + _Parameters = {undefined = _NodeId, Node_Options}) -> + case + pubsub_db_mnesia:read_node('dev', + Pubsub_Host, NodeId = exmpp_pubsub:nodeId()) + of + undefined -> + {result, + NodeId, + _Notify = case + pubsub_db_mnesia:create_node('dev', Pubsub_Host, Entity, NodeId, + _Level = 1, Node_Options, Pubsub_Features) + of + undefined -> + []; + Subscription -> + [_Notify_Subscription = { + _Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + Subscription + }] + end}; + _Pubsub_Node -> + create_node(Host, Pubsub_Host, Entity, Pubsub_Features, + {_NodeId = undefined, Node_Options}) + end; +%% +create_node(Host, Pubsub_Host, {_U,S,_R} = Entity, Pubsub_Features, + _Parameters = {NodeId, Node_Options}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {result, + NodeId, + _Notify = case + pubsub_db_mnesia:create_node('dev', Pubsub_Host, Entity, NodeId, + _Level = 1, Node_Options, Pubsub_Features) + of + undefined -> + []; + Subscription -> + [_Notify_Subscription = { + _Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + Subscription + }] + end}; + _Pubsub_Node -> + {error, 'conflict'} + end. + +%-- Delete_Node --% +-spec(delete_node/3 :: +( + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> %% + {result, + Notify :: [] + | [Notify_Delete :: { + Notification_Type::pubsub_options:notification_type(), + Recipients::[Entity::xmpp_jid:usr_bare()] + }] + } + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +delete_node(_Pubsub_Host, _Entity, {undefined = _NodeId}) -> + {error, 'item-not-found'}; +%% +delete_node(Pubsub_Host, Entity, {NodeId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + Pubsub_Node -> + case + lists:member(Entity, Owners = Pubsub_Node#pubsub_node_dev.owners) + of + true -> + {result, + _Notify = case + pubsub_db_mnesia:delete_node('dev', Pubsub_Node) + of + undefined -> + []; + Subscribers -> + [_Notify_Delete = { + _Notification_Type = get_value( + Pubsub_Node#pubsub_node_dev.options, + 'notification_type', 'headline'), + _Recipients = lists:append(Owners, Subscribers) + }] + end}; + false -> + {error, 'forbidden'} + end + end. + +%-- Purge_Node --% +-spec(purge_node/3 :: +( + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> %% + {result, + Notify :: [] + | [Notify_Purge :: { + Notification_Type::pubsub_options:notification_type(), + Recipients::[Entity::xmpp_jid:usr_bare()] + }] + } + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + % + | {error, 'feature-not-implemented', 'persistent-items'} +). + +purge_node(_Pubsub_Host, _Entity, {undefined = _NodeId}) -> + {error, 'item-not-found'}; +%% +purge_node(Pubsub_Host, Entity, {NodeId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + Pubsub_Node -> + case lists:member(Entity, Pubsub_Node#pubsub_node_dev.owners) of + true -> + case + _Persist_Items = get_value( + Node_Options = Pubsub_Node#pubsub_node_dev.options, + 'persist_items', true) + of + true -> + {result, + _Notify = case + pubsub_db_mnesia:purge_node('dev', Pubsub_Node) + of + undefined -> + []; + Subscribers -> + [_Notify_Purge = { + _Notification_Type = get_value( + Node_Options, + 'notification_type', 'headline'), + _Recipients = Subscribers + }] + end}; + false -> + {error, 'feature-not-implemented', 'persistent-items'} + end; + false -> + {error, 'forbidden'} + end + end. + +%-- Publish_Item --% +-spec(publish_item/6 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Entity :: xmpp_jid:usr_entity(), + Plugin :: exmpp_pubsub:plugin(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Item :: 0 | 1, + ItemId :: undefined | exmpp_pubsub:itemId(), + Payload :: exmpp_pubsub:payload() | [Xmlel::xmlel(),...], + Options :: {Node_Options :: [] | pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()} + }) + -> {result, + NodeId :: exmpp_pubsub:nodeId(), + ItemId :: exmpp_pubsub:itemId(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + SubId :: undefined | exmpp_pubsub:subId(), + Subscription :: undefined | exmpp_pubsub:subscription_subscribed(), + Broadcasted_Items :: { + Published_Items :: [Published_Item::{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item(), + Payload :: exmpp_pubsub:payload(), + Publisher :: xmpp_jid:usr_entity() + }], + % + Retracted_Items :: [Retracted_Item::{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item() + }] + }, + Pubsub_States::[Pubsub_State::mod_pubsub_dev:pubsub_state(),...]} + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} + % + | {error, 'bad-request', 'invalid-payload'} + | {error, 'bad-request', 'item-required'} + | {error, 'bad-request', 'item-forbidden'} + | {error, 'not-acceptable', 'payload-too-big'} +). + +%% +publish_item(Host, Pubsub_Host, {U,S,R} = Entity, Plugin, Pubsub_Features, + {undefined = _NodeId, Item, _ItemId, Payload, + {[] = _Node_Options, Item_Options}}) -> + case + pubsub_db_mnesia:read_node('dev', + Pubsub_Host, NodeId = exmpp_pubsub:nodeId()) + of + undefined -> + case + options_on_publish_item(none, {Item, Payload, + Options = { + Node_Options = Plugin:node_options('leaf'), + Item_Options + }}) + of + ok -> + {ItemId, Pubsub_State} = pubsub_db_mnesia:publish_item1('dev', + Pubsub_Host, Entity, NodeId, _Level = 1, _ItemId, Payload, + Options, Pubsub_Features), + Subscription = case + Pubsub_State#pubsub_state_dev.subscriptions + of + [] -> undefined; + [_Subscription] -> _Subscription + end, + {result, NodeId, ItemId, Node_Options, + _Node_Owners = [{U,S,undefined}], + _SubId = case Subscription of + undefined -> + undefined; + {_Subscription_State, SubId, _Resource, + _Subscription_Options} -> + SubId + end, + Subscription, + {_Published_Items = case + get_value({Node_Options, Item_Options}, + 'deliver_notifications', false) + of + true -> + [{ItemId, + Item_Options, + _Payload = case + get_value({Node_Options, Item_Options}, + 'deliver_payloads', false) + of + true -> + case Payload of + [Xmlel] -> Xmlel; + [] -> [] + end; + false -> [] + end, + _Publisher = {U,S,R} + }]; + false -> + [] + end, + _Retracted_Items = []}, + _Pubsub_States = [Pubsub_State]}; + Error -> + Error + end; + _Pusbub_Node -> + publish_item(Host, Pubsub_Host, {U,S,R}, Plugin, Pubsub_Features, + {undefined, Item, _ItemId, Payload, {[], Item_Options}}) + end; + +%% +publish_item(Host, Pubsub_Host, {U,S,R} = Entity, Plugin, Pubsub_Features, + {NodeId, Item, _ItemId, Payload, {[] = _Node_Options, Item_Options}}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + case + checks_on_publish_item(Host, {U,S,undefined}, Plugin, + {Item, Payload, _Node_Options = [], Item_Options}) + of + {ok, Pubsub_Features, Node_Options} -> + Options = {Node_Options, Item_Options}, + {ItemId, Pubsub_State} = pubsub_db_mnesia:publish_item1('dev', + Pubsub_Host, Entity, NodeId, _Level = 1, _ItemId, Payload, + Options, Pubsub_Features), + Subscription = case + Pubsub_State#pubsub_state_dev.subscriptions + of + [] -> undefined; + [_Subscription] -> _Subscription + end, + {result, NodeId, ItemId, Node_Options, + _Node_Owners = [{U,S,undefined}], + _SubId = case Subscription of + undefined -> + undefined; + {_Subscription_State, SubId, _Resource, + _Subscription_Options} -> + SubId + end, + Subscription, + {_Published_Items = case + get_value({Node_Options, Item_Options}, + 'deliver_notifications', false) + of + true -> + [{ItemId, + Item_Options, + _Payload = case + get_value({Node_Options, Item_Options}, + 'deliver_payloads', false) + of + true -> + case Payload of + [Xmlel] -> Xmlel; + [] -> [] + end; + false -> [] + end, + _Publisher = {U,S,R} + }]; + false -> + [] + end, + _Retracted_Items = []}, + _Pubsub_States = [Pubsub_State]}; + Error -> + Error + end; + Pubsub_Node -> + NodeIdx = Pubsub_Node#pubsub_node_dev.idx, + Pubsub_State = pubsub_db_mnesia:read_state('dev', + {U,S,undefined}, NodeIdx), + case + options_on_publish_item(Host, Entity, + _Affiliation = case Pubsub_State of + undefined -> 'none'; + _ -> Pubsub_State#pubsub_state_dev.affiliation + end, + _Subscriptions = case Pubsub_State of + undefined -> []; + _ -> Pubsub_State#pubsub_state_dev.subscriptions + end, + _Options = { + Node_Options = Pubsub_Node#pubsub_node_dev.options, + Item_Options + }, + Node_Owners = Pubsub_Node#pubsub_node_dev.owners, + Item, + Payload) + of + ok -> + {ItemId, Retracted_Items, Pubsub_States} = + pubsub_db_mnesia:publish_item2('dev', Pubsub_Host, + {U,S,R}, Pubsub_State, Pubsub_Node, + case _ItemId of + undefined -> + undefined; + _ -> + case + pubsub_db_mnesia:read_item('dev', + NodeIdx, _ItemId) + of + undefined -> _ItemId; + Pubsub_Item -> Pubsub_Item + end + end, + Payload, Item_Options, Pubsub_Features), + {result, NodeId, ItemId, Node_Options, Node_Owners, + _SubId = undefined, _Subscription = undefined, + {_Published_Items = case + get_value({Node_Options, Item_Options}, + 'deliver_notifications', false) + of + true -> + [{ItemId, + Item_Options, + _Payload = case + get_value({Node_Options, Item_Options}, + 'deliver_payloads', false) + of + true -> + case Payload of + [Xmlel] -> Xmlel; + [] -> [] + end; + false -> [] + end, + _Publisher = {U,S,R} + }]; + false -> + [] + end, + Retracted_Items}, + Pubsub_States}; + Error -> + Error + end + end. + + +%% +-spec(options_on_publish_item/8 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: exmpp_pubsub:subscriptions(), + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}, + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Item :: 0 | 1, + Payload :: exmpp_pubsub:payload() | [Xmlel::xmlel(),...]) + -> ok + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} + % + | {error, 'bad-request', 'invalid-payload'} + | {error, 'bad-request', 'item-required'} + | {error, 'bad-request', 'item-forbidden'} + | {error, 'not-acceptable', 'payload-too-big'} +). + +options_on_publish_item(_Host, _Entity, 'owner' = _Affiliation, _Subscriptions, + Options, _Node_Owners, Item, Payload) -> + options_on_publish_item(none, {Item, Payload, Options}); +%% +options_on_publish_item(Host, Entity, 'outcast' = _Affiliation, _Subscriptions, + {Node_Options, _Item_Options}, Node_Owners, _Item, _Payload) -> + case get_value(Node_Options, 'access_model') of + %% + 'open'-> + {error, 'forbidden'}; + %% + 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) + of + false -> + {error, 'item-not-found'}; + _Node_Owner -> + {error, 'forbidden'} + end; + %% + 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + _Rosters_Groups_Allowed = get_value(Node_Options, + 'roster_groups_allowed')) + of + false -> + {error, 'item-not-found'}; + {_Node_Owner, _Roster_Group_Allowed} -> + {error, 'forbidden'} + end; + %% + 'authorize' -> + {error, 'forbidden'}; + %% + 'whitelist' -> + {error, 'item-not-found'} + end; +%% +options_on_publish_item(Host, Entity, Affiliation, Subscriptions, + {Node_Options, Item_Options} = _Options, Node_Owners, Item, Payload) -> + %% 'pubsub#access_model' + case + check_access_model(Host, Entity, + _Access_Model = get_value(Node_Options, 'access_model'), + Affiliation, Subscriptions, Node_Owners, + _Rosters_Groups_Allowed = get_value(Node_Options, + 'roster_groups_allowed')) + of + ok -> + %% 'pubsub#publish_model' + case + check_publish_model( + _Publish_Model = get_value(Node_Options, 'publish_model'), + Affiliation, Subscriptions) + of + ok -> + options_on_publish_item(none, + {Item, Payload, {Node_Options, Item_Options}}); + Error -> + Error + end; + Error -> + Error + + end. + +%% +-spec(options_on_publish_item/2 :: +( + + Option :: none + | {pubsub_options:option_node_deliver_payloads(), + pubsub_options:option_node_persist_items()} + | pubsub_options:option_node_max_payload_size() + | pubsub_options:option_node_type(), + %% + Criteria :: {Item :: 0 | 1, + Payload :: exmpp_pubsub:payload() | [Xmlel::xmlel(),...], + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}} + | {Item :: 0 | 1, + Payload :: exmpp_pubsub:payload(), + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}} + % + | {Payload :: exmpp_pubsub:payload(), + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}} + % + | {Payload :: exmpp_pubsub:payload()}) + -> %% + ok + %%% + | {error, 'bad-request', 'invalid-payload'} + | {error, 'bad-request', 'item-required'} + | {error, 'bad-request', 'payload-required'} + | {error, 'bad-request', 'item-forbidden'} + | {error, 'not-acceptable', 'payload-too-big'} +). + +%% Payloads count +options_on_publish_item(none, {Item, [] = Payload, Options}) -> + options_on_publish_item( + {get_option(Options, 'deliver_payloads', false), + get_option(Options, 'persist_items', false)}, + {Item, Payload, Options}); +%% +options_on_publish_item(none, {Item, [Payload], Options}) -> + options_on_publish_item( + {get_option(Options, 'deliver_payloads', false), + get_option(Options, 'persist_items', false)}, + {Item, Payload, Options}); +%% +options_on_publish_item(none, _Parameters) -> + {error, 'bad-request', 'invalid-payload'}; +%% +%% 'pubsub#deliver_payloads' +%% 'pubsub#persist_items' +options_on_publish_item( + {{'deliver_payloads', _Deliver_Payloads}, {'persist_items', true}}, + {0 = _Item, _Payload, _Options}) -> + {error, 'bad-request', 'item-required'}; +%% +options_on_publish_item( + {{'deliver_payloads', true}, {'persist_items', _Persist_Items}}, + {_Item, [] = _Payload, _Options}) -> + {error, 'bad-request', 'payload-required'}; +%% +options_on_publish_item( + {{'deliver_payloads', false}, {'persist_items', false}}, + {1 = _Item, _Payload, _Options}) -> + {error, 'bad-request', 'item-forbidden'}; +%% +options_on_publish_item( + {{'deliver_payloads', Deliver_Payloads}, {'persist_items', Persist_Items}}, + {_Item, [] = _Payload, Options}) -> + ok; +%% +options_on_publish_item( + {{'deliver_payloads', Deliver_Payloads}, {'persist_items', Persist_Items}}, + {_Item, Payload, {Node_Options, Item_Options} = _Options}) -> + options_on_publish_item( + _Max_Payload_Size = get_option(Node_Options, 'max_payload_size', 0), + {Payload, {Node_Options, Item_Options}}); +%% 'pubsub#max_payload_size' +options_on_publish_item({'max_payload_size', Max_Payload_Size}, + {Payload, Options}) -> + case byte_size(term_to_binary(Payload)) =< Max_Payload_Size of + true -> + options_on_publish_item(get_option(Options, 'type', undefined), + {Payload}); + false -> + {error, 'not-acceptable', 'payload-too-big'} + end; +%% 'pubsub#type' +options_on_publish_item({'type', Type}, {Payload}) + when Type == undefined -> + %%orelse Type == Payload#xmlel.ns -> + ok; +%% +options_on_publish_item(_, _) -> + {error, 'bad-request', 'invalid-payload'}. + + + +%% +-spec(checks_on_publish_item/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_entity(), + Plugin :: exmpp_pubsub:plugin(), + Parameters :: {Item :: 0 | 1, + Payload :: [Xmlel::xmlel()], + Node_Options :: [], + Item_Options :: [] | pubsub_options:options_item()}) + -> {ok, + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Node_Options :: pubsub_options:options_node()} + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} + % + | {error, 'bad-request', 'invalid-payload'} + | {error, 'bad-request', 'item-required'} + | {error, 'bad-request', 'item-forbidden'} + | {error, 'not-acceptable', 'payload-too-big'} +). + + +checks_on_publish_item({U,S,_} = _Host, {U,S,_} = _Entity, Plugin, + {Item, Payload, [] = _Node_Options, Item_Options}) -> + case + lists:member(<<"auto-create">>, Pubsub_Features = Plugin:pubsub_features()) + of + true -> + case + options_on_publish_item(none, + {Item, + Payload, + _Options = { + Node_Options = Plugin:node_options('leaf'), + Item_Options + }}) + of + ok -> {ok, Pubsub_Features, Node_Options}; + Error -> Error + end; + false -> + {error, 'item-not-found'} + end; +%% +checks_on_publish_item(_Host, _Entity, _Plugin, _Parameters) + when is_tuple(_Host) -> + {error, 'foribdden'}; +%% +checks_on_publish_item(Host, {U,S,_R} = _Entity, Plugin, + {Item, Payload, [] = _Node_Options, Item_Options}) -> + case acl:match_rule(Host, pubsub_createnode, jlib:jid_to_string({U,S, <<>>})) of + allow -> + case + lists:member(<<"auto-create">>, + Pubsub_Features = Plugin:pubsub_features()) + of + true -> + case + options_on_publish_item(none, + {Item, Payload, + _Options = { + Node_Options = Plugin:node_options('leaf'), + Item_Options + }}) + of + ok -> {ok, Pubsub_Features, Node_Options}; + Error -> Error + end; + false -> + {error, 'item-not-found'} + end; + _Deny -> + {error, 'forbidden'} + end. + +%-- Retract_Item --% +-spec(retract_item/3 :: +( + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + ItemId :: undefined | exmpp_pubsub:itemId(), + Adhoc_Notify_Retract :: undefined | boolean()}) + -> {result, + Notify :: [] + | [Notify_Retract :: { + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item(), + Pubsub_States :: mod_pubsub_dev:pubsub_states() + }]} + %%% + | {error, 'bad-request', 'nodeid-required'} + | {error, 'bad-request', 'item-required'} + % + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +retract_item(_Pubsub_Host, _Entity, + {undefined = _NodeId, _ItemId, _Adhoc_Notify_Retract}) -> + {error, 'bad-request', 'nodeid-required'}; +%% +retract_item(_Pubsub_Host, _Entity, + {_NodeId, undefined = _ItemId, _Adhoc_Notify_Retract}) -> + {error, 'bad-request', 'item-required'}; +%% +retract_item(Pubsub_Host, Entity, {NodeId, ItemId, Adhoc_Notify_Retract}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + Pubsub_Node -> + case + pubsub_db_mnesia:read_state('dev', Entity, + Pubsub_Node#pubsub_node_dev.idx) + of + Pubsub_State + when Pubsub_State == undefined + orelse Pubsub_State#pubsub_state_dev.affiliation == 'outcast' -> + {error, 'forbidden'}; + Pubsub_State -> + case + pubsub_db_mnesia:retract_item('dev', Pubsub_Node, + Pubsub_State, ItemId, Adhoc_Notify_Retract) + of + {ok, undefined} -> + {result, _Notify = []}; + {ok, Item_Options, Pubsub_States} -> + {result, + _Notify = [_Notify_Retract = { + _Node_Owners = Pubsub_Node#pubsub_node_dev.owners, + _Node_Options = Pubsub_Node#pubsub_node_dev.options, + Item_Options, + Pubsub_States + }] + }; + Error -> + Error + end + end + end. + + +%-- Subscribe_Node --% +-spec(subscribe_node/7 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Plugin :: exmpp_pubsub:plugin(), + Options_Module :: module(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Parameters :: {NodeId :: exmpp_pubsub:nodeId(), + Resource :: undefined | xmpp_jid:resource_jid(), + Subscribe_Options :: [Xmlel::xmlel(),...] + | [Xmlel::xmlel()]}) + -> {result, + Subscription_Subscribed :: exmpp_pubsub:subscription_subscribed(), + Affiliation :: 'member' | 'owner' | 'publisher', + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Node_Options :: pubsub_options:options_node(), + Pubsub_Last_Item :: undefined | mod_pubsub_dev:pubsub_last_item()} + % + | {result, + Subscription_Pending :: exmpp_pubsub:subscription_pending(), + Affiliation :: 'member', + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Node_Options :: pubsub_options:options_node(), + Pubsub_Last_Item :: undefined} + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + % + | {error, 'invalid-options'} + | {error, 'not-acceptable'} + % + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-authorized', 'pending-subscription'} + | {error, 'not-allowed', 'closed-node'} +). + +subscribe_node(_Host, _Pubsub_Host, _Entity, _Plugin, _Options_Module, + _Pubsub_Features, {undefined = _NodeId, _Resource, _Subscribe_Options}) -> + {error, 'item-not-found'}; +%% +subscribe_node(Host, Pubsub_Host, {_, S,_} = Entity, Plugin, Options_Module, + Pubsub_Features, {NodeId, Resource, Subscribe_Options}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, options = Node_Options} -> + case + Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, Entity, + _Entity_Type = case S == Host of + true -> 'local'; + false -> 'remote' + end, + 'subscribe_options', + _Node_Type = get_value(Node_Options, 'node_type', 'leaf'), + Subscribe_Options) + of + {ok, Subscription_Options} -> + Pubsub_State = case + pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) + of + undefined -> + #pubsub_state_dev{ + id = {Entity, NodeIdx}, + nodeidx = NodeIdx + }; + _Pubsub_State -> + _Pubsub_State + end, + case + pubsub_db_mnesia:subscribe_node('dev', Host, Node_Options, + Node_Owners, Pubsub_State, Resource, + Subscription_Options, Pubsub_Features) + of + {result, Subscription, Pubsub_Last_Item} -> + {result, + Subscription, + case Pubsub_State#pubsub_state_dev.affiliation of + 'none' -> 'member'; + Affiliation -> Affiliation + end, + Node_Owners, Node_Options, Pubsub_Last_Item}; + Error -> + Error + end; + Error -> + Error + end + end. + +%-- Unsubscribe_Node --% +-spec(unsubscribe_node/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: exmpp_pubsub:nodeId(), + _Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + _SubId :: exmpp_pubsub:subId()}) + -> {result, + SubId :: exmpp_pubsub:subId()} + | + {result, + SubId :: exmpp_pubsub:subId(), + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Notification_Type :: pubsub_options:notification_type()} + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + | {error, 'unexpected', 'not-subscribed'} + % + | {error, 'unexpected', 'not-subscribed'} + | {error, 'not-acceptable', 'invalid-subid'} + | {error, 'bad-request', 'subid-required'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +unsubscribe_node(_Host, _Pubsub_Host, _Entity, + {undefined = _NodeId, _Resource, _SubId}) -> + {error, 'item-not-found'}; +%% +unsubscribe_node(Host, Pubsub_Host, Entity, {NodeId, _Resource, _SubId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, options = Node_Options} -> + case pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) of + Pubsub_State + when Pubsub_State == undefined + orelse Pubsub_State#pubsub_state_dev.affiliation == 'none' + orelse Pubsub_State#pubsub_state_dev.subscriptions == [] -> + {error, 'unexpected', 'not-subscribed'}; + Pubsub_State + when Pubsub_State#pubsub_state_dev.affiliation == 'publish-only'-> + {error, 'forbidden'}; + Pubsub_State -> + case + pubsub_db_mnesia:unsubscribe_node('dev', Host, + _Node_Access_Model = get_value(Node_Options, 'access_model'), + Pubsub_State, _Resource, _SubId) + of + {ok, Resource, SubId} -> + case get_value(Node_Options, 'notify_sub', false) of + true -> + {result, SubId, Node_Owners, + get_value(Node_Options, 'notification_type', + 'headline')}; + false -> + {result, SubId} + end; + Error -> + Error + end + end + end. + +%-- Set_Configure_Subscription --% +-spec(set_configure_subscription/7 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Plugin :: exmpp_pubsub:plugin(), + Options_Module :: module(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + SubId :: undefined | exmpp_pubsub:subId(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Subscribe_Options :: [Xmlel::xmlel(),...] + | [Xmlel::xmlel()]}) + -> {result, []} + %%% + | {error, 'item-not-found'} + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'forbidden'} + | {error, 'bad-request', 'invalid-options'} + % + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +set_configure_subscription(_Host, _Pubsub_Host, _Entity, _Plugin, _Options_Module, + _Pubsub_Features, {undefined = _NodeId, _SubId, _Resource, _Subscribe_Options}) -> + {error, 'item-not-found'}; +%% +set_configure_subscription(Host, Pubsub_Host, {_,S,_} = Entity, Plugin, Options_Module, + Pubsub_Features, {NodeId, SubId, Resource, Subscribe_Options}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, options = Node_Options} -> + case pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) of + undefined -> + {error, 'unexpected-request', 'not-subscribed'}; + #pubsub_state_dev{affiliation = Affiliation} + when Affiliation == 'publish-only' + orelse Affiliation == 'outcast' -> + {error, 'forbidden'}; + #pubsub_state_dev{subscriptions = []} -> + {error, 'unexpected-request', 'not-subscribed'}; + Pubsub_State -> + case + Options_Module:parse_xmlel_x(Plugin, Pubsub_Features, Entity, + _Entity_Type = case S == Host of + true -> 'local'; + false -> 'remote' + end, + 'subscribe_options', + _Node_Type = get_value(Node_Options, 'node_type', 'leaf'), + Subscribe_Options) + of + {ok, Subscription_Options} -> + case + pubsub_db_mnesia:set_configure_subscription('dev', + Pubsub_State, SubId, Resource, + Subscription_Options) + of + ok -> + {result, []}; + Error -> + Error + end; + _Error -> + {error, 'bad-request', 'invalid-options'} + end + end + end. + +%-- Get_Items --% +-spec(get_items/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pusbub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + _SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: non_neg_integer(), + ItemIds :: undefined} + | {NodeId :: undefined | exmpp_pubsub:nodeId(), + _SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: undefined, + ItemIds :: [] | exmpp_pubsub:itemIds()}) + -> {result, + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Cache :: {Presence_Cache :: undefined, + Groups_Cache :: undefined} + | {Presence_Cache :: true, + Groups_Cache :: undefined} + | {Presence_Cache :: undefined, + Groups_Cache :: [{Node_Owner :: xmpp_jid:usr_bare(), + Groups :: [Group::binary()]}]}, + Node_Owners :: mod_pubsub_dev:node_owners(), + Node_Access_Model :: pubsub_options:access_model(), + Node_Groups :: pubsub_options:rosters_groups_allowed(), + Affiliation :: exmpp_pubsub:affiliation(), + Access :: undefined | 'presence' | 'roster' | 'pending' | 'authorize'} + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-allowed', 'closed-node'} +). + +get_items(_Host, _Pubsub_Host, _Entity, + {undefined = _NodeId, _SubId, _Max_Items, _ItemIds}) -> + {error, 'item-not-found'}; +%% +get_items(Host, Pubsub_Host, Entity, {NodeId, _SubId, Max_Items, ItemIds}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, + options = Node_Options, itemids = Node_ItemIds} -> + {Affiliation, Access} = case lists:member(Entity, Node_Owners) of + true -> + {'owner', undefined}; + false -> + case pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) of + undefined -> + {'none', undefined}; + #pubsub_state_dev{affiliation = _Affiliation, + access = _Access} -> + {_Affiliation, _Access} + end + end, + Node_Access_Model = get_value(Node_Options, 'access_model'), + Node_Groups = get_value(Node_Options, 'roster_groups_allowed', []), + case + pubsub_db_mnesia:get_items('dev', Host, + {NodeIdx, Node_Owners, Node_ItemIds, Node_Access_Model, Node_Groups}, + {Entity, Affiliation, Access}, + {Max_Items, ItemIds, _SubId}) + of + {ok, Pubsub_Items, Cache} -> + {result, Pubsub_Items, Cache, Node_Owners, Node_Access_Model, + Node_Groups, Affiliation, Access}; + Error -> + Error + end + end. + +%-- Get_Entity_Affiliations --% +-spec(get_entity_affiliations/3 :: +( + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> Entity_Affiliations :: [Entity_Affiliation :: { + NodeId :: exmpp_pubsub:nodeId(), + Affiliation :: exmpp_pubsub:affiliation() + }] +). + +get_entity_affiliations(Pubsub_Host, Entity, {NodeId}) -> + pubsub_db_mnesia:get_entity_affiliations('dev', Pubsub_Host, Entity, NodeId). + +%-- Get_Entity_Subscriptions --% +-spec(get_entity_subscriptions/3 :: +( + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> Entity_Subscriptions :: [{ + NodeId :: exmpp_pubsub:nodeId(), + Subscriptions :: _%exmpp_pubsub:subscriptions() + }] +). + +get_entity_subscriptions(Pubsub_Host, Entity, {NodeId}) -> + pubsub_db_mnesia:get_entity_subscriptions('dev', Pubsub_Host, Entity, NodeId). + + +%-- Get_Node_Affiliations --% +-spec(get_node_affiliations/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> {result, Pubsub_States::[Pubsub_State::mod_pubsub_dev:pubsub_state(),...]} + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + +). + +get_node_affiliations(_Host, _Pubsub_Host, _Entity, {undefined = _NodeId}) -> + {error, 'item-not-found'}; +%% +get_node_affiliations(Host, Pubsub_Host, Entity, {NodeId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Owners, options = Options} -> + case lists:member(Entity, Owners) of + true -> + {result, + _Pubsub_States = pubsub_db_mnesia:index_read_state('dev', + NodeIdx)}; + false -> + %% Depending on the node access_model, the returned error + %% can be 'forbidden' or 'item-not-found' to not disclose + %% private nodes + case get_value(Options, 'access_model') of + %% + 'open' -> + {error, 'forbidden'}; + %% + 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, + Entity, Owners) + of + false -> + {error, 'item-not-found'}; + _Node_Owner -> + {error, 'forbidden'} + end; + %% + 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + _Rosters_Groups_Allowed = get_value( + Options, 'roster_groups_allowed')) + of + false -> + {error, 'item-not-found'}; + {_Node_Owner, _Roster_Group_Allowed} -> + {error, 'forbidden'} + end; + %% + 'authorize' -> + {error, 'forbidden'}; + %% + 'whitelist' -> + case + pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) + of + #pubsub_state_dev{affiliation = Affiliation} + when Affiliation == 'member' + orelse Affiliation == 'publish-only' + orelse Affiliation == 'publisher' -> + {error, 'forbidden'}; + _Pubsub_State -> + {error, 'item-not-found'} + end + end + end + end. + +%-- Get_Node_Subscriptions --% +-spec(get_node_subscriptions/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> {result, Pubsub_States::[Pubsub_State::mod_pubsub_dev:pubsub_state(),...]} + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} + +). + +get_node_subscriptions(_Host, _Pubsub_Host, _Entity, {undefined = _NodeId}) -> + {error, 'item-not-found'}; +%% +get_node_subscriptions(Host, Pubsub_Host, Entity, {NodeId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Owners, options = Options} -> + case lists:member(Entity, Owners) of + true -> + {result, + _Pubsub_States = pubsub_db_mnesia:index_read_state('dev', + NodeIdx)}; + false -> + %% Depending on the node access_model, the returned error + %% can be 'forbidden' or 'item-not-found' to not disclose + %% private nodes + case get_value(Options, 'access_model') of + %% + 'open' -> + {error, 'forbidden'}; + %% + 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, + Entity, Owners) + of + false -> + {error, 'item-not-found'}; + _Node_Owner -> + {error, 'forbidden'} + end; + %% + 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + _Rosters_Groups_Allowed = get_value(Options, + 'roster_groups_allowed')) + of + false -> + {error, 'item-not-found'}; + {_Node_Owner, _Roster_Group_Allowed} -> + {error, 'forbidden'} + end; + %% + 'authorize' -> + {error, 'forbidden'}; + %% + 'whitelist' -> + case + pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) + of + #pubsub_state_dev{affiliation = Affiliation} + when Affiliation == 'member' + orelse Affiliation == 'publish-only' + orelse Affiliation == 'publisher' -> + {error, 'forbidden'}; + _Pubsub_State -> + {error, 'item-not-found'} + end + end + end + end. + +%% Get_Configure_Subscription_Default +-spec(get_configure_subscription_default/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + NodeId :: exmpp_pubsub:nodeId()) + -> ok + %%% + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +get_configure_subscription_default(Host, Pubsub_Host, Entity, NodeId) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, options = Node_Options} -> + case lists:member(Entity, Node_Owners) of + true -> + ok; + false -> + Node_Access_Model = get_value(Node_Options, 'access_model'), + case pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) of + undefined + when Node_Access_Model == 'open' -> + ok; + undefined + when Node_Access_Model == 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, + Entity, Node_Owners) + of + false -> {error, 'forbidden'}; + _Node_Owner -> ok + end; + undefined + when Node_Access_Model == 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + _Rosters_Groups_Allowed = + get_value(Node_Options, + 'roster_groups_allowed', [])) + of + false -> {error, 'forbidden'}; + _ -> ok + end; + undefined -> + {error, 'forbidden'}; + #pubsub_state_dev{affiliation = Affiliation, + access = Access} + when Affiliation == 'publisher' + orelse Affiliation == 'member' + orelse (Affiliation == 'none' + andalso + Node_Access_Model == 'open') + orelse Access == 'presence' + orelse Access == 'roster' -> + ok; + #pubsub_state_dev{affiliation = 'none'} + when Node_Access_Model == 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, + Entity, Node_Owners) + of + false -> + {error, 'forbidden'}; + _Node_Owner -> + ok + end; + #pubsub_state_dev{affiliation = 'none'} + when Node_Access_Model == 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + _Rosters_Groups_Allowed = + get_value(Node_Options, + 'roster_groups_allowed', [])) + of + false -> + {error, 'forbidden'}; + _ -> + ok + end; + _ -> + {error, 'forbidden'} + + end + end + end. + +%-- Get_Configure_Subscription --% +-spec(get_configure_subscription/6 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Plugin :: exmpp_pubsub:plugin(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId(), + SubId :: undefined | exmpp_pubsub:subId(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()} + }) + -> {result, + Subscription :: exmpp_pubsub:subscription_subscribed() + | exmpp_pubsub:subscription_unconfigured()} + %%% + | {error, 'item-not-found'} + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'forbidden'} + % + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +get_configure_subscription(_Host, _Pubsub_Host, _Entity, _Plugin, + _Pubsub_Features, {undefined = _NodeId, _SubId, _Resource}) -> + {error, 'item-not-found'}; +%% +get_configure_subscription(_Host, Pubsub_Host, Entity, Plugin, + Pubsub_Features, {NodeId, SubId, Resource}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, options = Node_Options} -> + case pubsub_db_mnesia:read_state('dev', Entity, NodeIdx) of + undefined -> + {error, 'unexpected-request', 'not-subscribed'}; + #pubsub_state_dev{affiliation = Affiliation} + when Affiliation == 'publish-only' + orelse Affiliation == 'outcast' -> + {error, 'forbidden'}; + #pubsub_state_dev{subscriptions = []} -> + {error, 'unexpected-request', 'not-subscribed'}; + #pubsub_state_dev{subscriptions = Subscriptions} -> + case filter_subscription(Subscriptions, SubId, Resource) of + {ok, Subscription} -> {result, Subscription}; + Error -> Error + end + end + end. + +-spec(filter_subscription/3 :: +( + Subscriptions :: exmpp_pubsub:subscriptions(), + SubId :: undefined | exmpp_pubsub:subId(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}) + -> {ok, + Subscription :: exmpp_pubsub:subscription_subscribed() + | exmpp_pubsub:subscription_unconfigured()} + %%% + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +filter_subscription(Subscriptions, undefined = _SubId, Resource) -> + case lists:keytake(Resource, 3, Subscriptions) of + {value, {Subscription_State, SubId, Resource, Subscription_Options}, + _Subscriptions} -> + case lists:keymember(Resource, 3, _Subscriptions) of + false + when Subscription_State == 'pending' -> + {error, 'forbidden'}; + false -> + {ok, + {Subscription_State, SubId, Resource, Subscription_Options}}; + true -> + {error, 'bad-request', 'subid-required'} + end; + false -> + {error, 'unexpected-request', 'not-subscribed'} + end; +%% +filter_subscription(Subscriptions, SubId, Resource) -> + case lists:keyfind(SubId, 2, Subscriptions) of + %% + {'pending', _, Resource, _} -> + {error, 'forbidden'}; + %% + {Subscription_State, SubId, Resource, Subscription_Options} -> + {ok, {Subscription_State, SubId, Resource, Subscription_Options}}; + %% + {_, SubId, _, _} -> + {error, 'unexpected-request', 'not-subscribed'}; + false -> + case lists:keyfind(Resource, 3, Subscriptions) of + {'pending', _, Resource, _} -> + {error, 'forbidden'}; + {_, _, Resource, _} -> + {error, 'not-acceptable', 'invalid-subid'}; + _ -> + {error, 'unexpected-request', 'not-subscribed'} + end + end. + +%-- Get_Configure_Node --% +-spec(get_configure_node/4 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + Parameters :: {NodeId :: undefined | exmpp_pubsub:nodeId()}) + -> {result, Node_Options :: pubsub_options:options_node()} + %%% + | {error, 'bad-request', 'nodeid-required'} + | {error, 'item-not-found'} + | {error, 'forbidden'} +). + +get_configure_node(_Host, _Pubsub_Host, _Entity, {undefined = _NodeId}) -> + {error, 'bad-request', 'nodeid-required'}; +%% +get_configure_node(_Host, Pubsub_Host, Entity, {NodeId}) -> + case pubsub_db_mnesia:read_node('dev', Pubsub_Host, NodeId) of + undefined -> + {error, 'item-not-found'}; + #pubsub_node_dev{owners = Node_Owners, options = Node_Options} -> + case lists:member(Entity, Node_Owners) of + true -> {result, Node_Options}; + false -> {error, 'forbidden'} + end + end. diff --git a/src/mod_pubsub_ng/pubsub_db_mnesia.erl b/src/mod_pubsub_ng/pubsub_db_mnesia.erl new file mode 100644 index 000000000..ac41c36a0 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_db_mnesia.erl @@ -0,0 +1,2129 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http:%%www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_db_mnesia). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1 +]). + + +-export_type([ + suffix/0, + table/0 +]). + +-type(suffix() :: undefined | atom()). +-type(table() :: atom()). + + +%% +-spec(init/1 :: +( + Suffix :: undefined | atom()) + -> 'ok' +). + +init(Suffix) -> + create_table_pubsub_node(Suffix), + create_table_pubsub_state(Suffix), + create_table_pubsub_item(Suffix), + create_table_pubsub_last_item(Suffix), + create_table_pubsub_subscription_pending(Suffix), + pubsub_index_dev:init(Suffix), + pubsub_groups:create_table_pubsub_groups(Suffix). + +%% +-spec(table/2 :: +( + Base_Table :: atom(), + Suffix :: undefined | atom()) + -> Table :: atom() +). + +table(Base_Table, undefined = _Suffix) -> + _Table = Base_Table; +table(Base_Table, Suffix) -> + _Table = list_to_atom(atom_to_list(Base_Table) ++ "_" ++ atom_to_list(Suffix)). + + +%% +create_table_pubsub_node(Suffix) -> + mnesia:create_table(table('pubsub_node', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_node_dev}, + {attributes, record_info(fields, pubsub_node_dev)}]), + mnesia:add_table_index(pubsub_node_dev, idx). + +create_table_pubsub_state(Suffix) -> + mnesia:create_table(table('pubsub_state', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_state_dev}, + {attributes, record_info(fields, pubsub_state_dev)}]), + mnesia:add_table_index(pubsub_state_dev, nodeidx). + +create_table_pubsub_item(Suffix) -> + mnesia:create_table(table('pubsub_item', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_item_dev}, + {attributes, record_info(fields, pubsub_item_dev)}]), + mnesia:add_table_index(pubsub_item_dev, nodeidx). + +create_table_pubsub_last_item(Suffix) -> + mnesia:create_table(table('pubsub_last_item', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_last_item_dev}, + {attributes, record_info(fields, pubsub_last_item_dev)}]). + +create_table_pubsub_subscription_pending(Suffix) -> + mnesia:create_table(table('pubsub_subscription_pending', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_subscription_pending}, + {attributes, record_info(fields, pubsub_subscription_pending)}]), + mnesia:add_table_index(pubsub_subscription_pending, nodeidx). + +%% +transaction(Module, Function, Arguments) -> + case mnesia:transaction(fun() -> apply(Module, Function, Arguments) end) of + {atomic, Result} -> + Result; + {aborted, Reason} -> + ?INFO_MSG("DB TRANSACTION ERROR ~p", [Reason]), + {error, 'internal-server-error'} + end. + +%% +-spec(read_item/3 :: +( + Suffix :: atom(), + NodeIdx :: exmpp_pubsub:nodeIdx(), + ItemId :: exmpp_pubsub:itemId()) + -> Pubsub_Item :: undefined | mod_pubsub_dev:pubsub_item() +). + +read_item(Suffix, NodeIdx, ItemId) -> + _Pubsub_Item = case + mnesia:read({_Table = table('pubsub_item', Suffix), {ItemId, NodeIdx}}) + of + [Pubsub_Item] -> Pubsub_Item; + [] -> undefined + end. + +%% +-spec(read_node/3 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + NodeId :: exmpp_pubsub:nodeId()) + -> Pubsub_Node :: undefined | mod_pubsub_dev:pubsub_node() +). + +read_node(Suffix, Pubsub_Host, NodeId) -> + _Pubsub_Node = case + mnesia:read({_Table = table('pubsub_node', Suffix), + {Pubsub_Host, NodeId}}) + of + [Pubsub_Node] -> Pubsub_Node; + [] -> undefined + end. + +%% +-spec(read_state/3 :: +( + Suffix :: atom(), + Entity :: xmpp_jid:usr_entity(), + NodeIdx :: exmpp_pubsub:nodeIdx()) + -> Pubsub_State :: undefined | mod_pubsub_dev:pubsub_state() +). + +read_state(Suffix, {U,S,_R} = _Entity, NodeIdx) -> + _Pubsub_State = case + mnesia:read({_Table = table('pubsub_state', Suffix), + {{U,S,undefined}, NodeIdx}}) + of + [Pubsub_State] -> Pubsub_State; + [] -> undefined + end. + +%% +-spec(index_read_node/2 :: +( + Suffix :: atom(), + NodeIdx :: exmpp_pubsub:nodeIdx()) + -> Pubsub_Nodes::[Pubsub_Node::mod_pubsub_dev:pubsub_node()] +). + +index_read_node(Suffix, NodeIdx) -> + _Pubsub_Nodes = mnesia:index_read(_Table = table('pubsub_node', Suffix), + NodeIdx, idx). + +%% +-spec(index_read_state/2 :: +( + Suffix :: atom(), + NodeIdx :: exmpp_pubsub:nodeIdx()) + -> Pubsub_States::[Pubsub_State::mod_pubsub_dev:pubsub_state()] +). + +index_read_state(Suffix, NodeIdx) -> + _Pubsub_States = mnesia:index_read(_Table = table('pubsub_state', Suffix), + NodeIdx, nodeidx). + + +%-- Create_Node --% +-spec(create_node/7 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + NodeId :: exmpp_pubsub:nodeId(), + Level :: exmpp_pubsub:level(), + Node_Options :: pubsub_options:options_node(), + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> Subscription :: undefined | exmpp_pubsub:subscription_subscribed() +). + +create_node(Suffix, Pubsub_Host, {U,S,R} = _Entity, NodeId, Level, Node_Options, + Pubsub_Features) -> + %% Create Pubsub_Node + mnesia:write(table('pubsub_node', Suffix), + #pubsub_node_dev{ + id = {Pubsub_Host, NodeId}, + idx = NodeIdx = pubsub_index_dev:new(Suffix, 'node'), + level = Level, + creation = {_DateTime = now(), _Creator = {U,S,R}}, + owners = [{U,S,undefined}], + options = Node_Options + }, + write), + _Subscription = case + lists:member(<<"auto-subscribe">>, Pubsub_Features) + andalso + get_value(Node_Options, 'subscribe', false) + of + true -> + mnesia:write(table('pubsub_state', Suffix), + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + affiliation = 'owner', + subscriptions = [Subscription = { + _Subscription_State = 'subscribed', + _SubId = exmpp_pubsub:subId(), + _Resource = undefined, + _Subscription_Options = [] + }] + }, + write), + Subscription; + false -> + mnesia:write(table('pubsub_state', Suffix), + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + affiliation = 'owner' + }, + write), + undefined + end. + +%-- Purge_Node --% +%% TODO : purge Pubsub_Purge_Offline +%% TODO : purge Pubsub_Expiry_Item +-spec(purge_node/2 :: +( + Suffix :: atom(), + Pubsub_Node :: mod_pubsub_dev:pubsub_node()) + -> Recipients :: undefined | [Entity::xmpp_jid:usr_bare()] +). + +purge_node(Suffix, Pubsub_Node) -> + %% Update Pubsub_Node + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{itemids = []}, write), + %% Delete Pubsub_Last_Item + mnesia:delete(table('pubsub_last_item', Suffix), + NodeIdx = Pubsub_Node#pubsub_node_dev.idx, write), + %% + Table_Pubsub_State = table('pubsub_state', Suffix), + Table_Pubsub_Item = table('pubsub_item', Suffix), + _Recipients = case + get_value(Pubsub_Node#pubsub_node_dev.options, 'notify_retract', false) + of + true -> + %% Delete Pubsub_Items + lists:foreach(fun + (Pubsub_Item) -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, write) + % + end, _Pubsub_Items = mnesia:index_read(Table_Pubsub_Item, NodeIdx, + nodeidx)), + %% Update Pubsub_States + _Subscribers = lists:foldl(fun + %% + (Pubsub_State, Subscribers) + when (Pubsub_State#pubsub_state_dev.affiliation == 'owner' + orelse + Pubsub_State#pubsub_state_dev.affiliation == 'outcast' + orelse + Pubsub_State#pubsub_state_dev.affiliation == 'publish-only') + andalso Pubsub_State#pubsub_state_dev.itemids =/= [] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{itemids = []}, + write), + Subscribers; + %% + (#pubsub_state_dev{id = {Entity, _NodeIdx}} = Pubsub_State, + Subscribers) + when Pubsub_State#pubsub_state_dev.affiliation == 'publisher' + andalso Pubsub_State#pubsub_state_dev.itemids =/= [] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{itemids = []}, write), + case Pubsub_State#pubsub_state_dev.subscriptions of + [] = _Subscriptions -> + Subscribers; + _Subscriptions -> + [_Subscriber = Entity | Subscribers] + end; + %% + (#pubsub_state_dev{id = {Entity, _NodeIdx}} = Pubsub_State, + Subscribers) + when Pubsub_State#pubsub_state_dev.affiliation == 'publisher' + andalso Pubsub_State#pubsub_state_dev.subscriptions =/= [] -> + [_Subscriber = Entity | Subscribers]; + %% + (#pubsub_state_dev{id = {Entity, _NodeIdx}} = Pubsub_State, + Subscribers) + when Pubsub_State#pubsub_state_dev.affiliation == 'member' + andalso Pubsub_State#pubsub_state_dev.itemids =/= [] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{itemids = []}, write), + case + (Pubsub_State#pubsub_state_dev.subscriptions == [] + orelse + has_subscriptions( + Pubsub_State#pubsub_state_dev.subscriptions)) + of + true -> [_Subscriber = Entity | Subscribers]; + false -> Subscribers + end; + %% + (#pubsub_state_dev{id = {Entity, _NodeIdx}} = Pubsub_State, + Subscribers) + when Pubsub_State#pubsub_state_dev.affiliation == 'member'-> + case + (Pubsub_State#pubsub_state_dev.subscriptions == [] + orelse + has_subscriptions( + Pubsub_State#pubsub_state_dev.subscriptions)) + of + true -> [_Subscriber = Entity | Subscribers]; + false -> Subscribers + end; + %% + (_Pubsub_State, Subscribers) -> + Subscribers + % + end, + [], + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, + NodeIdx, nodeidx)); + false -> + %% Update Pubsub_States + lists:foreach(fun + (_Items_Owner = Entity) -> + case + mnesia:read(Table_Pubsub_State, {Entity, NodeIdx}, + write) + of + [Pubsub_State] + when Pubsub_State#pubsub_state_dev.itemids =/= [] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{itemids = []}, + write); + _Pubsub_State -> + ok + end + end, + _Items_Owners = lists:foldl(fun + %% Delete Pubsub_Items + (Pubsub_Item, Items_Owners) -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, + write), + lists:usort(lists:append(Items_Owners, + Pubsub_Item#pubsub_item_dev.owners)) + end, [], + _Pubsub_Items = mnesia:index_read(Table_Pubsub_Item, NodeIdx, + nodeidx)) + ), + _Subscribers = undefined + end. + +%-- Delete_Node --% +-spec(delete_node/2 :: +( + Suffix :: atom(), + Pubsub_Node :: mod_pubsub_dev:pubsub_node()) + -> Recipients :: undefined | [Entity::xmpp_jid:usr_bare()] +). + +delete_node(Suffix, Pubsub_Node) -> + %% Delete Pubsub_Node + mnesia:delete_object(table('pubsub_node', Suffix), Pubsub_Node, write), + pubsub_index_dev:free(Suffix, 'node', + NodeIdx = Pubsub_Node#pubsub_node_dev.idx), + %% Delete Pubsub_Last_Item + mnesia:delete(table('pubsub_last_item', Suffix), NodeIdx, write), + %% Delete Pubsub_Items + Table_Pubsub_Item = table('pubsub_item', Suffix), + lists:foreach(fun + (Pubsub_Item) -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, write) + % + end, + _Pubsub_Items = mnesia:index_read(Table_Pubsub_Item, NodeIdx, + nodeidx)), + %% Delete Pubsub_States + Table_Pubsub_State = table('pubsub_state', Suffix), + _Recipients = case + get_value(Pubsub_Node#pubsub_node_dev.options, 'notify_delete', false) + of + true -> + _Subscribers = lists:foldl(fun + (#pubsub_state_dev{id = {Entity, _NodeIdx}} = Pubsub_State, + Subscribers) -> + mnesia:delete_object(Table_Pubsub_State, Pubsub_State, + write), + case + {Pubsub_State#pubsub_state_dev.affiliation, + Pubsub_State#pubsub_state_dev.subscriptions} + of + %% + {'publisher' = _Affiliation, Subscriptions} + when Subscriptions =/= [] -> + [_Subscriber = Entity | Subscribers]; + %% + {'member' = _Affiliation, _Subscriptions} -> + case + (_Subscriptions == [] + orelse + has_subscriptions(_Subscriptions)) + of + true -> + [_Subscriber = Entity | Subscribers]; + false -> + Subscribers + end; + %% + {_Affiliation, _Subscriptions} -> + Subscribers + end + % + end, [], + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, + nodeidx)); + false -> + lists:foreach(fun + (Pubsub_State) -> + mnesia:delete_object(Table_Pubsub_State, Pubsub_State, + write) + end, + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, + nodeidx)), + _Subscribers = undefined + end. + +%-- Get_Entity_Subscriptions --% +-spec(get_entity_subscriptions/4 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + NodeId :: undefined | exmpp_pubsub:nodeId()) + -> Entity_Subscriptions :: [{ + NodeId :: exmpp_pubsub:nodeId(), + Subscriptions :: exmpp_pubsub:subscriptions() + }] +). + +get_entity_subscriptions(Suffix, Pubsub_Host, {U,S,_R} = _Entity, + undefined = _NodeId) -> + Table_Pubsub_Node = table('pubsub_node', Suffix), + _Entity_Subscriptions = lists:foldl(fun + ([NodeIdx, Subscriptions], Entity_Subscriptions) -> + case mnesia:index_read(Table_Pubsub_Node, NodeIdx, idx) of + %% IMPORTANT : Pubsub_Host must be used in the matching, + %% else it'd return all NodeIds subscriptions (in case of VHosts) + %% and not only the ones from to the current Pubsub_Host + [#pubsub_node_dev{id = {Pubsub_Host, NodeId}}] -> + [{NodeId, Subscriptions} | Entity_Subscriptions]; + _Pubsub_Node -> + Entity_Subscriptions + end + end, [], + mnesia:select(table('pubsub_state', Suffix), + [{#pubsub_state_dev{ + id = {{U,S,undefined}, '$1'}, + subscriptions = '$2', + _ = '_'}, + [{'=/=', '$2', []}], + ['$$']}])); +%% +get_entity_subscriptions(Suffix, Pubsub_Host, {U,S,_R} = _Entity, NodeId) -> + _Entity_Subscriptions = case + mnesia:read(table('pubsub_node', Suffix), {Pubsub_Host, NodeId}, read) + of + [] -> + []; + [#pubsub_node_dev{idx = NodeIdx}] -> + case + mnesia:read(table('pubsub_state', Suffix), + {{U,S,undefined}, NodeIdx}, read) + of + [#pubsub_state_dev{subscriptions = Subscriptions}] + when Subscriptions =/= [] -> + [{NodeId, Subscriptions}]; + _Pubsub_State -> + [] + end + end. + +%-- Get_Entity_Affiliations --% +-spec(get_entity_affiliations/4 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + NodeId :: undefined | exmpp_pubsub:nodeId()) + -> Entity_Affiliations :: [Entity_Affiliation :: { + NodeId :: exmpp_pubsub:nodeId(), + Affiliation :: exmpp_pubsub:affiliation() + }] +). + +get_entity_affiliations(Suffix, Pubsub_Host, {U,S,_R} = _Entity, + undefined = _NodeId) -> + Table_Pubsub_Node = table('pubsub_node', Suffix), + _Entity_Affiliations = lists:foldl(fun + ([NodeIdx, Affiliation], Entity_Affiliations) -> + case mnesia:index_read(Table_Pubsub_Node, NodeIdx, idx) of + %% IMPORTANT : Pubsub_Host must be used in the matching, + %% else it'd return all NodeIds affiliations (in case of VHosts) + %% and not only the ones from to the current Pubsub_Host + [#pubsub_node_dev{id = {Pubsub_Host, NodeId}}] -> + [{NodeId, Affiliation} | Entity_Affiliations]; + _Pubsub_Node -> + Entity_Affiliations + end + end, [], + mnesia:select(table('pubsub_state', Suffix), + [{#pubsub_state_dev{ + id = {{U,S,undefined}, '$1'}, + affiliation = '$2', + _ = '_'}, + [], + ['$$']}], + read) + ); +%% +get_entity_affiliations(Suffix, Pubsub_Host, {U,S,_R} = _Entity, NodeId) -> + _Entity_Affiliations = case + mnesia:read(table('pubsub_node', Suffix), {Pubsub_Host, NodeId}, read) + of + [] -> + []; + [#pubsub_node_dev{idx = NodeIdx}] -> + case + mnesia:read(table('pubsub_state', Suffix), + {{U,S,undefined}, NodeIdx}) + of + [#pubsub_state_dev{affiliation = Affiliation}] -> + [{NodeId, Affiliation}]; + _Pubsub_State -> + [] + end + end. + +%-- Publish_Item --% +-spec(publish_item1/9 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + NodeId :: undefined | exmpp_pubsub:nodeId(), + Level :: exmpp_pubsub:level(), + ItemId :: undefined | exmpp_pubsub:itemId(), + Payload :: [Xmlel::#xmlel{}] | exmpp_pubsub:payload(), + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}, + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> {ItemId :: exmpp_pubsub:itemId(), + Pubsub_State@Owner :: mod_pubsub_dev:pubsub_state_owner()} +). + +publish_item1(Suffix, Pubsub_Host, Entity, NodeId, Level, ItemId, + [Payload], Options, Pubsub_Features) -> + publish_item1(Suffix, Pubsub_Host, Entity, NodeId, Level, ItemId, Payload, + Options, Pubsub_Features); + +publish_item1(Suffix, Pubsub_Host, Entity, NodeId, Level, undefined = _ItemId, + Payload, Options, Pubsub_Features) -> + publish_item1(Suffix, Pubsub_Host, Entity, NodeId, Level, + exmpp_pubsub:itemId(), Payload, Options, Pubsub_Features); +%% +publish_item1(Suffix, Pubsub_Host, {U,S,R} = _Entity, NodeId, Level, ItemId, + Payload, {Node_Options, Item_Options} = Options, Pubsub_Features) -> + NodeIdx = pubsub_index_dev:new(Suffix, 'node'), + DateTime = now(), + Write_Item = get_value(Options, 'persist_items', false) andalso + (get_value(Node_Options, 'max_items', 0) > 0), + %% Create Pubsub_Node + mnesia:write(table('pubsub_node', Suffix), + #pubsub_node_dev{ + id = {Pubsub_Host, NodeId}, + idx = NodeIdx, + creation = {DateTime, _Creator = {U,S,R}}, + level = Level, + owners = [{U,S,undefined}], + itemids = case Write_Item of + true -> [ItemId]; + false -> [] + end, + options = Node_Options + }, + write), + %% Create Pubsub_Item + case Write_Item of + false -> + ok; + true -> + mnesia:write(table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {ItemId, NodeIdx}, + nodeidx = NodeIdx, + owners = [{U,S,undefined}], + creation = {DateTime, _Creator = {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write), + %%TODO : Register pubsub#item_expire + case get_value(Options, 'item_expire', undefined) of + undefined -> + ko; + Delay -> + ok + end + end, + %% Create Pubsub_Last_Item + case get_value(Options, 'send_last_published_item', 'never') of + 'never' -> + ok; + _Send_Last_Published_Item -> + mnesia:write(table('pubsub_last_item', Suffix), + #pubsub_last_item_dev{ + nodeidx = NodeIdx, + id = ItemId, + owners = [{U,S,undefined}], + creation = {DateTime, _Creator = {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write) + end, + %% Create Pubsub_State + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State = #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + itemids = case Write_Item of + true -> [ItemId]; + false -> [] + end, + affiliation = 'owner', + subscriptions = case + lists:member(<<"auto-subscribe">>, Pubsub_Features) + andalso + get_value(Node_Options, 'subscribe', false) + of + true -> + [{_Subscription_State = 'subscribed', + _SubId = exmpp_pubsub:subId(), + _Resource = undefined, + _Subscription_Options = []}]; + false -> + [] + end + }, + write), + {ItemId, Pubsub_State}. + +%% +-spec(publish_item2/9 :: +( + Suffix :: atom(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_entity(), + Pubsub_State :: undefined | mod_pubsub_dev:pubsub_state(), + Pubsub_Node :: mod_pubsub_dev:pubsub_node(), + _ :: undefined | exmpp_pubsub:itemId() | mod_pubsub_dev:pubsub_item(), + Payload :: [Xmlel::#xmlel{}] | exmpp_pubsub:payload(), + Options :: {Node_Options :: pubsub_options:options_node(), + Item_Options :: [] | pubsub_options:options_item()}, + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> {ItemId :: exmpp_pubsub:itemId(), + Retracted_Items :: [Retracted_Item::{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_node() + }], + Pubsub_States :: [Pubsub_State::mod_pubsub_dev:pubsub_state(),...]} +). + + +publish_item2(Suffix, Pubsub_Host, Entity, Pubsub_State, Pubsub_Node, Pubsub_Item, + [Payload], Item_Options, Pubsub_Features) -> + publish_item2(Suffix, Pubsub_Host, Entity, Pubsub_State, Pubsub_Node, + Pubsub_Item, Payload, Item_Options, Pubsub_Features); +%% +publish_item2(Suffix, Pubsub_Host, {U,S,R} = _Entity, Pubsub_State, Pubsub_Node, + #pubsub_item_dev{id = {ItemId, NodeIdx}} = Pubsub_Item, Payload, Item_Options, + Pubsub_Features) -> + Node_Options = Pubsub_Node#pubsub_node_dev.options, + DateTime = now(), + Table_Pubsub_State = table('pubsub_state', Suffix), + _Retracted_Items = case + {get_value(Node_Options, 'persist_items', false), + get_value(Node_Options, 'max_items', 0)} + of + %% + {Persist_Items, Max_Items} + when Persist_Items == false + orelse Max_Items == 0 -> + case Pubsub_State of + undefined -> + mnesia:write(table('pubsub_state', Suffix), + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx + }, + write), + []; + _Pubsub_State -> + [] + end; + %% + {true, undefined} -> + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = lists:reverse([ItemId + | lists:reverse(lists:delete(ItemId, + Pubsub_Node#pubsub_node_dev.itemids))]) + }, + write), + %% + mnesia:write(Table_Pubsub_State, + case Pubsub_State of + undefined -> + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + itemids = [ItemId] + }; + _ -> + Pubsub_State#pubsub_state_dev{ + itemids = [ItemId | lists:delete(ItemId, + Pubsub_State#pubsub_state_dev.itemids)] + } + end, + write), + mnesia:write(table('pubsub_item', Suffix), + Pubsub_Item#pubsub_item_dev{ + modification = {DateTime, {U,S,R}}, + payload = Payload, + options = Item_Options, + owners = [{U,S,undefined} | + lists:delete({U,S,undefined}, + Pubsub_Item#pubsub_item_dev.owners)] + }, + write), + []; + {true, Max_Items} -> + {New_ItemIds, Old_ItemIds} = max_items(Max_Items, + lists:reverse([ItemId + | lists:reverse(lists:delete(ItemId, + Pubsub_Node#pubsub_node_dev.itemids))])), + {Retracted_Items, Publisher_Retracted_ItemIds} = + delete_old_items(Suffix, + _Publisher = {U,S,undefined}, + NodeIdx, + _Notify_Retract = get_value(Node_Options, + 'notify_retract', false), + Old_ItemIds, {[], {[], []}}), + %% + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = New_ItemIds + }, + write), + mnesia:write(table('pubsub_item', Suffix), + Pubsub_Item#pubsub_item_dev{ + modification = {DateTime, {U,S,R}}, + payload = Payload, + options = Item_Options, + owners = [{U,S,undefined} | + lists:delete({U,S,undefined}, + Pubsub_Item#pubsub_item_dev.owners)] + }, + write), + mnesia:write(Table_Pubsub_State, + case Pubsub_State of + undefined -> + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + itemids = [ItemId] + }; + _ -> + Pubsub_State#pubsub_state_dev{ + itemids = [ItemId + | lists:foldl(fun + (Publisher_Retracted_ItemId, + Publisher_ItemIds) -> + lists:delete( + Publisher_Retracted_ItemId, + Publisher_ItemIds) + end, + lists:delete(ItemId, + Pubsub_State#pubsub_state_dev.itemids), + Publisher_Retracted_ItemIds)] + } + end, + write), + Retracted_Items + + end, + case get_value(Node_Options, 'send_last_published_item', 'never') of + 'never' -> + ok; + _Send_Last_Published_Item -> + mnesia:write(table('pubsub_last_item', Suffix), + #pubsub_last_item_dev{ + nodeidx = NodeIdx, + id = ItemId, + owners = [{U,S,undefined}], + creation = {DateTime, _Creator = {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write) + end, + {ItemId, _Retracted_Items, + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, nodeidx)}; +%% +publish_item2(Suffix, Pubsub_Host, {U,S,R} = Entity, Pubsub_State, Pubsub_Node, + _ItemId, Payload, Item_Options, Pubsub_Features) -> + NodeIdx = Pubsub_Node#pubsub_node_dev.idx, + Node_Options = Pubsub_Node#pubsub_node_dev.options, + ItemId = case _ItemId of + undefined -> exmpp_pubsub:itemId(); + _ -> _ItemId + end, + DateTime = now(), + Table_Pubsub_State = table('pubsub_state', Suffix), + _Retracted_Items = case + {get_value(Node_Options, 'persist_items', false), + get_value(Node_Options, 'max_items', 0)} + of + %% + {Persist_Items, Max_Items} + when Persist_Items == false + orelse Max_Items == 0 -> + case Pubsub_State of + undefined -> + mnesia:write(Table_Pubsub_State, + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx + }, + write), + []; + _Pubsub_State -> + [] + end; + %% + {true, undefined} -> + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = lists:reverse([ItemId + | lists:reverse(ItemId, + Pubsub_Node#pubsub_node_dev.itemids)]) + }, + write), + %% + mnesia:write(Table_Pubsub_State, + case Pubsub_State of + undefined -> + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + itemids = [ItemId] + }; + _ -> + Pubsub_State#pubsub_state_dev{ + itemids = [ItemId | + Pubsub_State#pubsub_state_dev.itemids] + } + end, + write), + mnesia:write(table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {ItemId, NodeIdx}, + nodeidx = NodeIdx, + owners = [{U,S,undefined}], + creation = {DateTime, {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write), + []; + {true, Max_Items} -> + {New_ItemIds, Old_ItemIds} = max_items(Max_Items, + lists:reverse([ItemId + | lists:reverse(Pubsub_Node#pubsub_node_dev.itemids)])), + {Retracted_Items, Publisher_Retracted_ItemIds} = + delete_old_items(Suffix, + _Publisher = {U,S,undefined}, + NodeIdx, + _Notify_Retract = get_value(Node_Options, + 'notify_retract', false), + Old_ItemIds, {[], {[], []}}), + %% + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = New_ItemIds + }, + write), + mnesia:write(table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {ItemId, NodeIdx}, + nodeidx = NodeIdx, + owners = [{U,S,undefined}], + creation = {DateTime, {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write), + %% + mnesia:write(Table_Pubsub_State, + case Pubsub_State of + undefined -> + #pubsub_state_dev{ + id = {{U,S,undefined}, NodeIdx}, + nodeidx = NodeIdx, + itemids = [ItemId] + }; + _ -> + Pubsub_State#pubsub_state_dev{ + itemids = [ItemId + | lists:foldl(fun + (Publisher_Retracted_ItemId, + Publisher_ItemIds) -> + lists:delete( + Publisher_Retracted_ItemId, + Publisher_ItemIds) + end, + Pubsub_State#pubsub_state_dev.itemids, + Publisher_Retracted_ItemIds)] + } + end, + write), + Retracted_Items + + end, + case get_value(Node_Options, 'send_last_published_item', 'never') of + 'never' -> + ok; + _Send_Last_Published_Item -> + mnesia:write(table('pubsub_last_item', Suffix), + #pubsub_last_item_dev{ + nodeidx = NodeIdx, + id = ItemId, + owners = [{U,S,undefined}], + creation = {DateTime, _Creator = {U,S,R}}, + payload = Payload, + options = Item_Options + }, + write) + end, + {ItemId, _Retracted_Items, + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, nodeidx)}. + +%% +-spec(max_items/2 :: +( + Max_Items :: undefined | non_neg_integer(), + New_ItemIds :: [ItemId::exmpp_pubsub:itemId()]) + -> ItemIds :: {New_ItemIds::[ItemId::exmpp_pubsub:itemId()], + Old_ItemIds::[ItemId::exmpp_pubsub:itemId()]} +). + +max_items(undefined = _Max_Items, New_ItemIds) -> + _ItemIds = {New_ItemIds, _Old_ItemIds = []}; +max_items(0 = _Max_Items, New_ItemIds) -> + _ItemIds = {_New_ItemIds = [], _Old_ItemIds = New_ItemIds}; +max_items(Max_Items, New_ItemIds) -> + _ItemIds = case Max_Items >= (Length_Items = length(New_ItemIds)) of + true -> + {New_ItemIds, _Old_ItemIds = []}; + false -> + diff_items((Length_Items - Max_Items), _Count = 0, {New_ItemIds, []}) + end. + +%% +-spec(diff_items/3 :: +( + Diff_Items :: pos_integer(), + Count :: non_neg_integer(), + ItemIds :: {New_ItemIds::[ItemId::exmpp_pubsub:itemId()], + Old_ItemIds::[ItemId::exmpp_pubsub:itemId()]}) + -> ItemIds :: {New_ItemIds::[ItemId::exmpp_pubsub:itemId()], + Old_ItemIds::[ItemId::exmpp_pubsub:itemId()]} +). + +diff_items(Diff_Items, _Count = Diff_Items, ItemIds) -> + ItemIds; +diff_items(Diff_Items, Count, {[ItemId | New_ItemIds], Old_ItemIds}) -> + diff_items(Diff_Items, Count + 1, {New_ItemIds, [ItemId | Old_ItemIds]}). + + +%% +-spec(delete_old_items/6 :: +( + Suffix :: atom(), + Publisher :: xmpp_jid:usr_bare(), + NodeIdx :: exmpp_pubsub:nodeIdx(), + Node_Notify_Retract :: boolean(), + ItemIds :: [ItemId::exmpp_pubsub:itemId()], + %% + {Retracted_Items :: [Retracted_Item::{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item() + }], + % + Publishers_Retracted_ItemIds :: { + %% + Entities_Retracted_ItemIds :: [Entity_Retracted_ItemIds::{ + Entity :: xmpp_jid:usr_bare(), + ItemIds :: [ItemId::exmpp_pubsub:itemId()] + }], + %% + Publisher_Retracted_ItemIds :: [ItemId::exmpp_pubsub:itemId()] + } + }) + -> {Retracted_Items :: [Retracted_Item::{ + ItemId :: exmpp_pubsub:itemId(), + Item_Options :: [] | pubsub_options:options_item() + }], + Publisher_Retracted_ItemIds :: [ItemId::exmpp_pubsub:itemId()]} +). + +delete_old_items(Suffix, _Publisher, NodeIdx, _Notify_Retract, [] = _ItemIds, + {Retracted_Items, {Entities_ItemIds, Publisher_ItemIds}}) -> + Table_Pubsub_State = table('pubsub_state', Suffix), + lists:foreach(fun + ({Entity, Entity_ItemIds}) -> + case mnesia:read(Table_Pubsub_State, {Entity, NodeIdx}, write) of + [Pubsub_State] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + itemids = lists:foldl(fun + (Retracted_ItemId, ItemIds) -> + lists:delete(Retracted_ItemId, ItemIds) + end, + Pubsub_State#pubsub_state_dev.itemids, + Entity_ItemIds) + }, + write); + [] -> %% shouldn't happen + ok + end + % + end, Entities_ItemIds), + {Retracted_Items, Publisher_ItemIds}; +%% +delete_old_items(Suffix, Publisher, NodeIdx, Notify_Retract, [ItemId | ItemIds], + {Retracted_Items, Publishers_ItemIds}) -> + Table_Pubsub_Item = table('pubsub_item', Suffix), + delete_old_items(Suffix, Publisher, NodeIdx, Notify_Retract, ItemIds, + case mnesia:read(Table_Pubsub_Item, {ItemId, NodeIdx}, write) of + [Pubsub_Item] -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, write), + %% @TODO: Unregister 'pubsub#item_expire' + {_Retracted_Items = case + get_value(Pubsub_Item#pubsub_item_dev.options, + 'notify_retract', Notify_Retract) + of + true -> + [{ItemId, Pubsub_Item#pubsub_item_dev.options} + | Retracted_Items]; + false -> + Retracted_Items + end, + %% + _Publishers_ItemIds = lists:foldl(fun + %% + (Item_Owner, {Entities_ItemIds, Publisher_ItemIds}) + when Item_Owner == Publisher -> + {Entities_ItemIds, + [ItemId | Publisher_ItemIds]}; + %% + (_Item_Owner = Entity, + {Entities_ItemIds, Publisher_ItemIds}) -> + {_Entities_ItemIds = case + lists:keyfind(Entity, 1, Entities_ItemIds) + of + {_Entity, Entity_ItemIds} -> + lists:keyreplace(Entity, 1, Entities_ItemIds, + {Entity, [ItemId | Entity_ItemIds]}); + false -> + [{Entity, [ItemId]} | Entities_ItemIds] + end, + Publisher_ItemIds} + end, Publishers_ItemIds, Pubsub_Item#pubsub_item_dev.owners)}; + [] -> + {Retracted_Items, Publishers_ItemIds} + end). + + +%-- Retract_Item --% +-spec(retract_item/5 :: +( + Suffix :: atom(), + Pubsub_Node :: mod_pubsub_dev:pubsub_node(), + Pubsub_State :: mod_pubsub_dev:pubsub_state_owner() + | mod_pubsub_dev:pubsub_state_member() + | mod_pubsub_dev:pubsub_state_publish_only() + | mod_pubsub_dev:pubsub_state_publisher() + | mod_pubsub_dev:pubsub_state_none(), + ItemId :: exmpp_pubsub:itemId(), + Adhoc_Notify_Retract :: undefined | boolean()) + -> {ok, undefined} + % + | {ok, + Item_Options :: [] | pubsub_options:options_item(), + Pubsub_States :: mod_pubsub_dev:pubsub_states()} + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} +). + +retract_item(Suffix, Pubsub_Node, + #pubsub_state_dev{id = {Entity, NodeIdx}} = Pubsub_State, ItemId, + Adhoc_Notify_Retract) -> + Table_Pubsub_Item = table('pubsub_item', Suffix), + case mnesia:read(Table_Pubsub_Item, {ItemId, NodeIdx}, write) of + [Pubsub_Item] + when Pubsub_State#pubsub_state_dev.affiliation == 'owner' + orelse Pubsub_State#pubsub_state_dev.affiliation == 'publisher' -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, write), + Table_Pubsub_Last_Item = table('pubsub_last_item', Suffix), + case mnesia:read(Table_Pubsub_Last_Item, NodeIdx, read) of + [Pubsub_Last_Item] + when Pubsub_Last_Item#pubsub_last_item_dev.id == ItemId -> + mnesia:delete_object(Table_Pubsub_Last_Item, + Pubsub_Last_Item, write); + _ -> + ok + end, + Table_Pubsub_State = table('pubsub_state', Suffix), + case + retract_item(NodeIdx, ItemId, Table_Pubsub_State, Entity, + _Item_Owners = Pubsub_Item#pubsub_item_dev.owners, false) + of + true -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + itemids = lists:delete(ItemId, + Pubsub_State#pubsub_state_dev.itemids) + }, + write); + false -> + ok + end, + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = lists:delete(ItemId, + Pubsub_Node#pubsub_node_dev.itemids) + }, + write), + case + Adhoc_Notify_Retract == true + orelse + get_value( + {Pubsub_Node#pubsub_node_dev.options, + Pubsub_Item#pubsub_item_dev.options}, + 'notify_retract', false) + of + true -> + {ok, + _Item_Options = Pubsub_Item#pubsub_item_dev.options, + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, nodeidx)}; + false -> + {ok, undefined} + end; + %% + [#pubsub_item_dev{creation = {_DateTime, _Creator = Entity}} = Pubsub_Item] -> + mnesia:delete_object(Table_Pubsub_Item, Pubsub_Item, write), + Table_Pubsub_Last_Item = table('pubsub_last_item', Suffix), + case mnesia:read(Table_Pubsub_Last_Item, NodeIdx, read) of + [Pubsub_Last_Item] + when Pubsub_Last_Item#pubsub_last_item_dev.id == ItemId -> + mnesia:delete_object(Table_Pubsub_Last_Item, + Pubsub_Last_Item, write); + _ -> + ok + end, + Table_Pubsub_State = table('pubsub_state', Suffix), + case + retract_item(NodeIdx, ItemId, Table_Pubsub_State, Entity, + _Item_Owners = Pubsub_Item#pubsub_item_dev.owners, false) + of + true -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + itemids = lists:delete(ItemId, + Pubsub_State#pubsub_state_dev.itemids) + }, + write); + false -> + ok + end, + mnesia:write(table('pubsub_node', Suffix), + Pubsub_Node#pubsub_node_dev{ + itemids = lists:delete(ItemId, + Pubsub_Node#pubsub_node_dev.itemids) + }, + write), + case + Adhoc_Notify_Retract == true + orelse + get_value( + {Pubsub_Node#pubsub_node_dev.options, + Pubsub_Item#pubsub_item_dev.options}, + 'notify_retract', false) + of + true -> + {ok, + _Item_Options = Pubsub_Item#pubsub_item_dev.options, + _Pubsub_States = mnesia:index_read(Table_Pubsub_State, NodeIdx, nodeidx)}; + false -> + {ok, undefined} + end; + [_Pubsub_Item] -> + {error, 'forbidden'}; + %% + [] -> + {error, 'item-not-found'} + end. + + +%% +-spec(retract_item/6 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + ItemId :: exmpp_pubsub:itemId(), + Table_Pubsub_State :: atom(), + Entity :: xmpp_jid:usr_bare() | undefined, + Item_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Retract_ItemId :: boolean()) + -> Retract_ItemId :: boolean() +). + +retract_item(_NodeIdx, _ItemId, _Table_Pubsub_State, _Entity, [] = _Item_Owners, + Retract_ItemId) -> + Retract_ItemId; +%% +retract_item(NodeIdx, ItemId, Table_Pubsub_State, Entity, + [_Item_Owner = Entity | Item_Owners], _Retract_ItemId) -> + retract_item(NodeIdx, ItemId, Table_Pubsub_State, undefined, Item_Owners, + true); +%% +retract_item(NodeIdx, ItemId, Table_Pubsub_State, _Entity, + [_Item_Owner = Entity | Item_Owners], Retract_ItemId) -> + case mnesia:read(Table_Pubsub_State, {Entity, NodeIdx}, write) of + [Pubsub_State] -> + mnesia:write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + itemids = lists:delete(ItemId, + Pubsub_State#pubsub_state_dev.itemids) + }, + write); + [] -> + ok + end, + retract_item(NodeIdx, ItemId, Table_Pubsub_State, _Entity, Item_Owners, + Retract_ItemId). + +%-- Subscribe_Node --% +-spec(subscribe_node/8 :: +( + Suffix :: atom(), + Host :: xmpp_jid:raw_jid_component_bare(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Pubsub_State :: mod_pubsub_dev:pubsub_state(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: [] | pubsub_options:options_subscription(), + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> {result, + Subscription_Subscribed :: exmpp_pubsub:subscription_subscribed(), + Pubsub_Last_Item :: undefined | mod_pubsub_dev:pubsub_last_item()} + % + | {result, + Subscription_Pending :: exmpp_pubsub:subscription_pending(), + Pubsub_Last_Item :: undefined} + %%% + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-authorized', 'pending-subscription'} + | {error, 'not-allowed', 'closed-node'} +). + +subscribe_node(Suffix, Host, Node_Options, Node_Owners, + #pubsub_state_dev{id = {{_U,S,_R} = Entity, _NodeIdx}} = Pubsub_State, Resource, + Subscription_Options, Pubsub_Features) -> + Affiliation = Pubsub_State#pubsub_state_dev.affiliation, + Subscriptions = Pubsub_State#pubsub_state_dev.subscriptions, + case + options_on_subscribe_node(Host, Node_Options, Node_Owners, Entity, + Resource, Affiliation, Subscriptions, Subscription_Options, + Pubsub_Features) + of + %% + {ok, 'new', + {'pending', SubId, _Resource, _Subscription_Options} = Subscription} -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + affiliation = case Affiliation of + 'none' -> 'member'; + Affiliation -> Affiliation + end, + subscriptions = [Subscription | Subscriptions] + }, + write), + Table_Pubsub_Subscription_Pending = table('pubsub_subscription_pending', + Suffix), + case + mnesia:read(Table_Pubsub_Subscription_Pending, + Pubsub_State#pubsub_state_dev.id, write) + of + [] -> + mnesia:write(Table_Pubsub_Subscription_Pending, + #pubsub_subscription_pending{ + id = Pubsub_State#pubsub_state_dev.id, + nodeidx = Pubsub_State#pubsub_state_dev.nodeidx, + subids = [SubId] + }, + write); + [Pubsub_Subscription_Pending] -> + mnesia:write(Table_Pubsub_Subscription_Pending, + Pubsub_Subscription_Pending#pubsub_subscription_pending{ + subids = [SubId + | Pubsub_Subscription_Pending + #pubsub_subscription_pending.subids] + }, + write) + end, + {result, Subscription, _Pubsub_Last_Item = undefined}; + %% + {ok, Type, Subscription} -> + case Type of + 'new' -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + affiliation = case Affiliation of + 'none' -> 'member'; + Affiliation -> Affiliation + end, + subscriptions = [Subscription | Subscriptions] + }, + write); + 'old' -> + ok + end, + {result, + Subscription, + _Pubsub_Last_Item = case + (get_value(Subscription_Options, 'deliver', true) == true) + andalso + (get_value(Subscription_Options, 'show-values') =/= []) + andalso + (get_value(Node_Options, 'send_last_published_item', 'never') + =/= 'never') + of + true -> + case + mnesia:read(table('pubsub_last_item', Suffix), + Pubsub_State#pubsub_state_dev.nodeidx, + read) + of + [] -> undefined; + [Pubsub_Last_Item] -> Pubsub_Last_Item + end; + false -> + undefined + end}; + Error -> + Error + end. + +%% +-spec(options_on_subscribe_node/9 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Entity :: xmpp_jid:usr_bare(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Subscription_Options :: [] | pubsub_options:options_subscription(), + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> {ok, + Type :: 'new' | 'old', + Subscription :: exmpp_pubsub:subscription_subscribed()} + % + | {ok, + Type :: 'new', + Subscription :: exmpp_pubsub:subscription_pending()} + %%% + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-authorized', 'pending-subscription'} + | {error, 'not-allowed', 'closed-node'} +). + +options_on_subscribe_node(Host, Node_Options, Node_Owners, Entity, + Resource, Affiliation, Subscriptions, Subscription_Options, Pubsub_Features) -> + options_on_subscribe_node(get_option(Node_Options, 'subscribe'), Host, + Node_Options, Node_Owners, Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, + _Multi_Subscribe = lists:member(<<"multi-subscribe">>, Pubsub_Features)). + +%% +-spec(options_on_subscribe_node/10 :: +( + Option :: pubsub_options:option_node_access_model() + | pubsub_options:option_node_subscribe(), + Host :: xmpp_jid:raw_jid_component_bare(), + Node_Options :: pubsub_options:options_node(), + Node_Owners :: [Entity::xmpp_jid:usr_bare(),...], + Entity :: xmpp_jid:usr_bare(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Subscription_Options :: [] | pubsub_options:options_subscription(), + Multi_Subscribe :: boolean()) + -> {ok, + Type :: 'new' | 'old', + Subscription :: exmpp_pubsub:subscription_subscribed()} + % + | {ok, + Type :: 'new', + Subscription :: exmpp_pubsub:subscription_pending()} + %%% + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-authorized', 'pending-subscription'} + | {error, 'not-allowed', 'closed-node'} +). + +options_on_subscribe_node({'subscribe', Subscribe}, _Host, _Node_Options, + _Node_Owners, _Entity, _Resource, Affiliation, _Subscriptions, + _Subscription_Options, _Multi_Subscribe) + when Subscribe == false + orelse Affiliation == 'outcast' + orelse Affiliation == 'publish-only' -> + {error, 'forbidden'}; +%% +options_on_subscribe_node({'subscribe', true}, Host, Node_Options, + Node_Owners, Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, Multi_Subscribe) -> + options_on_subscribe_node(get_option(Node_Options, 'access_model'), Host, + Node_Options, Node_Owners, Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, Multi_Subscribe); + +options_on_subscribe_node({'access_model', open}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, _Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', open}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, _Affiliation, _Subscriptions, + Subscription_Options, true = _Multi_Subscribe) -> + {ok, 'new', exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)}; +%% +options_on_subscribe_node({'access_model', presence}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', presence}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, _Subscriptions, + Subscription_Options, true = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)}; +%% +options_on_subscribe_node({'access_model', presence}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', presence}, Host, _Node_Options, + Node_Owners, Entity, Resource, _Affiliation, _Subscriptions, + Subscription_Options, _Multi_Subscribe) -> + case is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) of + false -> + {error, 'not-authorized', 'presence-subscription-required'}; + _Node_Owner -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', roster}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, _Subscriptions, + Subscription_Options, true = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)}; +%% +options_on_subscribe_node({'access_model', roster}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', roster}, _Host, Node_Options, + _Node_Owners, Entity, Resource, _Affiliation, _Subscriptions, + Subscription_Options, _Multi_Subscribe) -> + case + is_contact_in_allowed_roster_groups(Entity, + get_value(Node_Options, 'roster_groups_allowed', [])) + of + false -> + {error, 'not-authorized', 'not-in-roster-group'}; + {_Node_Owner, _Roster_Group} -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', authorize}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, _Subscriptions, + Subscription_Options, true = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)}; +%% +options_on_subscribe_node({'access_model', authorize}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', authorize}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, _Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, _Subscription} -> + {error, 'not-authorized', 'pending-subscription'}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_pending(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', whitelist}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, _Subscriptions, + Subscription_Options, true = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)}; +%% +options_on_subscribe_node({'access_model', whitelist}, _Host, _Node_Options, + _Node_Owners, _Entity, Resource, Affiliation, Subscriptions, + Subscription_Options, false = _Multi_Subscribe) + when Affiliation == 'owner' + orelse Affiliation == 'publisher' + orelse Affiliation == 'member' -> + case lists:keyfind(Resource, 3, Subscriptions) of + {_Resource, Subscription} -> + {ok, 'old', Subscription}; + false -> + {ok, + 'new', + exmpp_pubsub:subscription_subscribed(Resource, Subscription_Options)} + end; +%% +options_on_subscribe_node({'access_model', whitelist}, _Host, _Node_Options, + _Node_Owners, _Entity, _Resource, _Affiliation, _Subscriptions, + _Subscription_Options, _Multi_Subscribe) -> + {error, 'not-allowed', 'closed-node'}. + + +%-- Unsubscribe_Node --% +-spec(unsubscribe_node/6 :: +( + Suffix :: atom(), + Host :: xmpp_jid:raw_jid_component_bare(), + Node_Access_Model :: pubsub_options:access_model(), + Pubsub_State :: mod_pubsub_dev:pubsub_state_member() + | mod_pubsub_dev:pubsub_state_outcast() + | mod_pubsub_dev:pubsub_state_owner() + | mod_pubsub_dev:pubsub_state_publisher(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + SubId :: undefined | exmpp_pubsub:subId()) + -> {ok, + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + SubId :: exmpp_pubsub:subId()} + %%% + | {error, 'unexpected', 'not-subscribed'} + | {error, 'not-acceptable', 'invalid-subid'} + | {error, 'bad-request', 'subid-required'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +unsubscribe_node(Suffix, Host, Node_Access_Model, + #pubsub_state_dev{subscriptions = [{Subscription_State, SubId, Resource, _}]} + = Pubsub_State, Resource, _SubId) + when _SubId == undefined orelse _SubId == SubId -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + subscriptions = [] + }, + write), + %% unregister pending subscription + case Subscription_State of + 'pending' -> + unregister_pending_subscription(Suffix, + Pubsub_State#pubsub_state_dev.id, SubId); + _ -> + ok + end, + case Node_Access_Model of + 'roster' -> + ok; + _ -> + ok + end, + {ok, Resource, SubId}; +%% +unsubscribe_node(Suffix, Host, Node_Access_Model, + #pubsub_state_dev{subscriptions = [{_Subscription_State, SubId, _Resource, _}]} + = Pubsub_State, Resource, SubId) -> + {error, 'unexpected', 'not-subscribed'}; +%% +unsubscribe_node(Suffix, Host, Node_Access_Model, + #pubsub_state_dev{subscriptions = [{_Subscription_State, _SubId, Resource, _}]} + = Pubsub_State, Resource, SubId) -> + {error, 'not-acceptable', 'invalid-subid'}; +%% +unsubscribe_node(Suffix, Host, Node_Access_Model, Pubsub_State, Resource, + undefined = _SubId) -> + case lists:keytake(Resource, 3, Pubsub_State#pubsub_state_dev.subscriptions) of + {value, + {Subscription_State, SubId, Resource, _} = Subscription, Subscriptions} -> + case lists:keymember(Resource, 3, Subscriptions) of + false -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + subscriptions = Subscriptions + }, + write), + %% unregister pending subscription + case Subscription_State of + 'pending' -> + unregister_pending_subscription(Suffix, + Pubsub_State#pubsub_state_dev.id, SubId); + _ -> + ok + end, + case Node_Access_Model of + 'roster' -> + ok; + _ -> + ok + end, + %% TODO : unregister 'pubsub#tempsub' + {ok, Resource, SubId}; + true -> + {error, 'bad-request', 'subid-required'} + end; + false -> + {error, 'unexpected', 'not-subscribed'} + end; +%% +unsubscribe_node(Suffix, Host, Node_Access_Model, Pubsub_State, Resource, SubId) -> + case lists:keytake(SubId, 2, Pubsub_State#pubsub_state_dev.subscriptions) of + {value, + {Subscription_State, SubId, Resource, _} = Subscription, Subscriptions} -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + subscriptions = Subscriptions + }, + write), + %% unregister pending subscription + case Subscription_State of + 'pending' -> + unregister_pending_subscription(Suffix, + Pubsub_State#pubsub_state_dev.id, SubId); + _ -> + ok + end, + case Node_Access_Model of + 'roster' -> + ok; + _ -> + ok + end, + {ok, Resource, SubId}; + {value, {_Subscription_State, SubId, _Resource, _} = Subscription, _} -> + {error, 'unexpected', 'not-subscribed'}; + false -> + {error, 'not-acceptable', 'invalid-subid'} + end. + +%% +-spec(register_pending_subscription/3 :: +( + Suffix :: atom(), + StateId :: {Entity::xmpp_jid:usr_bare(), NodeIdx::exmpp_pubsub:nodeIdx()}, + SubId :: exmpp_pubsub:subId()) + -> 'ok' +). + +register_pending_subscription(Suffix, {Entity, NodeIdx} = _StateId, SubId) -> + Table_Pubsub_Subscription_Pending = table('pubsub_subscription_pending', Suffix), + case mnesia:read(Table_Pubsub_Subscription_Pending, {Entity, NodeIdx}, write) of + [] -> + mnesia:write(Table_Pubsub_Subscription_Pending, + #pubsub_subscription_pending{ + id = {Entity, NodeIdx}, + nodeidx = NodeIdx, + subids = [SubId] + }, + write); + [Pubsub_Subscription_Pending] -> + mnesia:write(Table_Pubsub_Subscription_Pending, + Pubsub_Subscription_Pending#pubsub_subscription_pending{ + subids = [SubId + | Pubsub_Subscription_Pending + #pubsub_subscription_pending.subids] + }, + write) + end. + +%% +-spec(unregister_pending_subscription/3 :: +( + Suffix :: atom(), + StateId :: {Entity::xmpp_jid:usr_bare(), NodeIdx::exmpp_pubsub:nodeIdx()}, + SubId :: exmpp_pubsub:subId()) + -> 'ok' +). + +unregister_pending_subscription(Suffix, StateId, SubId) -> + Table_Pubsub_Subscription_Pending = table('pubsub_subscription_pending', Suffix), + case mnesia:read(Table_Pubsub_Subscription_Pending, StateId, write) of + %% + [#pubsub_subscription_pending{subids = [SubId]} = Pubsub_Subscription_Pending] -> + mnesia:delete_object(Table_Pubsub_Subscription_Pending, + Pubsub_Subscription_Pending, write); + %% + [Pubsub_Subscription_Pending] -> + mnesia:write(Table_Pubsub_Subscription_Pending, + Pubsub_Subscription_Pending#pubsub_subscription_pending{ + subids = lists:delete(SubId, + Pubsub_Subscription_Pending#pubsub_subscription_pending.subids) + }, + write); + [] -> + ok + end. + +%-- Set_Configure_Subscription --% +-spec(set_configure_subscription/5 :: +( + Suffix :: atom(), + Pubsub_State :: mod_pubsub_dev:pubsub_state_member() + | mod_pubsub_dev:pubsub_state_owner() + | mod_pubsub_dev:pubsub_state_publisher(), + SubId :: undefined | exmpp_pubsub:subId(), + Resource :: undefined + | xmpp_jid:resource_jid() + | {'caps', xmpp_jid:resource_jid()}, + Subscription_Options :: pubsub_options:options_subscription()) + -> 'ok' + %%% + | {error, 'unexpected-request', 'not-subscribed'} + | {error, 'bad-request', 'subid-required'} + | {error, 'forbidden'} + | {error, 'not-acceptable', 'invalid-subid'} +). + +set_configure_subscription(Suffix, Pubsub_State, undefined = _SubId, Resource, + Subscription_Options) -> + case + lists:keytake(Resource, 3, Pubsub_State#pubsub_state_dev.subscriptions) + of + {value, {Subscription_State, SubId, Resource, _Subscription_Options}, + Subscriptions} -> + case lists:keymember(Resource, 3, Subscriptions) of + false + when Subscription_State == 'pending' -> + {error, 'forbidden'}; + false -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + subscriptions = [ + {Subscription_State, SubId, Resource, + Subscription_Options} + | Subscriptions] + }, + write), + 'ok'; + true -> + {error, 'bad-request', 'subid-required'} + end; + false -> + {error, 'unexpected-request', 'not-subscribed'} + end; +%% +set_configure_subscription(Suffix, Pubsub_State, SubId, Resource, + Subscription_Options) -> + case lists:keytake(SubId, 2, Pubsub_State#pubsub_state_dev.subscriptions) of + {value, {'pending', SubId, Resource, _Subscription_Options}, + _Subscriptions} -> + {error, 'forbidden'}; + {value, {Subscription_State, SubId, Resource, _Subscription_Options}, + Subscriptions} -> + mnesia:write(table('pubsub_state', Suffix), + Pubsub_State#pubsub_state_dev{ + subscriptions = [ + {Subscription_State, SubId, Resource, + Subscription_Options} + | Subscriptions] + }, + write), + 'ok'; + {value, {_Subscription_State, SubId, _Resource, _Subscription_Options}, + _Subscriptions} -> + {error, 'unexpected-request', 'not-subscribed'}; + false -> + case + lists:keytake(Resource, 3, + Pubsub_State#pubsub_state_dev.subscriptions) + of + {value, {'pending', _SubId, Resource, _Subscription_Options}, + _Subscriptions} -> + {error, 'forbidden'}; + {value, {_Subscription_State, _SubId, _Resource, _Subscription_Options}, + _Subscriptions} -> + {error, 'not-acceptable', 'invalid-subid'}; + false -> + {error, 'unexpected-request', 'not-subscribed'} + end + end. + +%-- Get_Items --% +-spec(get_items/5 :: +( + Suffix :: pubsub_db_mnesia:table(), + Host :: xmpp_jid:raw_jid_component_bare(), + Node_Parameters :: {NodeIdx :: exmpp_pubsub:nodeIdx(), + Node_Owners :: mod_pubsub_dev:node_owners(), + Node_ItemIds :: [] | exmpp_pubsub:itemIds(), + Node_Access_Model :: pubsub_options:access_model(), + Node_Groups :: pubsub_options:rosters_groups_allowed()}, + %% + Entity_Parameters :: {Entity :: xmpp_jid:usr_bare(), + Affiliation :: exmpp_pubsub:affiliation(), + Access :: undefined | 'pending' | 'roster' | 'presence' | 'authorize'}, + %% + Criteria :: {Max_Items :: non_neg_integer(), + ItemIds :: undefined, + _SubId :: exmpp_pubsub:subId()} + | {Max_Items :: undefined, + ItemIds :: exmpp_pubsub:itemIds(), + _SubId :: exmpp_pubsub:subId()}) + -> {ok, + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Cache :: {Presence_Cache :: undefined, + Groups_Cache :: undefined}} + % + | {ok, + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Cache :: {Presence_Cache :: true, + Groups_Cache :: undefined}} + % + | {ok, + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Cache :: {Presence_Cache :: undefined, + Groups_Cache :: [{Node_Owner :: xmpp_jid:usr_bare(), + Groups :: [Group::binary()]}]}} + %%% + | {error, 'forbidden'} + | {error, 'not-authorized', 'presence-subscription-required'} + | {error, 'not-authorized', 'not-in-roster-group'} + | {error, 'not-allowed', 'closed-node'} +). + +get_items(_Suffix, _Host, _Node_Parameters, {_Entity, Affiliation, Access}, _Criteria) + when Affiliation == 'outcast' + orelse Affiliation == 'publish-only' + orelse Access == 'pending' -> + {error, 'forbidden'}; +%% +get_items(Suffix, _Host, + {NodeIdx, _Node_Owners, Node_ItemIds, Node_Access_Model, _Node_Groups}, + {_Entity, Affiliation, Access}, {Max_Items, ItemIds, _SubId}) + when Affiliation == 'owner' + % + orelse Affiliation == 'publisher' + % + orelse Affiliation == 'member' + % + orelse Node_Access_Model == 'open' + % + orelse (Node_Access_Model == 'presence' + andalso + Access == 'presence' + orelse Access == 'roster') + % + orelse (Node_Access_Model == 'roster' + andalso + Access == 'roster') + % + orelse (Node_Access_Model == 'authorize' + andalso + Access == 'authorize') -> + {ok, + _Pusbub_Items = get_items(Suffix, NodeIdx, + get_max_itemids(Max_Items, ItemIds, Node_ItemIds)), + _Cache = {undefined, undefined}}; +% +get_items(Suffix, Host, + {NodeIdx, Node_Owners, Node_ItemIds, 'presence' = _Node_Access_Model, _Node_Groups}, + {Entity, _Affiliation, _Access}, {Max_Items, ItemIds, _SubId}) -> + case is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) of + false -> + {error, 'not-authorized', 'presence-subscription-required'}; + Node_Owner -> + {ok, + _Pusbub_Items = get_items(Suffix, NodeIdx, + get_max_itemids(Max_Items, ItemIds, Node_ItemIds)), + _Cache = {_Presence_Cache = true, _Groups_Cache = undefined}} + end; +%% +get_items(Suffix, _Host, + {NodeIdx, _Node_Owners, Node_ItemIds, 'roster' = _Node_Access_Model, Node_Groups}, + {Entity, _Affiliation, _Access}, {Max_Items, ItemIds, _SubId}) -> + case is_contact_in_allowed_roster_groups(Entity, Node_Groups) of + {Node_Owner, Roster_Group} -> + {ok, + _Pusbub_Items = get_items(Suffix, NodeIdx, + get_max_itemids(Max_Items, ItemIds, Node_ItemIds)), + _Cache = { + _Presence_Cache = undefined, + _Groups_Cache = [{Node_Owner, [Roster_Group]}] + } + }; + false -> + {error, 'not-authorized', 'not-in-roster-group'} + end; +%% +get_items(_Suffix, _Host, + {_NodeIdx, _Node_Owners, _Node_ItemIds, 'authorize' = _Node_Access_Model, _Node_Groups}, + _Entity_Parameters, _Criteria) -> + {error, 'not-allowed', 'closed-node'}; +%% +get_items(_Suffix, _Host, _Node_Parameters, _Entity_Parameters, _Criteria) -> + {error, 'forbidden'}. + +%% +-spec(get_max_itemids/3 :: +( + Max_Items :: undefined | non_neg_integer(), + ItemIds :: [] | exmpp_pubsub:itemIds(), + Node_ItemIds :: [] | exmpp_pubsub:itemIds()) + -> Max_ItemIds :: undefined | [] | exmpp_pubsub:itemIds() +). + +get_max_itemids(undefined = _Max_Items, [] = _ItemIds, _Node_ItemIds) -> + _Max_ItemIds = undefined; +%% +get_max_itemids(undefined = _Max_Items, ItemIds, Node_ItemIds) -> + _Max_ItemIds = lists:filter(fun + (ItemId) -> + lists:member(ItemId, Node_ItemIds) + end, ItemIds); +%% +get_max_itemids(Max_Items, undefined = _ItemIds, Node_ItemIds) -> + _Max_ItemIds = get_max_itemids(Max_Items, [], Node_ItemIds); +%% +get_max_itemids(_Max_Items, ItemIds, _Node_ItemIds) + when _Max_Items == 0 orelse _Node_ItemIds == [] -> + _Max_ItemIds = lists:reverse(ItemIds); +%% +get_max_itemids(Max_Items, ItemIds, [ItemId | Node_ItemIds]) -> + get_max_itemids(Max_Items - 1, [ItemId | ItemIds], Node_ItemIds). + +%% +-spec(get_items/3 :: +( + Suffix :: pubsub_db_mnesia:suffix(), + NodeIdx :: exmpp_pubsub:nodeIdx(), + ItemIds :: undefined | [] | exmpp_pubsub:itemIds()) + -> Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items() +). + +get_items(Suffix, NodeIdx, undefined = _ItemIds) -> + _Pubsub_Items = mnesia:index_read(table('pubsub_item', Suffix), NodeIdx, nodeidx); +%% +get_items(Suffix, NodeIdx, ItemIds) -> + Table_Pubsub_Item = table('pubsub_item', Suffix), + _Pubsub_Items = lists:foldl(fun + (ItemId, Pubsub_Items) -> + case + mnesia:index_match_object(Table_Pubsub_Item, + #pubsub_item_dev{ + id = {ItemId, NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx, + read) + of + [Pubsub_Item] -> [Pubsub_Item | Pubsub_Items]; + _ -> Pubsub_Items + end + end, [], ItemIds). diff --git a/src/mod_pubsub_ng/pubsub_dev.hrl b/src/mod_pubsub_ng/pubsub_dev.hrl new file mode 100644 index 000000000..9a695ca16 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_dev.hrl @@ -0,0 +1,146 @@ + + +%-include_lib("exmpp/include/exmpp.hrl"). +%-include_lib("exmpp/include/exmpp_jid.hrl"). +-include("ejabberd.hrl"). +-include("mod_roster.hrl"). +-include("jlib.hrl"). +%-include("pubsub_api.hrl"). + +-define(NS_JABBER_CLIENT, <<"jabber:client">>). + +%% +%% Pubsub Node +-record(pubsub_node_dev, +{ + id ,%:: {Pubsub_Host :: exmpp_pubsub:host(), + % NodeId :: undefined | exmpp_pubsub:nodeId()}, + idx ,%:: exmpp_pubsub:nodeIdx(), + creation ,%:: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()}, + level = 0 ,%:: exmpp_pubsub:level(), + owners = [] ,%:: [Owner::xmpp_jid:usr_bare(),...], + itemids = [] ,%:: [ItemId::exmpp_pubsub:itemId()], + options = [] %:: pubsub_options:options_node() +}). + +%% +%% Pubsub State +-record(pubsub_state_dev, +{ + id ,%:: {Entity :: xmpp_jid:usr_bare(), + % NodeIdx :: exmpp_pubsub:nodeIdx()}, + nodeidx ,%:: exmpp_pubsub:nodeIdx(), + affiliation = 'none' ,%:: exmpp_pubsub:affiliation(), + access ,%:: pubsub_options:access_model(), + subscriptions = [] ,%:: [] | exmpp_pubsub:subscriptions(), + %groups = [] ,%:: [Roster_Group::binary()], + itemids = [] %:: [ItemId::exmpp_pubsub:itemId()] +}). + +%% +%% Pubsub Item +-record(pubsub_item_dev, +{ + id ,%:: {ItemId::exmpp_pubsub:itemId(), NodeIdx::exmpp_pubsub:nodeIdx()}, + nodeidx ,%:: exmpp_pubsub:nodeIdx(), + owners = [] ,%:: [Owner::xmpp_jid:usr_bare(),...], + creation ,%:: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()}, + modification ,%:: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()}, + payload ,%:: exmpp_pubsub:payload(), + options = [] %:: [] | pubsub_options:options_item() +}). + +%% +%% Pubsub Last Item +-record(pubsub_last_item_dev, +{ + nodeidx ,%:: exmpp_pubsub:nodeIdx(), + id ,%:: exmpp_pubsub:itemId(), + owners = [] ,%:: [Owner::xmpp_jid:usr_bare(),...], + creation ,%:: {DateTime::erlang:timestamp(), Entity::xmpp_jid:usr_entity()}, + payload ,%:: exmpp_pubsub:payload(), + options = [] %:: [] | pubsub_options:options_item() +}). + +%% +%% Pubsub Index +-record(pubsub_index_dev, +{ + index :: exmpp_pubsub:index(), + last :: exmpp_pubsub:nodeIdx(), + free :: [exmpp_pubsub:nodeIdx()] +}). + +%% +%% Pubsub_Subscription_Pending +-record(pubsub_subscription_pending, +{ + id ,%:: {Entity::xmpp_jid:usr_bare(), NodeIdx::exmpp_pubsub:nodeIdx()} + nodeidx ,%:: exmpp_pubsub:nodeIdx() + subids %:: [SubId::exmpp_pubsub:subId(),...] +}). + + + +%% +%% Internal data structures +%% +-record(entity, +{ + id :: xmpp_jid:usr_bare(), + local :: boolean(), + affiliation :: 'member' | 'owner' | 'publisher', + subscriptions :: []%[exmpp_pubsub:subscription(),...] +}). + +%% +-record(node, +{ + id :: exmpp_pubsub:nodeId(), + owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + access_model :: pubsub_options:access_model(), +% itemreply :: 'owner' | 'publisher', +% notification_type :: 'headline' | 'normal', +% presence_based_delivery :: boolean(), + rosters_groups_allowed = [] :: [] | pubsub_options:rosters_groups_allowed() +}). + +%% +-record(item, +{ + access_model ,%:: undefined | pubsub_options:access_model(), + presence_based_delivery ,%:: undefined | boolean(), + rosters_groups_allowed = [],%:: [] | pubsub_options:rosters_groups_allowed() + stanza %::#xmlel{} +}). + +%% +-record(cache, +{ + %% Entity is online + presence :: undefined | mod_pubsub_dev:presence_cache(), + %% Entity has presence subscriptions with at least one node owner + presence_subscriptions :: undefined | boolean(), + %% Entity has presence subscriptions with at least one node owner + %% and is contained in at least one roster group from the node owner + rosters_groups :: undefined | boolean() %% TODO : use a list +}). + +%% +-record(subids, +{ + presence = undefined :: undefined | [] | mod_pubsub_dev:resources_subids(), + no_presence = undefined :: undefined | [] | mod_pubsub_dev:resources_subids() +}). + +%% +-record(event, +{ + host :: xmpp_jid:raw_jid_component_bare(), + component :: xmpp_jid:component_bare(), + entity :: mod_pubsub_dev:entity(), + node :: mod_pubsub_dev:n0de(), + cache :: mod_pubsub_dev:cache(), + subids :: mod_pubsub_dev:subids() +}). + diff --git a/src/mod_pubsub_ng/pubsub_disco.erl b/src/mod_pubsub_ng/pubsub_disco.erl new file mode 100644 index 000000000..935e4f47e --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_disco.erl @@ -0,0 +1,1437 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_disco). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1 +]). + + +-import(pubsub_db_mnesia, +[ + table/2 +]). + +-import(xmpp_xdata, +[ + xmlel_field/3, + xmlel_field_jid_multi/3, + xmlel_field_jid_single/3, + xmlel_field_list_single/4, + xmlel_field_text_single/3, + xmlel_x/2 +]). + +%% +-spec(iq_disco_info/9 :: +( + Suffix :: undefined | atom(), + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Privacy :: boolean(), + Entity :: xmpp_jid:usr_bare(), + Entity_Type :: 'local' | 'remote', + Plugin :: exmpp_pubsub:plugin(), + NodeId :: undefined | exmpp_pubsub:nodeId(), + Xmlels :: [] | [Xmlel_Set::xmlel(),...]) + -> {result, Xmlel_Query::xmlel()} + %%% +% | {error, 'feature-not-implemented', ?NS_DISCO_INFO} +% | {error, 'feature-not-implemented', ?NS_PUBSUB_META_DATA} +% % +% | {error, 'item-not-found'} +). + +iq_disco_info(_Suffix, Host, Pubsub_Host, _Privacy, _Entity, Entity_Type, + Plugin, undefined = _NodeId, _Xmlels) -> + Features = Plugin:features(), + case lists:member(?NS_DISCO_INFO, Plugin:features()) of + true -> + {result, + exmpp_pubsub:xmlel_query('disco#info', + lists:map(fun + ({Category, Name, Type}) -> + exmpp_pubsub:xmlel_identity('disco#info', + Category, Type, Name) + end, Plugin:identity()) + ++ + lists:map(fun + (Feature) -> + exmpp_pubsub:xmlel_feature('disco#info', Feature) + end, Features) + ++ + lists:map(fun + (Pubsub_Feature) -> + exmpp_pubsub:xmlel_feature('disco#info', + <<?NS_PUBSUB/binary, "#", Pubsub_Feature/binary>>) + end, lists:sort(Plugin:pubsub_features(Entity_Type))) + ) + }; + false -> + {error, 'feature-not-implemented', ?NS_DISCO_INFO} + end; +%% +iq_disco_info(Suffix, Host, Pubsub_Host, false = _Privacy, {_,S,_} = _Entity, + Entity_Type, Plugin, NodeId, _Xmlels) -> + Features = Plugin:features(), + case lists:member(?NS_DISCO_INFO, Features) of + true -> + case + mnesia:dirty_read(table('pubsub_node', Suffix), + {Pubsub_Host, NodeId}) + of + [#pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, + options = Node_Options, creation = Creation}] -> + Pubsub_Features = Plugin:pubsub_features(Entity_Type), + {result, + exmpp_pubsub:xmlel_query('disco#info', NodeId, + [exmpp_pubsub:xmlel_identity('disco#info', <<"pubsub">>, + get_value(Node_Options, 'node_type'), + get_value(Node_Options, 'title', undefined))] + ++ + [exmpp_pubsub:xmlel_identity('disco#info', Category, Type, Name) + || {Category, Name, Type} <- Plugin:identity()] + ++ + [exmpp_pubsub:xmlel_feature('disco#info', Feature) + || Feature <- Features] + ++ + [exmpp_pubsub:xmlel_feature('disco#info', + <<?NS_PUBSUB/binary, "#", Pubsub_Feature/binary>>) + || Pubsub_Feature <- Pubsub_Features] + ++ + case lists:member(<<"meta-data">>, Pubsub_Features) of + true -> + [xmlel_x_metadata(table('pubsub_state', Suffix), + NodeIdx, Node_Owners, Creation, + Node_Options)]; + false -> + [] + end)}; + _ -> + {error, 'item-not-found'} + end; + false -> + {error, 'feature-not-implemented', ?NS_DISCO_INFO} + end. + +%% +-spec(xmlel_x_metadata/5 :: +( + Table_Pubsub_State :: atom(), + NodeIdx :: exmpp_pubsub:nodeIdx(), + Node_Owners :: mod_pubsub_dev:node_owners(), + Creation :: mod_pubsub_dev:node_creation(), + Node_Options :: pubsub_options:options_node()) + -> Xmlel::xmlel() +). + +%% <x xmlns='jabber:x:data' type='result'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#meta-data</value> +%% </field> +%% <field var='pubsub#type' label='Payload type' type='text-single'> +%% <value>http://www.w3.org/2005/Atom</value> +%% </field> +%% <field var='pubsub#creator' label='Node creator' type='jid-single'> +%% <value>hamlet@denmark.lit</value> +%% </field> +%% <field var='pubsub#creation_date' label='Creation date' type='text-single'> +%% <value>2003-07-29T22:56Z</value> +%% </field> +%% <field var='pubsub#title' label='A short name for the node' type='text-single'> +%% <value>Princely Musings (Atom)</value> +%% </field> +%% <field var='pubsub#description' label='A description of the node' type='text-single'> +%% <value>Updates for Hamlet's Princely Musings weblog.</value> +%% </field> +%% <field var='pubsub#language' label='Default language' type='list-single'> +%% <value>en</value> +%% </field> +%% <field var='pubsub#contact' label='People to contact with questions' type='jid-multi'> +%% <value>bard@shakespeare.lit</value> +%% </field> +%% <field var='pubsub#owner' label='Node owners' type='jid-multi'> +%% <value>hamlet@denmark.lit</value> +%% </field> +%% <field var='pubsub#publisher' label='Publishers to this node' type='jid-multi'> +%% <value>hamlet@denmark.lit</value> +%% </field> +%% <field var='pubsub#num_subscribers' label='Number of subscribers to this node' type='text-single'> +%% <value>1066</value> +%% </field> +%% </x> + +xmlel_x_metadata(_Table_Pubsub_State, _NodeIdx, Node_Owners, + {TimeStamp, Creator} = _Creation, Node_Options) -> + {DateTime, _} = jlib:timestamp_to_iso(calendar:now_to_datetime(TimeStamp), utc), + Creation_Date = list_to_binary(DateTime ++ "Z"), + xmlel_x(<<"form">>, [ + xmlel_field(<<"FORM_TYPE">>, <<"result">>, [?NS_PUBSUB_META_DATA]), + % + xmlel_field_text_single(<<"pubsub#type">>, <<"Payload type">>, + get_value(Node_Options, 'type', undefined)), + % + xmlel_field_jid_single(<<"pubsub#creator">>, <<"Node creator">>, + pubsub_tools:jid_to_string(Creator)), + % + xmlel_field_text_single(<<"pubsub#creation_date">>, <<"Creation date">>, + Creation_Date), + % + xmlel_field_text_single(<<"pubsub#title">>, <<"Node title">>, + get_value(Node_Options, 'title', undefined)), + % + xmlel_field_text_single(<<"pubsub#description">>, <<"Node description">>, + get_value(Node_Options, 'description', undefined)), + % + xmlel_field_list_single(<<"pubsub#language">>, <<"Default language">>, + get_value(Node_Options, 'language', <<"en">>), []), + % + xmlel_field_jid_multi(<<"pubsub#contact">>, + <<"People to contact with questions">>, + get_value(Node_Options, 'contact', [])), + % + xmlel_field_jid_multi(<<"pubsub#owner">>, <<"Node owners">>, + [pubsub_tools:jid_to_string(Node_Owner) || Node_Owner <- Node_Owners]) + % +% xmlel_field_jid_multi(<<"pubsub#publisher">>, +% <<"Publishers to this node">>, +% [pubsub_tools:jid_to_string(Publisher) +% || Publisher +% <- mnesia:dirty_select(Table_Pubsub_State, +% [{#pubsub_state_dev{ +% id = {'$1', '_'}, +% nodeidx = NodeIdx, +% itemids = '$2', +% _ = '_'}, +% [{'=/=', '$2', []}], +% ['$1']}]) +% ] +% ), +% % +% xmlel_field_text_single(<<"pubsub#num_subscribers">>, +% <<"Number of subscribers to this node">>, +% list_to_binary(integer_to_list(length( +% mnesia:dirty_select(Table_Pubsub_State, +% [{#pubsub_state_dev{ +% id = {'$1', '_'}, +% nodeidx = NodeIdx, +% access = '$2', +% subscriptions = '$3', +% _ = '_'}, +% [{'=/=', '$2', 'pending'}, +% {'=/=', '$3', []}], +% ['$1']}]) +% ))) +% ) + ]). + +%% +-spec(iq_disco_items/9 :: +( + Suffix :: undefined | atom(), + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Privacy :: boolean(), + Entity :: xmpp_jid:usr_bare(), + Entity_Type :: 'local' | 'remote', + Plugin :: exmpp_pubsub:plugin(), + NodeId :: undefined | exmpp_pubsub:nodeId(), + Xmlels :: [] | [Xmlel_Set::xmlel(),...]) + -> {result, Xmlel_Query::xmlel()} + %%% +% | {error, 'feature-not-implemented', ?NS_DISCO_ITEMS} +% % +% | {error, 'item-not-found'} +). + +iq_disco_items(Suffix, Host, Pubsub_Host, Privacy, Entity, _Entity_Type, + Plugin, NodeId, _Xmlels) -> + case lists:member(?NS_DISCO_ITEMS, Plugin:features()) of + true -> disco_nodes(Privacy, NodeId, Suffix, Host, Pubsub_Host, Entity); + false -> {error, 'feature-not-implemented', ?NS_DISCO_ITEMS} + end. + +%% +-spec(disco_nodes/6 :: +( + Privacy :: boolean(), + NodeId :: undefined | exmpp_pubsub:nodeId(), + Suffix :: undefined | atom(), + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Entity :: xmpp_jid:usr_bare()) + -> {result, Xmlel_Query::xmlel()} + %%% + | {error, 'item-not-found'} +). + +disco_nodes(false, undefined, Suffix, _, Pubsub_Host, _) -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + [exmpp_pubsub:xmlel_item('disco#items', NodeId, Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + || #pubsub_node_dev{id = {_, NodeId}, options = Node_Options} + <- mnesia:dirty_match_object(table('pubsub_node', Suffix), + #pubsub_node_dev{ + id = {Pubsub_Host, '_'}, + level = 1, + _ = '_' + }) + ])}; +%% +disco_nodes(true, undefined, Suffix, Host, Pubsub_Host, Entity) -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + disco_nodes( + mnesia:dirty_match_object(table('pubsub_node', Suffix), + #pubsub_node_dev{ + id = {Pubsub_Host, '_'}, + _ = '_' + }), + Host, Entity, table('pubsub_state', Suffix), {[], [], []}))}; +%% +disco_nodes(false, NodeId, Suffix, _Host, Pubsub_Host, _Entity) -> + Table_Pubsub_Node = table('pubsub_node', Suffix), + case mnesia:dirty_read(Table_Pubsub_Node, {Pubsub_Host, NodeId}) of + [#pubsub_node_dev{idx = NodeIdx, options = Node_Options}] -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + [exmpp_pubsub:xmlel_item('disco#items', ItemId, + Pubsub_Host) + || #pubsub_item_dev{id = {ItemId, _}} + <- mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx) + ]; + 'collection' -> + lists:foldr(fun + (_NodeId, Xmlels_Item) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [#pubsub_node_dev{options = _Node_Options}] -> + [exmpp_pubsub:xmlel_item('disco#items', + _NodeId, Pubsub_Host, + get_value(_Node_Options, 'title', + undefined)) + |Xmlels_Item]; + _ -> + Xmlels_Item + end + end, [], get_value(Node_Options, 'children', [])) + end)}; + _ -> + {error, 'item_not-found'} + end; +%% +disco_nodes(true, NodeId, Suffix, Host, Pubsub_Host, Entity) -> + Table_Pubsub_Node = table('pubsub_node', Suffix), + case mnesia:dirty_read(Table_Pubsub_Node, {Pubsub_Host, NodeId}) of + [#pubsub_node_dev{idx = NodeIdx, owners = Node_Owners, + options = Node_Options}] -> + case lists:member(Entity, Node_Owners) of + true -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + false -> + Node_Access_Model = get_value(Node_Options, 'access_model'), + case + mnesia:dirty_index_match_object( + table('pubsub_state', Suffix), + #pubsub_state_dev{ + id = {Entity, NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx) + of + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'open' + andalso (Affiliation == 'member' + orelse + Affiliation == 'none' + orelse + Affiliation == 'publisher') -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + [] + when Node_Access_Model == 'open' -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + _ + when Node_Access_Model == 'open' -> + {error, 'item-not-found'}; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'presence' + andalso (Affiliation == 'member' + orelse + Affiliation == 'publisher' + orelse + Access == 'presence' + orelse + Access == 'roster') -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'presence' + andalso (Affiliation == 'outcast' + orelse + Affiliation == 'publish-only') -> + {error, 'item-not-found'}; + %% + _ + when Node_Access_Model == 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, Entity, + Node_Owners) + of + false -> + {error, 'item-not-found'}; + Node_Owner -> + {result, exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [Node_Owner], []}) + end)} + end; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'roster' + andalso (Affiliation == 'member' + orelse + Affiliation == 'publisher' + orelse + (Access == 'roster' + andalso + Affiliation == 'none')) -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'roster' + andalso (Affiliation == 'outcast' + orelse + Affiliation == 'publish-only') -> + {error, 'item-not-found'}; + %% + _ + when Node_Access_Model == 'roster' -> + Rosters_Groups_Allowed = get_value(Node_Options, + 'roster_groups_allowed', []), + case + is_contact_in_allowed_roster_groups(Entity, + Rosters_Groups_Allowed) + of + false -> + {error, 'item-not-found'}; + {Node_Owner, Roster_Group} -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + Rosters_Groups_Allowed, + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read( + Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, + 'children', [])), + Host, Entity, + table('pubsub_state', Suffix), + {[], [], [{Node_Owner, [Roster_Group]}]}) + end)} + end; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'authorize' + andalso ((Affiliation =/= 'publish-only' + andalso + Affiliation =/= 'outcast') + orelse + Access == 'authorize') -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + [] + when Node_Access_Model == 'authorize' -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read(Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + _ + when Node_Access_Model == 'authorize' -> + {error, 'item-not-found'}; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'whitelist' + andalso (Access == 'whitelist' + orelse + (Affiliation =/= 'none' + andalso + Affiliation =/= 'outcast' + andalso + Affiliation =/= 'publish-only')) -> + {result, + exmpp_pubsub:xmlel_query('disco#items', + case get_value(Node_Options, 'node_type') of + 'leaf' -> + disco_items( + mnesia:dirty_index_match_object( + table('pubsub_item', Suffix), + #pubsub_item_dev{ + id = {'_', NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx), + Host, Pubsub_Host, + get_value(Node_Options, 'access_model'), + get_value(Node_Options, + 'roster_groups_allowed', []), + Node_Owners, + Entity, + 'owner', + undefined, + {[], [], []}); + 'collection' -> + disco_nodes( + lists:foldr(fun + (_NodeId, Pubsub_Nodes) -> + case + mnesia:dirty_read( + Table_Pubsub_Node, + {Pubsub_Host, _NodeId}) + of + [Pubsub_Node] -> + [Pubsub_Node | Pubsub_Nodes]; + _ -> + Pubsub_Nodes + end + end, [], + get_value(Node_Options, 'children', [])), + Host, Entity, table('pubsub_state', Suffix), + {[], [], []}) + end)}; + %% + _ -> + {error, 'item-not-found'} + end + end; + _ -> + {error, 'item_not-found'} + end. + +%% +-spec(disco_nodes/5 :: +( + Pubsub_Nodes :: [] | mod_pubsub_dev:pubsub_nodes(), + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Table_Pubsub_State :: atom(), + Cache :: { + Xmlels_Items :: [Xmlel_Item::xmlel()], + Presence_Cache :: [Entity::xmpp_jid:usr_bare()], + Groups_Cache :: [{Entity::xmpp_jid:usr_bare(), Groups::[Group::binary(),...]}] + }) + -> Xmlels_Items::[Xmlel_Item::xmlel()] +). + +disco_nodes([] = _Pubsub_Nodes, _Host, _Entity, _Table_Pubsub_State, + {Xmlels_Item, _Presence_Cache, _Groups_Cache}) -> + Xmlels_Item; +%% +disco_nodes([#pubsub_node_dev{id = {Pubsub_Host, NodeId}, + idx = NodeIdx, owners = Node_Owners, options = Node_Options} | Pubsub_Nodes], + Host, Entity, Table_Pubsub_State, + {Xmlels_Item, Presence_Cache, Groups_Cache}) -> + disco_nodes(Pubsub_Nodes, Host, Entity, Table_Pubsub_State, + case lists:member(Entity, Node_Owners) of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + Node_Access_Model = get_value(Node_Options, 'access_model'), + case + mnesia:dirty_index_match_object(Table_Pubsub_State, + #pubsub_state_dev{ + id = {Entity, NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx) + of + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'open' + andalso (Affiliation == 'member' + orelse + Affiliation == 'none' + orelse + Affiliation == 'publisher') -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + [] + when Node_Access_Model == 'open' -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + _ + when Node_Access_Model == 'open' -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'presence' + andalso (Affiliation == 'member' + orelse + Affiliation == 'publisher' + orelse + Access == 'presence' + orelse + Access == 'roster') -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'presence' + andalso (Affiliation == 'outcast' + orelse + Affiliation == 'publish-only') -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + %% + _ + when Node_Access_Model == 'presence' + andalso Presence_Cache == [] -> + case + is_contact_subscribed_to_node_owners(Host, Entity, + Node_Owners) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + Node_Owner -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + [Node_Owner | Presence_Cache], Groups_Cache} + end; + %% + _ + when Node_Access_Model == 'presence' -> + case presence_cache(Presence_Cache, Node_Owners) of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_subscribed_to_node_owners(Host, + Entity, Node_Owners) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + Node_Owner -> + {[exmpp_pubsub:xmlel_item('disco#items', + NodeId, Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + [Node_Owner | Presence_Cache], Groups_Cache} + end + end; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'roster' + andalso (Affiliation == 'member' + orelse + Affiliation == 'publisher' + orelse + (Access == 'roster' + andalso + Affiliation == 'none')) -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + [#pubsub_state_dev{affiliation = Affiliation}] + when Node_Access_Model == 'roster' + andalso (Affiliation == 'outcast' + orelse + Affiliation == 'publish-only') -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + %% + _ + when Node_Access_Model == 'roster' + andalso Groups_Cache == [] -> + case + is_contact_in_allowed_roster_groups(Entity, + get_value(Node_Options, 'roster_groups_allowed')) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + {Node_Owner, Roster_Group} -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, + [{Node_Owner, [Roster_Group]} | Groups_Cache]} + end; + %% + _ + when Node_Access_Model == 'roster' -> + Rosters_Groups_Allowed = get_value(Node_Options, + 'roster_groups_allowed'), + case groups_cache(Groups_Cache, Rosters_Groups_Allowed) of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_in_allowed_roster_groups(Entity, + Rosters_Groups_Allowed) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + {Node_Owner, Roster_Group} -> + {[exmpp_pubsub:xmlel_item('disco#items', + NodeId, Pubsub_Host, + get_value(Node_Options, 'title', + undefined)) + |Xmlels_Item], + Presence_Cache, + case + lists:keyfind(Node_Owner, 1, + Groups_Cache) + of + {_, Groups} -> + lists:keyreplace(Node_Owner, 1, + Groups_Cache, + {Node_Owner, + [Roster_Group | Groups]}); + false -> + [{Node_Owner, [Roster_Group]} + | Groups_Cache] + end} + end + end; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'authorize' + andalso ((Affiliation =/= 'publish-only' + andalso + Affiliation =/= 'outcast') + orelse + Access == 'authorize') -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + [] + when Node_Access_Model == 'authorize' -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + _ + when Node_Access_Model == 'authorize' -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + %% + [#pubsub_state_dev{affiliation = Affiliation, access = Access}] + when Node_Access_Model == 'whitelist' + andalso (Access == 'whitelist' + orelse + (Affiliation =/= 'none' + andalso + Affiliation =/= 'outcast' + andalso + Affiliation =/= 'publish-only')) -> + {[exmpp_pubsub:xmlel_item('disco#items', NodeId, + Pubsub_Host, + get_value(Node_Options, 'title', undefined)) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + %% + _ -> + {Xmlels_Item, Presence_Cache, Groups_Cache} + end + end). + +%% +-spec(disco_items/10 :: +( + Pubsub_Items :: [] | mod_pubsub_dev:pubsub_items(), + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host_pubsub(), + Node_Access_Model :: pubsub_options:access_model(), + Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed(), + Node_Owners :: mod_pubsub_dev:node_owners(), + Entity :: xmpp_jid:usr_bare(), + Affiliation :: 'member' | 'none' | 'owner' | 'publisher', + Access :: undefined|'authorize'|'presence'|'roster'|'whitelist', + Cache :: { + Xmlels_Items :: [Xmlel_Item::xmlel()], + Presence_Cache :: [Entity::xmpp_jid:usr_bare()], + Groups_Cache :: [{Entity::xmpp_jid:usr_bare(), Groups::[Group::binary(),...]}] + }) + -> Xmlels_Items::[Xmlel_Item::xmlel()] +). + +disco_items([] = _Pubsub_Items, _Host, _Pubsub_Host, _Node_Access_Model, + _Rosters_Groups_Allowed, _Node_Owners, _Entity, _Affiliation, _Access, + {Xmlels_Items, _, _}) -> + Xmlels_Items; +%% +disco_items([#pubsub_item_dev{id = {ItemId, _}, options = Item_Options} | Pubsub_Items], + Host, Pubsub_Host, Node_Access_Model, Rosters_Groups_Allowed, Node_Owners, Entity, + Affiliation, Access, + {Xmlels_Item, Presence_Cache, Groups_Cache}) + when Item_Options == [] + orelse Affiliation == 'owner' + orelse Affiliation == 'publisher' -> + disco_items(Pubsub_Items, Host, Pubsub_Host, Node_Access_Model, + Rosters_Groups_Allowed, Node_Owners, Entity, Affiliation, Access, + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, Pubsub_Host) + | Xmlels_Item], Presence_Cache, Groups_Cache}); +%% +disco_items([#pubsub_item_dev{id = {ItemId, _}, options = Item_Options} | Pubsub_Items], + Host, Pubsub_Host, Node_Access_Model, Rosters_Groups_Allowed, Node_Owners, Entity, + Affiliation, Access, + {Xmlels_Item, Presence_Cache, Groups_Cache}) -> + disco_items(Pubsub_Items, Host, Pubsub_Host, Node_Access_Model, + Rosters_Groups_Allowed, Node_Owners, Entity, Affiliation, Access, + case get_value(Item_Options, 'access_model') of + Item_Access_Model + when Item_Access_Model == 'none' + orelse Item_Access_Model == 'open' + orelse (Item_Access_Model =/= 'roster' + andalso + Item_Access_Model == Node_Access_Model) + orelse (Item_Access_Model == 'presence' + andalso + (Node_Access_Model == 'roster' + orelse + Access == 'presence' + orelse + Access == 'roster')) + orelse Item_Access_Model == 'authorize' + orelse Item_Access_Model == 'whitelist' -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, Pubsub_Host) + | Xmlels_Item], Presence_Cache, Groups_Cache}; + %% + 'presence' + when Access == 'presence' + orelse Access == 'roster' -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, Pubsub_Host) + | Xmlels_Item], Presence_Cache, Groups_Cache}; + 'presence' + when Presence_Cache == [] -> + case + is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + Node_Owner -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, Pubsub_Host) + |Xmlels_Item], + [Node_Owner | Presence_Cache], Groups_Cache} + end; + 'presence' -> + case presence_cache(Presence_Cache, Node_Owners) of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, Pubsub_Host) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_subscribed_to_node_owners(Host, Entity, + Node_Owners) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + Node_Owner -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, + Pubsub_Host) + |Xmlels_Item], + [Node_Owner | Presence_Cache], Groups_Cache} + end + end; + 'roster' + when Rosters_Groups_Allowed == [] -> + case get_value(Item_Options, 'roster_groups_allowed') of + Item_Roster_Groups_Allowed + when Item_Roster_Groups_Allowed == 'none' + orelse Item_Roster_Groups_Allowed == [] -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + Item_Roster_Groups_Allowed -> + case + groups_cache(Groups_Cache, Item_Roster_Groups_Allowed) + of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, + Pubsub_Host) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_in_allowed_roster_groups(Entity, + Item_Roster_Groups_Allowed) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + {Node_Owner, Roster_Group} -> + {[exmpp_pubsub:xmlel_item('disco#items', + ItemId, Pubsub_Host) + |Xmlels_Item], + Presence_Cache, + case + lists:keyfind(Node_Owner, 1, + Groups_Cache) + of + {_, Groups} -> + lists:keyreplace(Node_Owner, 1, + Groups_Cache, + {Node_Owner, + [Roster_Group | Groups]}); + false -> + [{Node_Owner, [Roster_Group]} + | Groups_Cache] + end} + end + end + end; + 'roster' -> + case get_value(Item_Options, 'roster_groups_allowed') of + Item_Rosters_Groups_Allowed + when Item_Rosters_Groups_Allowed == 'none' + orelse Item_Rosters_Groups_Allowed == [] -> + case + groups_cache(Groups_Cache, Rosters_Groups_Allowed) + of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, + Pubsub_Host) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_in_allowed_roster_groups(Entity, + Rosters_Groups_Allowed) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + {Node_Owner, Roster_Group} -> + {[exmpp_pubsub:xmlel_item('disco#items', + ItemId, Pubsub_Host) + |Xmlels_Item], + Presence_Cache, + case + lists:keyfind(Node_Owner, 1, + Groups_Cache) + of + {_, Groups} -> + lists:keyreplace(Node_Owner, 1, + Groups_Cache, + {Node_Owner, + [Roster_Group | Groups]}); + false -> + [{Node_Owner, [Roster_Group]} + | Groups_Cache] + end} + end + end; + Item_Rosters_Groups_Allowed -> + case + groups_cache(Groups_Cache, Item_Rosters_Groups_Allowed) + of + true -> + {[exmpp_pubsub:xmlel_item('disco#items', ItemId, + Pubsub_Host) + |Xmlels_Item], + Presence_Cache, Groups_Cache}; + false -> + case + is_contact_in_allowed_roster_groups(Entity, + Item_Rosters_Groups_Allowed) + of + false -> + {Xmlels_Item, Presence_Cache, Groups_Cache}; + {Node_Owner, Roster_Group} -> + {[exmpp_pubsub:xmlel_item('disco#items', + ItemId, Pubsub_Host) + |Xmlels_Item], + Presence_Cache, + case + lists:keyfind(Node_Owner, 1, + Groups_Cache) + of + {_, Groups} -> + lists:keyreplace(Node_Owner, 1, + Groups_Cache, + {Node_Owner, + [Roster_Group | Groups]}); + false -> + [{Node_Owner, [Roster_Group]} + | Groups_Cache] + end} + end + end + end; + _ -> + {Xmlels_Item, Presence_Cache, Groups_Cache} + end). + + +%% +-spec(presence_cache/2 :: +( + Presence_Cache :: [Entity::xmpp_jid:usr_bare()], + Node_Owners :: [] | [Entity::xmpp_jid:usr_bare(),...]) + -> Presence_Cache::boolean() +). + +presence_cache([], _) -> + false; +%% +presence_cache([Entity | Presence_Cache], Node_Owners) -> + case lists:member(Entity, Node_Owners) of + true -> true; + false -> presence_cache(Presence_Cache, Node_Owners) + end. + +%% +-spec(groups_cache/2 :: +( + Groups_Cache :: [Group_Cache :: { + Entity::xmpp_jid:usr_bare(), + Groups::[Group::binary(),...] + }], + Rosters_Groups :: [Roster_Groups :: { + Entity::xmpp_jid:usr_bare(), + Groups::[Group::binary(),...] + },...]) + -> Groups_Cache::boolean() +). + +groups_cache([], _) -> + false; +%% +groups_cache([{Entity, Groups} | Groups_Cache], Rosters_Groups) -> + case lists:keyfind(Entity, 1, Rosters_Groups) of + {_, Roster_Groups} -> group_cache(Groups, Roster_Groups); + false -> groups_cache(Groups_Cache, Rosters_Groups) + end. + +%% +-spec(group_cache/2 :: +( + Groups :: [] | [Group::binary(),...], + Roster_Groups :: [Group::binary(),...]) + -> Group_Cache::boolean() +). + +group_cache([], _) -> + false; +%% +group_cache([Group | Groups], Roster_Groups) -> + case lists:member(Group, Roster_Groups) of + true -> true; + false -> group_cache(Groups, Roster_Groups) + end. diff --git a/src/mod_pubsub_ng/pubsub_groups.erl b/src/mod_pubsub_ng/pubsub_groups.erl new file mode 100644 index 000000000..c0ff2e96a --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_groups.erl @@ -0,0 +1,904 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_groups). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_acces_model/3, + check_acces_model/7, + check_publish_model/3, + get_entity_roster/1, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1, + get_resources/2, + get_resources_show/2, + rosters_groups_allowed_cache/2 +]). + +-import(pubsub_db_mnesia, +[ + table/2 +]). + + +-type(group() :: binary()). + +-type(node_group() :: group()). + +-type(roster_group() :: group()). + +-type(node_groups() :: [Node_Group::node_group(),...]). + +-type(roster_groups() :: [Roster_Group::roster_group(),...]). + +-type(nodeIdxs() :: [NodeIdx::exmpp_pubsub:nodeIdx(),...]). + +-type(n0de() + :: {NodeIdx :: exmpp_pubsub:nodeIdx(), + Groups :: node_groups()} +). + +-type(n0des() :: [Node::n0de(),...]). + +-type(subscription() + :: {NodeIdx :: exmpp_pubsub:nodeIdx(), + Groups :: roster_groups()} +). + +-type(subscriptions() :: [Subscription::subscription(),...]). + +-type(contact() + :: {Entity :: xmpp_jid:usr_bare(), + Subscriptions :: subscriptions()} +). + +-type(contacts() :: [Contact::contact(),...]). + + +-record(pubsub_groups, +{ + owner :: xmpp_jid:usr_bare(), + nodes = [] :: n0des(), + contacts = [] :: [] | contacts() +}). + + +create_table_pubsub_groups(Suffix) -> + mnesia:create_table(table('pubsub_groups', Suffix), + [{type, set}, + {disc_copies, [node()]}, + {record_name, pubsub_groups}, + {attributes, record_info(fields, pubsub_groups)}]). + + + +%% +-spec(register_groups/4 :: +( + Suffix :: atom(), + NodeIdx :: exmpp_pubsub:nodeIdx(), + Owner :: xmpp_jid:usr_bare(), + Groups :: [] | node_groups()) + -> undefined + % + | {Subscribers :: [Entity::xmpp_jid:usr_bare()], + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +register_groups(Suffix, NodeIdx, Owner, [] = _Groups) -> + case + mnesia:read(Table_Pubsub_Groups = table('pubsub_groups', 'dev'), + Owner, write) + of + %% + [] -> + undefined; + %% + [#pubsub_groups{nodes = [{NodeIdx, _Node_Groups}]} = Pubsub_Groups] -> + mnesia:delete_object(Table_Pubsub_Groups, Pubsub_Groups, write), + % + {_Subscribers = [], + _Unsubscribers = lists:foldl(fun + ({Entity, Subscriptions}, Unsubscribers) -> + case lists:keymember(NodeIdx, 1, Subscriptions) of + true -> [Entity | Unsubscribers]; + false -> Unsubscribers + end + % + end, [], Pubsub_Groups#pubsub_groups.contacts)}; + %% + [Pubsub_Groups] -> + case lists:keyfind(NodeIdx, 1, Pubsub_Groups#pubsub_groups.nodes) of + {_NodeIdx, Node_Groups} -> + {Contacts, Unsubscribers} = subscriptions1(NodeIdx, + Pubsub_Groups#pubsub_groups.contacts), + % + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = Contacts + }, + write), + % + {_Subscribers = [], + _Unsubscribers = Unsubscribers}; + false -> + undefined + end + end; +%% +register_groups(Suffix, NodeIdx, Owner, Groups) -> + case + mnesia:read(Table_Pubsub_Groups = table('pubsub_groups', 'dev'), Owner, + write) + of + [] -> + {Contacts, Subscribers} = subscriptions2(NodeIdx, Owner, Groups), + % + mnesia:write(Table_Pubsub_Groups, + #pubsub_groups{ + owner = Owner, + nodes = [{NodeIdx, Groups}], + contacts = Contacts + }, + write), + % + {_Subscribers = Subscribers, + _Unsubscribers = []}; + [Pubsub_Groups] -> + case lists:keyfind(NodeIdx, 1, Pubsub_Groups#pubsub_groups.nodes) of + {_NodeIdx, Node_Groups} -> + case diff_groups(Groups, Node_Groups) of + %% + {[] = _New_Groups, [] = _Old_Groups} -> + {_Subscribers = [], + _Unsubscribers = []}; + %% + {[] = _New_Groups, Old_Groups} -> + {Contacts, Unsubscribers} + = subscriptions3(NodeIdx, Old_Groups, + Pubsub_Groups#pubsub_groups.contacts), + % + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + nodes = lists:keyreplace(NodeIdx, 1, + Pubsub_Groups#pubsub_groups.nodes, + {NodeIdx, Groups}), + contacts = Contacts + }, + write), + % + {_Subscribers = [], + _Unsubscribers = Unsubscribers}; + %% + {_New_Groups, _Old_Groups} -> + {Contacts, Subscribers, Unsubscribers} + = subscriptions4(NodeIdx, Owner, Node_Groups, + Pubsub_Groups#pubsub_groups.contacts), + % + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + nodes = lists:keyreplace(NodeIdx, 1, + Pubsub_Groups#pubsub_groups.nodes, + {NodeIdx, Node_Groups}), + contacts = Contacts + }, + write), + % + {Subscribers = Subscribers, + _Unsubscribers = Unsubscribers} + end; + false -> + {Contacts, Subscribers} + = subscriptions5(NodeIdx, Owner, Groups, + _Contacts = Pubsub_Groups#pubsub_groups.contacts), + % + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + nodes = [{NodeIdx, Groups} + | Pubsub_Groups#pubsub_groups.nodes], + contacts = Contacts + }, + write), + % + {_Subscribers = Subscribers, + _Unsubscribers = []} + end + end. + + +%% TODO : fix this hook managment +-spec(monitor_contacts/3 :: +( + _ :: 'subscribed', + Entity :: xmpp_jid:usr_bare(), + Jid_Contacts :: xmpp_jid:entity_bare()) + -> undefined + % + | {Server :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Contact :: xmpp_jid:usr_bare(), + _ :: {'subscribed', NodeIdxs :: nodeIdxs()}} +). + +monitor_contacts('subscribed', {_, Server, _} = Entity, Jid_Contact) -> + case roster_groups(Entity, Contact = jlid:jid_to_lower(Jid_Contact)) of + [] -> + undefined; + Roster_Groups -> + case + mnesia:read(Table_Pubsub_Groups = table('pubsub_groups', dev), + _Owner = Entity, write) + of + [] -> + undefined; + [Pubsub_Groups] -> + case + filter_subscriptions(Pubsub_Groups#pubsub_groups.nodes, + Roster_Groups) + of + [] -> + undefined; + Subscriptions -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = case + lists:keyreplace(Contact, 1, + Pubsub_Groups#pubsub_groups.contacts, + {Contact, Subscriptions}) + of + _Contacts + when _Contacts == + Pubsub_Groups#pubsub_groups.contacts -> + [{Contact, Subscriptions} + | Pubsub_Groups#pubsub_groups.contacts]; + Contacts -> + Contacts + end + }, + write), + {Server, Entity, Contact, + {'subscribed', + _NodeIdx = lists:map(fun + ({NodeIdx, _Groups}) -> + NodeIdx + end, Subscriptions)}} + end + end + end. + + +%% +-spec(monitor_groups/2 :: +( + Server :: xmpp_jid:raw_jid_component_bare(), + Roster :: #roster{groups :: [] | roster_groups()}) + -> undefined + % + | {Server :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Diff_NodeIdxs :: {NodeIdxs_Subscriptions :: [] | nodeIdxs(), + NodeIdxs_Unsubscriptions :: [] | nodeIdxs()}} +). + +monitor_groups(Server, #roster{us = {U,S}} = _Roster) -> + case + mnesia:read(Table_Pubsub_Groups = table('pubsub_groups', dev), + _Owner = {U,S,undefined}, write) + of + [] -> + undefined; + [Pubsub_Groups] -> + case + lists:keyfind(_Roster#roster.jid, 1, + Pubsub_Groups#pubsub_groups.contacts) + of %% + {_Entity, Old_Subscriptions} + when _Roster#roster.subscription == 'delete' + orelse _Roster#roster.subscription == 'none' -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = lists:keydelete(_Roster#roster.jid, 1, + Pubsub_Groups#pubsub_groups.contacts) + }, + write), + {Server, + _Entity = _Roster#roster.jid, + _Diff_NodeIdxs = diff_nodeidxs(_New_Subscriptions = [], + Old_Subscriptions)}; + %% + {_Entity, Old_Subscriptions} + when (_Roster#roster.subscription == 'both' + orelse + _Roster#roster.subscription == 'from') + andalso _Roster#roster.groups == [] -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = lists:keydelete(_Roster#roster.jid, 1, + Pubsub_Groups#pubsub_groups.contacts) + }, + write), + {Server, + _Roster#roster.jid, + _Diff_NodeIdxs = diff_nodeidxs(_New_Subscriptions = [], + Old_Subscriptions)}; + %% + {_Entity, Old_Subscriptions} + when _Roster#roster.subscription == 'both' + orelse _Roster#roster.subscription == 'from' -> + case + filter_subscriptions(Pubsub_Groups#pubsub_groups.nodes, + _Roster#roster.groups) + of + [] -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = lists:keydelete(_Roster#roster.jid, + 1, Pubsub_Groups#pubsub_groups.contacts) + }, + write), + {Server, _Roster#roster.jid, + diff_nodeidxs(_New_Subscriptions = [], + Old_Subscriptions)}; + New_Subscriptions -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = lists:keyreplace(_Roster#roster.jid, + 1, Pubsub_Groups#pubsub_groups.contacts, + {_Roster#roster.jid, New_Subscriptions}) + }, + write), + {Server, + _Entity = _Roster#roster.jid, + _Diff_NodeIdxs = diff_nodeidxs(New_Subscriptions, + Old_Subscriptions)} + end; + %% + false + when (_Roster#roster.subscription == 'both' + orelse + _Roster#roster.subscription == 'from') + andalso _Roster#roster.groups =/= [] -> + case + filter_subscriptions(Pubsub_Groups#pubsub_groups.nodes, + _Roster#roster.groups) + of + [] -> + undefined; + New_Subscriptions -> + mnesia:write(Table_Pubsub_Groups, + Pubsub_Groups#pubsub_groups{ + contacts = [{_Roster#roster.jid, New_Subscriptions} + | Pubsub_Groups#pubsub_groups.contacts] + }, + write), + {Server, + _Entity = _Roster#roster.jid, + _Diff_NodeIdxs = diff_nodeidxs(New_Subscriptions, + _Old_Subscriptions = [])} + end; + %% + _ -> + undefined + end + end. + + + +%% +%% +-spec(subscriptions1/2 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Old_Contacts :: [] | contacts()) + -> Diff_Contacts :: {New_Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions1(NodeIdx, Contacts) -> + _Diff_Contacts = subscriptions1(NodeIdx, _Old_Contacts = Contacts, + {_New_Contacts = [], _Unsubscribers = []}). + + +%% +-spec(subscriptions1/3 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Old_Contacts :: [] | contacts(), + Diff_Contacts :: {Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]}) + -> Diff_Contacts :: {New_Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions1(_NodeIdx, [] = _Old_Contacts, Diff_Contacts) -> + Diff_Contacts; +%% +subscriptions1(NodeIdx, [{Entity, Subscriptions} | Old_Contacts], + {Contacts, Unsubscribers}) -> + subscriptions1(NodeIdx, Old_Contacts, + _Diff_Contacts = case lists:keydelete(NodeIdx, 1, Subscriptions) of + [] -> + {_Contacts = Contacts, + _Unsubscribers = [Entity | Unsubscribers]}; + Subscriptions -> + {_Contacts = [{Entity, Subscriptions} | Contacts], + _Unsubscribers = Unsubscribers}; + New_Subscriptions -> + {_Contacts = [{Entity, New_Subscriptions} | Contacts], + _Unsubscribers = [Entity | Unsubscribers]} + end). + + +%% +-spec(subscriptions2/3 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Owner :: xmpp_jid:usr_bare(), + Node_Groups :: node_groups()) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions2(NodeIdx, Owner, Node_Groups) -> + subscriptions2(NodeIdx, _Rosters = get_entity_roster(Owner), Node_Groups, + {_New_Contacts = [], _Subscribers = []}). + +%% +-spec(subscriptions2/4 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Rosters :: [Roster::#roster{groups :: [] | roster_groups()}], + Node_Groups :: node_groups(), + Diff_Contacts :: {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()]}) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions2(_NodeIdx, [] = _Rosters, _Node_Groups, Diff_Contacts) -> + Diff_Contacts; +%% +subscriptions2(NodeIdx, [_Roster | Rosters], Node_Groups, {Contacts, Subscribers}) + when _Roster#roster.groups =/= [] -> + subscriptions2(NodeIdx, Rosters, Node_Groups, + _Diff_Contacts = case + filter_groups(_Roster#roster.groups, Node_Groups, []) + of + [] -> + {_Contacts = Contacts, + _Subscribers = Subscribers}; + Roster_Groups -> + {_Contacts = [{_Roster#roster.jid, [{NodeIdx, Roster_Groups}]} + | Contacts], + _Subscribers = [_Roster#roster.jid | Subscribers]} + end); +%% +subscriptions2(NodeIdx, [_Roster | Rosters], Node_Groups, Diff_Contacts) -> + subscriptions2(NodeIdx, Rosters, Node_Groups, Diff_Contacts). + + +%% +-spec(subscriptions3/3 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Old_Groups :: roster_groups(), + Contacts :: contacts() | []) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). +subscriptions3(NodeIdx, Old_Groups, Contacts) -> + subscriptions3(NodeIdx, Contacts, Old_Groups, {[],[]}). + +%% +-spec(subscriptions3/4 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + Old_Contacts :: contacts() | [], + Old_Groups :: roster_groups(), + Diff_Contacts :: {Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]}) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions3(_NodeIdx, [] = _Contacts, _Old_Groups, Diff_Contacts) -> + Diff_Contacts; +%% +subscriptions3(NodeIdx, [{Entity, Subscriptions} | Old_Contacts], Old_Groups, + {Contacts, Unsubscribers}) -> + subscriptions3(NodeIdx, Old_Contacts, Old_Groups, + _Diff_Contacts = case lists:keyfind(NodeIdx, 1, Subscriptions) of + {_NodeIdx, Roster_Groups} -> + case delete_groups(Old_Groups, Roster_Groups) of + [] -> + {_Contacts = Contacts, + _Unsubscribers = [Entity | Unsubscribers]}; + Groups -> + {_Contacts = [{Entity, + lists:keyreplace(NodeIdx, 1, + Subscriptions, {NodeIdx, Groups})} + | Contacts], + _Unsubscribers = Unsubscribers} + end; + false -> + {_Contacts = [{Entity, Subscriptions} | Contacts], + _Unsubscribers = Unsubscribers} + end). + + +%% +-spec(subscriptions4/4 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + _ :: xmpp_jid:usr_bare() + | [Roster::#roster{groups :: [] | roster_groups()}], + Node_Groups :: node_groups(), + _ :: [] | contacts() + | {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()], + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]}) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()], + Unsubscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions4(_NodeIdx, [] = _Rosters, _Node_Groups, Diff_Contacts) -> + Diff_Contacts; +%% +subscriptions4(NodeIdx, [_Roster | Rosters], Node_Groups, + {Contacts, Subscribers, Unsubscribers}) + when _Roster#roster.groups =/= [] -> + subscriptions4(NodeIdx, Rosters, Node_Groups, + _Diff_Contacts = case + {lists:keyfind(Entity = _Roster#roster.jid, 1, Contacts), + filter_groups(_Roster#roster.groups, Node_Groups, [])} + of + %% + {false, [] = _Groups} -> + {_Contacts = Contacts, + _Subscribers = Subscribers, + _Unsubscribers = Unsubscribers}; + %% + {false, Groups} -> + {_Contacts = [{Entity, [{NodeIdx, Groups}]} | Contacts], + _Subscribers = [Entity | Subscribers], + _Unsubscribers = Unsubscribers}; + %% + {{_Entity, [{NodeIdx, _Roster_Groups}] = _Subscriptions}, [] = _Groups} -> + {_Contacts = lists:keydelete(Entity, 1, Contacts), + _Subscribers = Subscribers, + _Unsubscribers = [Entity | Unsubscribers]}; + %% + {{_Entity, Subscriptions}, [] = _Groups} -> + case lists:keydelete(NodeIdx, 1, Subscriptions) of + Subscriptions -> + {_Contacts = Contacts, + _Subscribers = Subscribers, + _Unsubscribers = Unsubscribers}; + New_Subscriptions -> + {_Contacts = lists:keyreplace(Entity, 1, Contacts, + {Entity, New_Subscriptions}), + _Subscribers = Subscribers, + _Unsubscribers = [Entity | Unsubscribers]} + end; + {{_Entity, Subscriptions}, Groups} -> + case lists:keyreplace(NodeIdx, 1, Subscriptions, {NodeIdx, Groups}) of + Subscriptions -> + {_Contacts = lists:keyreplace(Entity, 1, Contacts, + {Entity, + [{NodeIdx, Groups} | Subscriptions]}), + _Subscribers = [Entity | Subscribers], + _Unsubscribers = Unsubscribers}; + New_Subscriptions -> + {_Contacts = lists:keyreplace(Entity, 1, Contacts, + {Entity, New_Subscriptions}), + _Subscribers = Subscribers, + _Unsubscribers = Unsubscribers} + end + end); +%% +subscriptions4(NodeIdx, [_Roster | Rosters], Node_Groups, Diff_Contacts) -> + subscriptions4(NodeIdx, Rosters, Node_Groups, Diff_Contacts); +%% +subscriptions4(NodeIdx, Owner, Node_Groups, Contacts) -> + subscriptions4(NodeIdx, _Rosters = get_entity_roster(Owner), Node_Groups, + _Diff_Contacts = {Contacts, _Subscribers = [], _Unsubscribers = []}). + + + +-spec(subscriptions5/4 :: +( + NodeIdx :: exmpp_pubsub:nodeIdx(), + _ :: xmpp_jid:usr_bare() + | [Roster::#roster{groups :: [] | roster_groups()}], + Node_Groups :: node_groups(), + _ :: [] | contacts() + | {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()]}) + -> Diff_Contacts :: {Contacts :: [] | contacts(), + Subscribers :: [Entity::xmpp_jid:usr_bare()]} +). + +subscriptions5(NodeIdx, [_Roster | Rosters], Node_Groups, {Contacts, Subscribers}) + when _Roster#roster.groups =/= [] -> + subscriptions5(NodeIdx, Rosters, Node_Groups, + _Diff_Contacts = case + filter_groups(_Roster#roster.groups, Node_Groups, []) + of + [] -> + {_Contacts = Contacts, + _Subscribers = Subscribers}; + Groups -> + {_Contacts = case + lists:keyfind(_Roster#roster.jid, 1, Contacts) + of + false -> + [{_Roster#roster.jid, [{NodeIdx, Groups}]} | Contacts]; + {Entity, Subscriptions} -> + lists:keyreplace(Entity, 1, Contacts, + {Entity, [{NodeIdx, Groups} | Subscriptions]}) + end, + _Subscribers = [_Roster#roster.jid | Subscribers]} + end); +%% +subscriptions5(NodeIdx, [_Roster | Rosters], Node_Groups, Diff_Contacts) -> + subscriptions5(NodeIdx, Rosters, Node_Groups, Diff_Contacts); +%% +subscriptions5(NodeIdx, Owner, Node_Groups, Contacts) -> + subscriptions5(NodeIdx, _Rosters = get_entity_roster(Owner), Node_Groups, + _Diff_Contacts = {Contacts, _Subscribers = []}). + + +%% +-spec(delete_groups/2 :: +( + Old_Groups :: roster_groups(), + Groups :: [] | roster_groups()) + -> Groups :: [] | roster_groups() +). + +delete_groups(_Old_Groups, [] = _Groups) -> + _Groups = []; +%% +delete_groups([] = _Old_Groups, Groups) -> + Groups; +%% +delete_groups([Old_Group | Old_Groups], Groups) -> + _Groups = delete_groups(Old_Groups, lists:delete(Old_Group, Groups)). + + +%% +-spec(diff_groups/2 :: +( + Groups :: node_groups() | [], + _ :: node_groups() + | {New_Groups :: [] | node_groups(), + Old_Groups :: [] | node_groups()}) + -> Diff_Groups :: {New_Groups :: [] | node_groups(), + Old_Groups :: [] | node_groups()} +). + +diff_groups([] = _Groups, Diff_Groups) -> %% when is_tuple(Diff_Groups) + Diff_Groups; +%% +diff_groups([Group | Groups], {New_Groups, Old_Groups}) -> + diff_groups(Groups, + _Diff_Groups = case lists:delete(Group, Old_Groups) of + Old_Groups -> {[Group | New_Groups], Old_Groups}; + Old_Groups2 -> {New_Groups, Old_Groups2} + end); +%% +diff_groups(New_Groups, Old_Groups) -> + diff_groups(New_Groups, {[], Old_Groups}). + + +%% +-spec(filter_groups/3 :: +( + Roster_Groups :: roster_groups() | [], + Node_Groups :: node_groups(), + Groups :: [] | roster_groups()) + -> Groups :: [] | roster_groups() +). + +filter_groups([] = _Roster_Groups, _Node_Groups, Groups) -> + Groups; +%% +filter_groups([Roster_Group | Roster_Groups], Node_Groups, Groups) -> + filter_groups(Roster_Groups, Node_Groups, + _Groups = case lists:member(Roster_Group, Node_Groups) of + true -> [Roster_Group | Groups]; + false -> Groups + end). + + +%% +-spec(filter_subscriptions/2 :: +( + Nodes :: n0des(), + Roster_Groups :: roster_groups()) + -> Subscriptions :: [] | subscriptions() +). + +filter_subscriptions(Nodes, Roster_Groups) -> + _Subscriptions = filter_subscriptions(Nodes, Roster_Groups, []). + +%% +-spec(filter_subscriptions/3 :: +( + Nodes :: n0des(), + Roster_Groups :: roster_groups(), + Subscriptions :: [] | subscriptions()) + -> Subscriptions :: [] | subscriptions() +). + +filter_subscriptions([] = _Nodes, _Roster_Groups, Subscriptions) -> + Subscriptions; +%% +filter_subscriptions([{NodeIdx, Node_Groups} | Nodes], Roster_Groups, + Subscriptions) -> + filter_subscriptions(Nodes, Roster_Groups, + _Subscriptions = case filter_groups(Roster_Groups, Node_Groups, []) of + [] -> Subscriptions; + Groups -> [{NodeIdx, Groups} | Subscriptions] + end). + + +%% +-spec(diff_nodeidxs/2 :: +( + New_Subscriptions :: [] | subscriptions(), + Old_Subscriptions :: [] | subscriptions()) + -> Diff_NodeIdxs :: {NodeIdxs_Subscriptions :: [] | nodeIdxs(), + NodeIdxs_Unsubscriptions :: [] | nodeIdxs()} +). + +diff_nodeidxs(New_Subscriptions, Old_Subscriptions) -> + _Diff_NodeIdxs = diff_nodeidxs(New_Subscriptions, Old_Subscriptions, []). + +%% +-spec(diff_nodeidxs/3 :: +( + New_Subscriptions :: [] | subscriptions(), + Old_Subscriptions :: [] | subscriptions(), + NodeIdxs_Subscriptions :: [] | nodeIdxs()) + -> Diff_NodeIdxs :: {NodeIdxs_Subscriptions :: [] | nodeIdxs(), + NodeIdxs_Unsubscriptions :: [] | nodeIdxs()} +). + +diff_nodeidxs([] = _New_Subscriptions, Old_Subscriptions, + NodeIdxs_Subscriptions) -> + _Diff_NodeIdxs = { + _NodeIdxs_Subscriptions = NodeIdxs_Subscriptions, + _NodeIdxs_Unsubscriptions = lists:map(fun + ({NodeIdx, _Roster_Groups}) -> + NodeIdx + end, Old_Subscriptions)}; +%% +diff_nodeidxs([{NodeIdx, _Groups} | New_Subscriptions], [] = Old_Subscriptions, + NodeIdxs_Subscriptions) -> + diff_nodeidxs(New_Subscriptions, Old_Subscriptions, + [NodeIdx | NodeIdxs_Subscriptions]); +%% +diff_nodeidxs([{NodeIdx, _Groups} | New_Subscriptions], Old_Subscriptions, + NodeIdxs_Subscriptions) -> + case lists:keydelete(NodeIdx, 1, Old_Subscriptions) of + Old_Subscriptions -> + diff_nodeidxs(New_Subscriptions, Old_Subscriptions, + [NodeIdx | NodeIdxs_Subscriptions]); + Subscriptions -> + diff_nodeidxs(New_Subscriptions, _Old_Subscriptions = Subscriptions, + NodeIdxs_Subscriptions) + end. + + +%% TODO : use a new mod_roster_api for this call +-spec(roster_groups/2 :: +( + Entity :: xmpp_jid:usr_bare(), + Contact :: xmpp_jid:usr_bare()) + -> Roster_Groups :: [] | roster_groups() +). + +roster_groups({User, Server, _} = _Entity, Contact) -> + _Roster_Groups = case + gen_storage:transaction(Server, rosteritem, + fun() -> + gen_storage:select(Server, rostergroup, + [{'=', user_host_jid, {User, Server, Contact}}]) + end) + of + {atomic, _RosterGroups} -> + lists:map(fun + (_RosterGroup) -> + _RosterGroup#roster.groups + end, _RosterGroups); + _Error + -> {error, 'internal-server-error'} + end. + + +%% +-spec(select_roster_groups/1 :: +( + Entity :: xmpp_jid:usr_entity()) + -> Roster_Groups :: [] | roster_groups() +). + +select_roster_groups({User, Server, _} = Entity) -> + _Roster_Groups = case + gen_storage:transaction(Server, rosteritem, + fun() -> + gen_storage:select(Server, rostergroup, + [{'=', user_host_jid, {User, Server, '_'}}]) + end) + of + {atomic, _RosterGroups} -> + lists:map(fun + (_RosterGroup) -> + _RosterGroup#roster.groups + end, _RosterGroups); + _Error + -> {error, 'internal-server-error'} + end. + + +%% TESTS + +%% +monitor_roster_groups(Server, Roster) -> + ?INFO_MSG("SERVER ~p ROSTER ~p", [Server, Roster]), + Result = pubsub_db:transaction('mnesia', ?MODULE, monitor_groups, + [Server, Roster]), + ?INFO_MSG("MONITOR ROSTER GROUPS ~p", [Result]). + + +monitor_contacts2('subscribed', Entity, Jid_Contact) -> + Result = pubsub_db:transaction('mnesia', ?MODULE, monitor_contacts, + ['subscribed', Entity, Jid_Contact]), + ?INFO_MSG("MONITOR CONTACTS2 ~p", [Result]). + +register1(NodeIdx, User, Groups) -> + Server = <<"localhost">>, + Owner = {list_to_binary(atom_to_list(User)), Server, undefined}, + Groups_B = lists:map(fun(Group) -> list_to_binary(atom_to_list(Group)) end, Groups), + register_groups(dev, NodeIdx, Owner, Groups_B). + +register(NodeIdx, User, Groups) -> + pubsub_db:transaction('mnesia', ?MODULE, register1, [NodeIdx, User, Groups]). diff --git a/src/mod_pubsub_ng/pubsub_hooks.erl b/src/mod_pubsub_ng/pubsub_hooks.erl new file mode 100644 index 000000000..c5c1fc28a --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_hooks.erl @@ -0,0 +1,570 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_hooks). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + +-import(pubsub_tools, +[ + get_option/2, + get_option/3, + get_value/2, + get_value/3, + set_value/3, + %% + check_access_model/3, + check_access_model/7, + check_publish_model/3, + is_contact_subscribed_to_node_owners/3, + is_contact_in_allowed_roster_groups/2, + has_subscriptions/1 +]). + +-import(pubsub_db_mnesia, +[ + table/2 +]). + +%% 'node_config#send_last_published_item' +-spec(presence_online/3 :: +( + From :: xmpp_jid:entity_full(), + To :: xmpp_jid:entity_full(), + C2SPid :: pid()) + -> 'ok' +). + +presence_online(#jid{luser = U, lserver = S, lresource = R} = Jid, Jid, C2SPid) -> + ?INFO_MSG("PRESENCE ONLINE From ~p ~n, To ~p ~n, C2SPid ~p ~n", + [Jid, Jid, C2SPid]), + Pubsub_Features = node_flat_dev:pubsub_features(), + case lists:member(<<"last-published">>, Pubsub_Features) of + true -> + spawn(?MODULE, last_published_items, + [_Host = <<"localhost">>, {U,S,R}]); + false -> + ok + end; +%% +presence_online(_, _, _) -> + ok. + + +%% +-spec(last_published_items/2 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_full()) + -> 'ok' +). + +last_published_items(Host, {U,S,_R} = Entity) -> + Table_Pubsub_State = table('pubsub_state', 'dev'), + Table_Pubsub_Node = table('pubsub_node', 'dev'), + Table_Pubsub_Last_Item = table('pubsub_last_item', 'dev'), + lists:foreach(fun + (State) -> + last_published_items(Host, Entity, Table_Pubsub_Node, + Table_Pubsub_Last_Item, State) + end, + mnesia:dirty_select(Table_Pubsub_State, + [{#pubsub_state_dev{ + id = {{U,S,undefined}, '$1'}, + affiliation = '$2', + access = '$3', + subscriptions = '$4', + _ = '_' + }, + [{'=/=', '$2', 'outcast'}, + {'=/=', '$3', 'pending'}, + {'=/=', '$4', []}], + ['$$']}])). + +%% +-spec(last_published_items/5 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_full(), + Table_Pubsub_Node :: atom(), + Table_Pubsub_Last_Item :: atom(), + State :: [exmpp_pubsub:nodeIdx() | + 'member' | 'owner' | 'publisher' | + _ | + exmpp_pubsub:subscriptions(),... ]) + -> 'ok' +). + +last_published_items(Host, {_, _, Online_Resource} = Entity, + Table_Pubsub_Node, Table_Pubsub_Last_Item, + [NodeIdx, Affiliation, _Access, Subscriptions]) -> + case mnesia:dirty_index_read(Table_Pubsub_Node, NodeIdx, idx) of + [#pubsub_node_dev{ + id = {Pubsub_Host, NodeId}, + owners = Node_Owners, + options = Node_Options + }] -> + case get_value(Node_Options, 'send_last_published_item') of + 'on_sub_and_presence' -> + case mnesia:dirty_read(Table_Pubsub_Last_Item, NodeIdx) of + [#pubsub_last_item_dev{ + id = ItemId, + creation = {DateTime, Publisher}, + payload = Payload, + options = Item_Options + }] -> + spawn(pubsub_broadcast, broadcast_publish_last2, + [Host, Pubsub_Host, NodeId, Node_Options, + Node_Owners, + [{ItemId, Item_Options, Payload, Publisher, DateTime}], + Entity, Affiliation, + lists:foldl(fun + ({Subscription_State, SubId, Resource, Subscription_Options}, + Acc) + when Subscription_State =/= 'pending' + andalso (Resource == undefined + orelse + Resource == Online_Resource) -> + [{Subscription_State, SubId, + Online_Resource, Subscription_Options} + | Acc]; + (_Subscription, Acc) -> + Acc + end, [], Subscriptions)]); + _ -> + ok + end; + _ -> + ok + end; + _ -> + ok + end. + +%% 'node_config#purge_offline' +%% 'node_config#tempsub' +%% 'subscribe_options#expire' + +-spec(presence_offline/3 :: +( + _ :: _, + Jid :: xmpp_jid:entity_full(), + _ :: _) + -> 'ok' +). + +presence_offline({_DateTime, _Pid}, + #jid{luser = U, lserver = S, lresource = Offline_Resource} = _Jid, _) -> + Host = <<"localhost">>, + Pubsub_Features = node_flat_dev:pubsub_features(), + Subscribe = lists:member(<<"subscribe">>, Pubsub_Features), + Leased_Subscription = lists:member(<<"leased-subscription">>, Pubsub_Features), + Purge_Nodes = lists:member(<<"purge-nodes">>, Pubsub_Features), + Persistent_Items = lists:member(<<"persistent-items">>, Pubsub_Features), + Subscription_Notifications = lists:member(<<"subscription-notifications">>, + Pubsub_Features), + Table_Pubsub_State = table('pubsub_state', 'dev'), + Table_Pubsub_Node = table('pubsub_node', 'dev'), + Online_Resources = case ejabberd_sm:get_user_resources(U, S) of + [] -> false; + _ -> true + end, + lists:foreach(fun + (Pubsub_State) -> + presence_offline(Host, + _Entity = {U,S,undefined}, + Table_Pubsub_State, + Pubsub_State, + Table_Pubsub_Node, + Offline_Resource, + Online_Resources, + {Subscribe, + Leased_Subscription, + Purge_Nodes, + Persistent_Items, + Subscription_Notifications}) + end, + mnesia:dirty_match_object(Table_Pubsub_State, + #pubsub_state_dev{ + id = {{U,S,undefined}, '_'}, + _ = '_' + })). + +%% +-spec(presence_offline/8 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Table_Pubsub_State :: atom(), + Pubsub_State :: mod_pubsub_dev:pubsub_state(), + Table_Pubsub_Node :: atom(), + Offline_Resource :: xmpp_jid:resource_jid(), + Online_Resources :: boolean(), + Pubsub_Features :: {Subscribe :: boolean(), + Leased_Subscription :: boolean(), + Purge_Nodes :: boolean(), + Persistent_Items :: boolean(), + Subscription_Notifications :: boolean()}) + -> 'ok' +). + +presence_offline(Host, Entity, Table_Pubsub_State, Pubsub_State, + Table_Pubsub_Node, Offline_Resource, Online_Resources, + {Subscribe, Leased_Subscription, Purge_Nodes, Persistent_Items, + Subscription_Notifications}) -> + case + mnesia:dirty_index_read(Table_Pubsub_Node, + Pubsub_State#pubsub_state_dev.nodeidx, idx) + of + [#pubsub_node_dev{id = {Pubsub_Host, NodeId}} = Pubsub_Node] -> + Node_Options = Pubsub_Node#pubsub_node_dev.options, + + case + Subscribe == true + andalso + get_value(Node_Options, 'tempsub') == true + of + true -> + case + filter_tempsub_subscriptions( + get_value(Node_Options, 'notify_sub'), + Pubsub_State#pubsub_state_dev.subscriptions, + Offline_Resource, Online_Resources, {[], []}) + of + %% + {_Unexpired_Subscriptions, [] = _Expired_Subscriptions} -> + ok; + %% + {[] = _Unexpired_Subscriptions, Expired_Subscriptions} + when Pubsub_State#pubsub_state_dev.affiliation == 'member' + andalso Pubsub_State#pubsub_state_dev.itemids == [] -> + mnesia:dirty_delete_object(Table_Pubsub_State, + Pubsub_State), + Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + notify_subscriptions(Host, Pubsub_Host, + Entity, NodeId, Notification_Type, + _Recipients = case Subscription_Notifications of + true -> + case + lists:member(Entity, + Pubsub_Node#pubsub_node_dev.owners) + of + true -> + Pubsub_Node#pubsub_node_dev.owners; + false -> + [Entity + | Pubsub_Node#pubsub_node_dev.owners] + end; + false -> + Pubsub_Node#pubsub_node_dev.owners + end, + Expired_Subscriptions), + ok; + %% + {Unexpired_Subscriptions, Expired_Subscriptions} -> + mnesia:dirty_write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + subscriptions = Unexpired_Subscriptions + }), + Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + notify_subscriptions(Host, Pubsub_Host, + Entity, NodeId, Notification_Type, + _Recipients = case Subscription_Notifications of + true -> + case + lists:member(Entity, + Pubsub_Node#pubsub_node_dev.owners) + of + true -> + Pubsub_Node#pubsub_node_dev.owners; + false -> + [Entity + | Pubsub_Node#pubsub_node_dev.owners] + end; + false -> + Pubsub_Node#pubsub_node_dev.owners + end, + Expired_Subscriptions), + ok + end; + _ -> + case Subscribe == true andalso Leased_Subscription == true of + true -> + case + filter_expired_subscriptions( + get_value(Node_Options, 'notify_sub'), + Pubsub_State#pubsub_state_dev.subscriptions, + Offline_Resource, Online_Resources, {[], []}) + of + %% + {_Unexpired_Subscriptions, [] = _Expired_Subscriptions} -> + ok; + %% + {[] = _Unexpired_Subscriptions, Expired_Subscriptions} + when Pubsub_State#pubsub_state_dev.affiliation == 'member' -> + mnesia:dirty_delete_object(Table_Pubsub_State, + Pubsub_State), + Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + notify_subscriptions(Host, Pubsub_Host, + Entity, NodeId, Notification_Type, + _Recipients = case + Subscription_Notifications + of + true -> + case + lists:member(Entity, + Pubsub_Node#pubsub_node_dev.owners) + of + true -> + Pubsub_Node#pubsub_node_dev.owners; + false -> + [Entity + |Pubsub_Node#pubsub_node_dev.owners] + end; + false -> + Pubsub_Node#pubsub_node_dev.owners + end, + Expired_Subscriptions), + ok; + %% + {Unexpired_Subscriptions, Expired_Subscriptions} -> + mnesia:dirty_write(Table_Pubsub_State, + Pubsub_State#pubsub_state_dev{ + subscriptions = Unexpired_Subscriptions + }), + Notification_Type = get_value(Node_Options, + 'notification_type', 'headline'), + notify_subscriptions(Host, Pubsub_Host, + Entity, NodeId, Notification_Type, + _Recipients = case + Subscription_Notifications + of + true -> + case + lists:member(Entity, + Pubsub_Node#pubsub_node_dev.owners) + of + true -> + Pubsub_Node#pubsub_node_dev.owners; + false -> + [Entity + | Pubsub_Node#pubsub_node_dev.owners] + end; + false -> + Pubsub_Node#pubsub_node_dev.owners + end, + Expired_Subscriptions), + ok + end; + false -> + ok + end + end; + [] -> + ok + end. + +%% +-spec(notify_subscriptions/7 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Pubsub_Host :: exmpp_pubsub:host(), + Entity :: xmpp_jid:usr_bare(), + NodeId :: exmpp_pubsub:nodeId(), + Notification_Type :: 'message' | 'headline', + Recipients :: [Entity::xmpp_jid:usr_bare(),...], + Subscriptions :: [Subscription::exmpp_pubsub:subscription(),...]) + -> 'ok' +). + +notify_subscriptions(Host, Pubsub_Host, {U,S,_} = _Entity, NodeId, + Notification_Type, Recipients, Subscriptions) -> + Pubsub_Component_Jid = jlib:make_jid(<<>>, Pubsub_Host, <<>>), + Jids = [pubsub_tools:make_jid(Recipient) || Recipient <- Recipients], + lists:foreach(fun + ({_, SubId, Resource, _}) -> + Subscriber = jlib:jid_to_string({U,S,Resource}), + lists:foreach(fun + (Recipient) -> + spawn(pubsub_broadcast, notify_subscription, + [Host, NodeId, Pubsub_Component_Jid, Recipient, + Notification_Type, {Subscriber, 'none', SubId}]) + end, Jids) + end, Subscriptions). + +%% +filter_purged_offline_itemids([Publisher_ItemId | Publisher_ItemIds], + Table_Pubsub_Item, NodeIdx, {U,S,R} = _Entity, + {Node_ItemIds, Unpurged_ItemIds, Purged_ItemIds}) -> + filter_purged_offline_itemids(Publisher_ItemIds, Table_Pubsub_Item, NodeIdx, + {U,S,R}, + case + mnesia:dirty_index_match_object(Table_Pubsub_Item, + #pubsub_item_dev{ + id = {Publisher_ItemId, NodeIdx}, + nodeidx = NodeIdx, + _ = '_' + }, + nodeidx) + of + [#pubsub_item_dev{creation = {_DateTime, {U,S,_}}} = Pubsub_Item] -> + mnesia:dirty_delete_object(Table_Pubsub_Item, Pubsub_Item); + _ -> + {Node_ItemIds, + [Publisher_ItemId | Unpurged_ItemIds], + Purged_ItemIds} + end). + + +%% +-spec(filter_tempsub_subscriptions/5 :: +( + Notify_Sub :: boolean() | 'none', + Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Offline_Resource :: xmpp_jid:resource_jid(), + Online_Resources :: boolean(), + Filtered_TempSub_Subscriptions :: { + Unexpired_Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Expired_Subscriptions :: [] | exmpp_pubsub:subscriptions() + }) + -> Filtered_TempSub_Subscriptions :: { + Unexpired_Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Expired_Subscriptions :: [] | exmpp_pubsub:subscriptions() + } +). + +filter_tempsub_subscriptions(_Notify_Sub, [] = _Subscriptions, + _Offline_Resource, _Online_Resources, Filtered_TempSub_Subscriptions) -> + Filtered_TempSub_Subscriptions; +%% +filter_tempsub_subscriptions(Notify_Sub, + [{Subscription_State, SubId, Resource, Subscription_Options} | Subscriptions], + Offline_Resource, Online_Resources, + {Unexpired_Subscriptions, Expired_Subscriptions}) + when ((Online_Resources == false + andalso + (Resource == undefined orelse Resource == Offline_Resource)) + orelse + (Online_Resources == true + andalso + Resource == Offline_Resource)) -> + filter_tempsub_subscriptions(Notify_Sub, Subscriptions, Offline_Resource, + Online_Resources, + {Unexpired_Subscriptions, + _Expired_Subscriptions = case Notify_Sub of + true -> + [{Subscription_State, SubId, Resource, Subscription_Options} + | Expired_Subscriptions]; + _ -> + Expired_Subscriptions + end}); +%% +filter_tempsub_subscriptions(Notify_Sub, [Subscription | Subscriptions], + Offline_Resource, Online_Resources, + {Unexpired_Subscriptions, Expired_Subscriptions}) -> + filter_tempsub_subscriptions(Notify_Sub, Subscriptions, Offline_Resource, + Online_Resources, + {[Subscription | Unexpired_Subscriptions], Expired_Subscriptions}). + +%% +-spec(filter_expired_subscriptions/5 :: +( + Notify_Sub :: boolean() | 'none', + Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Offline_Resource :: xmpp_jid:resource_jid(), + Online_Resources :: boolean(), + Filtered_Expired_Subscriptions :: { + Unexpired_Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Expired_Subscriptions :: [] | exmpp_pubsub:subscriptions() + }) + -> Filtered_Expired_Subscriptions :: { + Unexpired_Subscriptions :: [] | exmpp_pubsub:subscriptions(), + Expired_Subscriptions :: [] | exmpp_pubsub:subscriptions() + } +). + +filter_expired_subscriptions(_Notify_Sub, [] = _Subscriptions, _Offline_Resource, + _Online_Resources, Filtered_Expired_Subscriptions) -> + Filtered_Expired_Subscriptions; +%% +filter_expired_subscriptions(Notify_Sub, + [{Subscription_State, SubId, Resource, Subscription_Options} | Subscriptions], + Offline_Resource, Online_Resources, + {Unexpired_Subscriptions, Expired_Subscriptions}) + when Subscription_Options =/= [] + andalso ((Online_Resources == false + andalso + (Resource == undefined orelse Resource == Offline_Resource)) + orelse + (Online_Resources == true + andalso + Resource == Offline_Resource)) -> + filter_expired_subscriptions(Notify_Sub, Subscriptions, Offline_Resource, + Online_Resources, + case get_value(Subscription_Options, 'expire') of + 'presence' -> + {Unexpired_Subscriptions, + _Expired_Subscriptions = case Notify_Sub of + true -> + [{Subscription_State, SubId, Resource, Subscription_Options} + | Expired_Subscriptions]; + false -> + Expired_Subscriptions + end}; + _ -> + {[{Subscription_State, SubId, Resource, Subscription_Options} + | Unexpired_Subscriptions], + Expired_Subscriptions} + end); +%% +filter_expired_subscriptions(Notify_Sub, [Subscription | Subscriptions], + Offline_Resource, Online_Resources, + {Unexpired_Subscriptions, Expired_Subscriptions}) -> + filter_expired_subscriptions(Notify_Sub, Subscriptions, Offline_Resource, + Online_Resources, + {[Subscription | Unexpired_Subscriptions], Expired_Subscriptions}). + + +%% +roster_process_item(Roster_Item, Server) -> + spawn(pubsub_groups, monitor_roster_groups, [Server, Roster_Item]), + Roster_Item. + + + +roster_in_subscription(Boolean, User, Server, Jid_Contact, + 'subscribed' = _Subscription_Type) -> + spawn(pubsub_groups, monitor_contacts2, + ['subscribed', {User, Server, undefined}, Jid_Contact]), + Boolean; +roster_in_subscription(Boolean, _User, _Server, _JID, _SubscriptionType) -> + Boolean. diff --git a/src/mod_pubsub_ng/pubsub_index_dev.erl b/src/mod_pubsub_ng/pubsub_index_dev.erl new file mode 100644 index 000000000..72a8c82e6 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_index_dev.erl @@ -0,0 +1,113 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Christophe Romain <christophe.romain@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%% important note: +%% new/1 and free/2 MUST be called inside a transaction bloc + +-module(pubsub_index_dev). +-author('christophe.romain@process-one.net'). + +-include("pubsub_dev.hrl"). + +-export([ + init/1, + new/2, + free/3 +]). + +-import(pubsub_db_mnesia, +[ + table/2 +]). + + +%% +-spec(init/1 :: +( + Suffix::atom()) -> 'ok' +). + +init(Suffix) -> + mnesia:create_table(table('pubsub_index_dev', Suffix), + [{disc_copies, [node()]}, + {record_name, pubsub_index_dev}, + {attributes, record_info(fields, pubsub_index_dev)}]). + +%% +-spec(new/2 :: +( + Suffix :: atom(), + Index :: exmpp_pubsub:index()) + -> Idx::exmpp_pubsub:nodeIdx() +). + +new(Suffix, Index) -> + case + mnesia:read(Table_Pubsub_Index = table('pubsub_index_dev', Suffix), + Index, write) + of + [#pubsub_index_dev{free = []} = Pubsub_Index] -> + mnesia:write(Table_Pubsub_Index, + Pubsub_Index#pubsub_index_dev{ + last = Idx = Pubsub_Index#pubsub_index_dev.last + 1 + }, + write), + Idx; + [#pubsub_index_dev{free = [Idx|Free]} = Pubsub_Index] -> + mnesia:write(Table_Pubsub_Index, + Pubsub_Index#pubsub_index_dev{ + free = Free + }, + write), + Idx; + _ -> + mnesia:write(Table_Pubsub_Index, + #pubsub_index_dev{index = Index, last = 1, free = []}, write), + 1 + end. + +%% +-spec(free/3 :: +( + Suffix :: atom(), + Index :: exmpp_pubsub:index(), + Idx :: exmpp_pubsub:nodeIdx()) + -> 'ok' +). + +free(Suffix, Index, Idx) -> + case + mnesia:read(Table_Pubsub_Index = table('pubsub_index_dev', Suffix), + Index, write) + of + [Pubsub_Index] -> + mnesia:write(Table_Pubsub_Index, + Pubsub_Index#pubsub_index_dev{ + free = [Idx | Pubsub_Index#pubsub_index_dev.free] + }, + write); + _ -> + ok + end. diff --git a/src/mod_pubsub_ng/pubsub_options.erl b/src/mod_pubsub_ng/pubsub_options.erl new file mode 100644 index 000000000..46c9c2dd4 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_options.erl @@ -0,0 +1,3369 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_options). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + +-import(xml, +[ + get_tag_cdata/1, + get_tag_attr_s/2, + remove_cdata/1 +]). + + +-import(pubsub_tools, +[ + get_entity_roster/1, + is_jid/1 +]). + + +%% -------------------------------------------------------------------- +%% Documentation / type definitions. +%% -------------------------------------------------------------------- + +%% Node Options +% pubsub#node_config FORM_TYPE + +% Defined options values + +%-- Access Model --% +-export_type([ + access_model/0, + access_model_open/0, + access_model_presence/0, + access_model_roster/0, + access_model_authorize/0, + access_model_whitelist/0 +]). + +%% @type access_model_open() = 'open'. +-type(access_model_open() :: 'open'). + +%% @type access_model_presence() = 'presence'. +-type(access_model_presence() :: 'presence'). + +%% @type access_model_roster() = 'roster'. +-type(access_model_roster() :: 'roster'). + +%% @type access_model_authorize() = 'authorize'. +-type(access_model_authorize() :: 'authorize'). + +%% @type access_model_whitelist() = 'whitelist'. +-type(access_model_whitelist() :: 'whitelist'). + +%% @type access_model() = Open | Presence | Roster | Authorize | Whitelist +%% Open = pubsub_options:access_model_open() +%% Presence = pubsub_options:access_model_presence() +%% Roster = pubsub_options:access_model_roster() +%% Authorize = pubsub_options:access_model_authorize() +%% Whitelist = pubsub_options:access_model_whitelist(). +-type(access_model() :: pubsub_options:access_model_open() + | pubsub_options:access_model_presence() + | pubsub_options:access_model_roster() + | pubsub_options:access_model_authorize() + | pubsub_options:access_model_whitelist() +). + +%% + +%-- Children_Association_Policy --% +-export_type([ + children_association_policy/0, + %% + children_association_policy_all/0, + children_association_policy_owners/0, + children_association_policy_whitelist/0 +]). + +%% @type children_association_policy_all() = 'all'. +-type(children_association_policy_all() :: 'all'). + +%% @type children_association_policy_owners() = 'owners'. +-type(children_association_policy_owners() :: 'owners'). + +%% @type children_association_policy_whitelist() = 'whitelist'. +-type(children_association_policy_whitelist() :: 'whitelist'). + +%% @type children_association_policy() = All | Owners | Whitelist +%% All = pubsub_options:children_association_policy_all() +%% Owners = pubsub_options:children_association_policy_owners() +%% Whitelist = pubsub_options:children_association_policy_whitelist(). +-type(children_association_policy() + :: pubsub_options:children_association_policy_all() + | pubsub_options:children_association_policy_owners() + | pubsub_options:children_association_policy_whitelist() +). + +%% + +%-- ItemReply -- %% +-export_type([ + item_reply/0, + item_reply_owner/0, + item_reply_publisher/0 +]). + +%% @type(item_reply_owner() = 'owner'). +-type(item_reply_owner() :: 'owner'). + +%% @type(item_reply_publisher() = 'publisher'). +-type(item_reply_publisher() :: 'publisher'). + +%% @type(item_reply() = Owner | Publisher). +-type(item_reply() :: pubsub_options:item_reply_owner() + | pubsub_options:item_reply_publisher() +). + + +-export_type([ + language/0, + description/0, + title/0 +]). + +-type(language() :: binary()). + +-type(description() :: binary()). + +-type(title() :: binary()). + +%% + +%-- Node types --%% + +-export_type([ + node_type/0, + node_type_leaf/0, + node_type_collection/0 +]). + +%% @type node_type_collection() = 'collection'. +-type(node_type_collection() :: 'collection'). + +%% @type node_type_leaf() = 'leaf'. +-type(node_type_leaf() :: 'leaf'). + +%% @type node_type() = Leaf | Collection +%% Leaf = pubsub_options:node_type_leaf() +%% Collection = pubsub_options:node_type_collection() +-type(node_type() :: pubsub_options:node_type_leaf() + | pubsub_options:node_type_collection() +). + +%% + +%-- Notification Types --%% +-export_type([ + notification_type/0, + notification_type_chat/0, + notification_type_headline/0, + notification_type_normal/0 +]). + +-type(notification_type_chat() :: 'chat'). + +-type(notification_type_headline() :: 'headline'). + +-type(notification_type_normal() :: 'normal'). + +-type(notification_type() + :: pubsub_options:notification_type_headline() + | pubsub_options:notification_type_normal() + %| pubsub_options:notification_type_chat() +). + +%% + +%-- Publish Model --% +-export_type([ + publish_model/0, + publish_model_publishers/0, + publish_model_subscribers/0, + publish_model_open/0 +]). + +%% @type publish_model_publishers() = 'publishers'. +-type(publish_model_publishers() :: 'publishers'). + +%% @type publish_model_subscribers() = 'subscribers'. +-type(publish_model_subscribers() :: 'subscribers'). + +%% @type publish_model_open() = 'open'. +-type(publish_model_open() :: 'open'). + +%% @type publish_model() = Publishers | Subscribers | Open +%% Publishers = pubsub_options:publish_model_publishers() +%% Subscribers = pubsub_options:publish_model_subscribers() +%% Open = pubsub_options:publish_model_open() +-type(publish_model() :: pubsub_options:publish_model_publishers() + | pubsub_options:publish_model_subscribers() + | pubsub_options:publish_model_open() +). + + +-export_type([ + roster_group/0, + roster_groups/0, + roster_groups_allowed/0, + rosters_groups_allowed/0 +]). + +-type(roster_group() :: binary()). +-type(roster_groups() :: [pubsub_options:roster_group()]). +-type(roster_groups_allowed() + :: {Node_Owner::xmpp_jid:usr_bare(), + Roster_Groups::pubsub_options:roster_groups()} +). +-type(rosters_groups_allowed() + :: [Roster_Group_Allowed::pubsub_options:roster_groups_allowed()] +). + +%% + +%-- Send Last Published Item --% +-export_type([ + send_last_published_item/0, + %% + send_last_published_item_never/0, + send_last_published_item_on_sub/0, + send_last_published_item_on_sub_and_presence/0 +]). + +%% @type send_last_published_item_never() = 'never'. +-type(send_last_published_item_never() :: 'never'). + +%% @type send_last_published_item_on_sub() = 'on_sub'. +-type(send_last_published_item_on_sub() :: 'on_sub'). + +%% @type send_last_published_item_on_sub_and_presence() = 'on_sub_and_presence'. +-type(send_last_published_item_on_sub_and_presence() :: 'on_sub_and_presence'). + +%% @type send_last_published_item() = Never | On_Sub | On_Sub_And_Presence +%% Never = pubsub_options:send_last_published_item_never() +%% On_Sub = pubsub_options:send_last_published_item_on_sub() +%% On_Sub_And_Presence = pubsub_options:send_last_published_item_on_sub_and_presence(). +-type(send_last_published_item() + :: pubsub_options:send_last_published_item_never() + | pubsub_options:send_last_published_item_on_sub() + | pubsub_options:send_last_published_item_on_sub_and_presence() +). + +%% + + +-export_type([ + option_node_access_model/0, + option_node_children_association_policy/0, + option_node_children_association_whitelist/0, + option_node_children/0, + option_node_children_max/0, + option_node_collections/0, + option_node_children_max_collections/0, + option_node_children_max_multi_collections/0, + option_node_contact/0, + option_node_deliver_notifications/0, + option_node_deliver_payloads/0, + option_node_description/0, + option_node_item_expire/0, + option_node_itemreply/0, + option_node_language/0, + option_node_max_items/0, + option_node_max_payload_size/0, + option_node_node_type_leaf/0, + option_node_node_type_collection/0, + option_node_node_type/0, + option_node_notification_type/0, + option_node_notify_config/0, + option_node_notify_delete/0, + option_node_notify_retract/0, + option_node_notify_sub/0, + option_node_persist_items/0, + option_node_presence_based_delivery/0, + option_node_publish_model/0, + option_node_purge_offline/0, + option_node_roster_groups_allowed/0, + option_node_send_last_published_item/0, + option_node_tempsub/0, + option_node_subscribe/0, + option_node_title/0, + option_node_type/0 +]). + +-type(option_node_access_model() :: + {Key :: 'access_model', + Value :: pubsub_options:access_model()} +). + +%-type(option_node_body_xslt() :: +% {Key :: 'body_xslt(', +% Value :: binary()} +%). + +-type(option_node_children_association_policy() :: + {Key :: 'children_association_policy', + Value :: pubsub_options:children_association_policy()} +). + +-type(option_node_children_association_whitelist() :: + {Key :: 'children_association_whitelist', + Value :: [Jid_Entity::xmpp_jid:raw_jid_entity_bare()]} +). + +-type(option_node_children() :: + {Key :: 'children', + Value :: [NodeId::exmpp_pubsub:nodeId()]} +). + +-type(option_node_children_max() :: + {Key :: 'children_max', + Value :: undefined | non_neg_integer()} +). + +-type(option_node_children_max_collections() :: + {Key :: 'children_max', + Value :: undefined | 0..1} +). + +-type(option_node_children_max_multi_collections() :: + {Key :: 'children_max', + Value :: undefined | non_neg_integer()} +). + + +-type(option_node_collections() :: + {Key :: 'collection', + Value :: [NodeId::exmpp_pubsub:nodeId()]} +). + +-type(option_node_contact() :: + {Key :: 'contact', + Value :: [Jid_Entity::xmpp_jid:raw_jid_entity()]} +). + +%-type(option_node_dataform_xslt() :: +% {Key :: 'dataform_xslt', +% Value :: binary()} +%). + +-type(option_node_deliver_notifications() :: + {Key :: 'deliver_notifications', + Value :: boolean()} +). + +-type(option_node_deliver_payloads() :: + {Key :: 'deliver_payloads', + Value :: boolean()} +). + +-type(option_node_description() :: + {Key :: 'description', + Value :: undefined | pubsub_options:description()} +). + +-type(option_node_item_expire() :: + {Key :: 'item_expire', + Value :: undefined | non_neg_integer()} +). + +-type(option_node_itemreply() :: + {Key :: 'itemreply', + Value :: pubsub_options:item_reply()} +). + +-type(option_node_language() :: + {Key :: 'language', + Value :: undefined | pubsub_options:language()} +). + +-type(option_node_max_items() :: + {Key :: 'max_items', + Value :: undefined | non_neg_integer()} +). + +-type(option_node_max_payload_size() :: + {Key :: 'max_payload_size', + Value :: undefined | non_neg_integer()} +). + +-type(option_node_node_type_leaf() :: + {Key :: 'node_type', + Value :: pubsub_options:node_type_leaf()} +). + +-type(option_node_node_type_collection() :: + {Key :: 'node_type', + Value :: pubsub_options:node_type_collection()} +). + +-type(option_node_node_type() + :: pubsub_options:option_node_node_type_leaf() + | pubsub_options:option_node_node_type_collection() +). + +-type(option_node_notification_type() :: + {Key :: 'notification_type', + Value :: pubsub_options:notification_type()} +). + +-type(option_node_notify_config() :: + {Key :: 'notify_config', + Value :: boolean()} +). + +-type(option_node_notify_delete() :: + {Key :: 'notify_delete', + Value :: boolean()} +). + +-type(option_node_notify_retract() :: + {Key :: 'notify_retract', + Value :: boolean()} +). + +-type(option_node_notify_sub() :: + {Key :: 'notify_sub', + Value :: boolean()} +). + +-type(option_node_persist_items() :: + {Key :: 'persist_items', + Value :: boolean()} +). + +-type(option_node_presence_based_delivery() :: + {Key :: 'presence_based_delivery', + Value :: boolean()} +). + +-type(option_node_publish_model() :: + {Key :: 'publish_model', + Value :: pubsub_options:publish_model()} +). + +-type(option_node_purge_offline() :: + {Key :: 'purge_offline', + Value :: boolean()} +). + +-type(option_node_roster_groups_allowed() :: + {Key :: 'roster_groups_allowed', + Value :: pubsub_options:rosters_groups_allowed()} +). + +-type(option_node_send_last_published_item() :: + {Key :: 'send_last_published_item', + Value :: pubsub_options:send_last_published_item()} +). + +-type(option_node_tempsub() :: + {Key :: 'tempsub', + Value :: boolean()} +). + +-type(option_node_subscribe() :: + {Key :: 'subscribe', + Value :: boolean()} +). + +-type(option_node_title() :: + {Key :: 'title', + Value :: undefined | pubsub_options:title()} +). + +-type(option_node_type() :: + {Key :: 'type', + Value :: undefined | binary()} +). + + + +%-- Node Option Type (local or remote user ; leaf or collection node) --% +-export_type([ + option_node/0, + options_node/0, + %% + option_node_leaf/0, + option_node_collection/0, + %% + options_node_leaf/0, + options_node_collection/0 +]). + +-type(option_node_leaf() + :: pubsub_options:option_node_access_model() + | pubsub_options:option_node_collections() + | pubsub_options:option_node_contact() + | pubsub_options:option_node_deliver_notifications() + | pubsub_options:option_node_deliver_payloads() + | pubsub_options:option_node_description() + | pubsub_options:option_node_item_expire() + | pubsub_options:option_node_itemreply() + | pubsub_options:option_node_language() + | pubsub_options:option_node_max_items() + | pubsub_options:option_node_max_payload_size() + | pubsub_options:option_node_node_type_leaf() + | pubsub_options:option_node_notification_type() + | pubsub_options:option_node_notify_config() + | pubsub_options:option_node_notify_delete() + | pubsub_options:option_node_notify_retract() + | pubsub_options:option_node_notify_sub() + | pubsub_options:option_node_persist_items() + | pubsub_options:option_node_presence_based_delivery() + | pubsub_options:option_node_publish_model() + | pubsub_options:option_node_purge_offline() + | pubsub_options:option_node_roster_groups_allowed() + | pubsub_options:option_node_send_last_published_item() + | pubsub_options:option_node_tempsub() + | pubsub_options:option_node_subscribe() + | pubsub_options:option_node_title() + | pubsub_options:option_node_type() +). + +%% +-type(option_node_collection() + :: pubsub_options:option_node_access_model() + | pubsub_options:option_node_children_association_policy() + | pubsub_options:option_node_children_association_whitelist() + | pubsub_options:option_node_children() + | pubsub_options:option_node_children_max() + | pubsub_options:option_node_collections() + | pubsub_options:option_node_contact() + | pubsub_options:option_node_deliver_notifications() + | pubsub_options:option_node_deliver_payloads() + | pubsub_options:option_node_description() + | pubsub_options:option_node_item_expire() + | pubsub_options:option_node_itemreply() + | pubsub_options:option_node_language() + | pubsub_options:option_node_max_items() + | pubsub_options:option_node_max_payload_size() + | pubsub_options:option_node_node_type_collection() + | pubsub_options:option_node_notification_type() + | pubsub_options:option_node_notify_config() + | pubsub_options:option_node_notify_delete() + | pubsub_options:option_node_notify_retract() + | pubsub_options:option_node_notify_sub() + | pubsub_options:option_node_persist_items() + | pubsub_options:option_node_presence_based_delivery() + | pubsub_options:option_node_publish_model() + | pubsub_options:option_node_purge_offline() + | pubsub_options:option_node_roster_groups_allowed() + | pubsub_options:option_node_send_last_published_item() + | pubsub_options:option_node_tempsub() + | pubsub_options:option_node_subscribe() + | pubsub_options:option_node_title() + | pubsub_options:option_node_type() +). + +%% +-type(option_node() + :: pubsub_options:option_node_leaf() + | pubsub_options:option_node_collection() +). + +%% +-type(options_node_leaf() + :: [pubsub_options:option_node_leaf(),...] +). + +%% +-type(options_node_collection() + :: [pubsub_options:option_node_collection(),...] +). + +%% +-type(options_node() + :: pubsub_options:options_node_leaf() + | pubsub_options:options_node_collection() +). + +%-- Publish-Options --% +%% pubsub#publish-options FORM_TYPE +-export_type([ + option_item_access_model/0, + option_item_deliver_notifications/0, + option_item_deliver_payloads/0, + option_item_item_expire/0, + option_item_itemreply/0, +%%option_item_max_payload_size/0, + option_item_notification_type/0, + option_item_notify_config/0, + option_item_notify_retract/0, + option_item_persist_items/0, + option_item_presence_based_delivery/0, + option_item_publish_model/0, + option_item_purge_offline/0, + option_item_roster_groups_allowed/0, + option_item_send_last_published_item/0, + option_item_type/0 +]). + +-type(option_item_access_model() :: + {Key :: 'access_model', + Value :: pubsub_options:access_model()} +). + +-type(option_item_deliver_notifications() :: + {Key :: 'deliver_notifications', + Value :: boolean()} +). + +-type(option_item_deliver_payloads() :: + {Key :: 'deliver_payloads', + Value :: boolean()} +). + +-type(option_item_item_expire() :: + {Key :: 'item_expire', + Value :: undefined | non_neg_integer()} +). + +-type(option_item_itemreply() :: + {Key :: 'itemreply', + Value :: pubsub_options:item_reply()} +). + +%-type(option_item_max_payload_size() :: +% {Key :: 'max_payload_size', +% Value :: undefined | non_neg_integer()} +%). + +-type(option_item_notification_type() :: + {Key :: 'notification_type', + Value :: pubsub_options:notification_type()} +). + +-type(option_item_notify_config() :: + {Key :: 'notify_config', + Value :: boolean()} +). + +-type(option_item_notify_retract() :: + {Key :: 'notify_retract', + Value :: boolean()} +). + +-type(option_item_persist_items() :: + {Key :: 'persist_items', + Value :: boolean()} +). + +-type(option_item_presence_based_delivery() :: + {Key :: 'presence_based_delivery', + Value :: boolean()} +). + +-type(option_item_publish_model() :: + {Key :: 'publish_model', + Value :: pubsub_options:publish_model()} +). + +-type(option_item_purge_offline() :: + {Key :: 'purge_offline', + Value :: boolean()} +). + +-type(option_item_roster_groups_allowed() :: + {Key :: 'roster_groups_allowed', + Value :: pubsub_options:rosters_groups_allowed()} +). + +-type(option_item_send_last_published_item() :: + {Key :: 'send_last_published_item', + Value :: pubsub_options:send_last_published_item()} +). + +-type(option_item_type() :: + {Key :: 'type', + Value :: undefined | binary()} +). + +-export_type([ + option_item/0, + options_item/0 +]). + +-type(option_item() + :: pubsub_options:option_item_access_model() + | pubsub_options:option_item_deliver_notifications() + | pubsub_options:option_item_deliver_payloads() + | pubsub_options:option_item_item_expire() + | pubsub_options:option_item_itemreply() +%% | pubsub_options:option_item_max_payload_size() + | pubsub_options:option_item_notification_type() + | pubsub_options:option_item_notify_config() + | pubsub_options:option_item_notify_retract() + | pubsub_options:option_item_persist_items() + | pubsub_options:option_item_presence_based_delivery() + | pubsub_options:option_item_publish_model() + | pubsub_options:option_item_purge_offline() + | pubsub_options:option_item_roster_groups_allowed() + | pubsub_options:option_item_send_last_published_item() + | pubsub_options:option_item_type() +). + +%% +-type(options_item() + :: [pubsub_options:option_item(),...] +). + + +%-- Subscription Options --%% +-export_type([ + option_subscription_deliver/0, + option_subscription_expire/0, + option_subscription_expire_presence/0, + option_subscription_expire_datetime/0, + option_subscription_include_body/0, + option_subscription_show_values/0, + option_subscription_subscription_type/0, + option_subscription_subscription_depth/0 +]). + +%% +-type(option_subscription_deliver() :: + {Key :: 'deliver', + Value :: boolean()} +). + +%-type(option_subscription_digest() :: +% {Key :: 'digest', +% Value :: boolean()} +%). + +%-type(option_subscription_digest_frequency() :: +% {Key :: 'digest_frequency', +% Value :: undefined | non_neg_integer()} +%). + +-type(option_subscription_expire_presence() :: + {Key :: 'expire', + Value :: undefined | 'presence'} +). + +-type(option_subscription_expire_datetime() :: + {Key :: 'expire', + Value :: undefined | erlang:timestamp()} +). + +-type(option_subscription_expire() + :: pubsub_options:option_subscription_expire_presence() + | pubsub_options:option_subscription_expire_datetime() +). + +-type(option_subscription_include_body() :: + {Key :: 'include_body', + Value :: boolean()} +). + +-type(option_subscription_show_values() :: + {Key :: 'show-values', + Value :: ['away' | 'chat' | 'dnd' | 'online' | 'xa']} +). + +-type(option_subscription_subscription_type() :: + {Key :: 'subscription_type', + Value :: 'all' | 'items' | 'nodes'} +). + +-type(option_subscription_subscription_depth() :: + {Key :: 'subscription_depth', + Value :: 'all' | exmpp_pubsub:level()} +). + + +-export_type([ + option_subscription/0, + %% + option_subscription_leaf/0, + option_subscription_collection/0, + %% + option_subscription_leaf_local/0, + option_subscription_leaf_remote/0, + %% + option_subscription_collection_local/0, + option_subscription_collection_remote/0 +]). + +%% +-type(option_subscription_leaf_local() + :: pubsub_options:option_subscription_deliver() + | pubsub_options:option_subscription_expire() + % | pubsub_options:option_subscription_include_body() + | pubsub_options:option_subscription_show_values() +). + +%% +-type(option_subscription_leaf_remote() + :: pubsub_options:option_subscription_deliver() + % | pubsub_options:option_subscription_expire_datetime() + % | pubsub_options:option_subscription_include_body() +). + +%% +-type(option_subscription_collection_local() + :: pubsub_options:option_subscription_deliver() + | pubsub_options:option_subscription_expire() + | pubsub_options:option_subscription_expire_presence() + | pubsub_options:option_subscription_expire_datetime() + %| pubsub_options:option_subscription_include_body() + | pubsub_options:option_subscription_show_values() + | pubsub_options:option_subscription_subscription_type() + | pubsub_options:option_subscription_subscription_depth() +). + +%% +-type(option_subscription_collection_remote() + :: pubsub_options:option_subscription_deliver() + | pubsub_options:option_subscription_expire_datetime() + | pubsub_options:option_subscription_include_body() + | pubsub_options:option_subscription_subscription_type() + | pubsub_options:option_subscription_subscription_depth() +). + +%% +-type(option_subscription_leaf() + :: pubsub_options:option_subscription_leaf_local() + | pubsub_options:option_subscription_leaf_remote() +). + +%% +-type(option_subscription_collection() + :: pubsub_options:option_subscription_collection_local() + | pubsub_options:option_subscription_collection_remote() +). + + +%% +-type(option_subscription() + :: pubsub_options:option_subscription_leaf() + | pubsub_options:option_subscription_collection() +). + + +-export_type([ + options_subscription/0, + %% + options_subscription_leaf/0, + options_subscription_collection/0, + %% + options_subscription_local/0, + options_subscription_remote/0, + %% + options_subscription_leaf_local/0, + options_subscription_leaf_remote/0, + %% + options_subscription_collection_local/0, + options_subscription_collection_remote/0 +]). + +%% +-type(options_subscription_leaf_local() + :: [pubsub_options:options_subscription_leaf_local(),...] +). + +%% +-type(options_subscription_leaf_remote() + :: [pubsub_options:options_subscription_leaf_remote(),...] +). + +%% +-type(options_subscription_collection_local() + :: [pubsub_options:options_subscription_collection_local(),...] +). + +%% +-type(options_subscription_collection_remote() + :: [pubsub_options:options_subscription_collection_remote(),...] +). + +%% +-type(options_subscription_leaf() + :: pubsub_options:options_subscription_leaf_local() + | pubsub_options:options_subscription_leaf_remote() +). + +%% +-type(options_subscription_collection() + :: pubsub_options:options_subscription_collection_local() + | pubsub_options:options_subscription_collection_remote() +). + +%% +-type(options_subscription_local() + :: pubsub_options:options_subscription_leaf_local() + | pubsub_options:options_subscription_collection_local() +). + +%% +-type(options_subscription_remote() + :: pubsub_options:options_subscription_leaf_remote() + | pubsub_options:options_subscription_collection_remote() +). + +-type(options_subscription() + :: pubsub_options:options_subscription_leaf() + | pubsub_options:options_subscription_collection() +). + +%% -------------------------------------------------------------------- +%% Functions. +%% -------------------------------------------------------------------- + + +xmlns(Xmlel) -> + get_tag_attr_s(<<"xmlns">>, Xmlel). + + +-define(Is_Xmlel_Field(Xmlel), +( + Xmlel#xmlel.name == <<"field">> +)). + +-define(Is_Xmlel_Value(Xmlel), +( + Xmlel#xmlel.name == <<"value">> andalso + Xmlel#xmlel.attrs == [] +)). + +-define(Is_Xmlel_X(Xmlel), +( + Xmlel#xmlel.name == <<"x">> +)). + +-define(Is_Xmlel_X_Submit(Xmlel), +( + %Xmlel#xmlel.ns == 'jabber:x:data' andalso + Xmlel#xmlel.name == <<"x">> % andalso + % Xmlel#xmlel.attrs == [#xmlattr{name = <<"type">>, value = <<"submit">>}] +)). + +-define(Is_Xmlel_X_Form(Xmlel), +( + %Xmlel#xmlel.ns == 'jabber:x:data' andalso + Xmlel#xmlel.name == <<"x">> andalso + Xmlel#xmlel.attrs == [{<<"type">>, <<"form">>}] +)). + + + +%% +%% +-define(Is_Default_Subscribe_Option(Option), +( + Option == {'deliver', true} + orelse + Option == {'expire', undefined} + orelse + Option == {'include_body', false} + orelse + Option == {'show-values', ['away' , 'chat' , 'dnd' , 'online' , 'xa']} +)). + + +%% +-spec(trim_subscription_options/1 :: +( + Subscription_Options::pubsub_options:options_subscription()) + -> Subscription_Options::pubsub_options:options_subscription() +). + +trim_subscription_options(Subscription_Options) -> + trim_subscription_options(Subscription_Options, []). + +%% +-spec(trim_subscription_options/2 :: +( + Options :: [] | pubsub_options:options_subscription(), + Subscription_Options :: [] | pubsub_options:options_subscription()) + -> Subscription_Options :: [] | pubsub_options:options_subscription() +). + +trim_subscription_options([] = _Options, Subscription_Options) -> + Subscription_Options; +%% +trim_subscription_options([Option | Options], Subscription_Options) + when ?Is_Default_Subscribe_Option(Option) -> + trim_subscription_options(Options, Subscription_Options); +%% +trim_subscription_options([Option | Options], Subscription_Options) -> + trim_subscription_options(Options, [Option | Subscription_Options]). + +%% +-spec(parse_xmlel_x/5 :: +( + Plugin :: exmpp_pubsub:plugin(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Form_Type :: 'node_config' | 'publish-options', + Xmlels :: [Xmlel_X::xmlel(),...] + | [Xmlel::xmlel()] + | []) + -> {ok, + Options :: pubsub_options:options_node() + | pubsub_options:options_item()} + %%% + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + +parse_xmlel_x(Plugin, _Pubsub_Features, _Entity, 'node_config', []) -> + {ok, Plugin:node_options('leaf')}; +parse_xmlel_x(_Plugin, _Pubsub_Features, _Entity, 'publish-options', []) -> + {ok, []}; +%% +parse_xmlel_x(Plugin, Pubsub_Features, {U,S,_} = _Entity, Form_Type, [Xmlel_X]) + when ?Is_Xmlel_X(Xmlel_X) -> + case + {xmlns(Xmlel_X), + options_type(remove_cdata(Xmlel_X#xmlel.children), Pubsub_Features)} + of + {?NS_XDATA, {ok, 'node_config' = Form_Type, Node_Type, Xmlels}} -> + parse_xmlels_field(Form_Type, Node_Type, Pubsub_Features, + {U,S,undefined}, Xmlels, Plugin:node_options(Node_Type)); + %% + {?NS_XDATA, {ok, 'publish-options' = Form_Type, Node_Type, Xmlels}} -> + parse_xmlels_field(Form_Type, Node_Type, Pubsub_Features, + {U,S,undefined}, Xmlels, Plugin:item_options()); + %% + {?NS_XDATA, Error} -> + Error; + _ -> + {error, 'invalid-options'} + end; +parse_xmlel_x(_Plugin, _Pubsub_Features, _Entity, _Form_Type, _Xmlels) -> + {error, 'invalid-options'}. + +%% +-spec(parse_xmlel_x/7 :: +( + Plugin :: exmpp_pubsub:plugin(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Entity_Type :: 'local' | 'remote', + Form_Type :: 'subscribe_options', + Node_Type :: 'leaf' | 'collection', + Xmlels :: [Xmlel_X::xmlel(),...] + | [Xmlel::xmlel()] + | []) + -> {ok, + Subscription_Options :: [] | pubsub_options:options_subscription_leaf()} + %%% + | {error, 'invalid-options'} + | {error, 'not-acceptable'} +). + +parse_xmlel_x(Plugin, _Pubsub_Features, _Entity, Entity_Type, 'subscribe_options', + Node_Type, []) -> + {ok, Plugin:subscription_options(Entity_Type, Node_Type)}; +%% +parse_xmlel_x(Plugin, Pubsub_Features, {U,S,_} = _Entity, Entity_Type, + 'subscribe_options' = Form_Type, Node_Type, [Xmlel_X]) + when ?Is_Xmlel_X(Xmlel_X) -> + case + {xmlns(Xmlel_X), + options_type(remove_cdata(Xmlel_X#xmlel.children), 'subscribe_options', [])} + of + {?NS_XDATA, {ok, 'subscribe_options', Xmlels}} -> + parse_xmlels_field(Form_Type, Node_Type, Pubsub_Features, + {U,S,undefined}, Entity_Type, Xmlels, + Plugin:default_subscription_options(Entity_Type, Node_Type)); + {?NS_XDATA, Error} -> + Error; + _ -> + {error, 'invalid-options'} + end; +%% +parse_xmlel_x(_Plugin, _Pubsub_Features, _Entity, _Entity_Type, 'subscribe_options', + _Node_Type, _Xmlels) -> + {error, 'invalid-options'}. + +%% +-spec(options_type/3 :: +( + Xmlels :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()], + Form_Type :: 'subscribe_options', + Xmlels_Fields :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()]) + -> {ok, + Form_Type :: 'subscribe_options', + Xmlels_Field :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()]} + %%% + | {error, 'not-acceptable'} +). + +options_type([] = _Xmlels_Field, 'subscribe_options', _Xmlels) -> + {error, 'not-acceptable'}; +%% +options_type([Xmlel_Field | Xmlels_Field], 'subscribe_options', Xmlels) + when ?Is_Xmlel_Field(Xmlel_Field) -> + case {xmlns(Xmlel_Field), get_tag_attr_s(<<"var">>, Xmlel_Field)} of + {_, <<>>} -> + {error, 'not-acceptable'}; + {?NS_XDATA, <<"FORM_TYPE">>} -> + case remove_cdata(Xmlel_Field#xmlel.children) of + [Xmlel_Value] when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, ?NS_PUBSUB_SUBSCRIBE_OPTIONS} -> + {ok, 'subscribe_options', Xmlels ++ Xmlels_Field}; + _ -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; + {?NS_XDATA, _} -> + options_type(Xmlels_Field, 'subscribe_options', + [Xmlel_Field | Xmlels]); + _ -> + {error, 'not-acceptable'} + end. + +%% +-spec(options_type/2 :: +( + Xmlels :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()], + Pubsub_Features :: exmpp_pubsub:pubsub_features()) + -> {ok, + Form_Type :: 'node_config' | 'publish-options', + Node_Type :: 'leaf' | 'collection', + Xmlels_Field :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()]} + %%% + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} +). + +options_type(Xmlels, Pubsub_Features) -> + options_type(Xmlels, + lists:member(<<"collections">>, Pubsub_Features), undefined, undefined, []). + +%% +-spec(options_type/5 :: +( + Xmlels :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()], + Collections :: boolean(), + Form_Type :: undefined | 'node_config' | 'publish-options', + Node_Type :: undefined | 'leaf' | 'collection', + Xmlels_Fields :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()]) + -> {ok, + Form_Type :: 'node_config' | 'publish-options', + Node_Type :: 'leaf' | 'collection', + Xmlels_Field :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()]} + %%% + | {error, 'not-acceptable'} + | {error, 'feature-not-implemented', 'collections'} +). + +options_type([] = _Xmlels, _Collections, Form_Type, undefined = _Node_Type, + Xmlels_Field) + when Form_Type =/= undefined -> + {ok, Form_Type, 'leaf', Xmlels_Field}; +%% +options_type([] = _Xmlels, _Collections, Form_Type, Node_Type, Xmlels_Field) + when Form_Type =/= undefined -> + {ok, Form_Type, Node_Type, Xmlels_Field}; +%% +options_type([] = _Xmlels, _Collections, undefined = _Form_Type, _Node_Type, + _Xmlels_Field) -> + {error, 'bad-options'}; +%% +options_type(Xmlels, _Collections, Form_Type, Node_Type, Xmlels_Field) + when Form_Type =/= undefined andalso Node_Type =/= undefined -> + {ok, Form_Type, Node_Type, Xmlels ++ Xmlels_Field}; +%% +options_type([Xmlel_Field | Xmlels_Field], Collections, Form_Type, Node_Type, + Xmlels) + when ?Is_Xmlel_Field(Xmlel_Field) -> + case {xmlns(Xmlel_Field), get_tag_attr_s(<<"var">>, Xmlel_Field)} of + {_, <<>>} -> + {error, 'not-acceptable'}; + {?NS_XDATA, <<"FORM_TYPE">>} -> + case remove_cdata(Xmlel_Field#xmlel.children) of + [Xmlel_Value] when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, ?NS_PUBSUB_NODE_CONFIG} -> + options_type(Xmlels_Field, Collections, 'node_config', + Node_Type, Xmlels); + {?NS_XDATA, ?NS_PUBSUB_PUBLISH_OPTIONS} -> + options_type(Xmlels_Field, Collections, + 'publish-options', Node_Type, Xmlels); + _ -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; + {?NS_XDATA, <<"pubsub#node_type">>} -> + case remove_cdata(Xmlel_Field#xmlel.children) of + [Xmlel_Value] when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<"leaf">>} -> + options_type(Xmlels_Field, Collections, Form_Type, + 'leaf', Xmlels); + {?NS_XDATA, <<"collection">>} -> + case Collections of + true -> + options_type(Xmlels_Field, Collections, + Form_Type, 'collection', Xmlels); + false -> + {error, + 'feature-not-implemented', 'collections'} + end; + _ -> + {error, 'not-acceptable'} + end + end; + {?NS_XDATA, _Xmlattr} -> + options_type(Xmlels_Field, Collections, Form_Type, Node_Type, + [Xmlel_Field | Xmlels]); + _ -> + {error, 'not-acceptable'} + end. + +%% +-spec(parse_xmlels_field/6 :: +( + Options_Type :: 'node_config' | 'publish-options', + Node_Type :: 'leaf' | 'collection', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Xmlels :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()], + Options :: pubsub_options:options_node() + | pubsub_options:options_item() + %% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'}) + -> {ok, + Options :: pubsub_options:options_node() + | pubsub_options:options_item()} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + +%% +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Features, _Entity, + _Xmlels, Error) + when is_tuple(Error) -> + Error; +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Features, _Entity, + [] = _Xmlels, Options) -> + {ok, Options}; +%% +parse_xmlels_field(Options_Type, Node_Type, Pubsub_Features, Entity, + [Xmlel_Field | Xmlels], Options) + when ?Is_Xmlel_Field(Xmlel_Field) -> + case xmlns(Xmlel_Field) of + ?NS_XDATA -> + parse_xmlels_field(Options_Type, Node_Type, Pubsub_Features, Entity, + Xmlels, + parse_xmlel_field_attrs( + get_tag_attr_s(<<"var">>, Xmlel_Field), + Options_Type, Node_Type, Pubsub_Features, Entity, + remove_cdata(Xmlel_Field#xmlel.children), + Options)); + _ -> + {error, 'invalid-options'} + end; +%% +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Feature, _Entity, + _Xmlels, _Options) -> + {error, 'invalid-options'}. + +%% +-spec(parse_xmlels_field/7 :: +( + Options_Type :: 'subscribe_options', + Node_Type :: 'leaf' | 'collection', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Entity_Type :: 'local' | 'remote', + Xmlels :: [Xmlel_Field::xmlel()] + | [Xmlel::xmlel()], + Options :: pubsub_options:options_subscription() + % + | {error, 'invalid-options'} + | {error, 'not-acceptable'}) + -> {ok, + Options :: pubsub_options:options_node() + | pubsub_options:options_item()} + %%% + | {error, 'invalid-options'} + | {error, 'not-acceptable'} +). + +%% +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Features, _Entity, + _Entity_Type, _Xmlels, Error) + when is_tuple(Error) -> + Error; +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Features, _Entity, + _Entity_Type, [] = _Xmlels, Options) -> + {ok, trim_subscription_options(Options)}; +%% +parse_xmlels_field(Options_Type, Node_Type, Pubsub_Features, Entity, + Entity_Type, [Xmlel_Field | Xmlels], Options) + when ?Is_Xmlel_Field(Xmlel_Field) -> + case xmlns(Xmlel_Field) of + ?NS_XDATA -> + parse_xmlels_field(Options_Type, Node_Type, Pubsub_Features, Entity, + Entity_Type, + Xmlels, + parse_xmlel_field_attrs( + get_tag_attr_s(<<"var">>, Xmlel_Field), + Options_Type, Node_Type, Pubsub_Features, Entity, Entity_Type, + remove_cdata(Xmlel_Field#xmlel.children), + Options)); + _ -> + {error, 'invalid-options'} + end; +%% +parse_xmlels_field(_Options_Type, _Node_Type, _Pubsub_Feature, _Entity, + _Entity_Type, _Xmlels, _Options) -> + {error, 'invalid-options'}. + +%% +-spec(parse_xmlel_field_attrs/7 :: +( + Pubsub_Option :: binary(), + Options_Type :: 'node_config' | 'publish-options', + Node_Type :: 'leaf' | 'collection', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Xmlels :: [Xmlel_Value::xmlel(),...] + | [Xmlel::xmlel(),...], + Options :: pubsub_options:options_node() + | pubsub_options:options_item()) + -> Options :: pubsub_options:options_node() + | pubsub_options:options_item() + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + +parse_xmlel_field_attrs(<<"pubsub#roster_groups_allowed">>, Options_Type, + Node_Type, Pubsub_Features, Entity, Xmlels, Options) + when (Options_Type == 'node_config' orelse Options_Type == 'publish-options') + andalso (Node_Type == 'leaf' orelse Node_Type == 'collection') -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> + case + case Options_Type of + 'node_config' -> + parse_pubsub_option_node(<<"roster_groups_allowed">>, + Node_Type, Pubsub_Features, Xmlels); + 'publish-options' -> + parse_pubsub_option_item(<<"roster_groups_allowed">>, + Pubsub_Features, Xmlels) + end + + of + {ok, Roster_Groups_Allowed} -> + case lists:keyfind('roster_groups_allowed', 1, Options) of + false -> + [{Entity, Roster_Groups_Allowed} | Options]; + {'roster_groups_allowed', Rosters_Groups_Allowed} -> + lists:keyreplace('roster_groups_allowed', 1, Options, + {'roster_groups_allowed', + _New_Rosters_Groups_Allowed = + case + lists:keyreplace(Entity, 1, + Rosters_Groups_Allowed, + {Entity, Roster_Groups_Allowed}) + of + Rosters_Groups_Allowed -> + [{Entity, Roster_Groups_Allowed} + | Rosters_Groups_Allowed]; + New_Rosters_Groups_Allowed -> + New_Rosters_Groups_Allowed + end}) + end; + Error -> + Error + end; + false -> + {error, 'invalid-options'} + end; +%% +parse_xmlel_field_attrs(<<"pubsub#", Pubsub_Option/binary>>, + 'node_config' = _Options_Type, Node_Type, Pubsub_Features, _Entity, Xmlels, + Node_Options) -> + case + lists:keymember( + Key = list_to_atom(binary_to_list(Pubsub_Option)), 1, Node_Options) + of + true -> + case + parse_pubsub_option_node(Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + of + {ok, Value} -> + lists:keyreplace(Key, 1, Node_Options, {Key, Value}); + Error -> + Error + end; + false -> + Node_Options%{error, 'invalid-options'} + end; +%% +parse_xmlel_field_attrs(<<"pubsub#", Pubsub_Option/binary>>, + 'publish-options' = _Options_Type, _Node_Type, Pubsub_Features, _Entity, Xmlels, + Item_Options) -> + case parse_pubsub_option_item(Pubsub_Option, Pubsub_Features, Xmlels) of + {ok, Value} + when Value == undefined + orelse Value == [] -> + lists:keydelete(_Key = list_to_atom(binary_to_list(Pubsub_Option)), + 1, Item_Options); + {ok, Value} -> + Key = list_to_atom(binary_to_list(Pubsub_Option)), + case lists:keyreplace(Key, 1, Item_Options, {Key, Value}) of + Item_Options -> [{Key, Value} | Item_Options]; + New_Item_Options -> New_Item_Options + end; + Error -> + Error + end; +%% +parse_xmlel_field_attrs(_Xmlattr_value, _Options_Type, _Node_Type, + _Pubsub_Features, _Entity, _Xmlels, _Options) -> + {error, 'invalid-options'}. + +%% +-spec(parse_xmlel_field_attrs/8 :: +( + Pubsub_Option :: binary(), + Options_Type :: 'subscribe_options', + Node_Type :: 'leaf' | 'collection', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Entity :: xmpp_jid:usr_entity(), + Entity_Type :: 'local' | 'remote', + Xmlels :: [Xmlel_Value::xmlel(),...] + | [Xmlel::xmlel(),...], + Subscription_Options :: pubsub_options:options_subscription()) + -> Subscription_Options :: pubsub_options:options_subscription() + %%% + | {error, 'invalid-options'} +). + +parse_xmlel_field_attrs(<<"pubsub#", Pubsub_Option/binary>>, + 'subscribe_options' = _Options_Type, Node_Type, Pubsub_Features, _Entity, + Entity_Type, Xmlels, Subscription_Options) -> + case + lists:keymember(Key = list_to_atom(binary_to_list(Pubsub_Option)), 1, + Subscription_Options) + of + true -> + case + parse_pubsub_option_subscription(Pubsub_Option, Node_Type, + Entity_Type, Pubsub_Features, Xmlels) + of + {ok, Value} + when ?Is_Default_Subscribe_Option({Key, Value}) -> + lists:keydelete(Key, 1, Subscription_Options); + {ok, Value} -> + lists:keyreplace(Key, 1, Subscription_Options, {Key, Value}); + Error -> + Error + end; + false -> + {error, 'invalid-options'} + end; +%% +parse_xmlel_field_attrs(_Xmlattr_value, _Options_Type, _Node_Type, + _Pubsub_Features, _Entity, _Entity_Type, _Xmlels, _Options) -> + {error, 'invalid-options'}. + + +%% +%% +-spec(parse_pubsub_option_node/4 :: +( + Pubsub_Option :: binary(), + Node_Type :: 'collection' | 'leaf', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Xmlels :: [Xmlel_Value::xmlel()] + | [Xmlel::xmlel()]) + -> {ok, Access_Model :: pubsub_options:access_model()} + % | {ok, Body_XSLT :: undefined | binary()} + | {ok, Children_Association_Policy :: 'all' | 'owners' | 'whitelist'} + | {ok, Children_Association_Whitelist :: [Jid::xmpp_jid:raw_jid_entity_bare()]} + | {ok, Children :: [Child_NodeId::exmpp_pubsub:nodeId()]} + | {ok, Children_Max :: undefined | non_neg_integer()} + | {ok, Collection :: [Parent_NodeId::exmpp_pubsub:nodeId()]} + | {ok, Contact :: [Jid::xmpp_jid:raw_jid_entity()]} + % | {ok, Dataform_XSLT :: undefined | binary()} + | {ok, Deliver_Notifications :: boolean()} + | {ok, Deliver_Payloads :: boolean()} + | {ok, Description :: undefined | binary()} + | {ok, Item_Expire :: undefined | non_neg_integer()} + | {ok, ItemReply :: pubsub_options:item_reply()} + | {ok, Language :: undefined | binary()} + | {ok, Max_Items :: undefined | non_neg_integer()} + | {ok, Max_Payload_Size :: undefined | non_neg_integer()} + | {ok, Node_Type :: pubsub_options:node_type()} + | {ok, Notification_Type :: pubsub_options:notification_type()} + | {ok, Notify_Config :: boolean()} + | {ok, Notify_Delete :: boolean()} + | {ok, Notify_Retract :: boolean()} + | {ok, Notify_Sub :: boolean()} + | {ok, Persist_Items :: boolean()} + | {ok, Presence_Based_Delivery :: boolean()} + | {ok, Publish_Model :: pubsub_options:publish_model()} + | {ok, Purge_Offline :: boolean()} + | {ok, Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed()} + | {ok, Send_Last_Published_Item :: pubsub_options:send_last_published_item()} + | {ok, Tempsub :: boolean()} + | {ok, Subscribe :: boolean()} + | {ok, Title :: undefined | binary()} + | {ok, Type :: undefined | atom()} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} + % + | {error, 'feature-not-implemented', 'collections'} + | {error, 'feature-not-implemented', 'multi-collections'} +). + +parse_pubsub_option_node(<<"access_model">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + parse_xmlel_value({'access_model', Pubsub_Features}, Xmlels); +%% +%%parse_pubsub_option_node(<<"pubsub#body_xslt">> = _Pubsub_Option, +%% 'leaf' = _Node_Type, Pusbub_Features, Xmlels) -> +%% case lists:member(<<"publish">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value('text-single', Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +parse_pubsub_option_node(<<"children_association_policy">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, Xmlels) -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['all', 'owners', 'whitelist']}, Xmlels); +%% +parse_pubsub_option_node(<<"children_association_whitelist">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, [] = _Xmlels) -> + {ok, []}; +parse_pubsub_option_node(<<"children_association_whitelist">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, Xmlels) -> + parse_xmlel_value({'jid-multi', 'bare'}, Xmlels); +%% +%% +parse_pubsub_option_node(<<"children">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, [] = Xmlels) -> + parse_xmlel_value('text-multi', Xmlels); +parse_pubsub_option_node(<<"children">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, [_] = Xmlels) -> + parse_xmlel_value('text-multi', Xmlels); +parse_pubsub_option_node(<<"children">> = _Pubsub_Option, + 'collection' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"multi-collections">>, Pubsub_Features) of + true -> parse_xmlel_value('text-multi', Xmlels); + false -> {error, 'feature-not-implemented', 'multi-collections'} + end; +%% +%% +parse_pubsub_option_node(<<"children_max">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, [] = _Xmlels) -> + {ok, undefined}; +parse_pubsub_option_node(<<"children_max">> = _Pubsub_Option, + 'collection' = _Node_Type, Pubsub_Features, [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + {ok, undefined}; + {?NS_XDATA, CData} -> + case catch list_to_integer(binary_to_list(CData)) of + Integer when Integer == 0 orelse Integer == 1 -> + {ok, Integer}; + Integer when is_integer(Integer) andalso Integer > 1 -> + case + lists:member(<<"multi-collections">>, Pubsub_Features) + of + true -> {ok, Integer}; + false -> {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; +parse_pubsub_option_node(<<"children_max">> = _Pubsub_Option, + 'collection' = _Node_Type, _Pubsub_Features, _Xmlels) -> + {error, 'not-acceptable'}; +%% +parse_pubsub_option_node(<<"collection">> = _Pubsub_Option, Node_Type, + Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"collections">>, Pubsub_Features) of + true -> {ok, []}; + false -> {error, 'feature-not-implemented', 'collections'} + end; +parse_pubsub_option_node(<<"collection">> = _Pubsub_Option, Node_Type, + Pubsub_Features, [_] = Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"collections">>, Pubsub_Features) of + true -> parse_xmlel_value('text-multi', Xmlels); + false -> {error, 'feature-not-implemented', 'collections'} + end; +parse_pubsub_option_node(<<"collection">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"collections">>, Pubsub_Features) of + true -> + case lists:member(<<"multi-collections">>, Pubsub_Features) of + true -> parse_xmlel_value('text-multi', Xmlels); + false -> {error, 'feature-not-implemented', 'multi-collections'} + end; + false -> + {error, 'feature-not-implemented', 'collections'} + end; +%% +parse_pubsub_option_node(<<"contact">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + {ok, []}; +parse_pubsub_option_node(<<"contact">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + parse_xmlel_value({'jid-multi', 'full'}, Xmlels); +%% +%%parse_pubsub_option_node(<<"pubsub#dataform_xslt">> = _Pubsub_Option, +%% 'leaf' = _Node_Type, Pusbub_Features, Xmlels) -> +%% case lists:member(<<"publish">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value('text-single', Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +parse_pubsub_option_node(<<"deliver_notifications">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"deliver_payloads">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"description">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + {ok, undefined}; +parse_pubsub_option_node(<<"description">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + parse_xmlel_value('text-single', Xmlels); +%% +parse_pubsub_option_node(<<"item_expire">> = _Pubsub_Option, 'leaf' = _Node_Type, + Pubsub_Features, [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_node(<<"item_expire">> = _Pubsub_Option, 'leaf' = _Node_Type, + Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'integer'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"itemreply">> = _Pubsub_Option, 'leaf' = _Node_Type, + Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['owner', 'publisher']}, Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"language">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + {ok, undefined}; +parse_pubsub_option_node(<<"language">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + parse_xmlel_value('text-single', Xmlels); +%% +parse_pubsub_option_node(<<"max_items">> = _Pubsub_Option, 'leaf' = _Node_Type, + Pubsub_Features, [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_node(<<"max_items">> = _Pubsub_Option, 'leaf' = _Node_Type, + Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'integer'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"max_payload_size">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_node(<<"max_payload_size">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'integer'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"notification_type">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['headline', 'normal']}, Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"notify_config">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"config-node">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"notify_delete">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"delete-nodes">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"notify_retract">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"delete-items">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"notify_sub">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"persist_items">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"persistent-items">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"presence_based_delivery">> = _Pubsub_Option, + Node_Type, Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"publish_model">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['open', 'publishers', 'subscribers']}, + Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"purge_offline">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"purge-nodes">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"roster_groups_allowed">> = _Pubsub_Option, + Node_Type, Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> {ok, []}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_node(<<"roster_groups_allowed">> = _Pubsub_Option, + Node_Type, Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> parse_xmlel_value('list-multi', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"send_last_published_item">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"last-published">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = case + lists:member(<<"presence-notifications">>, Pubsub_Features) + of + true -> ['never', 'on_sub', 'on_sub_and_presence']; + false -> ['never', 'on_sub'] + end}, + Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"tempsub">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"subscribe">> = _Pubsub_Option, Node_Type, + Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(<<"title">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + {ok, undefined}; +parse_pubsub_option_node(<<"title">> = _Pubsub_Option, Node_Type, + _Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + parse_xmlel_value('text-single', Xmlels); +%% +parse_pubsub_option_node(<<"type">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_node(<<"type">> = _Pubsub_Option, + 'leaf' = _Node_Type, Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'atom'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_node(_Pubsub_Option, _Node_Type, _Pubsub_Features, _Xmlels) -> + {error, 'invalid-options'}. + +%% +%% +-spec(parse_pubsub_option_item/3 :: +( + Pubsub_Option :: binary(), + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Xmlels :: [Xmlel_Value::xmlel()] + | [Xmlel::xmlel()]) + -> {ok, Access_Model :: pubsub_options:access_model()} + % | {ok, Body_XSLT :: undefined | binary()} + % | {ok, Dataform_XSLT :: undefined | binary()} + | {ok, Deliver_Notifications :: boolean()} + | {ok, Deliver_Payloads :: boolean()} + | {ok, Item_Expire :: undefined | non_neg_integer()} + | {ok, ItemReply :: pubsub_options:item_reply()} +%% | {ok, Max_Payload_Size :: undefined | non_neg_integer()} + | {ok, Notification_Type :: pubsub_options:notification_type()} + | {ok, Notify_Config :: boolean()} + | {ok, Notify_Retract :: boolean()} + | {ok, Persist_Items :: boolean()} + | {ok, Presence_Based_Delivery :: boolean()} +% | {ok, Publish_Model :: pubsub_options:publish_model()} + | {ok, Purge_Offline :: boolean()} + | {ok, Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed()} + | {ok, Send_Last_Published_Item :: pubsub_options:send_last_published_item()} + | {ok, Type :: undefined | atom()} + %%% + | {error, 'invalid-options'} + % + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} +). + +parse_pubsub_option_item(<<"access_model">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + parse_xmlel_value({'access_model', Pubsub_Features}, Xmlels); +%% +%%parse_pubsub_option_item(<<"pubsub#body_xslt">> = _Pubsub_Option, +%% Pusbub_Features, Xmlels) -> +%% case lists:member(<<"publish">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value('text-single', Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +%%parse_pubsub_option_item(<<"pubsub#dataform_xslt">> = _Pubsub_Option, +%% Pusbub_Features, Xmlels) -> +%% case lists:member(<<"publish">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value('text-single', Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +parse_pubsub_option_item(<<"deliver_notifications">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"deliver_payloads">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"item_expire">> = _Pubsub_Option, Pubsub_Features, + [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_item(<<"item_expire">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'integer'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"itemreply">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['owner', 'publisher']}, Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +%parse_pubsub_option_item(<<"max_payload_size">> = _Pubsub_Option, +% Pubsub_Features, Xmlels) -> +% case lists:member(<<"publish">>, Pubsub_Features) of +% true -> +% parse_xmlel_value({'text-single', 'integer'}, Xmlels); +% false -> +% {error, 'invalid-options'} +% end; +%% +parse_pubsub_option_item(<<"notification_type">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = ['headline', 'normal']}, Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"notify_config">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"config-node">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"notify_retract">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"delete-items">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"persist_items">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"persistent-items">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"presence_based_delivery">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +%parse_pubsub_option_item(<<"publish_model">> = _Pubsub_Option, +% Pubsub_Features, Xmlels) -> +% case lists:member(<<"publish">>, Pubsub_Features) of +% true -> +% parse_xmlel_value({'list-single', 'atom', +% _Default_Values = ['open', 'publishers', 'subscribers']}, +% Xmlels); +% false -> +% {error, 'invalid-options'} +% end; +%% +parse_pubsub_option_item(<<"purge_offline">> = _Pubsub_Option, Pubsub_Features, + Xmlels) -> + case lists:member(<<"purge-nodes">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"roster_groups_allowed">> = _Pubsub_Option, + Pubsub_Features, [] = _Xmlels) -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> {ok, []}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_item(<<"roster_groups_allowed">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> parse_xmlel_value('list-multi', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"send_last_published_item">> = _Pubsub_Option, + Pubsub_Features, Xmlels) -> + case lists:member(<<"last-published">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-single', 'atom', + _Default_Values = case + lists:member(<<"presence-notifications">>, Pubsub_Features) + of + true -> ['never', 'on_sub', 'on_sub_and_presence']; + false -> ['never', 'on_sub'] + end}, + Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(<<"type">> = _Pubsub_Option, Pubsub_Features, + [] = _Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_item(<<"type">> = _Pubsub_Option, Pubsub_Features, Xmlels) -> + case lists:member(<<"publish">>, Pubsub_Features) of + true -> parse_xmlel_value({'text-single', 'atom'}, Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_item(_Pubsub_Option, _Pubsub_Features, _Xmlels) -> + {error, 'invalid-options'}. + +%% +%% +-spec(parse_pubsub_option_subscription/5 :: +( + Pubsub_Option :: binary(), + Node_Type :: 'collection' | 'leaf', + Entity_Type :: 'local' | 'remote', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Xmlels :: [Xmlel_Value::xmlel()] + | [Xmlel::xmlel()]) + -> + % {ok, Digest :: boolean()} + % | {ok, Digest_Frequency :: undefined | non_neg_integer()} + {ok, Deliver :: boolean()} + | {ok, Expire :: undefined | 'presence' | erlang:timestamp()} +% | {ok, Include_Body :: boolean()} + | {ok, Show_Values :: ['away' | 'chat' | 'dnd' | 'online' | 'xa']} +% | {ok, Subscription_Type :: 'all' | 'items' | 'nodes'} +% | {ok, Subscription_Depth :: 'all' | non_neg_integer()} + %%% + | {error, 'invalid-options'} + % + | {error, 'not-acceptable'} +). + +%% parse_pubsub_option_subscription(<<"digest">> = _Pubsub_Option, +%% 'leaf' = _Node_Type, _Entity_Type, Pubsub_Features, Xmlels) -> +%% case lists:member(<<"subscribe">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value('boolean', Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +%% parse_pubsub_option_subscription(<<"digest_frequency">> = _Pubsub_Option, +%% 'leaf' = _Node_Type, _Entity_Type, Pubsub_Features, Xmlels) -> +%% case lists:member(<<"subscribe">>, Pubsub_Features) of +%% true -> +%% parse_xmlel_value({'text-single', 'integer'}, Xmlels); +%% false -> +%% {error, 'invalid-options'} +%% end; +%% +parse_pubsub_option_subscription(<<"deliver">> = _Pubsub_Option, Node_Type, + _Entity_Type, Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"subscribe">>, Pubsub_Features) of + true -> parse_xmlel_value('boolean', Xmlels); + false -> {error, 'invalid-options'} + end; +%% +parse_pubsub_option_subscription(<<"expire">> = _Pubsub_Option, Node_Type, + 'local' = _Entity_Type, Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case + lists:member(<<"presence-notifications">>, Pubsub_Features) + orelse + lists:member(<<"leased-subscriptions">>, Pubsub_Features) + of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_subscription(<<"expire">> = _Pubsub_Option, Node_Type, + 'local' = _Entity_Type, Pubsub_Features, [Xmlel_Value]) + when Node_Type == 'collection' orelse Node_Type == 'leaf' + andalso ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + {ok, undefined}; + {?NS_XDATA, <<"presence">>} -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> {ok, 'presence'}; + false -> {error, 'invalid-options'} + end; + {?NS_XDATA, CData} -> + case lists:member(<<"leased-subscriptions">>, Pubsub_Features) of + true -> + case jlib:datetime_string_to_timestamp(CData) of + undefined -> {error, 'not-acceptable'}; + DateTime -> {ok, DateTime} + end; + false -> + {error, 'invalid-options'} + end; + _ -> + {error, 'invalid-options'} + end; +% +parse_pubsub_option_subscription(<<"expire">> = _Pubsub_Option, Node_Type, + 'remote' = _Entity_Type, Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"leased-subscriptions">>, Pubsub_Features) of + true -> {ok, undefined}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_subscription(<<"expire">> = _Pubsub_Option, Node_Type, + 'remote' = _Entity_Type, Pubsub_Features, [Xmlel_Value]) + when Node_Type == 'collection' orelse Node_Type == 'leaf' + andalso ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + {ok, undefined}; + {?NS_XDATA, <<"presence">>} -> + {error, 'invalid-options'}; + {?NS_XDATA, CData} -> + case lists:member(<<"leased-subscriptions">>, Pubsub_Features) of + true -> + case jlib:datetime_string_to_timestamp(CData) of + undefined -> {error, 'not-acceptable'}; + DateTime -> {ok, DateTime} + end; + false -> + {error, 'invalid-options'} + end; + _ -> + {error, 'invalid-options'} + end; +%% +%parse_pubsub_option_subscription(<<"include_body">> = _Pubsub_Option, +% 'leaf' = _Node_Type, _Entity_Type, Pubsub_Features, Xmlels) -> +% case lists:member(<<"subscribe">>, Pubsub_Features) of +% true -> +% parse_xmlel_value('boolean', Xmlels); +% false -> +% {error, 'invalid-options'} +% end; +%% +parse_pubsub_option_subscription(<<"show-values">> = _Pubsub_Option, Node_Type, + 'local' = _Entity_Type, Pubsub_Features, [] = _Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> {ok, []}; + false -> {error, 'invalid-options'} + end; +parse_pubsub_option_subscription(<<"show-values">> = _Pubsub_Option, Node_Type, + 'local' = _Entity_Type, Pubsub_Features, Xmlels) + when Node_Type == 'collection' orelse Node_Type == 'leaf' -> + case lists:member(<<"presence-notifications">>, Pubsub_Features) of + true -> + parse_xmlel_value({'list-multi', 'atom', + _Default_Values = ['away' ,'chat', 'dnd', 'online', 'xa']}, + Xmlels); + false -> + {error, 'invalid-options'} + end; +%% +parse_pubsub_option_subscription(<<"show-values">> = _Pubsub_Option, _Node_Type, + 'remote' = _Entity_Type, _Pubsub_Features, _Xmlels) -> + {error, 'invalid-options'}; +%% +%parse_pubsub_option_subscription(<<"subscription_type">> = _Pubsub_Option, +% 'collection' = _Node_Type, _Entity_Type, _Pubsub_Features, Xmlels) -> +% parse_xmlel_value({'list-single', 'atom', +% _Default_Values = ['all', 'items', 'nodes']}, Xmlels); +%% +%parse_pubsub_option_subscription(<<"subscription_depth">> = _Pubsub_Option, +% 'collection' = _Node_Type, _Entity_Type, _Pubsub_Features, Xmlels) -> +% case Xmlels of +% [] -> +% {ok, 0}; +% [Xmlel_Value] when ?Is_Xmlel_Value(Xmlel_Value) -> +% case {Xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of +% {?NS_XDATA, <<>>} -> +% {ok, 0}; +% {?NS_XDATA, <<"all">>} -> +% {ok, 'all'}; +% {?NS_XDATA, CData} -> +% case catch list_to_integer(binary_to_list(CData)) of +% Integer +% when is_integer(Integer) andalso Integer > -1 -> +% {ok, Integer}; +% _Error -> +% {error, 'not-acceptable'} +% end; +% _ -> +% {error, 'not-acceptable'} +% end; +% _Xmlels -> +% {error, 'not-acceptable'} +% end; +%% +parse_pubsub_option_subscription(_Pubsub_Option, _Node_Type, _Entity_Type, + _Pubsub_Features, _Xmlels) -> + {error, 'invalid-options'}. + + +-spec(parse_xmlel_value/2 :: +( + DataType :: {'jid-multi', 'bare'} + | {'jid-multi', 'full'} + | {'list-multi', 'atom', Default_Values::[Default_Value::atom(),...]} + | 'list-multi' + | 'text-multi' + | {'access_model', Pubsub_Features::exmpp_pubsub:pubsub_features()} + | 'boolean' + | 'text-single' + | {'text-single', 'atom'} + | {'text-single', 'integer'} + | {'list-single', 'atom', Default_Values::[Default_Value::atom(),...]}, + Xmlels :: [Xmlel_Value::xmlel()] + | [Xmlel::xmlel()]) + -> %% + {ok, Values :: [] + | [Value::binary()] + | [Value::atom()] + | [Value::xmpp_jid:raw_jid_entity_bare()] + | [Value::xmpp_jid:raw_jid_entity()]} + | {ok, Value :: atom() + | binary() + | boolean() + | non_neg_integer() + | undefined} + %%% + | {error, 'invalid-options'} + | {error, 'jid-malformed'} + | {error, 'not-acceptable'} + | {error, 'not-acceptable', 'unsupported-access-model'} +). + +%% +parse_xmlel_value({'jid-multi', 'bare'}, [] = _Xmlels) -> + {ok, []}; +%% +parse_xmlel_value({'jid-multi', 'bare'}, Xmlels) -> + parse_xmlel_value({'jid-multi', 'bare'}, Xmlels, []); +%% +parse_xmlel_value({'jid-multi', 'full'}, [] = _Xmlels) -> + {ok, []}; +%% +parse_xmlel_value({'jid-multi', 'full'}, Xmlels) -> + parse_xmlel_value({'jid-multi', 'full'}, Xmlels, []); +%% +%% +parse_xmlel_value({'list-multi', 'atom', _Default_Values}, [] = _Xmlels) -> + {error, 'not-acceptable'}; +%% +parse_xmlel_value({'list-multi', 'atom', Default_Values}, Xmlels) -> + parse_xmlel_value({'list-multi', 'atom', Default_Values}, Xmlels, []); +%% +parse_xmlel_value('list-multi', Xmlels) -> + parse_xmlel_value('list-multi', Xmlels, []); +%% +%% +parse_xmlel_value('text-multi', [] = _Xmlels) -> + {ok, []}; +%% +parse_xmlel_value('text-multi', Xmlels) -> + parse_xmlel_value('text-multi', Xmlels, []); +%% + +parse_xmlel_value({'access_model', Pubsub_Features}, [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<"authorize">>} -> + case lists:member(<<"access-authorize">>, Pubsub_Features) of + true -> + {ok, 'authorize'}; + false -> + {error, 'not-acceptable', 'unsupported-access-model'} + end; + %% + {?NS_XDATA, <<"open">>} -> + case lists:member(<<"access-open">>, Pubsub_Features) of + true -> + {ok, 'open'}; + false -> + {error, 'not-acceptable', 'unsupported-access-model'} + end; + %% + {?NS_XDATA, <<"presence">>} -> + case lists:member(<<"access-presence">>, Pubsub_Features) of + true -> + {ok, 'presence'}; + false -> + {error, 'not-acceptable', 'unsupported-access-model'} + end; + %% + {?NS_XDATA, <<"roster">>} -> + case lists:member(<<"access-roster">>, Pubsub_Features) of + true -> + {ok, 'roster'}; + false -> + {error, 'not-acceptable', 'unsupported-access-model'} + end; + %% + {?NS_XDATA, <<"whitelist">>} -> + case lists:member(<<"access-whitelist">>, Pubsub_Features) of + true -> + {ok, 'whitelist'}; + false -> + {error, 'not-acceptable', 'unsupported-access-model'} + end; + %% + _ -> + {error, 'not-acceptable'} + end; +parse_xmlel_value({'access_model', _Pubsub_Features}, _Xmlels) -> + {error, 'not-acceptable'}; +%% +%% +parse_xmlel_value('boolean', [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, False} + when False == <<"0">> + orelse False == <<"false">> -> + {ok, false}; + {?NS_XDATA, True} + when True == <<"1">> + orelse True == <<"true">> -> + {ok, true}; + _ -> + {error, 'not-acceptable'} + end; +parse_xmlel_value('boolean', _Xmlels) -> + {error, 'not-acceptable'}; +%% +%% +parse_xmlel_value('text-single', [] = _Xmlels) -> + {ok, undefined}; +parse_xmlel_value('text-single', [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> {ok, undefined}; + {?NS_XDATA, CData} -> {ok, CData}; + _ -> {error, 'not-acceptable'} + end; +parse_xmlel_value('text-single', _Xmlels) -> + {error, 'not-acceptable'}; +%% +%% +parse_xmlel_value({'text-single', 'atom'}, [] = _Xmlels) -> + {ok, undefined}; +parse_xmlel_value({'text-single', 'atom'}, [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> {ok, undefined}; + {?NS_XDATA, CData} -> {ok, list_to_atom(binary_to_list(CData))}; + _ -> {error, 'not-acceptable'} + end; +parse_xmlel_value({'text-single', 'atom'}, _Xmlels) -> + {error, 'not-acceptable'}; +%% +%% +parse_xmlel_value({'text-single', 'integer'}, [] = _Xmlels) -> + {ok, undefined}; +parse_xmlel_value({'text-single', 'integer'}, [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + {ok, undefined}; + {?NS_XDATA, CData} -> + case catch list_to_integer(binary_to_list(CData)) of + Integer when is_integer(Integer) andalso Integer > -1 -> + {ok, Integer}; + _ -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; +parse_xmlel_value({'text-single', 'integer'}, _Xmlels) -> + {error, 'not-acceptable'}; +%% +%% +parse_xmlel_value({'list-single', 'atom', Default_Values}, [Xmlel_Value]) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {_, <<>>} -> + {error, 'not-acceptable'}; + {?NS_XDATA, CData} -> + case + lists:member(Value = list_to_atom(binary_to_list(CData)), + Default_Values) + of + true -> + {ok, Value}; + false -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; +parse_xmlel_value({'list-single', 'atom', _Default_Values}, _Xmlels) -> + {error, 'not-acceptable'}; +%% +parse_xmlel_value({_DataType, _Default_Values}, _Xmlels) -> + {error, 'not-acceptable'}. +%% + +%% +-spec(parse_xmlel_value/3 :: +( + DataType :: {'list-multi', 'atom', Default_Values::[Default_Value::atom(),...]} + | 'list-multi' + | 'text-multi' + | {'jid-multi', 'full'} + | {'jid-multi', 'bare'}, + Xmlels :: [Xmlel_Value::xmlel()] + | [Xmlel::xmlel()], + Values :: [Value::binary()] + | [Value::atom()] + | [Value::xmpp_jid:raw_jid_entity_bare()] + | [Value::xmpp_jid:raw_jid_entity_full()]) + -> {ok, + Values :: [Value::binary()] + | [Value::atom()] + | [Value::xmpp_jid:raw_jid_entity_bare()] + | [Value::xmpp_jid:raw_jid_entity_full()]} + %%% + | {error, 'not-acceptable'} + | {error, 'jid-malformed'} +). + +parse_xmlel_value(_DataType, _Xmlels = [], Values) -> + {ok, lists:usort(Values)}; +%% +%% +parse_xmlel_value({'list-multi', 'atom', Default_Values}, + [Xmlel_Value | Xmlels], Values) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + parse_xmlel_value({'list-multi', 'atom', Default_Values}, + Xmlels, Values); + {?NS_XDATA, CData} -> + case + lists:member(Value = list_to_atom(binary_to_list(CData)), + Default_Values) + of + true -> + parse_xmlel_value({'list-multi', 'atom', Default_Values}, + Xmlels, [Value | Values]); + false -> + {error, 'not-acceptable'} + end; + _ -> + {error, 'not-acceptable'} + end; +%% +%% +parse_xmlel_value('list-multi', [Xmlel_Value | Xmlels], Values) + when ?Is_Xmlel_Value(Xmlel_Value) -> + parse_xmlel_value('list-multi', Xmlels, + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> Values; + {?NS_XDATA, CData} -> [CData | Values]; + _ -> {error, 'not-acceptable'} + end); +%% +%% +parse_xmlel_value('text-multi', [Xmlel_Value | Xmlels] , Values) + when ?Is_Xmlel_Value(Xmlel_Value) -> + parse_xmlel_value('text-multi', Xmlels, + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> Values; + {?NS_XDATA, CData} -> [_Value = CData | Values]; + _ -> {error, 'not-acceptable'} + end); +%% +%% +parse_xmlel_value({'jid-multi', 'full'}, [Xmlel_Value | Xmlels], Values) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + parse_xmlel_value({'jid-multi', 'full'}, Xmlels, Values); + {?NS_XDATA, CData} -> + case is_jid(CData) of + #jid{} = Jid -> + parse_xmlel_value({'jid-multi', 'full'}, Xmlels, + [jlib:jid_to_string(Jid) | Values]); + Error -> + Error + end; + _ -> + {error, 'not-acceptable'} + end; +%% +parse_xmlel_value({'jid-multi', 'bare'}, [Xmlel_Value | Xmlels], Values) + when ?Is_Xmlel_Value(Xmlel_Value) -> + case {xmlns(Xmlel_Value), get_tag_cdata(Xmlel_Value)} of + {?NS_XDATA, <<>>} -> + parse_xmlel_value({'jid-multi', 'bare'}, Xmlels, Values); + {?NS_XDATA, CData} -> + case is_jid(CData) of + #jid{} = Jid -> + parse_xmlel_value({'jid-multi', 'bare'}, Xmlels, + [jlib:jid_to_string(Jid#jid{resource = <<>>, lresource = <<>>}) + | Values]); + Error -> + Error + end; + _ -> + {error, 'not-acceptable'} + end; +%% +parse_xmlel_value(_Data_Type, _Xmlels, _Values) -> + {error, 'not-acceptable'}. + +%% +%% +-spec(xdata_x/6 :: +( + Options_Type :: 'node_config' | 'subscribe_options', + Form_Type :: 'form' | 'result', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Options :: pubsub_options:options_node() + | pubsub_options:options_subscription()) + -> Xmlel_X::xmlel() +). + +xdata_x('node_config', 'form', Pubsub_Features, Host, Entity, Node_Options) -> + xmpp_xdata:xmlel_x(<<"form">>, + _Xmlels_Field = [ + xmpp_xdata:xmlel_field(<<"FORM_TYPE">>, <<"hidden">>, + [?NS_PUBSUB_NODE_CONFIG]) + |xdata_field(Pubsub_Features, Host, Entity, Node_Options, []) + ]); + +xdata_x('subscribe_options', 'form', Pubsub_Features, Host, Entity, + Subscription_Options) -> + xmpp_xdata:xmlel_x(<<"form">>, + _Xmlels_Field = [ + xmpp_xdata:xmlel_field(<<"FORM_TYPE">>, <<"hidden">>, + [?NS_PUBSUB_SUBSCRIBE_OPTIONS]) + |xdata_field(Pubsub_Features, Host, Entity, Subscription_Options, []) + ]); + +xdata_x('subscribe_options', 'result', Pubsub_Features, Host, Entity, + Subscription_Options) -> + xmpp_xdata:xmlel_x(<<"result">>, + _Xmlels_Field = [ + xmpp_xdata:xmlel_field(<<"FORM_TYPE">>, <<"hidden">>, + [?NS_PUBSUB_SUBSCRIBE_OPTIONS]) + |xdata_field(Pubsub_Features, Host, Entity, Subscription_Options, []) + ]). + +%% +%% +-spec(xdata_x/7 :: +( + Options_Type :: 'subscribe_options', + Form_Type :: 'form', + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Subscription_Options :: [] | pubsub_options:options_subscription(), + Default_Subscription_Options :: pubsub_options:options_subscription()) + -> Xmlel_X::xmlel() +). + + +%% +xdata_x('subscribe_options', 'form', Pubsub_Features, Host, Entity, + Subscription_Options, Default_Subscription_Options) -> + xmpp_xdata:xmlel_x(<<"form">>, + _Xmlels_Field = [ + xmpp_xdata:xmlel_field(<<"FORM_TYPE">>, <<"hidden">>, + [?NS_PUBSUB_SUBSCRIBE_OPTIONS]) + |xdata_field(Pubsub_Features, Host, Entity, + filter_subscription_options(Subscription_Options, + Default_Subscription_Options, []), + []) + ]). + +%% +-spec(filter_subscription_options/3 :: +( + Options :: [] | pubsub_options:options_subscription(), + Default_Subscription_Options :: pubsub_options:options_subscription(), + Subscription_Options :: [] | pubsub_options:options_subscription()) + -> Subscription_Options::pubsub_options:options_subscription() +). + +filter_subscription_options([] = _Options, Default_Subscription_Options, + Subscription_Options) -> + Default_Subscription_Options ++ Subscription_Options; +%% +filter_subscription_options([{Key, Value} | Options], Default_Subscription_Options, + Subscription_Options) -> + filter_subscription_options(Options, + lists:keydelete(Key, 1, Default_Subscription_Options), + [{Key, Value} | Subscription_Options]). + + +-define(Label_Deliver, <<"Enable delivery ?">>). +-define(Label_Digest, <<"Receive digest notifications ?">>). +-define(Label_Digest_Frequency, <<"Receive digest notifications (approx. one per day)?">>). +-define(Label_Expire, <<"Requested lease period">>). +-define(Label_Include_Body, <<"Receive message body in addition to payload ?">>). +-define(Label_Show_Values, <<"Select the presence types which are allowed to receive event notifications">>). + +%% +%% pubsub#subscribe_options +%% +-spec(xdata_field/5 :: +( + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_bare(), + Options :: [] | pubsub_options:options_subscription(), + Xmlels_Field :: [] | [Xmlel_Field::xmlel(),...]) + -> Xmlels_Field::[Xmlel_Field::xmlel(),...] +). +%% +xdata_field(_Pubsub_Features, _Host, _Entity, [] = _Options, Xmlels_Field) -> + lists:sort(Xmlels_Field); +%% 'pubsub#deliver' +xdata_field(Pubsub_Features, Host, Entity, [{'deliver', Deliver} | Options], + Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#deliver">>, + <<"Enable delivery?">>, + xdata_value('boolean', Deliver)) + | Xmlels_Field]); +%% 'pubsub#digest' +xdata_field(Pubsub_Features, Host, Entity, [{'digest', Digest} | Options], + Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#digest">>, + <<"Receive digest notifications (approx. one per day)?">>, + xdata_value('boolean', Digest)) + | Xmlels_Field]); +%% 'pubsub#digest_frequency' +xdata_field(Pubsub_Features, Host, Entity, + [{'digest_frequency', Digest_Frequency} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#digest_frequency">>, + <<"Receive digest notifications (approx. one per day)?">>, + xdata_value('integer', Digest_Frequency)) + | Xmlels_Field]); +%% 'pubsub#expire' +xdata_field(Pubsub_Features, Host, Entity, + [{'expire', Expire} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#expire">>, + <<"Requested lease period">>, + case is_atom(Expire) of + true -> xdata_value('atom', Expire); + false -> xdata_value('date', Expire) + end) + | Xmlels_Field]); +%% 'pubsub#include_body' +xdata_field(Pubsub_Features, Host, Entity, + [{'include_body', Include_Body} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#include_body">>, + <<"Receive message body in addition to payload?">>, + xdata_value('boolean', Include_Body)) + | Xmlels_Field]); +%% 'pubsub#show-values' +xdata_field(Pubsub_Features, Host, Entity, + [{'show-values', Show_Values} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_multi( + <<"pubsub#show-values">>, + <<"Select the presence types which are allowed to receive event notifications">>, + lists:map(fun + ('away') -> <<"away">>; + ('chat') -> <<"chat">>; + ('dnd') -> <<"dnd">>; + ('online') -> <<"online">>; + ('xa') -> <<"xa">> + end, Show_Values), + [{<<"away">>, <<"Away">>}, + {<<"chat">>, <<"Want to Chat">>}, + {<<"dnd">>, <<"Do Not Disturb">>}, + {<<"online">>, <<"Available">>}, + {<<"xa">>, <<"Extended Away">>}]) + | Xmlels_Field]); +%% 'pubsub#subscription_type' +xdata_field(Pubsub_Features, Host, Entity, + [{'subscription_type', Subscription_Type} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#subscription_type">>, + <<"Subscription Type">>, + case Subscription_Type of + undefined -> <<"items">>; + 'items' -> <<"items">>; + 'nodes' -> <<"nodes">>; + 'all' -> <<"all">> + end, + [{<<"items">>, <<"Receive notification of items only">>}, + {<<"nodes">>, <<"Receive notification of new nodes only">>}, + {<<"all">>, <<"Receive notification of items and nodes">>}]) + | Xmlels_Field]); +%% 'pubsub#subscription_depth' +xdata_field(Pubsub_Features, Host, Entity, + [{'subscription_depth', Subscription_Depth} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#subscription_depth">>, + <<"How far to traverse the node graph for notifications">>, + case Subscription_Depth of + undefined -> <<"1">>; + 'all' -> <<"all">>; + _ -> xdata_value('integer', Subscription_Depth) + + end, + [{<<"1">>, <<"Receive notification from direct child nodes only">>}, + {<<"all">>, <<"Receive notification from all descendent nodes">>}]) + | Xmlels_Field]); +%% +%% pubsub#node_config +%% +%% 'pubsub#access_model' +xdata_field(Pubsub_Features, Host, Entity, + [{'access_model', Access_Model} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#access_model">>, + <<"Specify the subscriber model">>, + case Access_Model of + % undefined -> <<"open">>; + 'authorize' -> <<"authorize">>; + 'open' -> <<"open">>; + 'presence' -> <<"presence">>; + 'roster' -> <<"roster">>; + 'whitelist' -> <<"whitelist">> + end, + xdata_access_model(Pubsub_Features, + ['authorize', 'open', 'presence', 'roster', 'whitelist'])) + | Xmlels_Field]); +%% 'pubsub#body_xslt' +xdata_field(Pubsub_Features, Host, Entity, + [{'body_xslt', Xslt} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#body_xslt">>, + <<"The URL of an XSL transformation which can be applied to " + "payloads in order to generate an appropriate message body element">>, + Xslt) + | Xmlels_Field]); +%% 'pubsub#children_association_policy' +xdata_field(Pubsub_Features, Host, Entity, + [{'children_association_policy', Children_Association_Policy} | Options], + Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#children_association_policy">>, + <<"Who may associate leaf nodes with a collection">>, + case Children_Association_Policy of + undefined -> <<"owners">>; + 'owners' -> <<"owners">>; + 'all' -> <<"all">>; + 'whitelist' -> <<"whitelist">> + end, + [{<<"all">>, <<"Anyone may associate leaf nodes with the collection">>}, + {<<"owners">>, <<"Only collection node owners may associate leaf" + " nodes with the collection">>}, + {<<"whitelist">>, <<"Only those on a whitelist may associate leaf " + "nodes with the collection">>}]) + | Xmlels_Field]); +%% 'pubsub#children_association_whitelist' +xdata_field(Pubsub_Features, Host, Entity, + [{'children_association_whitelist', Children_Association_Whitelist} + | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_jid_multi( + <<"pubsub#children_association_whitelist">>, + <<"The list of JIDs that may associate leaf nodes with a collection">>, + Children_Association_Whitelist) + | Xmlels_Field]); +%% 'pubsub#children' +xdata_field(Pubsub_Features, Host, Entity, + [{'children', Children} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_multi( + <<"pubsub#children">>, + <<"The child nodes (leaf or collection) associated with a collection">>, + Children) + | Xmlels_Field]); +%% 'pubsub#children_max' +xdata_field(Pubsub_Features, Host, Entity, + [{'children_max', Children_Max} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#children_max">>, + <<"The maximum number of child nodes that can be associated with a collection">>, + xdata_value('integer', Children_Max)) + | Xmlels_Field]); +%% 'pubsub#collection' +xdata_field(Pubsub_Features, Host, Entity, + [{'collection', Collection} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_multi( + <<"pubsub#collection">>, + <<"The collection(s) with which a node is affiliated">>, + Collection) + | Xmlels_Field]); +%% 'pubsub#contact' +xdata_field(Pubsub_Features, Host, Entity, + [{'contact', Contacts} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_jid_multi( + <<"pubsub#contact">>, + <<"The JIDs of those to contact with questions">>, + Contacts) + | Xmlels_Field]); +%% 'pubsub#dataform_xslt' +xdata_field(Pubsub_Features, Host, Entity, + [{'dataform_xslt', Dataform_Xslt} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#dataform_xslt">>, + <<"Payload XSLT">>, + xdata_value({'text-multi', 'atom'}, Dataform_Xslt)) + | Xmlels_Field]); +%% 'pubsub#deliver_notifications' +xdata_field(Pubsub_Features, Host, Entity, + [{'deliver_notifications', Deliver_Notifications} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#deliver_notifications">>, + <<"Whether to deliver event notifications">>, + xdata_value('boolean', Deliver_Notifications)) + | Xmlels_Field]); +%% 'pubsub#deliver_payloads' +xdata_field(Pubsub_Features, Host, Entity, + [{'deliver_payloads', Deliver_Payloads} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#deliver_payloads">>, + <<"Whether to deliver payloads with event notifications">>, + xdata_value('boolean', Deliver_Payloads)) + | Xmlels_Field]); +%% 'pubsub#description' +xdata_field(Pubsub_Features, Host, Entity, + [{'description', Description} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#description">>, + <<"A description of the node">>, + Description) + | Xmlels_Field]); +%% 'pubsub#item_expire' +xdata_field(Pubsub_Features, Host, Entity, + [{'item_expire', Item_Expire} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#item_expire">>, + <<"Number of seconds after which to automatically purge items">>, + xdata_value('integer', Item_Expire)) + | Xmlels_Field]); +%% 'pubsub#itemreply' +xdata_field(Pubsub_Features, Host, Entity, + [{'itemreply', ItemReply} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#itemreply">>, + <<"Whether owners or publisher should receive replies to items">>, + case ItemReply of + undefined -> <<"publisher">>; + 'owner' -> <<"owner">>; + 'publisher' -> <<"publisher">> + end, + [{<<"owner">>, <<"Statically specify a replyto of the node owner(s)">>}, + {<<"publisher">>, <<"Dynamically specify a replyto of the item publisher">>}]) + | Xmlels_Field]); +%% 'pubsub#language' +xdata_field(Pubsub_Features, Host, Entity, + [{'language', Language} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#language">>, + <<"The default language of the node">>, + case Language of + undefined -> <<"en">>; + _Language -> Language + end, + [{<<"en">>, <<"English">>} + %{<<"es">>, <<"Español">>}, + %{<<"fr">>, <<"Français">>}, + %{<<"pl">>, <<"Polski">>}, + %{<<"ru">>, <<"Русский">>} + ]) + | Xmlels_Field]); +%% 'pubsub#max_items' +xdata_field(Pubsub_Features, Host, Entity, + [{'max_items', Max_Items} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#max_items">>, + <<"The maximum number of items to persist">>, + xdata_value('integer', Max_Items)) + | Xmlels_Field]); +%% 'pubsub#max_payload_size' +xdata_field(Pubsub_Features, Host, Entity, + [{'max_payload_size', Max_Payload_Size} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#max_payload_size">>, + <<"The maximum payload size in bytes">>, + xdata_value('integer', Max_Payload_Size)) + | Xmlels_Field]); +%% 'pubsub#node_type' +xdata_field(Pubsub_Features, Host, Entity, + [{'node_type', Node_Type} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, + Entity, Options, + [case lists:member(<<"collections">>, Pubsub_Features) of + true -> + xmpp_xdata:xmlel_field_list_single( + <<"pubsub#node_type">>, + <<"Whether the node is a leaf (default) or a collection">>, + case Node_Type of + undefined -> <<"leaf">>; + 'leaf' -> <<"leaf">>; + 'collection' -> <<"collection">> + end, + [{<<"leaf">>, <<"The node is a leaf node (default)">>}, + {<<"collection">>, <<"The node is a collection node">>}]); + false -> + %% Collections not supported -> non-modifiable field +% xmpp_xdata:xmlel_field_fixed( +% <<"pubsub#node_type">>, +% <<"The node is a leaf node (default)">>, +% <<"leaf">>) + xmpp_xdata:xmlel_field_list_single( + <<"pubsub#node_type">>, + <<"Whether the node is a leaf (default) or a collection">>, + <<"leaf">>, + [{<<"leaf">>, <<"The node is a leaf node (default)">>}]) + end + |Xmlels_Field]); +%% 'pubsub#notification_type' +xdata_field(Pubsub_Features, Host, Entity, + [{'notification_type', Notification_Type} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#notification_type">>, + <<"Specify the delivery style for event notifications">>, + case Notification_Type of + undefined -> <<"headline">>; + 'headline' -> <<"headline">>; + 'normal' -> <<"normal">>; + 'chat' -> <<"chat">> + end, + [{<<"headline">>, <<"Messages of type headline">>}, + {<<"normal">>, <<"Messages of type normal">>} + %{<<"chat">>, <<"Messages of type chat">>} + %{<<"presence">>, <<"Presence">>} + %{<<"iq">>, <<"Iq">>} + ]) + | Xmlels_Field]); +%% 'pubsub#notify_config' +xdata_field(Pubsub_Features, Host, Entity, + [{'notify_config', Notify_Config} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#notify_config">>, + <<"Notify owners and subscribers when the node configuration changes">>, + xdata_value('boolean', Notify_Config)) + | Xmlels_Field]); +%% 'pubsub#notify_delete' +xdata_field(Pubsub_Features, Host, Entity, + [{'notify_delete', Notify_Delete} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#notify_delete">>, + <<"Notify owners and subscribers when the node is deleted">>, + xdata_value('boolean', Notify_Delete)) + | Xmlels_Field]); +%% 'pubsub#notify_retract' +xdata_field(Pubsub_Features, Host, Entity, + [{'notify_retract', Notify_Retract} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#notify_retract">>, + <<"Notify subscribers when items are removed from the node">>, + xdata_value('boolean', Notify_Retract)) + | Xmlels_Field]); +%% 'pubsub#notify_sub' +xdata_field(Pubsub_Features, Host, Entity, + [{'notify_sub', Notify_Sub} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#notify_sub">>, + <<"Notify owners about new subscribers and unsubscribes">>, + xdata_value('boolean', Notify_Sub)) + | Xmlels_Field]); +%% 'pubsub#persist_items' +xdata_field(Pubsub_Features, Host, Entity, + [{'persist_items', Persist_Items} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#persist_items">>, + <<"Persist items to storage">>, + xdata_value('boolean', Persist_Items)) + | Xmlels_Field]); +%% 'pubsub#presence_based_delivery' +xdata_field(Pubsub_Features, Host, Entity, + [{'presence_based_delivery', Presence_Based_Delivery} | Options] + , Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#presence_based_delivery">>, + <<"Deliver event notifications only to available users">>, + xdata_value('boolean', Presence_Based_Delivery)) + | Xmlels_Field]); +%% 'pubsub#publish_model' +xdata_field(Pubsub_Features, Host, Entity, + [{'publish_model', Publish_Model} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#publish_model">>, + <<"Specify the publisher model">>, + case Publish_Model of + % undefined -> <<"open">>; + 'open' -> <<"open">>; + 'publishers' -> <<"publishers">>; + 'subscribers' -> <<"subscribers">> + end, + [{<<"publishers">>, <<"Only publishers may publish">>}, + {<<"subscribers">>, <<"Subscribers may publish">>}, + {<<"open">>, <<"Anyone may publish">>}]) + | Xmlels_Field]); +%% 'pubsub#purge_offline' +xdata_field(Pubsub_Features, Host, Entity, + [{'purge_offline', Purge_Offline} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#purge_offline">>, + <<"Purge all items when the relevant publisher goes offline ?">>, + xdata_value('boolean', Purge_Offline)) + | Xmlels_Field]); +%% 'pubsub#roster_groups_allowed' +xdata_field(Pubsub_Features, Host, Entity, + [{'roster_groups_allowed', Rosters_Groups_Allowed} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_multi( + <<"pubsub#roster_groups_allowed">>, + <<"Roster group(s) allowed to subscribe and retrieve items">>, + case lists:keyfind(Entity, 1, Rosters_Groups_Allowed) of + false -> []; + {_Entity, Roster_Groups} -> Roster_Groups + end, + xdata_roster(Host, Entity)) + | Xmlels_Field]); +%% 'pubsub#send_last_published_item' +xdata_field(Pubsub_Features, Host, Entity, + [{'send_last_published_item', Send_Last_Published_Item} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_list_single( + <<"pubsub#send_last_published_item">>, + <<"When to send the last published item">>, + case Send_Last_Published_Item of + undefined -> <<"on_sub_and_presence">>; + 'never' -> <<"never">>; + 'on_sub_and_presence' -> <<"on_sub_and_presence">>; + 'on_sub' -> <<"on_sub">> + end, + [{<<"never">>, <<"Never">>}, + {<<"on_sub">>, <<"When a new subscription is processed">>}, + {<<"on_sub_and_presence">>, <<"When a new subscription is processed " + "and whenever a subscriber comes online">>}]) + | Xmlels_Field]); +%% 'pubsub#subscribe' +xdata_field(Pubsub_Features, Host, Entity, + [{'subscribe', Subscribe} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#subscribe">>, + <<"Whether to allow subscriptions">>, + xdata_value('boolean', Subscribe)) + | Xmlels_Field]); +%% 'pubsub#tempsub' +xdata_field(Pubsub_Features, Host, Entity, + [{'tempsub', Tempsub} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_boolean( + <<"pubsub#tempsub">>, + <<"Make all subscriptions temporary, based on subscriber presence">>, + xdata_value('boolean', Tempsub)) + | Xmlels_Field]); +%% 'pubsub#title' +xdata_field(Pubsub_Features, Host, Entity, + [{'title', Title} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#title">>, + <<"A friendly name for the node">>, + Title) + | Xmlels_Field]); +%% 'pubsub#type' +xdata_field(Pubsub_Features, Host, Entity, + [{'type', Type} | Options], Xmlels_Field) -> + xdata_field(Pubsub_Features, Host, Entity, Options, + [xmpp_xdata:xmlel_field_text_single( + <<"pubsub#type">>, + <<"Specify the type of payload data to be provided at this node">>, + Type) + | Xmlels_Field]). + + + +%% +-spec(xdata_value/2 :: +( + DataType :: 'atom' + | 'boolean' + | 'date' + | 'integer' + | {'text-multi', 'atom'}, + Value :: undefined + | atom() + | boolean() + | erlang:timestamp() + | non_neg_integer() + | [atom()]) + -> Xdata_Value :: binary() | [binary(),...] +). + +xdata_value(_, undefined) -> undefined; +%% +xdata_value('atom', Value) -> list_to_binary(atom_to_list(Value)); +%% +xdata_value('boolean', false) -> <<"false">>; +xdata_value('boolean', true) -> <<"true">>; +%% +xdata_value('date', TimeStamp) -> + {DateTime, _} = jlib:timestamp_to_iso(calendar:now_to_datetime(TimeStamp), utc), + list_to_binary(DateTime ++ "Z"); +%% +xdata_value('integer', Value) -> list_to_binary(integer_to_list(Value)); +%% +xdata_value({'text-multi', 'atom'}, Values) -> + [list_to_binary(atom_to_list(Value)) || Value <- Values]. + +%% +-spec(xdata_access_model/3 :: +( + Access_Model :: pubsub_options:access_model(), + Pubsub_Feature :: boolean(), + Access_Models :: [{binary(), binary()}]) + -> Access_Models :: [{binary(), binary()}] +). + +xdata_access_model(_Access_Model, false, Access_Models) -> + Access_Models; +%% +xdata_access_model('authorize' = _Access_Model, true, Access_Models) -> + [{<<"authorize">>, <<"Subscription requests must be approved and only " + "subscribers may retrieve items">>} + | Access_Models]; +%% +xdata_access_model('open' = _Access_Model, true, Access_Models) -> + [{<<"open">>, <<"Anyone may subscribe and retrieve items">>} + | Access_Models]; +%% +xdata_access_model('presence' = _Access_Model, true, Access_Models) -> + [{<<"presence">>, <<"Anyone with a presence subscription of both or from may " + "subscribe and retrieve items">>} + | Access_Models]; +%% +xdata_access_model('roster' = _Access_Model, true, Access_Models) -> + [{<<"roster">>, <<"Anyone in the specified roster group(s) may subscribe and " + "retrieve items">>} + | Access_Models]; +%% +xdata_access_model('whitelist' = _Access_Model, true, Access_Models) -> + [{<<"whitelist">>, <<"Only those on a whitelist may subscribe and retrieve items">>} + | Access_Models]. + +%% +-spec(xdata_access_model/2 :: +( + Pubsub_Features :: exmpp_pubsub:pubsub_features(), + Default_Access_Model :: ['authorize'|'open'|'presence'|'roster'|'whitelist',...]) + -> Access_Models :: [{binary(), binary()},...] +). + +xdata_access_model(Pubsub_Features, Default_Access_Models) -> + lists:foldr(fun + ('authorize', Access_Models) -> + xdata_access_model('authorize', + lists:member(<<"access-authorize">>, Pubsub_Features), Access_Models); + %% + ('open', Access_Models) -> + xdata_access_model('open', + lists:member(<<"access-open">>, Pubsub_Features), Access_Models); + %% + ('presence', Access_Models) -> + xdata_access_model('presence', + lists:member(<<"access-presence">>, Pubsub_Features), Access_Models); + %% + ('roster', Access_Models) -> + xdata_access_model('roster', + lists:member(<<"access-roster">>, Pubsub_Features), Access_Models); + %% + ('whitelist', Access_Models) -> + xdata_access_model('whitelist', + lists:member(<<"access-whitelist">>, Pubsub_Features), Access_Models) + % + end, [], Default_Access_Models). + +%% + +%% Local Entity +-spec(xdata_roster/2 :: +( + Host :: binary(), + Entity :: xmpp_jid:usr_bare()) + -> Roster_Groups :: [Roster_Group::binary()] +). + +xdata_roster(Host, {_, Host, _} = Entity) -> + _Roster_Groups = lists:usort(lists:foldl(fun + (#roster{groups = Groups}, Roster_Groups) -> + lists:foldl(fun + (Group, Roster_Groups_Bis) -> + [Group | Roster_Groups_Bis] + end, Roster_Groups, Groups) + end, [], _Roster = get_entity_roster(Entity))); +%% Remote Entity +xdata_roster(_Host, _Entity) -> + []. diff --git a/src/mod_pubsub_ng/pubsub_parser.erl b/src/mod_pubsub_ng/pubsub_parser.erl new file mode 100644 index 000000000..9bc7de36d --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_parser.erl @@ -0,0 +1,1442 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_parser). +-author('karim.gemayel@process-one.net'). + +-compile({no_auto_import,[node/1]}). + +-compile(export_all). + +-include("pubsub_dev.hrl"). +-include("pubsub_api.hrl"). + +-import(xml, +[ + get_tag_attr_s/2, + get_attr_s/2, + remove_cdata/1 +]). + + +%-type('xmlel_affiliations#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'affiliations', children::[]} +%). + +%-type('xmlel_affiliations#owner'() :: +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'affiliations', children::[]} +%). + +%-type('xmlel_configure#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'configure'} +%). + +%-type('xmlel_configure#owner'() :: +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'configure', children::[]} +%). + +%-type('xmlel_create#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'create', children::[]} +%). + +%-type('xmlel_default#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'default'} +%). + +%-type('xmlel_default#owner'() :: +%% #xmlel{ns::?NS_PUBSUB_OWNER, name::'default', attrs::[], children::[]} +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'default', children::[]} +%). + +%-type('xmlel_delete#owner'() :: +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'delete'} +%). + +%-type('xmlel_items#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'items'} +%). + +%-type('xmlel_options1#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'options', attrs::[]} +%). + +%-type('xmlel_options2#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'options'} +%). + +%-type('xmlel_publish#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'publish'} +%). + +%-type('xmlel_publish-options#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'publish-options'} +%). + +%-type('xmlel_purge#owner'() :: +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'purge', children::[]} +%). + +%-type('xmlel_retract#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'retract'} +%). + +%-type('xmlel_subscribe#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'subscribe', children::[]} +%). + +%-type('xmlel_subscriptions#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'subscriptions', children::[]} +%). + +%-type('xmlel_subscriptions#owner'() :: +% #xmlel{ns::?NS_PUBSUB_OWNER, name::'subscriptions', children::[]} +%). + +%-type('xmlel_unsubscribe#'() :: +% #xmlel{ns::?NS_PUBSUB, name::'unsubscribe', children::[]} +%). + + +-define(Is_Xmlel_Affiliations(Xmlel), +( + Xmlel#xmlel.name == <<"affiliations">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Configure(Xmlel), +( + Xmlel#xmlel.name == <<"configure">> andalso + Xmlel#xmlel.attrs == [] +)). + +-define(Is_Xmlel_Create(Xmlel), +( + Xmlel#xmlel.name == <<"create">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Default(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"default">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Delete(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB_OWNER andalso + Xmlel#xmlel.name == <<"delete">> +)). + +-define(Is_Xmlel_Item(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"item">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Items1(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"items">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Items2(Xmlel), +( +%Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"items">> +)). + +-define(Is_Xmlel_Options1(Xmlel), +( +%Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"options">> andalso + Xmlel#xmlel.attrs == [] +)). + +-define(Is_Xmlel_Options2(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"options">> +)). + +-define(Is_Xmlel_Publish(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"publish">> +)). + +-define(Is_Xmlel_Publish_Options(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"publish-options">> andalso + Xmlel#xmlel.attrs == [] +)). + +-define(Is_Xmlel_Purge(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB_OWNER andalso + Xmlel#xmlel.name == <<"purge">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Redirect(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB_OWNER andalso + Xmlel#xmlel.name == <<"redirect">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Retract(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"retract">> andalso + Xmlel#xmlel.children =/= [] +)). + +-define(Is_Xmlel_Subscribe(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"subscribe">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Subscriptions(Xmlel), +( + %Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"subscriptions">> andalso + Xmlel#xmlel.children == [] +)). + +-define(Is_Xmlel_Unsubscribe(Xmlel), +( +%Xmlel#xmlel.ns == ?NS_PUBSUB andalso + Xmlel#xmlel.name == <<"unsubscribe">> andalso + Xmlel#xmlel.children == [] +)). + + +%% +-spec(parse/4 :: +( + IQ_Type :: 'get' | 'set', + Xmlels :: %-- Create_Node --% +% ['xmlel_create#'(),...] | +% %% +% ['xmlel_create#'() +% |'xmlel_configure#'(),...] | +% %% %-- Delete_Node --% +% ['xmlel_delete#owner'(),...] | +% %% %-- Purge_Node --% +% ['xmlel_purge#owner'(),...] | +% %% %-- Publish_Item --% +% ['xmlel_publish#'(),...] | +% %% +% ['xmlel_publish#'() +% |'xmlel_configure#'(),...] | +% %% +% ['xmlel_publish#'() +% |'xmlel_publish-options#'(),...] | +% %% +% ['xmlel_publish#'() +% |'xmlel_configure#'() +% |'xmlel_publish-options#'(),...] | +% %% %-- Retract_Item --% +% ['xmlel_retract#'(),...] | +% %% %-- Subscribe_Node --% +% ['xmlel_subscribe#'(),...] | +% ['xmlel_subscribe#'() +% |'xmlel_options1#'(),...] | +% %% %-- Unsubscribe_Node --% +% ['xmlel_unsubscribe#'(),...] | +% %% %-- Set_Configure_Subscription --% +% ['xmlel_options2#'(),...] | +% %% %-- Get_Items --% +% ['xmlel_items#'(),...] | +% %% %-- Get_Entity_Affiliations --% +% ['xmlel_affiliations#'(),...] | +% %% %-- Get_Entity_Subscriptions --% +% ['xmlel_subscriptions#'(),...] | +% %% %-- Get_Node_Affiliations --% +% ['xmlel_affiliations#owner'(),...] | +% %% %-- Get_Node_Subscriptions --% +% ['xmlel_subscriptions#owner'(),...] | +% %% %-- Get_Configure_Subscription_Default --% +% ['xmlel_default#'(),...] | +% %% %-- Get_Configure_Subscription --% +% ['xmlel_options2#'(),...] | +% %% %-- Get_Configure_Node_Default --% +% ['xmlel_default#owner'(),...] | +% %% %-- Get_Configure_Node --% +% ['xmlel_configure#owner'(),...] | + %% %-- Other Stanza --% + [xmlel()], + %% + Rsm :: boolean(), + API_Core :: #api_core{}) + -> %-- Create_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'create_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Node_Config :: [Xmlel::xmlel()] + } + } + | %-- Delete_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'delete_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + RedirectURI :: undefined | binary() + } + } + | %-- Purge_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'purge_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Publish_Item --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'publish_item', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Item :: 0, + ItemId :: undefined, + Payload :: exmpp_pubsub:payload_empty(), + Node_Config :: [Xmlel::xmlel()], + Publish_Options :: [Xmlel::xmlel()] + } + } + | + {result, + Module :: 'pubsub_core' | module(), + Function :: 'publish_item', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Item :: 1, + ItemId :: undefined | exmpp_pubsub:itemId(), + Payload :: exmpp_pubsub:payload() | [Xmlel::xmlel(),...], + Node_Config :: [Xmlel::xmlel()], + Publish_Options :: [Xmlel::xmlel()] + } + } + | %-- Retract_Item --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'retract_item', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + ItemId :: undefined | exmpp_pubsub:itemId(), + Notify :: undefined | boolean() + } + } + | %-- Subscribe_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'subscribe_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + Subscribe_Options :: [Xmlel::xmlel()] + } + } + | %-- Unsubscribe_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'unsubscribe_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + SubId :: undefined | exmpp_pubsub:subId() + } + } + | %-- Set_Configure_Subscription --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'set_configure_subscription', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + SubId :: undefined | exmpp_pubsub:subId(), + Subscribe_Options :: [Xmlel::xmlel()] + } + } + | %-- Get_Items --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_items', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: non_neg_integer(), + ItemIds :: undefined + } + } + | + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_items', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + SubId :: undefined | exmpp_pubsub:subId(), + Max_Items :: undefined, + ItemIds :: [] | exmpp_pubsub:itemIds() + } + } + | %-- Get_Entity_Affiliations --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_entity_affiliations', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Entity_Subscriptions --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_entity_subscriptions', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Node_Affiliations --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_node_affiliations', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Node_Subscriptions --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_node_subscriptions', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Configure_Subscription_Default --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_configure_subscription_default', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Configure_Subscription --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_configure_subscription', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId(), + Jid :: binary() | undefined, + SubId :: undefined | exmpp_pubsub:subId() + } + } + | %-- Get_Configure_Node_Default --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_configure_node_default', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + | %-- Get_Configure_Node --% + {result, + Module :: 'pubsub_core' | module(), + Function :: 'get_configure_node', + Parameters :: { + NodeId :: undefined | exmpp_pubsub:nodeId() + } + } + %%% + | {error, 'invalid-payload'} +). + +%-- Create_Node --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <create node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Create], _Rsm, API_Core) + when ?Is_Xmlel_Create(Xmlel_Create) -> + case xmlns(Xmlel_Create) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.create_node, + _Function = 'create_node', + _Parameters = { + _NodeId = node(Xmlel_Create), + _Node_Config = [] + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <create node='princely_musings'/> +%% <configure> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#node_config</value> +%% </field> +%% <field var='pubsub#title'><value>Princely Musings (Atom)</value></field> +%% </x> +%% </configure> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Create, Xmlel_Configure], _Rsm, API_Core) + when ?Is_Xmlel_Create(Xmlel_Create) + andalso ?Is_Xmlel_Configure(Xmlel_Configure) -> + case {xmlns(Xmlel_Create), xmlns(Xmlel_Configure)} of + {NS, NS} when NS == ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.create_node, + _Function = 'create_node', + _Parameters = { + _NodeId = node(Xmlel_Create), + _Node_Config = remove_cdata(Xmlel_Configure#xmlel.children) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Delete_Node --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <delete node='princely_musings'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <delete node='princely_musings'> +%% <redirect uri='xmpp:hamlet@denmark.lit?;node=blog'/> +%% </delete> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Delete], _Rsm, API_Core) + when ?Is_Xmlel_Delete(Xmlel_Delete) -> + case {xmlns(Xmlel_Delete), remove_cdata(Xmlel_Delete#xmlel.children)} of + {?NS_PUBSUB_OWNER, []} -> + {result, + _Module = API_Core#api_core.delete_node, + _Function = 'delete_node', + _Parameters = { + _NodeId = node(Xmlel_Delete), + _RedirectURI = undefined + } + }; + {?NS_PUBSUB_OWNER, [Xmlel_Redirect]} + when ?Is_Xmlel_Redirect(Xmlel_Redirect) -> + case xmlns(Xmlel_Redirect) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.delete_node, + _Function = 'delete_node', + _Parameters = { + _NodeId = node(Xmlel_Delete), + _RedirectURI = uri(Xmlel_Redirect) + } + }; + _ -> + {error, 'invalid-payload'} + end; + _ -> + {error, 'invalid-payload'} + end; + +%-- Purge_Node --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <purge node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Purge], _Rsm, API_Core) + when ?Is_Xmlel_Purge(Xmlel_Purge) -> + case xmlns(Xmlel_Purge) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.purge_node, + _Function = 'purge_node', + _Parameters = { + _NodeId = node(Xmlel_Purge) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Publish_Item --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <publish node='princely_musings'/> +%% <configure> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#node_config</value> +%% </field> +%% <field var='pubsub#title'><value>Princely Musings (Atom)</value></field> +%% </x> +%% </configure> +%% <publish-options> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#publish-options</value> +%% </field> +%% <field var='pubsub#access_model'><value>presence</value></field> +%% </x> +%% </publish-options> +%% </pubsub> +%% </iq> + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <publish node='princely_musings'> +%% <item id='bnd81g37d61f49fgn581'/> +%% </publish> +%% <configure> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#node_config</value> +%% </field> +%% <field var='pubsub#title'><value>Princely Musings (Atom)</value></field> +%% </x> +%% </configure> +%% <publish-options> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#publish-options</value> +%% </field> +%% <field var='pubsub#access_model'><value>presence</value></field> +%% </x> +%% </publish-options> +%% </pubsub> +%% </iq> + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <publish node='princely_musings'> +%% <item id='bnd81g37d61f49fgn581'> +%% <entry xmlns='http://www.w3.org/2005/Atom'> +%% <title>Soliloquy</title> +%% </entry> +%% </item> +%% </publish> +%% <publish-options> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#publish-options</value> +%% </field> +%% <field var='pubsub#access_model'><value>presence</value></field> +%% </x> +%% </publish-options> +%% </pubsub> +%% </iq> + +%% +parse('set' = _IQ_Type, [Xmlel_Publish], _Rsm, API_Core) + when ?Is_Xmlel_Publish(Xmlel_Publish) -> + case {xmlns(Xmlel_Publish), remove_cdata(Xmlel_Publish#xmlel.children)} of + %% + {?NS_PUBSUB, []} -> + {result, + _Module = API_Core#api_core.publish_item, + _Function = 'publish_item', + _Parameters = { + _NodeId = node(Xmlel_Publish), + _Item = 0, + _ItemId = undefined, + _Payload = [], + _Node_Config = [], + _Publish_Options = [] + } + }; + %% +% [#xmlel{ns = ?NS_PUBSUB, name = 'item'} = Xmlel_Item] -> + {?NS_PUBSUB, [#xmlel{name = <<"item">>} = Xmlel_Item]} -> + {result, + _Module = API_Core#api_core.publish_item, + _Function = 'publish_item', + _Parameters = { + _NodeId = node(Xmlel_Publish), + _Item = 1, + _ItemId = id(Xmlel_Item), + _Payload = remove_cdata(Xmlel_Item#xmlel.children), + _Node_Config = [], + _Publish_Options = [] + } + }; + %% + _ -> + {error, 'invalid-payload'} + end; +%% +parse('set' = _IQ_Type, [Xmlel_Publish, Xmlel_Configure], _Rsm, API_Core) + when ?Is_Xmlel_Publish(Xmlel_Publish) + andalso ?Is_Xmlel_Configure(Xmlel_Configure) -> + case {xmlns(Xmlel_Publish), xmlns(Xmlel_Configure)} of + {NS, NS} when NS == ?NS_PUBSUB -> + case parse('set', [Xmlel_Publish], _Rsm, API_Core) of + {result, Module, Function, {NodeId, Item, ItemId, Payload, _, _}} -> + {result, + Module, + Function, + _Parameters = { + NodeId, + Item, + ItemId, + Payload, + _Node_Config = remove_cdata(Xmlel_Configure#xmlel.children), + _Publish_Options = [] + } + }; + Error -> + Error + end; + _ -> + {error, 'invalid-paylaod'} + end; +%% +parse('set' = _IQ_Type, [Xmlel_Publish, Xmlel_Publish_Options], _Rsm, API_Core) + when ?Is_Xmlel_Publish(Xmlel_Publish) + andalso ?Is_Xmlel_Publish_Options(Xmlel_Publish_Options) -> + case {xmlns(Xmlel_Publish), xmlns(Xmlel_Publish_Options)} of + {NS, NS} when NS == ?NS_PUBSUB -> + case parse('set', [Xmlel_Publish], _Rsm, API_Core) of + {result, Module, Function, {NodeId, Item, ItemId, Payload, _, _}} -> + {result, + Module, + Function, + _Parameters = { + NodeId, + Item, + ItemId, + Payload, + _Node_Config = [], + _Publish_Options = remove_cdata( + Xmlel_Publish_Options#xmlel.children) + } + }; + Error -> + Error + end; + _ -> + {error, 'invalid-payload'} + end; + +%% +parse('set' = _IQ_Type, [Xmlel_Publish, Xmlel_Configure, Xmlel_Publish_Options], + _Rsm, API_Core) + when ?Is_Xmlel_Publish(Xmlel_Publish) + andalso ?Is_Xmlel_Configure(Xmlel_Configure) + andalso ?Is_Xmlel_Publish_Options(Xmlel_Publish_Options) -> + case {xmlns(Xmlel_Publish), xmlns(Xmlel_Configure), xmlns(Xmlel_Publish_Options)} of + {NS, NS, NS} when NS == ?NS_PUBSUB -> + case parse('set', [Xmlel_Publish, Xmlel_Configure], _Rsm, API_Core) of + {result, Module, Function, + {NodeId, Item, ItemId, Payload, Node_Config, _}} -> + {result, + Module, + Function, + _Parameters = { + NodeId, + Item, + ItemId, + Payload, + Node_Config, + _Publish_Options = remove_cdata( + Xmlel_Publish_Options#xmlel.children) + } + }; + Error -> + Error + end; + _ -> + {error, 'invalid-payload'} + end; +%% +%% +parse('set' = _IQ_Type, [Xmlel_Publish, Xmlel_Publish_Options, Xmlel_Configure], + Rsm, API_Core) + when ?Is_Xmlel_Publish(Xmlel_Publish) + andalso ?Is_Xmlel_Publish_Options(Xmlel_Publish_Options) + andalso ?Is_Xmlel_Configure(Xmlel_Configure) -> + case {xmlns(Xmlel_Publish), xmlns(Xmlel_Publish_Options), xmlns(Xmlel_Configure)} of + {NS, NS, NS} when NS == ?NS_PUBSUB -> + parse('set', [Xmlel_Publish, Xmlel_Configure, Xmlel_Publish_Options], + Rsm, API_Core); + _ -> + {error, 'invalid-payload'} + end; + +%-- Get_Entity_Affiliations --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <affiliations/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <affiliations node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Affiliations], _Rsm, API_Core) + when ?Is_Xmlel_Affiliations(Xmlel_Affiliations) -> + case xmlns(Xmlel_Affiliations) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.get_entity_affiliations, + _Function = 'get_entity_affiliations', + _Parameters = { + _NodeId = node(Xmlel_Affiliations) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% TODO : RSM + +%-- Get_Entity_Subscriptions --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <subscriptions/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <subscriptions node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Subscriptions], _Rsm, API_Core) + when ?Is_Xmlel_Subscriptions(Xmlel_Subscriptions) -> + case xmlns(Xmlel_Subscriptions) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.get_entity_subscriptions, + _Function = 'get_entity_subscriptions', + _Parameters = { + _NodeId = node(Xmlel_Subscriptions) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% TODO : RSM + +%-- Get_Node_Affiliations --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <affiliations node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Affiliations], _Rsm, API_Core) + when ?Is_Xmlel_Affiliations(Xmlel_Affiliations) -> + case xmlns(Xmlel_Affiliations) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.get_node_affiliations, + _Function = 'get_node_affiliations', + _Parameters = { + _NodeId = node(Xmlel_Affiliations) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% TODO : RSM + +%-- Get_Node_Subscriptions --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <subscriptions node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Subscriptions], _Rsm, API_Core) + when ?Is_Xmlel_Subscriptions(Xmlel_Subscriptions) -> + case xmlns(Xmlel_Subscriptions) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.get_node_subscriptions, + _Function = 'get_node_subscriptions', + _Parameters = { + _NodeId = node(Xmlel_Subscriptions) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% TODO : RSM + +%-- Retract_Item --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <retract node='princely_musings'> +%% <item id='ae890ac52d0df67ed7cfdf51b644e901'/> +%% </retract> +%% </pubsub> +%% </iq> +%% +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <retract node='princely_musings' notify='true'> +%% <item id='ae890ac52d0df67ed7cfdf51b644e901'/> +%% </retract> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Retract], _Rsm, API_Core) + when ?Is_Xmlel_Retract(Xmlel_Retract) -> + case {xmlns(Xmlel_Retract), notify(Xmlel_Retract)} of + {?NS_PUBSUB, undefined} -> + {result, + _Module = API_Core#api_core.retract_item, + _Function = 'retract_item', + _Parameters = { + _NodeId = node(Xmlel_Retract), + _ItemId = case remove_cdata(Xmlel_Retract#xmlel.children) of + [Xmlel_Item] + when ?Is_Xmlel_Item(Xmlel_Item) -> + case xmlns(Xmlel_Item) of + ?NS_PUBSUB -> id(Xmlel_Item); + _ -> undefined + end; + _ -> + undefined + + end, + _Notify = undefined + } + }; + {?NS_PUBSUB, False} + when False == <<"false">> orelse False == <<"0">> -> + {result, + _Module = API_Core#api_core.retract_item, + _Function = 'retract_item', + _Parameters = { + _NodeId = node(Xmlel_Retract), + _ItemId = case remove_cdata(Xmlel_Retract#xmlel.children) of + [Xmlel_Item] + when ?Is_Xmlel_Item(Xmlel_Item) -> + case xmlns(Xmlel_Item) of + ?NS_PUBSUB -> id(Xmlel_Item); + _ -> undefined + end; + _ -> + undefined + + end, + _Notify = false + } + }; + {?NS_PUBSUB, True} + when True == <<"true">> orelse True == <<"1">> -> + {result, + _Module = API_Core#api_core.retract_item, + _Function = 'retract_item', + _Parameters = { + _NodeId = node(Xmlel_Retract), + _ItemId = case remove_cdata(Xmlel_Retract#xmlel.children) of + [Xmlel_Item] + when ?Is_Xmlel_Item(Xmlel_Item) -> + case xmlns(Xmlel_Item) of + ?NS_PUBSUB -> id(Xmlel_Item); + _ -> undefined + end; + _ -> + undefined + + end, + _Notify = true + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Subscribe_Node --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <subscribe node='princely_musings' jid='francisco@denmark.lit'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <subscribe node='princely_musings' jid='francisco@denmark.lit'/> +%% <options> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#subscribe_options</value> +%% </field> +%% <field var='pubsub#deliver'><value>1</value></field> +%% <field var='pubsub#show-values'> +%% <value>chat</value> +%% <value>online</value> +%% <value>away</value> +%% </field> +%% </x> +%% </options> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Subscribe], _Rsm, API_Core) + when ?Is_Xmlel_Subscribe(Xmlel_Subscribe) -> + case xmlns(Xmlel_Subscribe) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.subscribe_node, + _Function = 'subscribe_node', + _Parameters = { + _NodeId = node(Xmlel_Subscribe), + _Jid = id(Xmlel_Subscribe), + _Subscribe_Options = [] + } + }; + _ -> + {error, 'invalid-payload'} + end; +%% +parse('set' = _IQ_Type, [Xmlel_Subscribe, Xmlel_Options], _Rsm, API_Core) + when ?Is_Xmlel_Subscribe(Xmlel_Subscribe) + andalso ?Is_Xmlel_Options1(Xmlel_Options) -> + case {xmlns(Xmlel_Subscribe), xmlns(Xmlel_Options)} of + {NS, NS} when NS == ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.subscribe_node, + _Function = 'subscribe_node', + _Parameters = { + _NodeId = node(Xmlel_Subscribe), + _Jid = jid(Xmlel_Subscribe), + _Subscribe_Options = remove_cdata(Xmlel_Options#xmlel.children) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Unsubscribe_Node --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <unsubscribe node='princely_musings' jid='francisco@denmark.lit'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <unsubscribe node='princely_musings' jid='francisco@denmark.lit' subid='SubId'/> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Unsubscribe], _Rsm, API_Core) + when ?Is_Xmlel_Unsubscribe(Xmlel_Unsubscribe) -> + case xmlns(Xmlel_Unsubscribe) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.unsubscribe_node, + _Function = 'unsubscribe_node', + _Parameters = { + _NodeId = node(Xmlel_Unsubscribe), + _Jid = jid(Xmlel_Unsubscribe), + _SubId = subid(Xmlel_Unsubscribe) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Set_Configure_Subscription --% + +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <options node='princely_musings' jid='francisco@denmark.lit'> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#subscribe_options</value> +%% </field> +%% <field var='pubsub#deliver'><value>1</value></field> +%% <field var='pubsub#show-values'> +%% <value>chat</value> +%% <value>online</value> +%% <value>away</value> +%% </field> +%% </x> +%% </options> +%% </pubsub> +%% </iq> +%% +%% <iq type='set'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <options node='princely_musings' jid='francisco@denmark.lit' subid='SubId'> +%% <x xmlns='jabber:x:data' type='submit'> +%% <field var='FORM_TYPE' type='hidden'> +%% <value>http://jabber.org/protocol/pubsub#subscribe_options</value> +%% </field> +%% <field var='pubsub#deliver'><value>1</value></field> +%% <field var='pubsub#show-values'> +%% <value>chat</value> +%% <value>online</value> +%% <value>away</value> +%% </field> +%% </x> +%% </options> +%% </pubsub> +%% </iq> + +parse('set' = _IQ_Type, [Xmlel_Options], _Rsm, API_Core) + when ?Is_Xmlel_Options2(Xmlel_Options) -> + case xmlns(Xmlel_Options) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.set_configure_subscription, + _Function = 'set_configure_subscription', + _Parameters = { + _NodeId = node(Xmlel_Options), + _Jid = jid(Xmlel_Options), + _SubId = subid(Xmlel_Options), + _Subscribe_Options = remove_cdata(Xmlel_Options#xmlel.children) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Get_Items --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <items node='princely_musings'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <items node='princely_musings' max_items='2'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <items node='princely_musings'> +%% <item id='368866411b877c30064a5f62b917cffe'/> +%% <item id='4e30f35051b7b8b42abe083742187228'/> +%% </items> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Items], _Rsm, API_Core) + when ?Is_Xmlel_Items1(Xmlel_Items) -> + case {xmlns(Xmlel_Items), max_items(Xmlel_Items)} of + {?NS_PUBSUB, undefined} -> + {result, + _Module = API_Core#api_core.get_items, + _Function = 'get_items', + _Parameters = { + _NodeId = node(Xmlel_Items), + _SubId = subid(Xmlel_Items), + _Max_Items = undefined, + _ItemIds = [] + } + }; + {?NS_PUBSUB, Binary} -> + case catch list_to_integer(binary_to_list(Binary)) of + Integer + when is_integer(Integer) andalso Integer >= 0 -> + {result, + _Module = API_Core#api_core.get_items, + _Function = 'get_items', + _Parameters = { + _NodeId = node(Xmlel_Items), + _SubId = subid(Xmlel_Items), + _Max_Items = Integer, + _ItemIds = undefined + } + }; + _ -> + {error, 'invalid-payload'} + end; + _ -> + {error, 'invalid-payload'} + end; +%% +parse('get' = _IQ_Type, [Xmlel_Items], _Rsm, API_Core) + when ?Is_Xmlel_Items2(Xmlel_Items) -> + case + {xmlns(Xmlel_Items), + parse_xmlels_item(remove_cdata(Xmlel_Items#xmlel.children))} + of + {?NS_PUBSUB, {ok, ItemIds}} -> + {result, + _Module = API_Core#api_core.get_items, + _Function = 'get_items', + _Parameters = { + _NodeId = node(Xmlel_Items), + _SubId = subid(Xmlel_Items), + _Max_Items = undefined, + ItemIds + } + }; + {_, _Error} -> + {error, 'invalid-payload'} + end; + +%-- Get_Configure_Subscription_Default --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <default/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <default node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Default], _Rsm, API_Core) + when ?Is_Xmlel_Default(Xmlel_Default) -> + case xmlns(Xmlel_Default) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.get_configure_subscription_default, + _Function = 'get_configure_subscription_default', + _Parameters = { + _NodeId = node(Xmlel_Default) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Get_Configure_Subscription --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <options node='princely_musings' jid='francisco@denmark.lit'/> +%% </pubsub> +%% </iq> +%% +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub'> +%% <options node='princely_musings' jid='francisco@denmark.lit' subid='SubId'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Options], _Rsm, API_Core) + when ?Is_Xmlel_Options2(Xmlel_Options) -> + case xmlns(Xmlel_Options) of + ?NS_PUBSUB -> + {result, + _Module = API_Core#api_core.get_configure_subscription, + _Function = 'get_configure_subscription', + _Parameters = { + _NodeId = node(Xmlel_Options), + _Jid = jid(Xmlel_Options), + _SubId = subid(Xmlel_Options) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Get_Configure_Node_Default --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <default/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Default], _Rsm, API_Core) + when ?Is_Xmlel_Default(Xmlel_Default) -> + case xmlns(Xmlel_Default) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.get_configure_node_default, + _Function = 'get_configure_node_default', + _Parameters = { + _NodeId = node(Xmlel_Default) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%-- Get_Configure_Node --% + +%% <iq type='get'> +%% <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'> +%% <configure node='princely_musings'/> +%% </pubsub> +%% </iq> + +parse('get' = _IQ_Type, [Xmlel_Configure], _Rsm, API_Core) + when ?Is_Xmlel_Configure(Xmlel_Configure) -> + case xmlns(Xmlel_Configure) of + ?NS_PUBSUB_OWNER -> + {result, + _Module = API_Core#api_core.get_configure_node, + _Function = 'get_configure_node', + _Parameters = { + _NodeId = node(Xmlel_Configure) + } + }; + _ -> + {error, 'invalid-payload'} + end; + +%% +parse(_IQ_Type, _Xmlels, _Rsm, _API_Core) -> + {error, 'invalid-payload'}. + + + +%%%%%%%%%%%%%%%%%%%%% +%% Parsing Helpers %% +%%%%%%%%%%%%%%%%%%%%% + + +%% +-spec(parse_xmlels_item/1 :: +( + Xmlels :: [Xmlel_Item::xmlel(),...]) + -> {ok, ItemIds :: [] | exmpp_pubsub:itemIds()} + %%% + | {error, 'invalid-payload'} +). + +parse_xmlels_item(Xmlels) -> + parse_xmlels_item(Xmlels, []). + +%% +-spec(parse_xmlels_item/2 :: +( + Xmlels :: [Xmlel_Item::xmlel(),...], + ItemIds :: [] | exmpp_pubsub:itemIds()) + -> {ok, ItemIds :: [] | exmpp_pubsub:itemIds()} + %%% + | {error, 'invalid-payload'} +). + + +%% +parse_xmlels_item([Xmlel_Item | Xmlels], ItemIds) + when ?Is_Xmlel_Item(Xmlel_Item) -> + case {xmlns(Xmlel_Item), id(Xmlel_Item)} of + {?NS_PUBSUB, ItemId} + when ItemId /= undefined -> + parse_xmlels_item(Xmlels, [ItemId | ItemIds]); + _ -> + {error, 'invalid-payload'} + end; +parse_xmlels_item([] = _Xmlels_Item, ItemIds) -> + {ok, ItemIds}; +%% +parse_xmlels_item(_Xmlels, _ItemIds) -> + {error, 'invalid-payload'}. + + +xmlns(Xmlel) -> + get_tag_attr_s(<<"xmlns">>, Xmlel). + +node(Xmlel) -> + case get_tag_attr_s(<<"node">>, Xmlel) of + <<>> -> undefined; + NodeId -> NodeId + end. + +id(Xmlel) -> + case get_tag_attr_s(<<"id">>, Xmlel) of + <<>> -> undefined; + ItemId -> ItemId + end. + +uri(Xmlel) -> + case get_tag_attr_s(<<"uri">>, Xmlel) of + <<>> -> undefined; + URI -> URI + end. + +jid(Xmlel) -> + case get_tag_attr_s(<<"jid">>, Xmlel) of + <<>> -> undefined; + Jid -> Jid + end. + +subid(Xmlel) -> + case get_tag_attr_s(<<"subid">>, Xmlel) of + <<>> -> undefined; + SubId -> SubId + end. + +notify(Xmlel) -> + case get_tag_attr_s(<<"notify">>, Xmlel) of + <<>> -> undefined; + Notify -> Notify + end. + +max_items(Xmlel) -> + case get_tag_attr_s(<<"max_items">>, Xmlel) of + <<>> -> undefined; + Max_Items -> Max_Items + end. diff --git a/src/mod_pubsub_ng/pubsub_tools.erl b/src/mod_pubsub_ng/pubsub_tools.erl new file mode 100644 index 000000000..9bb5d2504 --- /dev/null +++ b/src/mod_pubsub_ng/pubsub_tools.erl @@ -0,0 +1,685 @@ +%%% ==================================================================== +%%% ``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-2011, ProcessOne +%%% All Rights Reserved.'' +%%% This software is copyright 2006-2011, ProcessOne. +%%% +%%% @copyright 2006-2011 ProcessOne +%%% @author Karim Gemayel <karim.gemayel@process-one.net> +%%% [http://www.process-one.net/] +%%% @version {@vsn}, {@date} {@time} +%%% @end +%%% ==================================================================== + +%%% @headerfile "pubsub_dev.hrl" + +-module(pubsub_tools). +-author('karim.gemayel@process-one.net'). + +-compile(export_all). + +-include("pubsub_dev.hrl"). + +jid_to_string({undefined, S, undefined}) -> + jlib:jid_to_string({<<>>, S, <<>>}); +jid_to_string({undefined, S, R}) -> + jlib:jid_to_string({<<>>, S, R}); +jid_to_string({U, S, undefined}) -> + jlib:jid_to_string({U, S, <<>>}); +jid_to_string(USR) -> + jlib:jid_to_string(USR). + +make_jid({undefined, S, undefined}) -> + jlib:make_jid(<<>>, S, <<>>); +make_jid({U, S, undefined}) -> + jlib:make_jid({U, S, <<>>}); +make_jid(Entity) -> + jlib:make_jid(Entity). + +%% @doc Determine if data is a JID +-spec(is_jid/1 :: +( + Data :: binary() | undefined) + -> xmpp_jid:entity() | {error, 'jid-malformed'} +). + +is_jid(Data) -> + try jlib:string_to_jid(Data) of + #jid{} = Jid -> Jid + catch + _Error -> {error, 'jid-malformed'} + end. + +%%% +%-spec(get_value/2 :: +%( +% Options :: {Node_Options :: pubsub_options:options_node(), +% Item_Options :: [] | pubsub_options:options_item()} +% | pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription() +% | [], +% Key :: atom()) +% -> Value :: atom() +% | binary() +% | boolean() +% | non_neg_integer() +% | undefined +% | [] +% | 'none' +% | [atom(),...] +% | [binary(),...] +% | [boolean(),...] +% | [non_neg_integer(),...] +%). + +get_value({Node_Options, [] = _Item_Options}, Key) -> + get_value(Node_Options, Key, 0); +%% +get_value({Node_Options, Item_Options}, Key) -> + case get_value(Item_Options, Key, 'none') of + 'none' -> get_value(Node_Options, Key, false); + Value -> Value + end; +%% +get_value([], _Key) -> 'none'; +%% +get_value(Options, Key) -> + get_value(Options, Key, 'none'). + +%%% +%-spec(get_value/3 :: +%( +% Options :: {Node_Options :: pubsub_options:options_node(), +% Item_Options :: [] | pubsub_options:options_item()} +% | pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription(), +% Key :: atom(), +% Default :: term()) +% -> Value :: atom() +% | binary() +% | boolean() +% | non_neg_integer() +% | undefined +% | [] +% | [atom(),...] +% | [binary(),...] +% | [boolean(),...] +% | [non_neg_integer(),...] +%). +get_value({Node_Options, [] = _Item_Options}, Key, Default) -> + get_value(Node_Options, Key, Default); +%% +get_value({Node_Options, Item_Options}, Key, Default) -> + case get_value(Item_Options, Key, 'none') of + 'none' -> get_value(Node_Options, Key, Default); + Value -> Value + end; +%% +get_value(Options, Key, Default) -> + case lists:keyfind(Key, 1, Options) of + {_Key, Value} -> Value; + false -> Default + end. + +%%% +%-spec(set_value/3 :: +%( +% Options :: pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription(), +% Key :: atom(), +% Value :: atom() +% | binary() +% | boolean() +% | non_neg_integer() +% | undefined +% | [] +% | [atom(),...] +% | [binary(),...] +% | [boolean(),...] +% | [non_neg_integer(),...]) +% -> Options :: pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription() +%). + +set_value(Options, Key, Value) -> + lists:keyreplace(Key, 1, Options, {Key, Value}). + +%% + +%-spec(get_option/2 :: +%( +% Options :: {Node_Options :: pubsub_options:options_node(), +% Item_Options :: [] | pubsub_options:options_item()} +% | pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription() +% | [], +% Key :: atom()) +% -> Option :: pubsub_options:option_item() +% | pubsub_options:option_node() +% | pubsub_options:option_subscription() +% | 'none' +%). + +get_option({Node_Options, [] = _Item_Options}, Key) -> + get_option(Node_Options, Key, 'none'); +%% +get_option({Node_Options, Item_Options}, Key) -> + case get_option(Item_Options, Key, 'none') of + 'none' -> get_option(Node_Options, Key, 'none'); + Option -> Option + end; +%% +get_option([], _Key) -> 'none'; +%% +get_option(Options, Key) -> + get_option(Options, Key, 'none'). + +%%% +%-spec(get_option/3 :: +%( +% Options :: {Node_Options :: pubsub_options:options_node(), +% Item_Options :: [] | pubsub_options:options_item()} +% | pubsub_options:options_item() +% | pubsub_options:options_node() +% | pubsub_options:options_subscription(), +% Key :: atom(), +% Default :: term()) +% -> Option :: pubsub_options:option_item() +% | pubsub_options:option_node() +% | pubsub_options:option_subscription() +% | term() +%). + +get_option({Node_Options, [] = _Item_Options}, Key, Default) -> + get_option(Node_Options, Key, Default); +%% +get_option({Node_Options, Item_Options}, Key, Default) -> + case get_option(Item_Options, Key, 'none') of + 'none' -> get_option(Node_Options, Key, Default); + Option -> Option + end; +%% +get_option(Options, Key, Default) -> + case lists:keyfind(Key, 1, Options) of + {_Key, Value} -> _Option = {Key, Value}; + false -> Default + end. + +%% +-spec(get_entity_roster/1 :: +( + Entity :: xmpp_jid:usr_entity() + | xmpp_jid:entity()) + -> Roster::[Roster_Item::#roster{}] +). + +get_entity_roster({U,S, _R} = _USR_Entity) -> + _Roster = ejabberd_hooks:run_fold(roster_get, S, [], [{U,S}]); +get_entity_roster(#jid{luser = U, lserver = S} = _Jid_Entity) -> + _Roster = get_entity_roster({U,S, undefined}). + +%% +-spec(check_access_model/3 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_entity(), + Criteria :: {Access_Model :: pubsub_options:access_model(), + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: exmpp_pubsub:subscriptions(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed()}) + -> ok + %%% + | {error, 'forbidden'} +). + +check_access_model(_Host, _Entity, + {_Access_Model, 'outcast' = _Affiliation, _Subscriptions, _Node_Owners, + _Rosters_Groups_Allowed}) -> + {error, 'forbidden'}; +%% +check_access_model(_Host, _Entity, + {'open' = _Access_Model, _Affiliation, _Subscriptions, _Node_Owners, + _Rosters_Groups_Allowed}) -> + ok; +%% +check_access_model(Host, Entity, + {'presence' = _Access_Model, _Affiliation, _Subscriptions, Node_Owners, + _Rosters_Groups_Allowed}) -> + case is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) of + false -> + {error, 'forbidden'}; + _Node_Owner -> + ok + end; +%% +check_access_model(_Host, Entity, + {'roster' = _Access_Model, _Affiliation, _Subscriptions, _Node_Owners, + Rosters_Groups_Allowed}) -> + case is_contact_in_allowed_roster_groups(Entity, Rosters_Groups_Allowed) of + false -> + {error, 'forbidden'}; + {_Node_Owner, _Roster_Group_Allowed} -> + ok + end; +%% +check_access_model(_Host, _Entity, + {'whitelist' = _Access_Model, Affiliation, _Subscriptions, _Node_Owners, + _Roster_Groups_Allowed}) + when Affiliation == 'member' + %%orelse Affiliation == 'owner' + orelse Affiliation == 'publish-only' + orelse Affiliation == 'publisher' -> + ok; +check_access_model(_Host, _Entity, + {'whitelist' = _Access_Model, _Affiliation, _Subscriptions, _Node_Owners, + _Roster_Groups_Allowed}) -> + {error, 'forbidden'}; +%% +check_access_model(_Host, _Entity, + {'authorize' = _Access_Model, _Affiliation, Subscriptions, _Node_Owners, + _Roster_Groups_Allowed}) -> + case has_subscriptions(Subscriptions) of + true -> ok; + false -> {error, 'forbidden'} + end. + + + +%% +-spec(check_access_model/7 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Entity :: xmpp_jid:usr_entity(), + Access_Model :: pubsub_options:access_model(), + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: exmpp_pubsub:subscriptions(), + Node_Owners :: [Node_Owner::xmpp_jid:usr_bare(),...], + Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed()) + -> ok + %%% + | {error, 'forbidden'} + | {error, 'item-not-found'} +). + +check_access_model(_Host, _Entity, _Access_Model, 'owner' = _Affiliation, + _Subscriptions, _Node_Owners, _Rosters_Groups_Allowed) -> + ok; +%% +check_access_model(Host, Entity, Access_Model, 'outcast' = _Affiliation, + _Subscriptions, Node_Owners, Rosters_Groups_Allowed) -> + case Access_Model of + %% + 'open'-> + {error, 'forbidden'}; + %% + 'presence' -> + case + is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) + of + false -> + {error, 'item-not-found'}; + _Node_Owner -> + {error, 'forbidden'} + end; + %% + 'roster' -> + case + is_contact_in_allowed_roster_groups(Entity, + Rosters_Groups_Allowed) + of + false -> + {error, 'item-not-found'}; + {_Node_Owner, _Roster_Group_Allowed} -> + {error, 'forbidden'} + end; + %% + 'authorize' -> + {error, 'forbidden'}; + %% + 'whitelist' -> + {error, 'item-not-found'} + end; +%% +check_access_model(_Host, _Entity, 'open' = _Access_Model, _Affiliation, + _Subscriptions, _Node_Owners, _Rosters_Groups_Allowed) -> + ok; +%% +check_access_model(Host, Entity, 'presence' = _Access_Model, _Affiliation, + _Subscriptions, Node_Owners, _Rosters_Groups_Allowed) -> + case is_contact_subscribed_to_node_owners(Host, Entity, Node_Owners) of + false -> + {error, 'item-not-found'}; + _Node_Owner -> + ok + end; +%% +check_access_model(_Host, Entity, 'roster' = _Access_Model, _Affiliation, + _Subscriptions, _Node_Owners, Rosters_Groups_Allowed) -> + case is_contact_in_allowed_roster_groups(Entity, Rosters_Groups_Allowed) of + false -> + {error, 'item-not-found'}; + {_Node_Owner, _Roster_Group_Allowed} -> + ok + end; +%% +check_access_model(_Host, _Entity, 'whitelist' = _Access_Model, Affiliation, + _Subscriptions, _Node_Owners, _Roster_Groups_Allowed) -> + case Affiliation of + Affiliation + when Affiliation == 'member' + orelse Affiliation == 'publish-only' + orelse Affiliation == 'publisher' -> + ok; + _Affiliation -> + {error, 'item-not-found'} + end; +%% +check_access_model(_Host, _Entity, 'authorize' = _Access_Model, _Affiliation, + Subscriptions, _Node_Owners, _Roster_Groups_Allowed) -> + case has_subscriptions(Subscriptions) of + true -> + ok; + false -> + {error, 'item-not-found'} + end. + + +-spec(check_publish_model/3 :: +( + Publish_Model :: pubsub_options:publish_model(), + Affiliation :: exmpp_pubsub:affiliation(), + Subscriptions :: exmpp_pubsub:subscriptions()) + -> ok + %%% + | {error, 'forbidden'} +). + +check_publish_model('open' = _Publish_Model, _Affiliation, _Subscriptions) -> + ok; +%% +check_publish_model('publishers' = _Publish_Model, Affiliation, _Subscriptions) -> + case Affiliation of + Affiliation + when Affiliation == 'owner' + orelse Affiliation == 'publish-only' + orelse Affiliation == 'publisher' -> + ok; + _Affiliation -> + {error, 'forbidden'} + end; +%% +check_publish_model('subscribers' = _Publish_Model, Affiliation, Subscriptions) -> + case Affiliation of + 'owner' -> + ok; + _Affiliation -> + case has_subscriptions(Subscriptions) of + true -> + ok; + false -> + {error, 'forbidden'} + end + end. + +%% +-spec(has_subscriptions/1 :: +( + Subscriptions :: exmpp_pubsub:subscriptions()) + -> Has_Subscriptions::boolean() +). + +has_subscriptions([] = _Subscriptions) -> + false; +has_subscriptions( + [{'pending' = _Subscription_State, _SubId, _Resource, _Subscription_Options}]) -> + false; +has_subscriptions(_Subscriptions) -> + true. + +%% +%% @doc Check if a contact is subscribed to at least one local node owner +%% amongst a list of (remote and local) node owners +-spec(is_contact_subscribed_to_node_owners/3 :: +( + Host :: xmpp_jid:raw_jid_component_bare(), + Contact :: xmpp_jid:usr_entity(), + Node_Owners :: [] | [Node_Owner::xmpp_jid:usr_bare(),...]) + -> Is_Contact_Subscribed_To_Node_Owners :: false | xmpp_jid:usr_bare() +). + +is_contact_subscribed_to_node_owners(_Host, _Contact, [] = _Node_Owners) -> + false; +%% The node owner is a local entity, check if the contact is subscribed to it +is_contact_subscribed_to_node_owners(Host, Contact, + [{_U, Host, _R} = Local_Node_Owner | Node_Owners] = _Node_Owners) -> + _Is_Contact_Subscribed_To_Node_Owners = case + is_contact_subscribed_to_entity(Contact, + _Local_Node_Owner_Roster = get_entity_roster(Local_Node_Owner)) + of + true -> + Local_Node_Owner; + false -> + is_contact_subscribed_to_node_owners(Host, Contact, Node_Owners) + end; +%% The node owner is a remote entity, don't check if the contact is subscribed to it +is_contact_subscribed_to_node_owners(Host, Contact, + [_Remote_Node_Owner | Node_Owners] = _Node_Owners) -> + is_contact_subscribed_to_node_owners(Host, Contact, Node_Owners). + +%% +%% @doc Check if an entity is in a #roster{} +%% with #roster.subscription == 'from' or +%% with #roster.subscription == 'both' +-spec(is_contact_subscribed_to_entity/2 :: +( + Contact :: xmpp_jid:usr_entity(), + Entity_Roster :: [] | [Roster_Item::#roster{},...]) + -> Is_Contact_Subscribed::boolean() +). + +is_contact_subscribed_to_entity(_Contact, [] = _Entity_Roster) -> + _Is_Contact_Subscribed = false; +%% Contact is included in a #roster{} item with +%% a subscription of type 'from' and 'both' (i.e. is subscribed to it) +is_contact_subscribed_to_entity({U, S, _R} = _Contact, + [#roster{jid = {U, S, _}, subscription = Subscription} = _Roster_Item + | _Roster_Items] = _Entity_Roster) + when Subscription == 'from' orelse Subscription == 'both' -> + _Is_Contact_Subscribed = true; +%% Contact is not included in a #roster{} item +%% or has a subscription of type different than 'from' and 'both' +%% ( i.e. is not subscribed to it), +%% check next roster items +is_contact_subscribed_to_entity(Contact, + [_Roster_Item | Roster_Items] = _Entity_Roster) -> + _Is_Contact_Subscribed = is_contact_subscribed_to_entity(Contact, + Roster_Items). + +%% +-spec(is_contact_in_allowed_roster_groups/2 :: +( + Contact :: xmpp_jid:usr_entity(), + Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed()) + -> Roster_Group_Allowed :: false + | {Entity :: xmpp_jid:usr_bare(), + Roster_Group :: pubsub_options:roster_group()} +). + +is_contact_in_allowed_roster_groups(_Contact, [] = _Rosters_Groups_Allowed) -> + false; +%% +is_contact_in_allowed_roster_groups(Contact, + [{Entity, Entity_Roster_Groups_Allowed} | Rosters_Groups_Allowed] + = _Rosters_Groups_Allowed) -> + case + is_contact_in_allowed_roster_group(Contact, + _Entity_Roster = get_entity_roster(Entity), + Entity_Roster_Groups_Allowed) + of + false -> + is_contact_in_allowed_roster_groups(Contact, Rosters_Groups_Allowed); + Roster_Group -> + {Entity, Roster_Group} + end. + +%% +-spec(is_contact_in_allowed_roster_group/3 :: +( + Contact :: xmpp_jid:usr_entity(), + Entity_Roster :: [] | [Roster_Item::#roster{groups::pubsub_options:roster_groups()}], + Roster_Groups_Allowed :: [Roster_Group_Allowed::pubsub_options:roster_group(),...]) + -> Roster_Group :: false | pubsub_options:roster_group() +). + +is_contact_in_allowed_roster_group(_Contact, [] = _Entity_Roster, + _Roster_Groups_Allowed) -> + false; +%% +is_contact_in_allowed_roster_group({U, S, _R} = _Contact, + [#roster{jid = {U, S, _}, subscription = Subscription, groups = Roster_Groups} + | _Other_Roster_Items] = _Entity_Roster, Roster_Groups_Allowed) + when Subscription == 'from' orelse Subscription == 'both' -> + is_contact_in_allowed_roster_group(Roster_Groups, Roster_Groups_Allowed); +%% +is_contact_in_allowed_roster_group(Contact, + [_Roster_Item | Roster_Items] = _Entity_Roster, Roster_Groups_Allowed) -> + is_contact_in_allowed_roster_group(Contact, Roster_Items, + Roster_Groups_Allowed). + +%% +-spec(is_contact_in_allowed_roster_group/2 :: +( + Roster_Groups :: [] | [Roster_Group::pubsub_options:roster_group()], + Roster_Groups_Allowed :: [Roster_Group_Allowed::pubsub_options:roster_group(),...]) + -> Roster_Group :: false | pubsub_options:roster_group() +). + +is_contact_in_allowed_roster_group([] = _Roster_Groups, _Roster_Groups_Allowed) -> + false; +%% +is_contact_in_allowed_roster_group([Roster_Group | Roster_Groups] + = _Roster_Groups, Roster_Groups_Allowed) -> + case lists:member(Roster_Group, Roster_Groups_Allowed) of + true -> + Roster_Group; + false -> + is_contact_in_allowed_roster_group(Roster_Groups, + Roster_Groups_Allowed) + end. + +%% +-spec(get_user_resources/2 :: +( + User :: binary(), + Server :: binary()) + -> Resources::[Resource::xmpp_jid:resource_jid()] +). + +get_user_resources(User, Server) -> + ejabberd_sm:get_user_resources(User, Server). + +%% +-spec(get_resources_show/2 :: +( + User :: binary(), + Server :: binary()) + -> Resources_Show :: [{Resource :: xmpp_jid:resource_jid(), + Show :: 'away' | 'chat' | 'dnd' | 'online' | 'xa' }] +). + +get_resources_show(User, Server) -> + _Resources_Show = lists:foldl(fun + (Resource, Resources_Show) -> + case ejabberd_sm:get_session_pid(User, Server, Resource) of + C2SPid when is_pid(C2SPid) -> + case ejabberd_c2s:get_presence(C2SPid) of + {_User, _Resource, Show, _} -> + [{Resource, list_to_atom(Show)} | Resources_Show]; + _ -> + Resources_Show + end; + _ -> + Resources_Show + end + end, [], _Resources = ejabberd_sm:get_user_resources(User, Server)). + + +%% +-spec(rosters_groups_allowed_cache/2 :: +( + Rosters_Groups_Allowed :: pubsub_options:rosters_groups_allowed(), + Rosters_Groups_Allowed_Cache :: pubsub_options:rosters_groups_allowed()) + -> Is_Roster_Groups_Cached::boolean() +). + +rosters_groups_allowed_cache(_Rosters_Groups_Allowed, + [] = _Rosters_Groups_Allowed_Cache) -> + false; +%% +rosters_groups_allowed_cache([] = _Rosters_Groups_Allowed, + _Rosters_Groups_Allowed_Cache) -> + false; +%% +rosters_groups_allowed_cache( + [{Entity, Roster_Groups_Allowed} | Rosters_Groups_Allowed], + Rosters_Groups_Allowed_Cache) -> + case lists:keyfind(Entity, 1, Rosters_Groups_Allowed_Cache) of + {_Entity, Roster_Groups_Allowed_Cache} -> + case + roster_groups_allowed_cache(Roster_Groups_Allowed, + Roster_Groups_Allowed_Cache) + of + true -> + true; + false -> + rosters_groups_allowed_cache(Rosters_Groups_Allowed, + Rosters_Groups_Allowed_Cache) + end; + false -> + rosters_groups_allowed_cache(Rosters_Groups_Allowed, + Rosters_Groups_Allowed_Cache) + end. + +%% +-spec(roster_groups_allowed_cache/2 :: +( + Roster_Groups_Allowed :: [Roster_Group_Allowed::pubsub_options:roster_group()], + Roster_Groups_Allowed_Cache :: [Roster_Group_Allowed_Cache::pubsub_options:roster_group()]) + -> Is_Roster_Groups_Cached :: boolean() +). + +roster_groups_allowed_cache(_Roster_Groups_Allowed, + [] = _Roster_Groups_Allowed_Cache) -> + false; +%% +roster_groups_allowed_cache([] = _Roster_Groups_Allowed, + _Roster_Groups_Allowed_Cache) -> + false; +%% +roster_groups_allowed_cache([Roster_Group_Allowed | Roster_Groups_Allowed], + Roster_Groups_Allowed_Cache) -> + case lists:member(Roster_Group_Allowed, Roster_Groups_Allowed_Cache) of + true -> + true; + false -> + roster_groups_allowed_cache(Roster_Groups_Allowed, + Roster_Groups_Allowed_Cache) + end. + diff --git a/src/mod_pubsub_ng/xmpp_xdata.erl b/src/mod_pubsub_ng/xmpp_xdata.erl new file mode 100644 index 000000000..879549c46 --- /dev/null +++ b/src/mod_pubsub_ng/xmpp_xdata.erl @@ -0,0 +1,108 @@ +-module(xmpp_xdata). + + +-include("jlib.hrl"). +-include("ejabberd.hrl"). +-compile(export_all). + +xmlcdata(CData) -> + {xmlcdata, CData}. + +xmlattr(Name, Value) -> +% #xmlattr{name = Name, value = Value}. + {Name, Value}. + +xmlattr_label(Label) -> + xmlattr(<<"label">>, Label). + +xmlattr_type(Type) -> + xmlattr(<<"type">>, Type). + +xmlattr_var(Var) -> + xmlattr(<<"var">>, Var). + + +xmlel(NS, Name, Attrs, Children) -> + #xmlel{name = Name, attrs = [{<<"xmlns">>, NS} | Attrs], children = Children}. + +xmlel(Name, Attrs, Children) -> + xmlel(?NS_XDATA, Name, Attrs, Children). + +xmlel_desc(CData) -> + xmlel(<<"desc">>, [], [xmlcdata(CData)]). + +xmlel_value(Value) -> + xmlel(<<"value">>, [], [xmlcdata(Value)]). + +xmlel_option({Value, Label}) -> + xmlel(<<"option">>, [xmlattr_label(Label)], [xmlel_value(Value)]); +xmlel_option(Value) -> + xmlel(<<"option">>, [], [xmlel_value(Value)]). + +xmlel_field(Var, Type, Values) -> + xmlel(<<"field">>, + [xmlattr_var(Var), + xmlattr_type(Type)], + [xmlel_value(Value) || Value <- Values]). + +xmlel_field(Var, Type, Label, Values, Options) -> + xmlel(<<"field">>, + [xmlattr_var(Var), + xmlattr_label(Label), + xmlattr_type(Type)], + [xmlel_option(Option) || Option <- Options] + ++ + [xmlel_value(Value) || Value <- Values]). + +xmlel_field_boolean(Var, Label, Value) -> + xmlel_field(Var, <<"boolean">>, Label, [Value], []). + +%% +xmlel_field_fixed(Var, Label, Value) -> + xmlel_field(Var, <<"fixed">>, Label, [Value], []). + +%% +xmlel_field_hidden(Var, Value) -> + xmlel_field(Var, <<"hidden">>, [Value]). + +%% +xmlel_field_list_single(Var, Label, Value, Options) -> + xmlel_field(Var, <<"list-single">>, Label, [Value], Options). + +%% +xmlel_field_list_multi(Var, Label, Values, Options) -> + xmlel_field(Var, <<"list-multi">>, Label, Values, Options). + +%% +xmlel_field_text_single(Var, Label, undefined = _Text) -> + xmlel_field(Var, <<"text-single">>, Label, [], []); +%% +xmlel_field_text_single(Var, Label, Text) -> + xmlel_field(Var, <<"text-single">>, Label, [Text], []). + +%% +xmlel_field_text_multi(Var, Label, Values) -> + xmlel_field(Var, <<"text-multi">>, Label, Values, []). + +%% +xmlel_field_jid_multi(Var, Label, Jids) -> + xmlel_field(Var, <<"jid-multi">>, Label, Jids, []). + +%% +xmlel_field_jid_single(Var, Label, Jid) -> + xmlel_field(Var, <<"jid-single">>, Label, [Jid], []). + +%% +xmlel_x(Type, Fields) -> + xmlel(<<"x">>, [xmlattr_type(Type)], Fields). + +%% +xmlel_title(Title) -> + xmlel(<<"title">>, [], [xmlcdata(Title)]). + +%% +xmlel_instructions(Instructions) when is_list(Instructions) -> + lists:map(fun xmlel_instructions/1, Instructions); +%% +xmlel_instructions(Instruction) when is_binary(Instruction) -> + xmlel(<<"instructions">>, [], [xmlcdata(Instruction)]). |