%%% ==================================================================== %%% ``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. %%% %%% @author Brian Cully %%% @version {@vsn}, {@date} {@time} %%% @end %%% ==================================================================== -module(pubsub_subscription). -author("bjc@kublai.com"). %% API -export([ init/0, subscribe_node/3, unsubscribe_node/3, get_subscription/3, set_subscription/4, get_options_xform/2, parse_options_xform/1 ]). % Internal function also exported for use in transactional bloc from pubsub plugins -export([ add_subscription/3, delete_subscription/3, read_subscription/3, write_subscription/4 ]). -include("pubsub.hrl"). -define(PUBSUB_DELIVER, "pubsub#deliver"). -define(PUBSUB_DIGEST, "pubsub#digest"). -define(PUBSUB_DIGEST_FREQUENCY, "pubsub#digest_frequency"). -define(PUBSUB_EXPIRE, "pubsub#expire"). -define(PUBSUB_INCLUDE_BODY, "pubsub#include_body"). -define(PUBSUB_SHOW_VALUES, "pubsub#show-values"). -define(PUBSUB_SUBSCRIPTION_TYPE, "pubsub#subscription_type"). -define(PUBSUB_SUBSCRIPTION_DEPTH, "pubsub#subscription_depth"). -define(DELIVER_LABEL, "Whether an entity wants to receive or disable notifications"). -define(DIGEST_LABEL, "Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually"). -define(DIGEST_FREQUENCY_LABEL, "The minimum number of milliseconds between sending any two notification digests"). -define(EXPIRE_LABEL, "The DateTime at which a leased subscription will end or has ended"). -define(INCLUDE_BODY_LABEL, "Whether an entity wants to receive an XMPP message body in addition to the payload format"). -define(SHOW_VALUES_LABEL, "The presence states for which an entity wants to receive notifications"). -define(SUBSCRIPTION_TYPE_LABEL, "Type of notification to receive"). -define(SUBSCRIPTION_DEPTH_LABEL, "Depth from subscription for which to receive notifications"). -define(SHOW_VALUE_AWAY_LABEL, "XMPP Show Value of Away"). -define(SHOW_VALUE_CHAT_LABEL, "XMPP Show Value of Chat"). -define(SHOW_VALUE_DND_LABEL, "XMPP Show Value of DND (Do Not Disturb)"). -define(SHOW_VALUE_ONLINE_LABEL, "Mere Availability in XMPP (No Show Value)"). -define(SHOW_VALUE_XA_LABEL, "XMPP Show Value of XA (Extended Away)"). -define(SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL, "Receive notification of new items only"). -define(SUBSCRIPTION_TYPE_VALUE_NODES_LABEL, "Receive notification of new nodes only"). -define(SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL, "Receive notification from direct child nodes only"). -define(SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL, "Receive notification from all descendent nodes"). %%==================================================================== %% API %%==================================================================== -spec(init/0 :: () -> 'ok'). init() -> ok = create_table(). -spec(subscribe_node/3 :: ( Entity :: jidEntity() | fullUsr(), NodeIdx :: nodeIdx(), Options :: [nodeOption()]) -> {'result', SubId::subId()} | {'error', _} ). subscribe_node(#jid{node = U, domain = S, resource = R}, NodeIdx, Options) -> subscribe_node({U,S,R}, NodeIdx, Options); subscribe_node(Entity, NodeIdx, Options) -> try mnesia:sync_dirty(fun add_subscription/3, [Entity, NodeIdx, Options]) of {error, Error} -> {error, Error}; Result -> {result, Result} catch Error -> Error end. -spec(unsubscribe_node/3 :: ( JID :: jidEntity(), NodeIdx :: nodeIdx(), SubId :: subId()) -> {'result','ok'} | {'error', _} ). unsubscribe_node(#jid{node = U, domain = S, resource = R} = _JID, NodeIdx, SubId) -> try mnesia:sync_dirty(fun delete_subscription/3, [{U,S,R}, NodeIdx, SubId]) of {error, Error} -> {error, Error}; Result -> {result, Result} catch Error -> Error end. -spec(get_subscription/3 :: ( JID :: jidEntity(), NodeIdx :: nodeIdx(), SubId :: subId()) -> {'result', pubsubSubscription()} | {'error', _} ). get_subscription(#jid{node = U, domain = S, resource = R} = _JID, NodeIdx, SubId) -> try mnesia:sync_dirty(fun read_subscription/3, [{U,S,R}, NodeIdx, SubId]) of {error, Error} -> {error, Error}; Subscription -> {result, Subscription} catch Error -> Error end. -spec(set_subscription/4 :: ( JID :: jidEntity(), NodeIdx :: nodeIdx(), SubId :: subId(), Options :: [nodeOption()]) -> {'result', 'ok'} | {'error', _} ). set_subscription(#jid{node = U, domain = S, resource = R} = _JID, NodeIdx, SubId, Options) -> try mnesia:sync_dirty(fun write_subscription/4, [{U,S,R}, NodeIdx, SubId, Options]) of {error, Error} -> {error, Error}; Result -> {result, Result} catch Error -> Error end. %% TODO : check input type data get_options_xform(Lang, Options) -> Keys = [deliver, digest, digest_frequency, expire, include_body, show_values, subscription_type, subscription_depth], XFields = [get_option_xfield(Lang, Key, Options) || Key <- Keys], {result, #xmlel{ns = ?NS_DATA_FORMS, name = 'x', attrs = [?XMLATTR(<<"type">>, <<"form">>)], children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR(<<"var">>, <<"FORM_TYPE">>), ?XMLATTR(<<"type">>, <<"hidden">>)], children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'value', children = [?XMLCDATA(?NS_PUBSUB_SUBSCRIBE_OPTIONS_s)]}]}] ++ XFields}}. %% TODO : check input type data parse_options_xform(XFields) -> case XFields of [] -> {result, []}; _ -> case exmpp_xml:remove_cdata_from_list(XFields) of [] -> {result, []}; [#xmlel{name = 'x'} = XEl] -> case jlib:parse_xdata_submit(XEl) of XData when is_list(XData) -> case set_xoption(XData, []) of Opts when is_list(Opts) -> {result, Opts}; Other -> Other end; Other -> Other end; Other -> Other end end. %%==================================================================== %% Internal functions %%==================================================================== -spec(create_table/0 :: () -> 'ok'). create_table() -> case mnesia:create_table(pubsub_subscription, [{disc_copies, [node()]}, {attributes, record_info(fields, pubsub_subscription)}, {type, set}]) of {atomic, ok} -> ok; {aborted, {already_exists, _}} -> ok; Other -> Other end. -spec(add_subscription/3 :: ( Entity :: fullUsr(), NodeIdx :: nodeIdx(), Options :: [nodeOption()]) -> SubId::subId() ). add_subscription(_JID, _NodeID, []) -> make_subid(); add_subscription(_Entity, _NodeIdx, Options) -> SubId = make_subid(), mnesia:write(#pubsub_subscription{subid = SubId, options = Options}), SubId. -spec(delete_subscription/3 :: ( Entity :: fullUsr(), NodeIdx :: nodeIdx(), SubId :: subId()) -> 'ok' ). delete_subscription(_Entity, _NodeIdx, SubId) -> mnesia:delete({pubsub_subscription, SubId}). -spec(read_subscription/3 :: ( Entity :: fullUsr(), NodeIdx :: nodeIdx(), SubId :: subId()) -> pubsubSubscription() | {'error', 'notfound'} ). read_subscription(_Entity, _NodeIdx, SubId) -> case mnesia:read({pubsub_subscription, SubId}) of [Subscription] -> Subscription; _ -> {'error', 'notfound'} end. -spec(write_subscription/4 :: ( Entity :: fullUsr(), NodeIdx :: nodeIdx(), SubId :: subId(), Options :: [nodeOption()]) -> 'ok' ). write_subscription(_Entity, _NodeIdx, SubId, Options) -> mnesia:write(#pubsub_subscription{subid = SubId, options = Options}). -spec(make_subid/0 :: () -> SubId::subId()). make_subid() -> {T1, T2, T3} = now(), list_to_binary(lists:flatten(io_lib:fwrite("~.16B~.16B~.16B", [T1, T2, T3]))). %% %% Subscription XForm processing. %% %% Return processed options, with types converted and so forth, using %% Opts as defaults. %% TODO : check input type data set_xoption([], Opts) -> Opts; set_xoption([{Var, Value} | T], Opts) -> NewOpts = case var_xfield(Var) of {error, _} -> Opts; Key -> Val = val_xfield(Key, Value), lists:keystore(Key, 1, Opts, {Key, Val}) end, set_xoption(T, NewOpts). %% Return the options list's key for an XForm var. var_xfield(?PUBSUB_DELIVER) -> 'deliver'; var_xfield(?PUBSUB_DIGEST) -> 'digest'; var_xfield(?PUBSUB_DIGEST_FREQUENCY) -> 'digest_frequency'; var_xfield(?PUBSUB_EXPIRE) -> 'expire'; var_xfield(?PUBSUB_INCLUDE_BODY) -> 'include_body'; var_xfield(?PUBSUB_SHOW_VALUES) -> 'show_values'; var_xfield(?PUBSUB_SUBSCRIPTION_TYPE) -> 'subscription_type'; var_xfield(?PUBSUB_SUBSCRIPTION_DEPTH) -> 'subscription_depth'; var_xfield(_) -> {error, 'badarg'}. %% Convert Values for option list's Key. val_xfield('deliver', [Val]) -> xopt_to_bool(Val); val_xfield('digest', [Val]) -> xopt_to_bool(Val); val_xfield('digest_frequency', [Val]) -> list_to_integer(Val); val_xfield('expire', [Val]) -> jlib:datetime_string_to_timestamp(Val); val_xfield('include_body', [Val]) -> xopt_to_bool(Val); val_xfield('show_values', Vals) -> Vals; val_xfield('subscription_type', ["items"]) -> items; val_xfield('subscription_type', ["nodes"]) -> nodes; val_xfield('subscription_depth', ["all"]) -> all; val_xfield('subscription_depth', [Depth]) -> case catch list_to_integer(Depth) of Integer when is_integer(Integer) -> Integer; _ -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'not-acceptable')} end. %% Convert XForm booleans to Erlang booleans. xopt_to_bool("0") -> false; xopt_to_bool("1") -> true; xopt_to_bool("false") -> false; xopt_to_bool("true") -> true; xopt_to_bool(_) -> {error, exmpp_stanza:error(?NS_JABBER_CLIENT, 'not-acceptable')}. %% Return a field for an XForm for Key, with data filled in, if %% applicable, from Options. %% TODO : check input type data get_option_xfield(Lang, Key, Options) -> Var = xfield_var(Key), Label = xfield_label(Key), {Type, OptEls} = type_and_options(xfield_type(Key), Lang), Vals = case lists:keysearch(Key, 1, Options) of {value, {_, Val}} -> [tr_xfield_values(Vals) || Vals <- xfield_val(Key, Val)]; false -> [] end, #xmlel{ns = ?NS_DATA_FORMS, name = 'field', attrs = [?XMLATTR(<<"var">>, Var), ?XMLATTR(<<"type">>, Type), ?XMLATTR(<<"label">>, translate:translate(Lang, Label))], children = OptEls ++ Vals}. %% TODO : check input type data type_and_options({Type, Options}, Lang) -> {Type, [tr_xfield_options(O, Lang) || O <- Options]}; type_and_options(Type, _Lang) -> {Type, []}. %% TODO : check input type data tr_xfield_options({Value, Label}, Lang) -> #xmlel{ns = ?NS_DATA_FORMS, name = 'option', attrs = [?XMLATTR(<<"label">>, translate:translate(Lang, Label))], children = [#xmlel{ns = ?NS_DATA_FORMS, name = 'value', children = [?XMLCDATA(Value)]}]}. %% TODO : check input type data tr_xfield_values(Value) -> #xmlel{ns = ?NS_DATA_FORMS, name ='value', children = [?XMLCDATA(Value)]}. %% Return the XForm variable name for a subscription option key. xfield_var('deliver') -> ?PUBSUB_DELIVER; xfield_var('digest') -> ?PUBSUB_DIGEST; xfield_var('digest_frequency') -> ?PUBSUB_DIGEST_FREQUENCY; xfield_var('expire') -> ?PUBSUB_EXPIRE; xfield_var('include_body') -> ?PUBSUB_INCLUDE_BODY; xfield_var('show_values') -> ?PUBSUB_SHOW_VALUES; xfield_var('subscription_type') -> ?PUBSUB_SUBSCRIPTION_TYPE; xfield_var('subscription_depth') -> ?PUBSUB_SUBSCRIPTION_DEPTH. %% Return the XForm variable type for a subscription option key. xfield_type('deliver') -> "boolean"; xfield_type('digest') -> "boolean"; xfield_type('digest_frequency') -> "text-single"; xfield_type('expire') -> "text-single"; xfield_type('include_body') -> "boolean"; xfield_type('show_values') -> {"list-multi", [{"away", ?SHOW_VALUE_AWAY_LABEL}, {"chat", ?SHOW_VALUE_CHAT_LABEL}, {"dnd", ?SHOW_VALUE_DND_LABEL}, {"online", ?SHOW_VALUE_ONLINE_LABEL}, {"xa", ?SHOW_VALUE_XA_LABEL}]}; xfield_type(subscription_type) -> {"list-single", [{"items", ?SUBSCRIPTION_TYPE_VALUE_ITEMS_LABEL}, {"nodes", ?SUBSCRIPTION_TYPE_VALUE_NODES_LABEL}]}; xfield_type(subscription_depth) -> {"list-single", [{"1", ?SUBSCRIPTION_DEPTH_VALUE_ONE_LABEL}, {"all", ?SUBSCRIPTION_DEPTH_VALUE_ALL_LABEL}]}. %% Return the XForm variable label for a subscription option key. xfield_label('deliver') -> ?DELIVER_LABEL; xfield_label('digest') -> ?DIGEST_LABEL; xfield_label('digest_frequency') -> ?DIGEST_FREQUENCY_LABEL; xfield_label('expire') -> ?EXPIRE_LABEL; xfield_label('include_body') -> ?INCLUDE_BODY_LABEL; xfield_label('show_values') -> ?SHOW_VALUES_LABEL; xfield_label('subscription_type') -> ?SUBSCRIPTION_TYPE_LABEL; xfield_label('subscription_depth') -> ?SUBSCRIPTION_DEPTH_LABEL. %% Return the XForm value for a subscription option key. xfield_val('deliver', Val) -> [bool_to_xopt(Val)]; xfield_val('digest', Val) -> [bool_to_xopt(Val)]; xfield_val('digest_frequency', Val) -> [integer_to_list(Val)]; xfield_val('expire', Val) -> [jlib:now_to_utc_string(Val)]; xfield_val('include_body', Val) -> [bool_to_xopt(Val)]; xfield_val('show_values', Val) -> Val; xfield_val('subscription_type', 'items') -> ["items"]; xfield_val('subscription_type', 'nodes') -> ["nodes"]; xfield_val('subscription_depth', 'all') -> ["all"]; xfield_val('subscription_depth', Depth) -> [integer_to_list(Depth)]. %% Convert erlang booleans to XForms. bool_to_xopt('false') -> "false"; bool_to_xopt('true') -> "true".