Current section
Files
Jump to
Current section
Files
src/escalus_pubsub_stanza.erl
%%%===================================================================
%%% @copyright (C) 2016, Erlang Solutions Ltd.
%%% @doc Stanzas for testing XEP-0060 PubSub
%%% @end
%%%===================================================================
-module(escalus_pubsub_stanza).
-include("escalus_xmlns.hrl").
-include_lib("exml/include/exml.hrl").
-export([
discover_nodes/2,
create_node/2, create_node/3, delete_node/2,
get_configuration/2, set_configuration/3,
get_default_configuration/1,
get_affiliations/2, set_affiliations/3,
subscribe/3, subscribe/4, unsubscribe/3,
submit_subscription_response/2,
get_pending_subscriptions/2,
get_user_subscriptions/2,
get_subscription_options/3,
set_subscription_options/4,
get_node_subscriptions/2,
set_subscriptions/3,
pubsub_owner_iq/3,
pubsub_iq/3,
pubsub_iq/4,
iq/3,
publish/2, publish/3, publish/4, publish_raw/3,
publish_with_options/3, publish_with_options/4, publish_with_options/5,
retract/4, retract_raw/4,
get_items/2, get_items/3,
purge_all_items/2
]).
%% Note: Some helpers produce stanzas with missing elements/attrs when node name is undefined.
%% This is allowed in order to facilitate testing error conditions.
-type pubsub_node_name() :: binary() | undefined.
-type item_id() :: binary() | undefined.
-export_type([pubsub_node_name/0]).
-type form_field() :: {Var :: binary(), Value :: binary() | [binary()]}.
-type get_items_opts() :: #{max_items => integer(), item_ids => [item_id()]}.
-type retract_opts() :: #{notify => binary()}.
%%-----------------------------------------------------------------------------
%% Request construction
%%-----------------------------------------------------------------------------
%% ---------------- disco ----------------
-spec discover_nodes(binary(), pubsub_node_name()) -> exml:element().
discover_nodes(Id, NodeName) ->
QueryElement = escalus_stanza:query_el(?NS_DISCO_ITEMS, node_attr(NodeName), []),
iq(<<"get">>, Id, [QueryElement]).
%% ---------------- create & delete ----------------
-spec create_node(binary(), pubsub_node_name()) -> exml:element().
create_node(Id, Node) ->
create_node(Id, Node, []).
-spec create_node(binary(), pubsub_node_name(), [form_field()] | undefined) -> exml:element().
create_node(Id, NodeName, ConfigFields) ->
Elements = [create_node_element(NodeName) | configure_node_form(ConfigFields, undefined)],
pubsub_iq(<<"set">>, Id, Elements).
-spec delete_node(binary(), pubsub_node_name()) -> exml:element().
delete_node(Id, NodeName) ->
Elements = [delete_element(NodeName)],
pubsub_owner_iq(<<"set">>, Id, Elements).
%% ---------------- configuration ----------------
-spec get_configuration(binary(), pubsub_node_name()) -> exml:element().
get_configuration(Id, NodeName) ->
Elements = [#xmlel{name = <<"configure">>, attrs = node_attr(NodeName)}],
pubsub_owner_iq(<<"get">>, Id, Elements).
-spec set_configuration(binary(), pubsub_node_name(), [form_field()] | undefined) -> exml:element().
set_configuration(Id, NodeName, ConfigFields) ->
Elements = configure_node_form(ConfigFields, NodeName),
pubsub_owner_iq(<<"set">>, Id, Elements).
-spec get_default_configuration(binary()) -> exml:element().
get_default_configuration(Id) ->
Elements = [default_element()],
pubsub_owner_iq(<<"get">>, Id, Elements).
%% ---------------- affiliations ----------------
-spec get_affiliations(binary(), pubsub_node_name()) -> exml:element().
get_affiliations(Id, NodeName) ->
Elements = [#xmlel{name = <<"affiliations">>,
attrs = node_attr(NodeName)}],
pubsub_owner_iq(<<"get">>, Id, Elements).
-spec set_affiliations(binary(), pubsub_node_name(), [{escalus_utils:jid_spec(), binary()}]) ->
exml:element().
set_affiliations(Id, NodeName, AffChange) ->
AffList = [ #xmlel{name = <<"affiliation">>,
attrs = #{<<"jid">> => get_short_jid(U),
<<"affiliation">> => A}}
|| {U, A} <- AffChange ],
Affiliations = #xmlel{name = <<"affiliations">>,
attrs = node_attr(NodeName),
children = AffList},
pubsub_owner_iq(<<"set">>, Id, [Affiliations]).
%% ---------------- subscriptions ----------------
-spec subscribe(escalus_utils:jid_spec(), binary(), pubsub_node_name()) -> exml:element().
subscribe(User, Id, Node) ->
subscribe(User, Id, Node, []).
-spec subscribe(escalus_utils:jid_spec(), binary(), pubsub_node_name(),
[form_field()] | undefined) ->
exml:element().
subscribe(User, Id, NodeName, ConfigFields) ->
Elements = [subscribe_element(NodeName, User) | subscribe_options_form(ConfigFields)],
pubsub_iq(<<"set">>, Id, Elements).
-spec unsubscribe(escalus_utils:jid_spec(), binary(), pubsub_node_name()) -> exml:element().
unsubscribe(User, Id, NodeName) ->
Elements = [unsubscribe_element(NodeName, User)],
pubsub_iq(<<"set">>, Id, Elements).
-spec submit_subscription_response(binary(), [form_field()]) -> exml:element().
submit_subscription_response(Id, Form) ->
Fields = [ encode_form_field(F) || F <- Form ],
XEl = escalus_stanza:x_data_form(<<"submit">>, Fields),
#xmlel{ name = <<"message">>,
attrs = #{<<"id">> => Id},
children = [XEl] }.
-spec get_pending_subscriptions(binary(), pubsub_node_name()) -> exml:element().
get_pending_subscriptions(Id, NodeName) ->
Payload = pending_subscriptions_form(NodeName),
Node = <<"http://jabber.org/protocol/pubsub#get-pending">>,
CommandIQ = escalus_stanza:adhoc_request(Node, Payload),
escalus_stanza:set_id(CommandIQ, Id).
-spec get_user_subscriptions(binary(), pubsub_node_name()) -> exml:element().
get_user_subscriptions(Id, NodeName) ->
Element = subscriptions_element(NodeName, []),
pubsub_iq(<<"get">>, Id, [Element]).
-spec get_subscription_options(escalus_utils:jid_spec(), binary(), pubsub_node_name()) ->
exml:element().
get_subscription_options(User, Id, NodeName) ->
Element = subscription_options(NodeName, User),
pubsub_iq(<<"get">>, Id, [Element]).
-spec set_subscription_options(escalus_utils:jid_spec(), binary(), pubsub_node_name(),
[form_field()] | undefined) -> exml:element().
set_subscription_options(User, Id, NodeName, Options) ->
Attrs = #{<<"jid">> => escalus_utils:get_jid(User)},
Children = form(<<"options">>, NodeName, Attrs, <<"subscribe_options">>, Options),
pubsub_iq(<<"set">>, Id, Children).
-spec get_node_subscriptions(binary(), pubsub_node_name()) -> exml:element().
get_node_subscriptions(Id, NodeName) ->
Elements = [subscriptions_element(NodeName, [])],
pubsub_owner_iq(<<"get">>, Id, Elements).
-spec set_subscriptions(binary(), [{escalus_utils:jid_spec(), binary()}], pubsub_node_name()) ->
exml:element().
set_subscriptions(Id, Subscriptions, NodeName) ->
SubElements = [subscription_element(Jid, SubState) || {Jid, SubState} <- Subscriptions],
Elements = [subscriptions_element(NodeName, SubElements)],
pubsub_owner_iq(<<"set">>, Id, Elements).
%% ---------------- publish & items management ----------------
-spec publish(binary(), pubsub_node_name()) -> exml:element().
publish(Id, Node) ->
publish_raw([], Id, Node).
-spec publish(exml:element(), binary(), pubsub_node_name()) -> exml:element().
publish(ContentElement, Id, Node) ->
publish(undefined, ContentElement, Id, Node).
-spec publish(binary() | undefined, exml:element(), binary(), pubsub_node_name()) -> exml:element().
publish(ItemId, ContentElement, Id, Node) ->
publish_raw([item_element(ItemId, ContentElement)], Id, Node).
-spec publish_with_options(binary(), pubsub_node_name(), [form_field()] | undefined) ->
exml:element().
publish_with_options(Id, Node, PublishOptions) ->
publish_raw([], publish_options_form(PublishOptions), Id, Node).
-spec publish_with_options(exml:element(), binary(), pubsub_node_name(),
[form_field()] | undefined) -> exml:element().
publish_with_options(ContentElement, Id, Node, PublishOptions) ->
publish_with_options(undefined, ContentElement, Id, Node, PublishOptions).
-spec publish_with_options(item_id(), exml:element(), binary(), pubsub_node_name(),
[form_field()] | undefined) -> exml:element().
publish_with_options(ItemId, ContentElement, Id, NodeName, PublishOptions) ->
ItemElement = item_element(ItemId, ContentElement),
publish_raw([ItemElement], publish_options_form(PublishOptions), Id, NodeName).
-spec publish_raw([exml:element()], binary(), pubsub_node_name()) -> exml:element().
publish_raw(Children, Id, NodeName) ->
Elements = [publish_element(NodeName, Children)],
pubsub_iq(<<"set">>, Id, Elements).
-spec publish_raw([exml:element()], [exml:element()], binary(), pubsub_node_name()) ->
exml:element().
publish_raw(Children, ExtraElements, Id, NodeName) ->
Elements = [publish_element(NodeName, Children) | ExtraElements],
pubsub_iq(<<"set">>, Id, Elements).
-spec retract(binary(), pubsub_node_name(), [item_id()], retract_opts()) -> exml:element().
retract(Id, NodeName, ItemIds, Options) ->
ItemEls = [item_element(ItemId, undefined) || ItemId <- ItemIds],
retract_raw(Id, NodeName, ItemEls, Options).
-spec retract_raw(binary(), pubsub_node_name(), [exml:element()], retract_opts()) -> exml:element().
retract_raw(Id, NodeName, ItemEls, Options) ->
pubsub_iq(<<"set">>, Id, [retract_element(NodeName, ItemEls, Options)]).
-spec get_items(binary(), pubsub_node_name()) -> exml:element().
get_items(Id, NodeName) ->
get_items(Id, NodeName, #{}).
-spec get_items(binary(), pubsub_node_name(), get_items_opts()) ->
exml:element().
get_items(Id, NodeName, Options) ->
Elements = [items_element(NodeName, Options)],
pubsub_iq(<<"get">>, Id, Elements).
-spec purge_all_items(binary(), pubsub_node_name()) -> exml:element().
purge_all_items(Id, NodeName) ->
Elements = [purge_element(NodeName)],
pubsub_owner_iq(<<"set">>, Id, Elements).
%%-----------------------------------------------------------------------------
%% XML element construction
%%-----------------------------------------------------------------------------
%% Whole stanzas
-spec pubsub_owner_iq(binary(), binary(), [exml:cdata() | exml:element()]) -> exml:element().
pubsub_owner_iq(Type, Id, Elements) ->
pubsub_iq(Type, Id, Elements, ?NS_PUBSUB_OWNER).
-spec pubsub_iq(binary(), binary(), [exml:cdata() | exml:element()]) -> exml:element().
pubsub_iq(Type, Id, Elements) ->
pubsub_iq(Type, Id, Elements, ?NS_PUBSUB).
-spec pubsub_iq(binary(), binary(), [exml:cdata() | exml:element()], binary()) ->
exml:element().
pubsub_iq(Type, Id, Elements, NS) ->
PubSubElement = pubsub_element(Elements, NS),
iq(Type, Id, [PubSubElement]).
-spec iq(binary(), binary(), [exml:cdata() | exml:element()]) -> exml:element().
iq(Type, Id, Elements) ->
Stanza = escalus_stanza:iq(Type, Elements),
escalus_stanza:set_id(Stanza, Id).
%% Form utils
configure_node_form(Fields, NodeName) ->
form(<<"configure">>, NodeName, #{}, <<"node_config">>, Fields).
subscribe_options_form(Fields) ->
form(<<"options">>, undefined, #{}, <<"subscribe_options">>, Fields).
publish_options_form(Fields) ->
form(<<"publish-options">>, undefined, #{}, <<"publish-options">>, Fields).
form(_, _, _, _, undefined) ->
[];
form(FormName, NodeName, Attrs, Type, Fields) ->
FormTypeField = form_type_field_element(Type),
FormFields = [encode_form_field(F) || F <- Fields],
[form_element(FormName, NodeName, Attrs, [FormTypeField | FormFields])].
-spec pending_subscriptions_form(pubsub_node_name()) -> [exml:element()].
pending_subscriptions_form(undefined) ->
[];
pending_subscriptions_form(NodeName) ->
Fields = [encode_form_field({<<"pubsub#node">>, NodeName})],
[escalus_stanza:x_data_form(<<"submit">>, Fields)].
%% Elements
create_node_element(NodeName) ->
#xmlel{name = <<"create">>, attrs = node_attr(NodeName)}.
-spec pubsub_element([exml:cdata() | exml:element()], binary()) -> exml:element().
pubsub_element(Children, NS) ->
#xmlel{name = <<"pubsub">>,
attrs = skip_undefined(#{<<"xmlns">> => NS}),
children = Children}.
delete_element(NodeName) ->
#xmlel{name = <<"delete">>,
attrs = node_attr(NodeName)}.
subscribe_element(NodeName, User) ->
#xmlel{name = <<"subscribe">>,
attrs = (node_attr(NodeName))#{<<"jid">> => escalus_utils:get_jid(User)}}.
unsubscribe_element(NodeName, User) ->
#xmlel{name = <<"unsubscribe">>,
attrs = (node_attr(NodeName))#{<<"jid">> => escalus_utils:get_jid(User)}}.
publish_element(NodeName, Children) ->
#xmlel{name = <<"publish">>,
attrs = node_attr(NodeName),
children = Children}.
-spec items_element(pubsub_node_name(), get_items_opts()) -> exml:element().
items_element(NodeName, Options) ->
Attrs = #{<<"node">> => NodeName,
<<"max_items">> => maps:get(max_items, Options, undefined)},
ItemEls = [item_element(ItemId, undefined) || ItemId <- maps:get(item_ids, Options, [])],
#xmlel{name = <<"items">>,
attrs = skip_undefined(Attrs),
children = ItemEls}.
item_element(ItemId, ContentElement) ->
#xmlel{name = <<"item">>,
attrs = skip_undefined(#{<<"id">> => ItemId}),
children = maybe_children([ContentElement])}.
-spec retract_element(pubsub_node_name(), [exml:element()], retract_opts()) -> exml:element().
retract_element(NodeName, ItemEls, Options) ->
Attrs = #{<<"node">> => NodeName,
<<"notify">> => maps:get(notify, Options, undefined)},
#xmlel{name = <<"retract">>,
attrs = skip_undefined(Attrs),
children = ItemEls}.
purge_element(NodeName) ->
#xmlel{name = <<"purge">>,
attrs = node_attr(NodeName)}.
subscriptions_element(NodeName, Children) ->
#xmlel{name = <<"subscriptions">>,
attrs = node_attr(NodeName),
children = Children}.
subscription_element(User, SubscriptionState) ->
#xmlel{name = <<"subscription">>,
attrs = #{<<"jid">> => escalus_utils:get_jid(User),
<<"subscription">> => SubscriptionState}}.
subscription_options(NodeName, User) ->
#xmlel{name = <<"options">>,
attrs = (node_attr(NodeName))#{<<"jid">> => escalus_utils:get_jid(User)}}.
default_element() ->
#xmlel{name = <<"default">>}.
form_element(FormName, NodeName, Attrs, FieldElements) ->
#xmlel{name = FormName,
attrs = maps:merge(node_attr(NodeName), Attrs),
children = [escalus_stanza:x_data_form(<<"submit">>, FieldElements)]}.
form_type_field_element(FormType) ->
Content = << <<"http://jabber.org/protocol/pubsub#">>/binary, FormType/binary >>,
escalus_stanza:field_el(<<"FORM_TYPE">>, <<"hidden">>, Content).
encode_form_field({Var, Values}) when is_list(Values) ->
encode_form_field(Var, Values);
encode_form_field({Var, Value}) ->
encode_form_field(Var, [Value]).
encode_form_field(Var, Values) ->
escalus_stanza:field_el(Var, undefined, Values).
%% Helpers
maybe_children(L) ->
lists:filter(fun(undefined) -> false;
({_, undefined}) -> false;
(_) -> true
end, L).
node_attr(NodeName) ->
skip_undefined(#{<<"node">> => NodeName}).
skip_undefined(L) ->
maps:filter(fun(_, Val) -> undefined =/= Val end, L).
-doc "Like escalus_utils:get_short_jid/1, but accepts server JIDs".
-spec get_short_jid(escalus_utils:jid_spec()) -> binary().
get_short_jid(ClientOrJid) ->
case is_binary(ClientOrJid) andalso binary:match(ClientOrJid, ~"/") =:= nomatch of
true -> ClientOrJid; % already no resource
false -> escalus_utils:get_short_jid(ClientOrJid)
end.