diff --git a/Makefile.in b/Makefile.in
index 07502f06e..728b2fa98 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -110,7 +110,11 @@ edoc:
spec:
$(ERL) -noinput +B -pa ebin -pa deps/*/ebin -eval \
- 'case fxml_gen:compile("tools/xmpp_codec.spec", [{add_type_specs, xmpp_element}, {erl_dir, "src"}, {hrl_dir, "include"}]) of ok -> halt(0); _ -> halt(1) end.'
+ 'case fxml_gen:compile("specs/xmpp_codec.spec", [{add_type_specs, xmpp_element}, {erl_dir, "src"}, {hrl_dir, "include"}]) of ok -> halt(0); _ -> halt(1) end.'
+
+xdata:
+ $(ERL) -noinput +B -pa ebin -pa deps/*/ebin -eval \
+ 'case xdata_codec:compile("specs", [{erl_dir, "src"}, {hrl_dir, "include"}]) of ok -> halt(0); _ -> halt(1) end.'
JOIN_PATHS=$(if $(wordlist 2,1000,$(1)),$(firstword $(1))/$(call JOIN_PATHS,$(wordlist 2,1000,$(1))),$(1))
diff --git a/include/flex_offline.hrl b/include/flex_offline.hrl
new file mode 100644
index 000000000..74a38fbb3
--- /dev/null
+++ b/include/flex_offline.hrl
@@ -0,0 +1,10 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: flex_offline.xdata
+%% Form type: http://jabber.org/protocol/offline
+%% Document: XEP-0013
+
+
+-type property() :: {'number_of_messages', non_neg_integer()}.
+-type result() :: [property()].
+
+-type form() :: [property() | xdata_field()].
diff --git a/include/muc_register.hrl b/include/muc_register.hrl
new file mode 100644
index 000000000..0cfc928c2
--- /dev/null
+++ b/include/muc_register.hrl
@@ -0,0 +1,16 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: muc_register.xdata
+%% Form type: http://jabber.org/protocol/muc#register
+%% Document: XEP-0045
+
+
+-type property() :: {'allow', boolean()} |
+ {'email', binary()} |
+ {'faqentry', [binary()]} |
+ {'first', binary()} |
+ {'last', binary()} |
+ {'roomnick', binary()} |
+ {'url', binary()}.
+-type result() :: [property()].
+
+-type form() :: [property() | xdata_field()].
diff --git a/include/muc_request.hrl b/include/muc_request.hrl
new file mode 100644
index 000000000..bc14be35f
--- /dev/null
+++ b/include/muc_request.hrl
@@ -0,0 +1,16 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: muc_request.xdata
+%% Form type: http://jabber.org/protocol/muc#request
+%% Document: XEP-0045
+
+
+-type property() :: {'role', participant} |
+ {'jid', jid:jid()} |
+ {'roomnick', binary()} |
+ {'request_allow', boolean()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'role', participant, options(participant)}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/muc_roomconfig.hrl b/include/muc_roomconfig.hrl
new file mode 100644
index 000000000..89cfc3346
--- /dev/null
+++ b/include/muc_roomconfig.hrl
@@ -0,0 +1,55 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: muc_roomconfig.xdata
+%% Form type: http://jabber.org/protocol/muc#roomconfig
+%% Document: XEP-0045
+
+-type 'allow_private_messages_from_visitors'() :: nobody | moderators | anyone.
+-type 'maxusers'() :: none | non_neg_integer().
+-type 'presencebroadcast'() :: moderator | participant | visitor.
+-type 'whois'() :: moderators | anyone.
+
+-type property() :: {'maxhistoryfetch', binary()} |
+ {'allowpm', binary()} |
+ {'allow_private_messages', boolean()} |
+ {'allow_private_messages_from_visitors', 'allow_private_messages_from_visitors'()} |
+ {'allow_visitor_status', boolean()} |
+ {'allow_visitor_nickchange', boolean()} |
+ {'allow_voice_requests', boolean()} |
+ {'allow_subscription', boolean()} |
+ {'voice_request_min_interval', non_neg_integer()} |
+ {'captcha_protected', boolean()} |
+ {'captcha_whitelist', [jid:jid()]} |
+ {'allow_query_users', boolean()} |
+ {'allowinvites', boolean()} |
+ {'changesubject', boolean()} |
+ {'enablelogging', boolean()} |
+ {'getmemberlist', [binary()]} |
+ {'lang', binary()} |
+ {'pubsub', binary()} |
+ {'maxusers', 'maxusers'()} |
+ {'membersonly', boolean()} |
+ {'moderatedroom', boolean()} |
+ {'members_by_default', boolean()} |
+ {'passwordprotectedroom', boolean()} |
+ {'persistentroom', boolean()} |
+ {'presencebroadcast', ['presencebroadcast'()]} |
+ {'publicroom', boolean()} |
+ {'public_list', boolean()} |
+ {'roomadmins', [jid:jid()]} |
+ {'roomdesc', binary()} |
+ {'roomname', binary()} |
+ {'roomowners', [jid:jid()]} |
+ {'roomsecret', binary()} |
+ {'whois', 'whois'()} |
+ {'mam', boolean()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'allowpm', binary(), options(binary())} |
+ {'allow_private_messages_from_visitors', 'allow_private_messages_from_visitors'(), options('allow_private_messages_from_visitors'())} |
+ {'getmemberlist', [binary()], options(binary())} |
+ {'maxusers', 'maxusers'(), options('maxusers'())} |
+ {'presencebroadcast', ['presencebroadcast'()], options('presencebroadcast'())} |
+ {'whois', 'whois'(), options('whois'())}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/muc_roominfo.hrl b/include/muc_roominfo.hrl
new file mode 100644
index 000000000..cf4f4ebf0
--- /dev/null
+++ b/include/muc_roominfo.hrl
@@ -0,0 +1,18 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: muc_roominfo.xdata
+%% Form type: http://jabber.org/protocol/muc#roominfo
+%% Document: XEP-0045
+
+
+-type property() :: {'maxhistoryfetch', non_neg_integer()} |
+ {'contactjid', [jid:jid()]} |
+ {'description', binary()} |
+ {'lang', binary()} |
+ {'ldapgroup', binary()} |
+ {'logs', binary()} |
+ {'occupants', non_neg_integer()} |
+ {'subject', binary()} |
+ {'subjectmod', boolean()}.
+-type result() :: [property()].
+
+-type form() :: [property() | xdata_field()].
diff --git a/include/pubsub_get_pending.hrl b/include/pubsub_get_pending.hrl
new file mode 100644
index 000000000..4ddf9bad0
--- /dev/null
+++ b/include/pubsub_get_pending.hrl
@@ -0,0 +1,13 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: pubsub_get_pending.xdata
+%% Form type: http://jabber.org/protocol/pubsub#subscribe_authorization
+%% Document: XEP-0060
+
+
+-type property() :: {'node', binary()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'node', binary(), options(binary())}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/pubsub_node_config.hrl b/include/pubsub_node_config.hrl
new file mode 100644
index 000000000..e1519cdc0
--- /dev/null
+++ b/include/pubsub_node_config.hrl
@@ -0,0 +1,60 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: pubsub_node_config.xdata
+%% Form type: http://jabber.org/protocol/pubsub#node_config
+%% Document: XEP-0060
+
+-type 'access_model'() :: authorize | open | presence | roster | whitelist.
+-type 'children_association_policy'() :: all | owners | whitelist.
+-type 'itemreply'() :: owner | publisher | none.
+-type 'node_type'() :: leaf | collection.
+-type 'notification_type'() :: normal | headline.
+-type 'publish_model'() :: publishers | subscribers | open.
+-type 'send_last_published_item'() :: never | on_sub | on_sub_and_presence.
+
+-type property() :: {'access_model', 'access_model'()} |
+ {'body_xslt', binary()} |
+ {'children_association_policy', 'children_association_policy'()} |
+ {'children_association_whitelist', [jid:jid()]} |
+ {'children', [binary()]} |
+ {'children_max', binary()} |
+ {'collection', [binary()]} |
+ {'contact', [jid:jid()]} |
+ {'dataform_xslt', binary()} |
+ {'deliver_notifications', boolean()} |
+ {'deliver_payloads', boolean()} |
+ {'description', binary()} |
+ {'item_expire', binary()} |
+ {'itemreply', 'itemreply'()} |
+ {'language', binary()} |
+ {'max_items', non_neg_integer()} |
+ {'max_payload_size', non_neg_integer()} |
+ {'node_type', 'node_type'()} |
+ {'notification_type', 'notification_type'()} |
+ {'notify_config', boolean()} |
+ {'notify_delete', boolean()} |
+ {'notify_retract', boolean()} |
+ {'notify_sub', boolean()} |
+ {'persist_items', boolean()} |
+ {'presence_based_delivery', boolean()} |
+ {'publish_model', 'publish_model'()} |
+ {'purge_offline', boolean()} |
+ {'roster_groups_allowed', [binary()]} |
+ {'send_last_published_item', 'send_last_published_item'()} |
+ {'tempsub', boolean()} |
+ {'subscribe', boolean()} |
+ {'title', binary()} |
+ {'type', binary()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'access_model', 'access_model'(), options('access_model'())} |
+ {'children_association_policy', 'children_association_policy'(), options('children_association_policy'())} |
+ {'itemreply', 'itemreply'(), options('itemreply'())} |
+ {'language', binary(), options(binary())} |
+ {'node_type', 'node_type'(), options('node_type'())} |
+ {'notification_type', 'notification_type'(), options('notification_type'())} |
+ {'publish_model', 'publish_model'(), options('publish_model'())} |
+ {'roster_groups_allowed', [binary()], options(binary())} |
+ {'send_last_published_item', 'send_last_published_item'(), options('send_last_published_item'())}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/pubsub_publish_options.hrl b/include/pubsub_publish_options.hrl
new file mode 100644
index 000000000..3b04b4826
--- /dev/null
+++ b/include/pubsub_publish_options.hrl
@@ -0,0 +1,14 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: pubsub_publish_options.xdata
+%% Form type: http://jabber.org/protocol/pubsub#publish-options
+%% Document: XEP-0060
+
+-type 'access_model'() :: authorize | open | presence | roster | whitelist.
+
+-type property() :: {'access_model', 'access_model'()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'access_model', 'access_model'(), options('access_model'())}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/pubsub_subscribe_authorization.hrl b/include/pubsub_subscribe_authorization.hrl
new file mode 100644
index 000000000..fb67ab47a
--- /dev/null
+++ b/include/pubsub_subscribe_authorization.hrl
@@ -0,0 +1,13 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: pubsub_subscribe_authorization.xdata
+%% Form type: http://jabber.org/protocol/pubsub#subscribe_authorization
+%% Document: XEP-0060
+
+
+-type property() :: {'allow', boolean()} |
+ {'node', binary()} |
+ {'subscriber_jid', jid:jid()} |
+ {'subid', binary()}.
+-type result() :: [property()].
+
+-type form() :: [property() | xdata_field()].
diff --git a/include/pubsub_subscribe_options.hrl b/include/pubsub_subscribe_options.hrl
new file mode 100644
index 000000000..9a05822a5
--- /dev/null
+++ b/include/pubsub_subscribe_options.hrl
@@ -0,0 +1,25 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: pubsub_subscribe_options.xdata
+%% Form type: http://jabber.org/protocol/pubsub#subscribe_options
+%% Document: XEP-0060
+
+-type 'show-values'() :: away | chat | dnd | online | xa.
+-type 'subscription_type'() :: items | nodes.
+-type 'subscription_depth'() :: 1 | all.
+
+-type property() :: {'deliver', boolean()} |
+ {'digest', boolean()} |
+ {'digest_frequency', binary()} |
+ {'expire', binary()} |
+ {'include_body', boolean()} |
+ {'show-values', ['show-values'()]} |
+ {'subscription_type', 'subscription_type'()} |
+ {'subscription_depth', 'subscription_depth'()}.
+-type result() :: [property()].
+
+-type options(T) :: [{binary(), T}].
+-type property_with_options() ::
+ {'show-values', ['show-values'()], options('show-values'())} |
+ {'subscription_type', 'subscription_type'(), options('subscription_type'())} |
+ {'subscription_depth', 'subscription_depth'(), options('subscription_depth'())}.
+-type form() :: [property() | property_with_options() | xdata_field()].
diff --git a/include/xmpp_codec.hrl b/include/xmpp_codec.hrl
index 8ffb37808..443769bb7 100644
--- a/include/xmpp_codec.hrl
+++ b/include/xmpp_codec.hrl
@@ -8,7 +8,7 @@
-record(ps_affiliation, {xmlns = <<>> :: binary(),
node = <<>> :: binary(),
type :: member | none | outcast |
- owner | publisher | 'publish-only',
+ owner | publisher | publish_only,
jid :: jid:jid()}).
-type ps_affiliation() :: #ps_affiliation{}.
@@ -510,7 +510,7 @@
type :: 'boolean' | 'fixed' | 'hidden' | 'jid-multi' | 'jid-single' | 'list-multi' | 'list-single' | 'text-multi' | 'text-private' | 'text-single',
var = <<>> :: binary(),
required = false :: boolean(),
- desc :: binary(),
+ desc = <<>> :: binary(),
values = [] :: [binary()],
options = [] :: [#xdata_option{}],
sub_els = [] :: [xmpp_element() | fxml:xmlel()]}).
diff --git a/specs/flex_offline.cfg b/specs/flex_offline.cfg
new file mode 100644
index 000000000..5d5b2685e
--- /dev/null
+++ b/specs/flex_offline.cfg
@@ -0,0 +1 @@
+[{decode, [{<<"number_of_messages">>, {dec_int, [0, infinity]}}]}].
diff --git a/specs/flex_offline.xdata b/specs/flex_offline.xdata
new file mode 100644
index 000000000..d0c786289
--- /dev/null
+++ b/specs/flex_offline.xdata
@@ -0,0 +1,12 @@
+
+ http://jabber.org/protocol/offline
+ XEP-0013
+
+ Service Discovery extension for number of messages
+ in an offline message queue.
+
+
+
diff --git a/specs/muc_register.cfg b/specs/muc_register.cfg
new file mode 100644
index 000000000..3efd0cf0b
--- /dev/null
+++ b/specs/muc_register.cfg
@@ -0,0 +1,2 @@
+[{prefix, <<"muc#register_">>},
+ {required, [<<"muc#register_roomnick">>]}].
diff --git a/specs/muc_register.xdata b/specs/muc_register.xdata
new file mode 100644
index 000000000..e808b44e0
--- /dev/null
+++ b/specs/muc_register.xdata
@@ -0,0 +1,37 @@
+
+ http://jabber.org/protocol/muc#register
+ XEP-0045
+
+ Forms enabling user registration with a
+ Multi-User Chat (MUC) room or admin approval
+ of user registration requests.
+
+
+
+
+
+
+
+
+
diff --git a/specs/muc_request.cfg b/specs/muc_request.cfg
new file mode 100644
index 000000000..4811b32e2
--- /dev/null
+++ b/specs/muc_request.cfg
@@ -0,0 +1,2 @@
+[{prefix, <<"muc#">>},
+ {required, [<<"muc#role">>]}].
diff --git a/specs/muc_request.xdata b/specs/muc_request.xdata
new file mode 100644
index 000000000..64fa1388b
--- /dev/null
+++ b/specs/muc_request.xdata
@@ -0,0 +1,31 @@
+
+ http://jabber.org/protocol/muc#request
+ XEP-0045
+
+ Forms enabling voice requests in a
+ Multi-User Chat (MUC) room or admin
+ approval of such requests.
+
+
+
+
+
+
+
+
+
+
diff --git a/specs/muc_roomconfig.cfg b/specs/muc_roomconfig.cfg
new file mode 100644
index 000000000..de16099d5
--- /dev/null
+++ b/specs/muc_roomconfig.cfg
@@ -0,0 +1,11 @@
+[{prefix, <<"muc#roomconfig_">>},
+ {prefix, <<"muc#">>},
+ {decode, [{<<"muc#roomconfig_maxusers">>,
+ {dec_enum_int, [[none], 0, infinity]}},
+ {<<"voice_request_min_interval">>,
+ {dec_int, [0, infinity]}}]}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/muc_roomconfig.xdata b/specs/muc_roomconfig.xdata
new file mode 100644
index 000000000..cab6465dc
--- /dev/null
+++ b/specs/muc_roomconfig.xdata
@@ -0,0 +1,192 @@
+
+ http://jabber.org/protocol/muc#roomconfig
+ XEP-0045
+
+ Forms enabling creation and configuration of
+ a Multi-User Chat (MUC) room.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/specs/muc_roominfo.cfg b/specs/muc_roominfo.cfg
new file mode 100644
index 000000000..98a985e7b
--- /dev/null
+++ b/specs/muc_roominfo.cfg
@@ -0,0 +1,11 @@
+[{prefix, <<"muc#roominfo_">>},
+ {prefix, <<"muc#">>},
+ {decode, [{<<"muc#maxhistoryfetch">>,
+ {dec_int, [0, infinity]}},
+ {<<"muc#roominfo_occupants">>,
+ {dec_int, [0, infinity]}}]}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/muc_roominfo.xdata b/specs/muc_roominfo.xdata
new file mode 100644
index 000000000..70f696383
--- /dev/null
+++ b/specs/muc_roominfo.xdata
@@ -0,0 +1,55 @@
+
+ http://jabber.org/protocol/muc#roominfo
+ XEP-0045
+
+ Forms enabling the communication of extended service discovery
+ information about a Multi-User Chat (MUC) room.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/specs/pubsub_get_pending.cfg b/specs/pubsub_get_pending.cfg
new file mode 100644
index 000000000..4ae57ac88
--- /dev/null
+++ b/specs/pubsub_get_pending.cfg
@@ -0,0 +1,7 @@
+[{prefix, <<"pubsub#">>},
+ {required, [<<"pubsub#node">>]}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/pubsub_get_pending.xdata b/specs/pubsub_get_pending.xdata
new file mode 100644
index 000000000..1a6f55297
--- /dev/null
+++ b/specs/pubsub_get_pending.xdata
@@ -0,0 +1,15 @@
+
+ http://jabber.org/protocol/pubsub#subscribe_authorization
+ XEP-0060
+ Forms enabling authorization of subscriptions to pubsub nodes
+
+
+
+
diff --git a/specs/pubsub_node_config.cfg b/specs/pubsub_node_config.cfg
new file mode 100644
index 000000000..f3bf72d2d
--- /dev/null
+++ b/specs/pubsub_node_config.cfg
@@ -0,0 +1,8 @@
+[{prefix, <<"pubsub#">>},
+ {decode, [{<<"pubsub#max_items">>, {dec_int, [0,infinity]}},
+ {<<"pubsub#max_payload_size">>, {dec_int, [0,infinity]}}]}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/pubsub_node_config.xdata b/specs/pubsub_node_config.xdata
new file mode 100644
index 000000000..1691808f7
--- /dev/null
+++ b/specs/pubsub_node_config.xdata
@@ -0,0 +1,189 @@
+
+ http://jabber.org/protocol/pubsub#node_config
+ XEP-0060
+ Forms enabling configuration of pubsub nodes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+
+
+
+
+
diff --git a/specs/pubsub_publish_options.cfg b/specs/pubsub_publish_options.cfg
new file mode 100644
index 000000000..2a853e834
--- /dev/null
+++ b/specs/pubsub_publish_options.cfg
@@ -0,0 +1,6 @@
+[{prefix, <<"pubsub#">>}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/pubsub_publish_options.xdata b/specs/pubsub_publish_options.xdata
new file mode 100644
index 000000000..26cac6ad5
--- /dev/null
+++ b/specs/pubsub_publish_options.xdata
@@ -0,0 +1,34 @@
+
+ http://jabber.org/protocol/pubsub#publish-options
+ XEP-0060
+
+ Forms enabling publication with options; each field must specify whether it
+ defines METADATA to be attached to the item, a per-item OVERRIDE of the node
+ configuration, or a PRECONDITION to be checked against the node configuration.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/specs/pubsub_subscribe_authorization.cfg b/specs/pubsub_subscribe_authorization.cfg
new file mode 100644
index 000000000..1f38fdb27
--- /dev/null
+++ b/specs/pubsub_subscribe_authorization.cfg
@@ -0,0 +1,7 @@
+[{prefix, <<"pubsub#">>},
+ {required, [<<"pubsub#node">>, <<"pubsub#subscriber_jid">>, <<"pubsub#allow">>]}].
+
+%% Local Variables:
+%% mode: erlang
+%% End:
+%% vim: set filetype=erlang tabstop=8:
diff --git a/specs/pubsub_subscribe_authorization.xdata b/specs/pubsub_subscribe_authorization.xdata
new file mode 100644
index 000000000..250d8754b
--- /dev/null
+++ b/specs/pubsub_subscribe_authorization.xdata
@@ -0,0 +1,27 @@
+
+ http://jabber.org/protocol/pubsub#subscribe_authorization
+ XEP-0060
+ Forms enabling authorization of subscriptions to pubsub nodes
+
+
+
+
+
+
+
diff --git a/specs/pubsub_subscribe_options.cfg b/specs/pubsub_subscribe_options.cfg
new file mode 100644
index 000000000..e4ac06ad9
--- /dev/null
+++ b/specs/pubsub_subscribe_options.cfg
@@ -0,0 +1 @@
+[{prefix, <<"pubsub#">>}].
diff --git a/specs/pubsub_subscribe_options.xdata b/specs/pubsub_subscribe_options.xdata
new file mode 100644
index 000000000..59b134676
--- /dev/null
+++ b/specs/pubsub_subscribe_options.xdata
@@ -0,0 +1,70 @@
+
+ http://jabber.org/protocol/pubsub#subscribe_options
+ XEP-0060
+ Forms enabling configuration of subscription options for pubsub nodes
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/xmpp_codec.spec b/specs/xmpp_codec.spec
similarity index 99%
rename from tools/xmpp_codec.spec
rename to specs/xmpp_codec.spec
index 1f9b50066..81f674bf3 100644
--- a/tools/xmpp_codec.spec
+++ b/specs/xmpp_codec.spec
@@ -1610,6 +1610,7 @@
default = false,
min = 0, max = 1},
#ref{name = xdata_field_desc,
+ default = <<"">>,
label = '$desc',
min = 0, max = 1},
#ref{name = xdata_field_value,
@@ -1682,7 +1683,7 @@
-record(ps_affiliation, {xmlns = <<>> :: binary(),
node = <<>> :: binary(),
type :: member | none | outcast |
- owner | publisher | 'publish-only',
+ owner | publisher | publish_only,
jid :: jid:jid()}).
-type ps_affiliation() :: #ps_affiliation{}.
@@ -1695,9 +1696,8 @@
#attr{name = <<"affiliation">>,
label = '$type',
required = true,
- dec = {dec_enum, [[member, none, outcast, owner,
- publisher, 'publish-only']]},
- enc = {enc_enum, []}}]}).
+ dec = {dec_ps_aff, []},
+ enc = {enc_ps_aff, []}}]}).
-xml(pubsub_owner_affiliation,
#elem{name = <<"affiliation">>,
@@ -1711,9 +1711,8 @@
#attr{name = <<"affiliation">>,
label = '$type',
required = true,
- dec = {dec_enum, [[member, none, outcast, owner,
- publisher, 'publish-only']]},
- enc = {enc_enum, []}}]}).
+ dec = {dec_ps_aff, []},
+ enc = {enc_ps_aff, []}}]}).
-xml(pubsub_event_configuration,
#elem{name = <<"configuration">>,
@@ -3501,6 +3500,22 @@ dec_version(S) ->
enc_version({Maj, Min}) ->
<<(integer_to_binary(Maj))/binary, $., (integer_to_binary(Min))/binary>>.
+-spec dec_ps_aff(_) -> member | none | outcast |
+ owner | publisher | publish_only.
+dec_ps_aff(<<"member">>) -> member;
+dec_ps_aff(<<"none">>) -> none;
+dec_ps_aff(<<"outcast">>) -> outcast;
+dec_ps_aff(<<"owner">>) -> owner;
+dec_ps_aff(<<"publisher">>) -> publisher;
+dec_ps_aff(<<"publish-only">>) -> publish_only.
+
+enc_ps_aff(member) -> <<"member">>;
+enc_ps_aff(none) -> <<"none">>;
+enc_ps_aff(outcast) -> <<"outcast">>;
+enc_ps_aff(owner) -> <<"owner">>;
+enc_ps_aff(publisher) -> <<"publisher">>;
+enc_ps_aff(publish_only) -> <<"publish-only">>.
+
%% Local Variables:
%% mode: erlang
%% End:
diff --git a/src/ejabberd_captcha.erl b/src/ejabberd_captcha.erl
index 9650f3773..a122eda8e 100644
--- a/src/ejabberd_captcha.erl
+++ b/src/ejabberd_captcha.erl
@@ -73,6 +73,7 @@ captcha_text(Lang) ->
mk_ocr_field(Lang, CID, Type) ->
URI = #media_uri{type = Type, uri = <<"cid:", CID/binary>>},
#xdata_field{var = <<"ocr">>,
+ type = 'text-single',
label = captcha_text(Lang),
required = true,
sub_els = [#media{uri = [URI]}]}.
diff --git a/src/flex_offline.erl b/src/flex_offline.erl
new file mode 100644
index 000000000..090ab3ddf
--- /dev/null
+++ b/src/flex_offline.erl
@@ -0,0 +1,128 @@
+%% Created automatically by xdata generator (xdata_codec.erl)
+%% Source: flex_offline.xdata
+%% Form type: http://jabber.org/protocol/offline
+%% Document: XEP-0013
+
+-module(flex_offline).
+
+-export([decode/1, decode/2, encode/1, encode/2,
+ format_error/1]).
+
+-include("xmpp_codec.hrl").
+
+-include("flex_offline.hrl").
+
+-export_type([{property, 0}, {result, 0}, {form, 0}]).
+
+dec_int(Val, Min, Max) ->
+ case list_to_integer(binary_to_list(Val)) of
+ Int when Int =< Max, Min == infinity -> Int;
+ Int when Int =< Max, Int >= Min -> Int
+ end.
+
+enc_int(Int) -> integer_to_binary(Int).
+
+format_error({form_type_mismatch, Type}) ->
+ <<"FORM_TYPE doesn't match '", Type/binary, "'">>;
+format_error({bad_var_value, Var, Type}) ->
+ <<"Bad value of field '", Var/binary, "' of type '",
+ Type/binary, "'">>;
+format_error({missing_value, Var, Type}) ->
+ <<"Missing value of field '", Var/binary, "' of type '",
+ Type/binary, "'">>;
+format_error({too_many_values, Var, Type}) ->
+ <<"Too many values for field '", Var/binary,
+ "' of type '", Type/binary, "'">>;
+format_error({unknown_var, Var, Type}) ->
+ <<"Unknown field '", Var/binary, "' of type '",
+ Type/binary, "'">>;
+format_error({missing_required_var, Var, Type}) ->
+ <<"Missing required field '", Var/binary, "' of type '",
+ Type/binary, "'">>.
+
+decode(Fs) -> decode(Fs, []).
+
+decode(Fs, Acc) ->
+ case lists:keyfind(<<"FORM_TYPE">>, #xdata_field.var,
+ Fs)
+ of
+ false -> decode(Fs, Acc, []);
+ #xdata_field{values =
+ [<<"http://jabber.org/protocol/offline">>]} ->
+ decode(Fs, Acc, []);
+ _ ->
+ erlang:error({?MODULE,
+ {form_type_mismatch,
+ <<"http://jabber.org/protocol/offline">>}})
+ end.
+
+encode(Cfg) -> encode(Cfg, fun (Text) -> Text end).
+
+encode(List, Translate) when is_list(List) ->
+ Fs = [case Opt of
+ {number_of_messages, Val} ->
+ [encode_number_of_messages(Val, Translate)];
+ {number_of_messages, _, _} ->
+ erlang:error({badarg, Opt});
+ #xdata_field{} -> [Opt];
+ _ -> []
+ end
+ || Opt <- List],
+ FormType = #xdata_field{var = <<"FORM_TYPE">>,
+ type = hidden,
+ values =
+ [<<"http://jabber.org/protocol/offline">>]},
+ [FormType | lists:flatten(Fs)].
+
+decode([#xdata_field{var = <<"number_of_messages">>,
+ values = [Value]}
+ | Fs],
+ Acc, Required) ->
+ try dec_int(Value, 0, infinity) of
+ Result ->
+ decode(Fs, [{number_of_messages, Result} | Acc],
+ lists:delete(<<"number_of_messages">>, Required))
+ catch
+ _:_ ->
+ erlang:error({?MODULE,
+ {bad_var_value, <<"number_of_messages">>,
+ <<"http://jabber.org/protocol/offline">>}})
+ end;
+decode([#xdata_field{var = <<"number_of_messages">>,
+ values = []} =
+ F
+ | Fs],
+ Acc, Required) ->
+ decode([F#xdata_field{var = <<"number_of_messages">>,
+ values = [<<>>]}
+ | Fs],
+ Acc, Required);
+decode([#xdata_field{var = <<"number_of_messages">>}
+ | _],
+ _, _) ->
+ erlang:error({?MODULE,
+ {too_many_values, <<"number_of_messages">>,
+ <<"http://jabber.org/protocol/offline">>}});
+decode([#xdata_field{var = Var} | Fs], Acc, Required) ->
+ if Var /= <<"FORM_TYPE">> ->
+ erlang:error({?MODULE,
+ {unknown_var, Var,
+ <<"http://jabber.org/protocol/offline">>}});
+ true -> decode(Fs, Acc, Required)
+ end;
+decode([], _, [Var | _]) ->
+ erlang:error({?MODULE,
+ {missing_required_var, Var,
+ <<"http://jabber.org/protocol/offline">>}});
+decode([], Acc, []) -> Acc.
+
+encode_number_of_messages(Value, Translate) ->
+ Values = case Value of
+ undefined -> [];
+ Value -> [enc_int(Value)]
+ end,
+ Opts = [],
+ #xdata_field{var = <<"number_of_messages">>,
+ values = Values, required = false, type = 'text-single',
+ options = Opts, desc = <<>>,
+ label = Translate(<<"Number of Offline Messages">>)}.
diff --git a/src/mod_caps.erl b/src/mod_caps.erl
index 4d9bdd61f..65a745d2d 100644
--- a/src/mod_caps.erl
+++ b/src/mod_caps.erl
@@ -488,14 +488,11 @@ concat_identities(#disco_info{identities = Identities}) ->
-spec concat_info(disco_info()) -> iolist().
concat_info(#disco_info{xdata = Xs}) ->
lists:sort(
- [concat_xdata_fields(Fs) || #xdata{type = result, fields = Fs} <- Xs]).
+ [concat_xdata_fields(X) || #xdata{type = result} = X <- Xs]).
--spec concat_xdata_fields([xdata_field()]) -> iolist().
-concat_xdata_fields(Fields) ->
- Form = case lists:keyfind(<<"FORM_TYPE">>, #xdata_field.var, Fields) of
- #xdata_field{values = Values} -> Values;
- false -> []
- end,
+-spec concat_xdata_fields(xdata()) -> iolist().
+concat_xdata_fields(#xdata{fields = Fields} = X) ->
+ Form = xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X),
Res = [[Var, $<, lists:sort([[Val, $<] || Val <- Values])]
|| #xdata_field{var = Var, values = Values} <- Fields,
is_binary(Var), Var /= <<"FORM_TYPE">>],
diff --git a/src/mod_mam.erl b/src/mod_mam.erl
index 8569ee020..1daae5aa2 100644
--- a/src/mod_mam.erl
+++ b/src/mod_mam.erl
@@ -37,7 +37,7 @@
process_iq_v0_2/1, process_iq_v0_3/1, disco_sm_features/5,
remove_user/2, remove_room/3, mod_opt_type/1, muc_process_iq/2,
muc_filter_message/5, message_is_archived/5, delete_old_messages/2,
- get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/4]).
+ get_commands_spec/0, msg_to_el/4, get_room_config/4, set_room_option/3]).
-include("xmpp.hrl").
-include("logger.hrl").
@@ -191,39 +191,17 @@ remove_room(LServer, Name, Host) ->
Mod:remove_room(LServer, LName, LHost),
ok.
--spec get_room_config([xdata_field()], mod_muc_room:state(), jid(), binary())
- -> [xdata_field()].
-get_room_config(XFields, RoomState, _From, Lang) ->
+-spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(),
+ jid(), binary()) -> [muc_roomconfig:property()].
+get_room_config(Fields, RoomState, _From, _Lang) ->
Config = RoomState#state.config,
- Label = <<"Enable message archiving">>,
- Var = <<"muc#roomconfig_mam">>,
- Val = case Config#config.mam of
- true -> <<"1">>;
- _ -> <<"0">>
- end,
- XField = #xdata_field{type = boolean,
- label = translate:translate(Lang, Label),
- var = Var,
- values = [Val]},
- XFields ++ [XField].
+ Fields ++ [{mam, Config#config.mam}].
--spec set_room_option({pos_integer(), _}, binary(), [binary()], binary())
+-spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary())
-> {pos_integer(), _}.
-set_room_option(_Acc, <<"muc#roomconfig_mam">> = Opt, Vals, Lang) ->
- try
- Val = case Vals of
- [<<"0">>|_] -> false;
- [<<"false">>|_] -> false;
- [<<"1">>|_] -> true;
- [<<"true">>|_] -> true
- end,
- {#config.mam, Val}
- catch _:{case_clause, _} ->
- Txt = <<"Value of '~s' should be boolean">>,
- ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
- {error, xmpp:err_bad_request(ErrTxt, Lang)}
- end;
-set_room_option(Acc, _Opt, _Vals, _Lang) ->
+set_room_option(_Acc, {mam, Val}, _Lang) ->
+ {#config.mam, Val};
+set_room_option(Acc, _Property, _Lang) ->
Acc.
-spec user_receive_packet(stanza(), ejabberd_c2s:state(), jid(), jid(), jid()) -> stanza().
diff --git a/src/mod_muc.erl b/src/mod_muc.erl
index 66604394b..b189508a8 100644
--- a/src/mod_muc.erl
+++ b/src/mod_muc.erl
@@ -668,19 +668,18 @@ get_nick(ServerHost, Host, From) ->
Mod:get_nick(LServer, Host, From).
iq_get_register_info(ServerHost, Host, From, Lang) ->
- {Nick, NickVals, Registered} = case get_nick(ServerHost, Host, From) of
- error -> {<<"">>, [], false};
- N -> {N, [N], true}
- end,
+ {Nick, Registered} = case get_nick(ServerHost, Host, From) of
+ error -> {<<"">>, false};
+ N -> {N, true}
+ end,
Title = <<(translate:translate(
Lang, <<"Nickname Registration at ">>))/binary, Host/binary>>,
Inst = translate:translate(Lang, <<"Enter nickname you want to register">>),
- Field = #xdata_field{type = 'text-single',
- label = translate:translate(Lang, <<"Nickname">>),
- var = <<"nick">>,
- values = NickVals},
+ Fields = muc_register:encode(
+ [{roomnick, Nick}],
+ fun(T) -> translate:translate(Lang, T) end),
X = #xdata{type = form, title = Title,
- instructions = [Inst], fields = [Field]},
+ instructions = [Inst], fields = Fields},
#register{nick = Nick,
registered = Registered,
instructions =
@@ -717,12 +716,13 @@ process_iq_register_set(ServerHost, Host, From,
#register{nick = Nick, xdata = XData}, Lang) ->
case XData of
#xdata{type = submit, fields = Fs} ->
- case lists:keyfind(<<"nick">>, #xdata_field.var, Fs) of
- #xdata_field{values = [N]} ->
- iq_set_register_info(ServerHost, Host, From, N, Lang);
- _ ->
- ErrText = <<"You must fill in field \"Nickname\" in the form">>,
- {error, xmpp:err_not_acceptable(ErrText, Lang)}
+ try
+ Options = muc_register:decode(Fs),
+ N = proplists:get_value(roomnick, Options),
+ iq_set_register_info(ServerHost, Host, From, N, Lang)
+ catch _:{muc_register, Why} ->
+ ErrText = muc_register:format_error(Why),
+ {error, xmpp:err_bad_request(ErrText, Lang)}
end;
#xdata{} ->
Txt = <<"Incorrect data form">>,
diff --git a/src/mod_muc_room.erl b/src/mod_muc_room.erl
index 339b85ecb..52401f835 100644
--- a/src/mod_muc_room.erl
+++ b/src/mod_muc_room.erl
@@ -754,100 +754,131 @@ process_groupchat_message(From, #message{lang = Lang} = Packet, StateData) ->
-spec process_normal_message(jid(), message(), state()) -> state().
process_normal_message(From, #message{lang = Lang} = Pkt, StateData) ->
- IsInvitation = is_invitation(Pkt),
- IsVoiceRequest = is_voice_request(Pkt) and
- is_visitor(From, StateData),
- IsVoiceApprovement = is_voice_approvement(Pkt) and
- not is_visitor(From, StateData),
- if IsInvitation ->
- case check_invitation(From, Pkt, StateData) of
- {error, Error} ->
- ejabberd_router:route_error(StateData#state.jid, From, Pkt, Error),
- StateData;
- IJID ->
- Config = StateData#state.config,
- case Config#config.members_only of
- true ->
- case get_affiliation(IJID, StateData) of
- none ->
- NSD = set_affiliation(IJID, member, StateData),
- send_affiliation(IJID, member, StateData),
- store_room(NSD),
- NSD;
- _ ->
- StateData
- end;
- false ->
- StateData
- end
- end;
- IsVoiceRequest ->
- case (StateData#state.config)#config.allow_voice_requests of
+ Action = lists:foldl(
+ fun(_, {error, _} = Err) ->
+ Err;
+ (#muc_user{invites = [#muc_invite{to = undefined}]}, _) ->
+ Txt = <<"No 'to' attribute found">>,
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ (#muc_user{invites = [I]}, _) ->
+ {ok, I};
+ (#muc_user{invites = [_|_]}, _) ->
+ Txt = <<"Multiple elements are not allowed">>,
+ {error, xmpp:err_resource_constraint(Txt, Lang)};
+ (#xdata{type = submit, fields = Fs}, _) ->
+ try {ok, muc_request:decode(Fs)}
+ catch _:{muc_request, Why} ->
+ Txt = muc_request:format_error(Why),
+ {error, xmpp:err_bad_request(Txt, Lang)}
+ end;
+ (_, Acc) ->
+ Acc
+ end, ok, xmpp:get_els(Pkt)),
+ case Action of
+ {ok, #muc_invite{} = Invitation} ->
+ process_invitation(From, Pkt, Invitation, StateData);
+ {ok, [{role, participant}]} ->
+ process_voice_request(From, Pkt, StateData);
+ {ok, VoiceApproval} ->
+ process_voice_approval(From, Pkt, VoiceApproval, StateData);
+ {error, Err} ->
+ ejabberd_router:route_error(StateData#state.jid, From, Pkt, Err),
+ StateData;
+ ok ->
+ StateData
+ end.
+
+-spec process_invitation(jid(), message(), muc_invite(), state()) -> state().
+process_invitation(From, Pkt, Invitation, StateData) ->
+ Lang = xmpp:get_lang(Pkt),
+ case check_invitation(From, Invitation, Lang, StateData) of
+ {error, Error} ->
+ ejabberd_router:route_error(StateData#state.jid, From, Pkt, Error),
+ StateData;
+ IJID ->
+ Config = StateData#state.config,
+ case Config#config.members_only of
true ->
- MinInterval = (StateData#state.config)#config.voice_request_min_interval,
- BareFrom = jid:remove_resource(jid:tolower(From)),
- NowPriority = -p1_time_compat:system_time(micro_seconds),
- CleanPriority = NowPriority + MinInterval * 1000000,
- Times = clean_treap(StateData#state.last_voice_request_time,
- CleanPriority),
- case treap:lookup(BareFrom, Times) of
- error ->
- Times1 = treap:insert(BareFrom,
- NowPriority,
- true, Times),
- NSD = StateData#state{last_voice_request_time = Times1},
- send_voice_request(From, Lang, NSD),
+ case get_affiliation(IJID, StateData) of
+ none ->
+ NSD = set_affiliation(IJID, member, StateData),
+ send_affiliation(IJID, member, StateData),
+ store_room(NSD),
NSD;
- {ok, _, _} ->
- ErrText = <<"Please, wait for a while before sending "
- "new voice request">>,
- Err = xmpp:err_not_acceptable(ErrText, Lang),
- ejabberd_router:route_error(
- StateData#state.jid, From, Pkt, Err),
- StateData#state{last_voice_request_time = Times}
+ _ ->
+ StateData
end;
false ->
- ErrText = <<"Voice requests are disabled in this conference">>,
- Err = xmpp:err_forbidden(ErrText, Lang),
+ StateData
+ end
+ end.
+
+-spec process_voice_request(jid(), message(), state()) -> state().
+process_voice_request(From, Pkt, StateData) ->
+ Lang = xmpp:get_lang(Pkt),
+ case (StateData#state.config)#config.allow_voice_requests of
+ true ->
+ MinInterval = (StateData#state.config)#config.voice_request_min_interval,
+ BareFrom = jid:remove_resource(jid:tolower(From)),
+ NowPriority = -p1_time_compat:system_time(micro_seconds),
+ CleanPriority = NowPriority + MinInterval * 1000000,
+ Times = clean_treap(StateData#state.last_voice_request_time,
+ CleanPriority),
+ case treap:lookup(BareFrom, Times) of
+ error ->
+ Times1 = treap:insert(BareFrom,
+ NowPriority,
+ true, Times),
+ NSD = StateData#state{last_voice_request_time = Times1},
+ send_voice_request(From, Lang, NSD),
+ NSD;
+ {ok, _, _} ->
+ ErrText = <<"Please, wait for a while before sending "
+ "new voice request">>,
+ Err = xmpp:err_not_acceptable(ErrText, Lang),
ejabberd_router:route_error(
StateData#state.jid, From, Pkt, Err),
- StateData
+ StateData#state{last_voice_request_time = Times}
end;
- IsVoiceApprovement ->
- case is_moderator(From, StateData) of
- true ->
- case extract_jid_from_voice_approvement(Pkt) of
- error ->
- ErrText = <<"Failed to extract JID from your voice "
- "request approval">>,
- Err = xmpp:err_bad_request(ErrText, Lang),
- ejabberd_router:route_error(
- StateData#state.jid, From, Pkt, Err),
- StateData;
- TargetJid ->
- case is_visitor(TargetJid, StateData) of
- true ->
- Reason = <<>>,
- NSD = set_role(TargetJid,
- participant,
- StateData),
- catch send_new_presence(TargetJid,
- Reason,
- NSD,
- StateData),
- NSD;
- _ ->
- StateData
- end
+ false ->
+ ErrText = <<"Voice requests are disabled in this conference">>,
+ Err = xmpp:err_forbidden(ErrText, Lang),
+ ejabberd_router:route_error(
+ StateData#state.jid, From, Pkt, Err),
+ StateData
+ end.
+
+-spec process_voice_approval(jid(), message(), [muc_request:property()], state()) -> state().
+process_voice_approval(From, Pkt, VoiceApproval, StateData) ->
+ Lang = xmpp:get_lang(Pkt),
+ case is_moderator(From, StateData) of
+ true ->
+ case lists:keyfind(jid, 1, VoiceApproval) of
+ {_, TargetJid} ->
+ Allow = proplists:get_bool(request_allow, VoiceApproval),
+ case is_visitor(TargetJid, StateData) of
+ true when Allow ->
+ Reason = <<>>,
+ NSD = set_role(TargetJid, participant, StateData),
+ catch send_new_presence(
+ TargetJid, Reason, NSD, StateData),
+ NSD;
+ _ ->
+ StateData
end;
- _ ->
- ErrText = <<"Only moderators can approve voice requests">>,
- Err = xmpp:err_not_allowed(ErrText, Lang),
+ false ->
+ ErrText = <<"Failed to extract JID from your voice "
+ "request approval">>,
+ Err = xmpp:err_bad_request(ErrText, Lang),
ejabberd_router:route_error(
StateData#state.jid, From, Pkt, Err),
StateData
end;
- true ->
+ false ->
+ ErrText = <<"Only moderators can approve voice requests">>,
+ Err = xmpp:err_not_allowed(ErrText, Lang),
+ ejabberd_router:route_error(
+ StateData#state.jid, From, Pkt, Err),
StateData
end.
@@ -3090,34 +3121,6 @@ is_password_settings_correct(XData, StateData) ->
_ -> true
end.
--define(XFIELD(Type, Label, Var, Vals),
- #xdata_field{type = Type,
- label = translate:translate(Lang, Label),
- var = Var,
- values = Vals}).
-
--define(BOOLXFIELD(Label, Var, Val),
- ?XFIELD(boolean, Label, Var,
- case Val of
- true -> [<<"1">>];
- _ -> [<<"0">>]
- end)).
-
--define(STRINGXFIELD(Label, Var, Val),
- ?XFIELD('text-single', Label, Var, [Val])).
-
--define(PRIVATEXFIELD(Label, Var, Val),
- ?XFIELD('text-private', Label, Var, [Val])).
-
--define(JIDMULTIXFIELD(Label, Var, JIDList),
- ?XFIELD('jid-multi', Label, Var,
- [jid:to_string(JID) || JID <- JIDList])).
-
--spec make_options([{binary(), binary()}], binary()) -> [xdata_option()].
-make_options(Options, Lang) ->
- [#xdata_option{label = translate:translate(Lang, Label),
- value = Value} || {Label, Value} <- Options].
-
-spec get_default_room_maxusers(state()) -> non_neg_integer().
get_default_room_maxusers(RoomState) ->
DefRoomOpts =
@@ -3138,424 +3141,156 @@ get_config(Lang, StateData, From) ->
{MaxUsersRoomInteger, MaxUsersRoomString} =
case get_max_users(StateData) of
N when is_integer(N) ->
- {N, integer_to_binary(N)};
- _ -> {0, <<"none">>}
+ {N, N};
+ _ -> {0, none}
end,
Title = iolist_to_binary(
io_lib:format(
translate:translate(Lang, <<"Configuration of room ~s">>),
[jid:to_string(StateData#state.jid)])),
- Fs = [#xdata_field{type = hidden,
- var = <<"FORM_TYPE">>,
- values = [<<"http://jabber.org/protocol/muc#roomconfig">>]},
- ?STRINGXFIELD(<<"Room title">>,
- <<"muc#roomconfig_roomname">>, (Config#config.title)),
- ?STRINGXFIELD(<<"Room description">>,
- <<"muc#roomconfig_roomdesc">>,
- (Config#config.description))] ++
+ Fs = [{roomname, Config#config.title},
+ {roomdesc, Config#config.description}] ++
case acl:match_rule(StateData#state.server_host, AccessPersistent, From) of
- allow ->
- [?BOOLXFIELD(<<"Make room persistent">>,
- <<"muc#roomconfig_persistentroom">>,
- (Config#config.persistent))];
+ allow -> [{persistentroom, Config#config.persistent}];
deny -> []
end ++
- [?BOOLXFIELD(<<"Make room public searchable">>,
- <<"muc#roomconfig_publicroom">>,
- (Config#config.public)),
- ?BOOLXFIELD(<<"Make participants list public">>,
- <<"public_list">>, (Config#config.public_list)),
- ?BOOLXFIELD(<<"Make room password protected">>,
- <<"muc#roomconfig_passwordprotectedroom">>,
- (Config#config.password_protected)),
- ?PRIVATEXFIELD(<<"Password">>,
- <<"muc#roomconfig_roomsecret">>,
- case Config#config.password_protected of
- true -> Config#config.password;
- false -> <<"">>
- end),
- #xdata_field{type = 'list-single',
- label = translate:translate(
- Lang, <<"Maximum Number of Occupants">>),
- var = <<"muc#roomconfig_maxusers">>,
- values = [MaxUsersRoomString],
- options =
- if is_integer(ServiceMaxUsers) -> [];
- true -> make_options(
- [{<<"No limit">>, <<"none">>}],
- Lang)
- end ++
- make_options(
- [{integer_to_binary(N), integer_to_binary(N)}
- || N <- lists:usort([ServiceMaxUsers,
- DefaultRoomMaxUsers,
- MaxUsersRoomInteger
- | ?MAX_USERS_DEFAULT_LIST]),
- N =< ServiceMaxUsers],
- Lang)},
- #xdata_field{type = 'list-single',
- label = translate:translate(
- Lang, <<"Present real Jabber IDs to">>),
- var = <<"muc#roomconfig_whois">>,
- values = [if Config#config.anonymous -> <<"moderators">>;
- true -> <<"anyone">>
- end],
- options = make_options(
- [{<<"moderators only">>, <<"moderators">>},
- {<<"anyone">>, <<"anyone">>}],
- Lang)},
- #xdata_field{type = 'list-multi',
- label = translate:translate(
- Lang,
- <<"Roles for which Presence is Broadcasted">>),
- var = <<"muc#roomconfig_presencebroadcast">>,
- values = [atom_to_binary(Role, utf8)
- || Role <- Config#config.presence_broadcast],
- options = make_options(
- [{<<"Moderator">>, <<"moderator">>},
- {<<"Participant">>, <<"participant">>},
- {<<"Visitor">>, <<"visitor">>}],
- Lang)},
- ?BOOLXFIELD(<<"Make room members-only">>,
- <<"muc#roomconfig_membersonly">>,
- (Config#config.members_only)),
- ?BOOLXFIELD(<<"Make room moderated">>,
- <<"muc#roomconfig_moderatedroom">>,
- (Config#config.moderated)),
- ?BOOLXFIELD(<<"Default users as participants">>,
- <<"members_by_default">>,
- (Config#config.members_by_default)),
- ?BOOLXFIELD(<<"Allow users to change the subject">>,
- <<"muc#roomconfig_changesubject">>,
- (Config#config.allow_change_subj)),
- ?BOOLXFIELD(<<"Allow users to send private messages">>,
- <<"allow_private_messages">>,
- (Config#config.allow_private_messages)),
- #xdata_field{type = 'list-single',
- label = translate:translate(
- Lang,
- <<"Allow visitors to send private messages to">>),
- var = <<"allow_private_messages_from_visitors">>,
- values = [case Config#config.allow_private_messages_from_visitors of
- anyone -> <<"anyone">>;
- moderators -> <<"moderators">>;
- nobody -> <<"nobody">>
- end],
- options = make_options(
- [{<<"nobody">>, <<"nobody">>},
- {<<"moderators only">>, <<"moderators">>},
- {<<"anyone">>, <<"anyone">>}],
- Lang)},
- ?BOOLXFIELD(<<"Allow users to query other users">>,
- <<"allow_query_users">>,
- (Config#config.allow_query_users)),
- ?BOOLXFIELD(<<"Allow users to send invites">>,
- <<"muc#roomconfig_allowinvites">>,
- (Config#config.allow_user_invites)),
- ?BOOLXFIELD(<<"Allow visitors to send status text in "
- "presence updates">>,
- <<"muc#roomconfig_allowvisitorstatus">>,
- (Config#config.allow_visitor_status)),
- ?BOOLXFIELD(<<"Allow visitors to change nickname">>,
- <<"muc#roomconfig_allowvisitornickchange">>,
- (Config#config.allow_visitor_nickchange)),
- ?BOOLXFIELD(<<"Allow visitors to send voice requests">>,
- <<"muc#roomconfig_allowvoicerequests">>,
- (Config#config.allow_voice_requests)),
- ?BOOLXFIELD(<<"Allow subscription">>,
- <<"muc#roomconfig_allow_subscription">>,
- (Config#config.allow_subscription)),
- ?STRINGXFIELD(<<"Minimum interval between voice requests "
- "(in seconds)">>,
- <<"muc#roomconfig_voicerequestmininterval">>,
- integer_to_binary(Config#config.voice_request_min_interval))]
+ [{publicroom, Config#config.public},
+ {public_list, Config#config.public_list},
+ {passwordprotectedroom, Config#config.password_protected},
+ {roomsecret, case Config#config.password_protected of
+ true -> Config#config.password;
+ false -> <<"">>
+ end},
+ {maxusers, MaxUsersRoomString,
+ [if is_integer(ServiceMaxUsers) -> [];
+ true -> [{<<"No limit">>, <<"none">>}]
+ end] ++ [{integer_to_binary(N), N}
+ || N <- lists:usort([ServiceMaxUsers,
+ DefaultRoomMaxUsers,
+ MaxUsersRoomInteger
+ | ?MAX_USERS_DEFAULT_LIST]),
+ N =< ServiceMaxUsers]},
+ {whois, if Config#config.anonymous -> moderators;
+ true -> anyone
+ end},
+ {presencebroadcast, Config#config.presence_broadcast},
+ {membersonly, Config#config.members_only},
+ {moderatedroom, Config#config.moderated},
+ {members_by_default, Config#config.members_by_default},
+ {changesubject, Config#config.allow_change_subj},
+ {allow_private_messages, Config#config.allow_private_messages},
+ {allow_private_messages_from_visitors,
+ Config#config.allow_private_messages_from_visitors},
+ {allow_query_users, Config#config.allow_query_users},
+ {allowinvites, Config#config.allow_user_invites},
+ {allow_visitor_status, Config#config.allow_visitor_status},
+ {allow_visitor_nickchange, Config#config.allow_visitor_nickchange},
+ {allow_voice_requests, Config#config.allow_voice_requests},
+ {allow_subscription, Config#config.allow_subscription},
+ {voice_request_min_interval, Config#config.voice_request_min_interval}]
++
case ejabberd_captcha:is_feature_available() of
- true ->
- [?BOOLXFIELD(<<"Make room CAPTCHA protected">>,
- <<"captcha_protected">>,
- (Config#config.captcha_protected))];
+ true -> [{captcha_protected, Config#config.captcha_protected}];
false -> []
end ++
- [?JIDMULTIXFIELD(<<"Exclude Jabber IDs from CAPTCHA challenge">>,
- <<"muc#roomconfig_captcha_whitelist">>,
- ((?SETS):to_list(Config#config.captcha_whitelist)))]
+ [{captcha_whitelist,
+ lists:map(fun jid:make/1, ?SETS:to_list(Config#config.captcha_whitelist))}]
++
case mod_muc_log:check_access_log(StateData#state.server_host, From) of
- allow ->
- [?BOOLXFIELD(<<"Enable logging">>,
- <<"muc#roomconfig_enablelogging">>,
- (Config#config.logging))];
+ allow -> [{enablelogging, Config#config.logging}];
deny -> []
end,
Fields = ejabberd_hooks:run_fold(get_room_config,
StateData#state.server_host,
Fs,
[StateData, From, Lang]),
- #xdata{type = form, title = Title, fields = Fields}.
+ #xdata{type = form, title = Title,
+ fields = muc_roomconfig:encode(
+ Fields, fun(T) -> translate:translate(Lang, T) end)}.
-spec set_config(xdata(), state(), binary()) -> {error, stanza_error()} |
{result, undefined, state()}.
set_config(#xdata{fields = Fields}, StateData, Lang) ->
- Options = [{Var, Vals} || #xdata_field{var = Var, values = Vals} <- Fields],
- case set_xoption(Options, StateData#state.config,
- StateData#state.server_host, Lang) of
- #config{} = Config ->
- Res = change_config(Config, StateData),
- {result, _, NSD} = Res,
- Type = case {(StateData#state.config)#config.logging,
- Config#config.logging}
- of
- {true, false} -> roomconfig_change_disabledlogging;
- {false, true} -> roomconfig_change_enabledlogging;
- {_, _} -> roomconfig_change
- end,
- Users = [{U#user.jid, U#user.nick, U#user.role}
- || {_, U} <- (?DICT):to_list(StateData#state.users)],
- add_to_log(Type, Users, NSD),
- Res;
- Err -> Err
+ try
+ Options = muc_roomconfig:decode(Fields),
+ #config{} = Config = set_config(Options, StateData#state.config,
+ StateData#state.server_host, Lang),
+ {result, _, NSD} = Res = change_config(Config, StateData),
+ Type = case {(StateData#state.config)#config.logging,
+ Config#config.logging}
+ of
+ {true, false} -> roomconfig_change_disabledlogging;
+ {false, true} -> roomconfig_change_enabledlogging;
+ {_, _} -> roomconfig_change
+ end,
+ Users = [{U#user.jid, U#user.nick, U#user.role}
+ || {_, U} <- (?DICT):to_list(StateData#state.users)],
+ add_to_log(Type, Users, NSD),
+ Res
+ catch _:{muc_roomconfig, Why} ->
+ Txt = muc_roomconfig:format_error(Why),
+ {error, xmpp:err_bad_request(Txt, Lang)};
+ _:{badmatch, {error, #stanza_error{}} = Err} ->
+ Err
end.
get_config_opt_name(Pos) ->
Fs = [config|record_info(fields, config)],
lists:nth(Pos, Fs).
--define(SET_BOOL_XOPT(Opt, Val),
- case Val of
- <<"0">> ->
- set_xoption(Opts, setelement(Opt, Config, false), ServerHost, Lang);
- <<"false">> ->
- set_xoption(Opts, setelement(Opt, Config, false), ServerHost, Lang);
- <<"1">> -> set_xoption(Opts, setelement(Opt, Config, true), ServerHost, Lang);
- <<"true">> ->
- set_xoption(Opts, setelement(Opt, Config, true), ServerHost, Lang);
- _ ->
- Txt = <<"Value of '~s' should be boolean">>,
- OptName = get_config_opt_name(Opt),
- ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])),
- {error, xmpp:err_bad_request(ErrTxt, Lang)}
- end).
-
--define(SET_NAT_XOPT(Opt, Val),
- case catch binary_to_integer(Val) of
- I when is_integer(I), I > 0 ->
- set_xoption(Opts, setelement(Opt, Config, I), ServerHost, Lang);
- _ ->
- Txt = <<"Value of '~s' should be integer">>,
- OptName = get_config_opt_name(Opt),
- ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])),
- {error, xmpp:err_bad_request(ErrTxt, Lang)}
- end).
-
--define(SET_STRING_XOPT(Opt, Vals),
- try
- V = case Vals of
- [] -> <<"">>;
- [Val] -> Val;
- _ when is_atom(Vals) -> Vals
- end,
- set_xoption(Opts, setelement(Opt, Config, V), ServerHost, Lang)
- catch _:_ ->
- Txt = <<"Incorrect value of option '~s'">>,
- OptName = get_config_opt_name(Opt),
- ErrTxt = iolist_to_binary(io_lib:format(Txt, [OptName])),
- {error, xmpp:err_bad_request(ErrTxt, Lang)}
- end).
-
--define(SET_JIDMULTI_XOPT(Opt, Vals),
- begin
- Set = lists:foldl(fun ({U, S, R}, Set1) ->
- (?SETS):add_element({U, S, R}, Set1);
- (#jid{luser = U, lserver = S, lresource = R},
- Set1) ->
- (?SETS):add_element({U, S, R}, Set1);
- (_, Set1) -> Set1
- end,
- (?SETS):empty(), Vals),
- set_xoption(Opts, setelement(Opt, Config, Set), ServerHost, Lang)
- end).
-
--spec set_xoption([{binary(), [binary()]}], #config{},
+-spec set_config([muc_roomconfig:property()], #config{},
binary(), binary()) -> #config{} | {error, stanza_error()}.
-set_xoption([], Config, _ServerHost, _Lang) -> Config;
-set_xoption([{<<"muc#roomconfig_roomname">>, Vals}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_STRING_XOPT(#config.title, Vals);
-set_xoption([{<<"muc#roomconfig_roomdesc">>, Vals}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_STRING_XOPT(#config.description, Vals);
-set_xoption([{<<"muc#roomconfig_changesubject">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_change_subj, Val);
-set_xoption([{<<"allow_query_users">>, [Val]} | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_query_users, Val);
-set_xoption([{<<"allow_private_messages">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_private_messages, Val);
-set_xoption([{<<"allow_private_messages_from_visitors">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- case Val of
- <<"anyone">> ->
- ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors,
- anyone);
- <<"moderators">> ->
- ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors,
- moderators);
- <<"nobody">> ->
- ?SET_STRING_XOPT(#config.allow_private_messages_from_visitors,
- nobody);
- _ ->
- Txt = <<"Value of 'allow_private_messages_from_visitors' "
- "should be anyone|moderators|nobody">>,
- {error, xmpp:err_bad_request(Txt, Lang)}
- end;
-set_xoption([{<<"muc#roomconfig_allowvisitorstatus">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_visitor_status, Val);
-set_xoption([{<<"muc#roomconfig_allowvisitornickchange">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_visitor_nickchange, Val);
-set_xoption([{<<"muc#roomconfig_publicroom">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.public, Val);
-set_xoption([{<<"public_list">>, [Val]} | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.public_list, Val);
-set_xoption([{<<"muc#roomconfig_persistentroom">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.persistent, Val);
-set_xoption([{<<"muc#roomconfig_moderatedroom">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.moderated, Val);
-set_xoption([{<<"members_by_default">>, [Val]} | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.members_by_default, Val);
-set_xoption([{<<"muc#roomconfig_membersonly">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.members_only, Val);
-set_xoption([{<<"captcha_protected">>, [Val]} | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.captcha_protected, Val);
-set_xoption([{<<"muc#roomconfig_allowinvites">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_user_invites, Val);
-set_xoption([{<<"muc#roomconfig_allow_subscription">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_subscription, Val);
-set_xoption([{<<"muc#roomconfig_passwordprotectedroom">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.password_protected, Val);
-set_xoption([{<<"muc#roomconfig_roomsecret">>, Vals}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_STRING_XOPT(#config.password, Vals);
-set_xoption([{<<"anonymous">>, [Val]} | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.anonymous, Val);
-set_xoption([{<<"muc#roomconfig_presencebroadcast">>, Vals} | Opts],
- Config, ServerHost, Lang) ->
- Roles =
- lists:foldl(
- fun(_S, error) -> error;
- (S, {M, P, V}) ->
- case S of
- <<"moderator">> -> {true, P, V};
- <<"participant">> -> {M, true, V};
- <<"visitor">> -> {M, P, true};
- _ -> error
- end
- end, {false, false, false}, Vals),
- case Roles of
- error ->
- Txt = <<"Value of 'muc#roomconfig_presencebroadcast' should "
- "be moderator|participant|visitor">>,
- {error, xmpp:err_bad_request(Txt, Lang)};
- {M, P, V} ->
- Res =
- if M -> [moderator]; true -> [] end ++
- if P -> [participant]; true -> [] end ++
- if V -> [visitor]; true -> [] end,
- set_xoption(Opts, Config#config{presence_broadcast = Res},
- ServerHost, Lang)
- end;
-set_xoption([{<<"muc#roomconfig_allowvoicerequests">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.allow_voice_requests, Val);
-set_xoption([{<<"muc#roomconfig_voicerequestmininterval">>,
- [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_NAT_XOPT(#config.voice_request_min_interval, Val);
-set_xoption([{<<"muc#roomconfig_whois">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- case Val of
- <<"moderators">> ->
- ?SET_BOOL_XOPT(#config.anonymous,
- (iolist_to_binary(integer_to_list(1))));
- <<"anyone">> ->
- ?SET_BOOL_XOPT(#config.anonymous,
- (iolist_to_binary(integer_to_list(0))));
- _ ->
- Txt = <<"Value of 'muc#roomconfig_whois' should be "
- "moderators|anyone">>,
- {error, xmpp:err_bad_request(Txt, Lang)}
- end;
-set_xoption([{<<"muc#roomconfig_maxusers">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- case Val of
- <<"none">> -> ?SET_STRING_XOPT(#config.max_users, none);
- _ -> ?SET_NAT_XOPT(#config.max_users, Val)
- end;
-set_xoption([{<<"muc#roomconfig_enablelogging">>, [Val]}
- | Opts],
- Config, ServerHost, Lang) ->
- ?SET_BOOL_XOPT(#config.logging, Val);
-set_xoption([{<<"muc#roomconfig_captcha_whitelist">>,
- Vals}
- | Opts],
- Config, ServerHost, Lang) ->
- JIDs = [jid:from_string(Val) || Val <- Vals],
- ?SET_JIDMULTI_XOPT(#config.captcha_whitelist, JIDs);
-set_xoption([{<<"FORM_TYPE">>, _} | Opts], Config, ServerHost, Lang) ->
- set_xoption(Opts, Config, ServerHost, Lang);
-set_xoption([{Opt, Vals} | Opts], Config, ServerHost, Lang) ->
- Txt = <<"Unknown option '~s'">>,
- ErrTxt = iolist_to_binary(io_lib:format(Txt, [Opt])),
- Err = {error, xmpp:err_bad_request(ErrTxt, Lang)},
- case ejabberd_hooks:run_fold(set_room_option,
- ServerHost,
- Err,
- [Opt, Vals, Lang]) of
- {error, Reason} ->
- {error, Reason};
- {Pos, Val} ->
- set_xoption(Opts, setelement(Pos, Config, Val), ServerHost, Lang)
- end.
+set_config(Opts, Config, ServerHost, Lang) ->
+ lists:foldl(
+ fun(_, {error, _} = Err) -> Err;
+ ({roomname, Title}, C) -> C#config{title = Title};
+ ({roomdesc, Desc}, C) -> C#config{description = Desc};
+ ({changesubject, V}, C) -> C#config{allow_change_subj = V};
+ ({allow_query_users, V}, C) -> C#config{allow_query_users = V};
+ ({allow_private_messages, V}, C) ->
+ C#config{allow_private_messages = V};
+ ({allow_private_messages_from_visitors, V}, C) ->
+ C#config{allow_private_messages_from_visitors = V};
+ ({allow_visitor_status, V}, C) -> C#config{allow_visitor_status = V};
+ ({allow_visitor_nickchange, V}, C) ->
+ C#config{allow_visitor_nickchange = V};
+ ({publicroom, V}, C) -> C#config{public = V};
+ ({public_list, V}, C) -> C#config{public_list = V};
+ ({persistentroom, V}, C) -> C#config{persistent = V};
+ ({moderatedroom, V}, C) -> C#config{moderated = V};
+ ({members_by_default, V}, C) -> C#config{members_by_default = V};
+ ({membersonly, V}, C) -> C#config{members_only = V};
+ ({captcha_protected, V}, C) -> C#config{captcha_protected = V};
+ ({allowinvites, V}, C) -> C#config{allow_user_invites = V};
+ ({allow_subscription, V}, C) -> C#config{allow_subscription = V};
+ ({passwordprotectedroom, V}, C) -> C#config{password_protected = V};
+ ({roomsecret, V}, C) -> C#config{password = V};
+ ({anonymous, V}, C) -> C#config{anonymous = V};
+ ({presencebroadcast, V}, C) -> C#config{presence_broadcast = V};
+ ({allow_voice_requests, V}, C) -> C#config{allow_voice_requests = V};
+ ({voice_request_min_interval, V}, C) ->
+ C#config{voice_request_min_interval = V};
+ ({whois, moderators}, C) -> C#config{anonymous = true};
+ ({whois, anyone}, C) -> C#config{anonymous = false};
+ ({maxusers, V}, C) -> C#config{max_users = V};
+ ({enablelogging, V}, C) -> C#config{logging = V};
+ ({captcha_whitelist, Js}, C) ->
+ LJIDs = [jid:tolower(J) || J <- Js],
+ C#config{captcha_whitelist = ?SETS:from_list(LJIDs)};
+ ({O, V} = Opt, C) ->
+ case ejabberd_hooks:run_fold(set_room_option,
+ ServerHost,
+ {0, undefined},
+ [Opt, Lang]) of
+ {0, undefined} ->
+ ?ERROR_MSG("set_room_option hook failed for "
+ "option '~s' with value ~p", [O, V]),
+ Txt = <<"Failed to process option '", O/binary, "'">>,
+ {error, xmpp:err_internal_server_error(Txt, Lang)};
+ {Pos, Val} ->
+ setelement(Pos, C, Val)
+ end
+ end, Config, Opts).
-spec change_config(#config{}, state()) -> {result, undefined, state()}.
change_config(Config, StateData) ->
@@ -3872,33 +3607,13 @@ process_iq_disco_info(_From, #iq{type = get, lang = Lang}, StateData) ->
name = get_title(StateData)}],
features = Feats}}.
--spec mk_rfieldt('boolean' | 'fixed' | 'hidden' |
- 'jid-multi' | 'jid-single' | 'list-multi' |
- 'list-single' | 'text-multi' | 'text-private' |
- 'text-single', binary(), binary()) -> xdata_field().
-mk_rfieldt(Type, Var, Val) ->
- #xdata_field{type = Type, var = Var, values = [Val]}.
-
--spec mk_rfield(binary(), binary(), binary(), binary()) -> xdata_field().
-mk_rfield(Label, Var, Val, Lang) ->
- #xdata_field{type = 'text-single',
- label = translate:translate(Lang, Label),
- var = Var,
- values = [Val]}.
-
-spec iq_disco_info_extras(binary(), state()) -> xdata().
iq_disco_info_extras(Lang, StateData) ->
- Len = (?DICT):size(StateData#state.users),
- RoomDescription = (StateData#state.config)#config.description,
+ Fs = [{description, (StateData#state.config)#config.description},
+ {occupants, ?DICT:size(StateData#state.users)}],
#xdata{type = result,
- fields = [mk_rfieldt(hidden, <<"FORM_TYPE">>,
- "http://jabber.org/protocol/muc#roominfo"),
- mk_rfield(<<"Room description">>,
- <<"muc#roominfo_description">>,
- RoomDescription, Lang),
- mk_rfield(<<"Number of occupants">>,
- <<"muc#roominfo_occupants">>,
- integer_to_binary(Len), Lang)]}.
+ fields = muc_roominfo:encode(
+ Fs, fun(T) -> translate:translate(Lang, T) end)}.
-spec process_iq_disco_items(jid(), iq(), state()) ->
{error, stanza_error()} | {result, disco_items()}.
@@ -4105,39 +3820,16 @@ get_mucroom_disco_items(StateData) ->
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Voice request support
--spec is_voice_request(message()) -> boolean().
-is_voice_request(Packet) ->
- Els = xmpp:get_els(Packet),
- lists:any(
- fun(#xdata{} = X) ->
- case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X),
- xmpp_util:get_xdata_values(<<"muc#role">>, X)} of
- {[<<"http://jabber.org/protocol/muc#request">>],
- [<<"participant">>]} ->
- true;
- _ ->
- false
- end;
- (_) ->
- false
- end, Els).
-
-spec prepare_request_form(jid(), binary(), binary()) -> message().
prepare_request_form(Requester, Nick, Lang) ->
Title = translate:translate(Lang, <<"Voice request">>),
Instruction = translate:translate(
Lang, <<"Either approve or decline the voice request.">>),
- Fs = [#xdata_field{var = <<"FORM_TYPE">>,
- type = hidden,
- values = [<<"http://jabber.org/protocol/muc#request">>]},
- #xdata_field{var = <<"muc#role">>,
- type = hidden,
- values = [<<"participant">>]},
- ?STRINGXFIELD(<<"User JID">>, <<"muc#jid">>,
- jid:to_string(Requester)),
- ?STRINGXFIELD(<<"Nickname">>, <<"muc#roomnick">>, Nick),
- ?BOOLXFIELD(<<"Grant voice to this person?">>,
- <<"muc#request_allow">>, false)],
+ Fs = muc_request:encode([{role, participant},
+ {jid, Requester},
+ {roomnick, Nick},
+ {request_allow, false}],
+ fun(T) -> translate:translate(Lang, T) end),
#message{type = normal,
sub_els = [#xdata{type = form,
title = Title,
@@ -4155,59 +3847,11 @@ send_voice_request(From, Lang, StateData) ->
end,
Moderators).
--spec is_voice_approvement(message()) -> boolean().
-is_voice_approvement(Packet) ->
- Els = xmpp:get_els(Packet),
- lists:any(
- fun(#xdata{} = X) ->
- case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X),
- xmpp_util:get_xdata_values(<<"muc#role">>, X),
- xmpp_util:get_xdata_values(<<"muc#request_allow">>, X)} of
- {[<<"http://jabber.org/protocol/muc#request">>],
- [<<"participant">>], [Flag]} when Flag == <<"true">>;
- Flag == <<"1">> ->
- true;
- _ ->
- false
- end;
- (_) ->
- false
- end, Els).
-
--spec extract_jid_from_voice_approvement(message()) -> jid() | error.
-extract_jid_from_voice_approvement(Packet) ->
- Els = xmpp:get_els(Packet),
- lists:foldl(
- fun(#xdata{} = X, error) ->
- case {xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X),
- xmpp_util:get_xdata_values(<<"muc#role">>, X),
- xmpp_util:get_xdata_values(<<"muc#request_allow">>, X),
- xmpp_util:get_xdata_values(<<"muc#jid">>, X)} of
- {[<<"http://jabber.org/protocol/muc#request">>],
- [<<"participant">>], [Flag], [J]} when Flag == <<"true">>;
- Flag == <<"1">> ->
- jid:from_string(J);
- _ ->
- error
- end;
- (_, Acc) ->
- Acc
- end, error, Els).
-
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Invitation support
--spec is_invitation(message()) -> boolean().
-is_invitation(Packet) ->
- Els = xmpp:get_els(Packet),
- lists:any(
- fun(#muc_user{invites = [_|_]}) -> true;
- (_) -> false
- end, Els).
-
--spec check_invitation(jid(), message(), state()) -> {error, stanza_error()} | jid().
-check_invitation(From, Packet, StateData) ->
- Lang = xmpp:get_lang(Packet),
+-spec check_invitation(jid(), muc_invite(), binary(), state()) -> {error, stanza_error()} | jid().
+check_invitation(From, Invitation, Lang, StateData) ->
FAffiliation = get_affiliation(From, StateData),
CanInvite = (StateData#state.config)#config.allow_user_invites
orelse
@@ -4217,57 +3861,46 @@ check_invitation(From, Packet, StateData) ->
Txt = <<"Invitations are not allowed in this conference">>,
{error, xmpp:err_not_allowed(Txt, Lang)};
true ->
- case xmpp:get_subtag(Packet, #muc_user{}) of
- #muc_user{invites = [#muc_invite{to = undefined}]} ->
- Txt = <<"No 'to' attribute found">>,
- {error, xmpp:err_bad_request(Txt, Lang)};
- #muc_user{invites = [#muc_invite{to = JID, reason = Reason} = I]} ->
- Invite = I#muc_invite{to = undefined, from = From},
- Password = case (StateData#state.config)#config.password_protected of
- true ->
- (StateData#state.config)#config.password;
- false ->
- undefined
- end,
- XUser = #muc_user{password = Password, invites = [Invite]},
- XConference = #x_conference{jid = jid:make(StateData#state.room,
- StateData#state.host),
- reason = Reason},
- Body = iolist_to_binary(
- [io_lib:format(
- translate:translate(
- Lang,
- <<"~s invites you to the room ~s">>),
- [jid:to_string(From),
- jid:to_string({StateData#state.room,
- StateData#state.host,
- <<"">>})]),
- case (StateData#state.config)#config.password_protected of
- true ->
- <<", ",
- (translate:translate(
- Lang, <<"the password is">>))/binary,
- " '",
- ((StateData#state.config)#config.password)/binary,
- "'">>;
- _ -> <<"">>
- end,
- case Reason of
- <<"">> -> <<"">>;
- _ -> <<" (", Reason/binary, ") ">>
- end]),
- Msg = #message{type = normal,
- body = xmpp:mk_text(Body),
- sub_els = [XUser, XConference]},
- ejabberd_router:route(StateData#state.jid, JID, Msg),
- JID;
- #muc_user{invites = [_|_]} ->
- Txt = <<"Multiple elements are not allowed">>,
- {error, xmpp:err_forbidden(Txt, Lang)};
- _ ->
- Txt = <<"No element found">>,
- {error, xmpp:err_bad_request(Txt, Lang)}
- end
+ #muc_invite{to = JID, reason = Reason} = Invitation,
+ Invite = Invitation#muc_invite{to = undefined, from = From},
+ Password = case (StateData#state.config)#config.password_protected of
+ true ->
+ (StateData#state.config)#config.password;
+ false ->
+ undefined
+ end,
+ XUser = #muc_user{password = Password, invites = [Invite]},
+ XConference = #x_conference{jid = jid:make(StateData#state.room,
+ StateData#state.host),
+ reason = Reason},
+ Body = iolist_to_binary(
+ [io_lib:format(
+ translate:translate(
+ Lang,
+ <<"~s invites you to the room ~s">>),
+ [jid:to_string(From),
+ jid:to_string({StateData#state.room,
+ StateData#state.host,
+ <<"">>})]),
+ case (StateData#state.config)#config.password_protected of
+ true ->
+ <<", ",
+ (translate:translate(
+ Lang, <<"the password is">>))/binary,
+ " '",
+ ((StateData#state.config)#config.password)/binary,
+ "'">>;
+ _ -> <<"">>
+ end,
+ case Reason of
+ <<"">> -> <<"">>;
+ _ -> <<" (", Reason/binary, ") ">>
+ end]),
+ Msg = #message{type = normal,
+ body = xmpp:mk_text(Body),
+ sub_els = [XUser, XConference]},
+ ejabberd_router:route(StateData#state.jid, JID, Msg),
+ JID
end.
%% Handle a message sent to the room by a non-participant.
diff --git a/src/mod_offline.erl b/src/mod_offline.erl
index 258c97d4f..240650234 100644
--- a/src/mod_offline.erl
+++ b/src/mod_offline.erl
@@ -300,8 +300,7 @@ get_sm_items(Acc, _From, _To, _Node, _Lang) ->
-spec get_info([xdata()], binary(), module(), binary(), binary()) -> [xdata()];
([xdata()], jid(), jid(), binary(), binary()) -> [xdata()].
get_info(_Acc, #jid{luser = U, lserver = S, lresource = R},
- #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, _Lang) ->
- N = integer_to_binary(count_offline_messages(U, S)),
+ #jid{luser = U, lserver = S}, ?NS_FLEX_OFFLINE, Lang) ->
case ejabberd_sm:get_session_pid(U, S, R) of
Pid when is_pid(Pid) ->
Pid ! dont_ask_offline;
@@ -309,11 +308,9 @@ get_info(_Acc, #jid{luser = U, lserver = S, lresource = R},
ok
end,
[#xdata{type = result,
- fields = [#xdata_field{var = <<"FORM_TYPE">>,
- type = hidden,
- values = [?NS_FLEX_OFFLINE]},
- #xdata_field{var = <<"number_of_messages">>,
- values = [N]}]}];
+ fields = flex_offline:encode(
+ [{number_of_messages, count_offline_messages(U, S)}],
+ fun(T) -> translate:translate(Lang, T) end)}];
get_info(Acc, _From, _To, _Node, _Lang) ->
Acc.
diff --git a/src/mod_pubsub.erl b/src/mod_pubsub.erl
index b55115c9a..e3fe64c16 100644
--- a/src/mod_pubsub.erl
+++ b/src/mod_pubsub.erl
@@ -1028,8 +1028,11 @@ do_route(Host, From, To, Packet) ->
case find_authorization_response(Packet) of
undefined ->
ok;
- XData ->
- handle_authorization_response(Host, From, To, Packet, XData)
+ {error, Err} ->
+ ejabberd_router:route_error(To, From, Packet, Err);
+ AuthResponse ->
+ handle_authorization_response(
+ Host, From, To, Packet, AuthResponse)
end;
_ ->
Err = xmpp:err_service_unavailable(),
@@ -1200,19 +1203,28 @@ iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang,
ServerHost = serverhost(Host),
Plugins = config(ServerHost, plugins),
Config = case Configure of
- {_, XData} -> get_xdata_fields(XData);
+ {_, XData} -> decode_node_config(XData, Host, Lang);
undefined -> []
end,
Type = hd(Plugins),
- create_node(Host, ServerHost, Node, From, Type, Access, Config);
+ case Config of
+ {error, _} = Err ->
+ Err;
+ _ ->
+ create_node(Host, ServerHost, Node, From, Type, Access, Config)
+ end;
{set, #pubsub{publish = #ps_publish{node = Node, items = Items},
publish_options = XData, _ = undefined}} ->
ServerHost = serverhost(Host),
case Items of
[#ps_item{id = ItemId, xml_els = Payload}] ->
- PubOpts = get_xdata_fields(XData),
- publish_item(Host, ServerHost, Node, From, ItemId,
- Payload, PubOpts, Access);
+ case decode_publish_options(XData, Lang) of
+ {error, _} = Err ->
+ Err;
+ PubOpts ->
+ publish_item(Host, ServerHost, Node, From, ItemId,
+ Payload, PubOpts, Access)
+ end;
[] ->
{error, extended_error(xmpp:err_bad_request(), err_item_required())};
_ ->
@@ -1236,10 +1248,17 @@ iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang,
{set, #pubsub{subscribe = #ps_subscribe{node = Node, jid = JID},
options = Options, _ = undefined}} ->
Config = case Options of
- #ps_options{xdata = XData} -> get_xdata_fields(XData);
- _ -> []
+ #ps_options{xdata = XData} ->
+ decode_subscribe_options(XData, Lang);
+ _ ->
+ []
end,
- subscribe_node(Host, Node, From, JID, Config);
+ case Config of
+ {error, _} = Err ->
+ Err;
+ _ ->
+ subscribe_node(Host, Node, From, JID, Config)
+ end;
{set, #pubsub{unsubscribe = #ps_unsubscribe{node = Node, jid = JID, subid = SubId},
_ = undefined}} ->
unsubscribe_node(Host, Node, From, JID, SubId);
@@ -1262,7 +1281,12 @@ iq_pubsub(Host, Access, #iq{from = From, type = IQType, lang = Lang,
{set, #pubsub{options = #ps_options{node = Node, subid = SubId,
jid = JID, xdata = XData},
_ = undefined}} ->
- set_options(Host, Node, JID, SubId, get_xdata_fields(XData));
+ case decode_subscribe_options(XData, Lang) of
+ {error, _} = Err ->
+ Err;
+ Config ->
+ set_options(Host, Node, JID, SubId, Config)
+ end;
{set, #pubsub{}} ->
{error, xmpp:err_bad_request()};
_ ->
@@ -1284,8 +1308,12 @@ iq_pubsub_owner(Host, #iq{type = IQType, from = From,
#xdata{type = cancel} ->
{result, #pubsub_owner{}};
#xdata{type = submit} ->
- Config = get_xdata_fields(XData),
- set_configure(Host, Node, From, Config, Lang);
+ case decode_node_config(XData, Host, Lang) of
+ {error, _} = Err ->
+ Err;
+ Config ->
+ set_configure(Host, Node, From, Config, Lang)
+ end;
#xdata{} ->
{error, xmpp:err_bad_request(<<"Incorrect data form">>, Lang)}
end;
@@ -1318,19 +1346,20 @@ adhoc_request(Host, _ServerHost, Owner,
send_pending_node_form(Host, Owner, Lang, Plugins);
adhoc_request(Host, _ServerHost, Owner,
#adhoc_command{node = ?NS_PUBSUB_GET_PENDING, lang = Lang,
- action = execute, xdata = #xdata{} = XData},
+ action = execute, xdata = #xdata{} = XData} = Request,
_Access, _Plugins) ->
- Config = get_xdata_fields(XData),
- case set_xoption(Host, Config, []) of
- XForm when is_list(XForm) ->
- case lists:keysearch(node, 1, XForm) of
- {value, {_, Node}} ->
- send_pending_auth_events(Host, Node, Owner, Lang);
- false ->
- {error, extended_error(xmpp:err_bad_request(), err_invalid_payload())}
- end;
- Err ->
- Err
+ case decode_get_pending(XData, Lang) of
+ {error, _} = Err ->
+ Err;
+ Config ->
+ Node = proplists:get_value(node, Config),
+ case send_pending_auth_events(Host, Node, Owner, Lang) of
+ ok ->
+ xmpp_util:make_adhoc_response(
+ Request, #adhoc_command{action = completed});
+ Err ->
+ Err
+ end
end;
adhoc_request(_Host, _ServerHost, _Owner,
#adhoc_command{action = cancel}, _Access, _Plugins) ->
@@ -1353,12 +1382,9 @@ send_pending_node_form(Host, Owner, _Lang, Plugins) ->
Ps ->
case get_pending_nodes(Host, Owner, Ps) of
{ok, Nodes} ->
- XOpts = [#xdata_option{value = Node} || Node <- Nodes],
XForm = #xdata{type = form,
- fields = [#xdata_field{
- type = 'list-single',
- var = <<"pubsub#node">>,
- options = lists:usort(XOpts)}]},
+ fields = pubsub_get_pending:encode(
+ [{node, Nodes}])},
#adhoc_command{status = executing, action = execute,
xdata = XForm};
Err ->
@@ -1423,24 +1449,11 @@ send_authorization_request(#pubsub_node{nodeid = {Host, Node},
Subscriber) ->
%% TODO: pass lang to this function
Lang = <<"en">>,
- Fs = [#xdata_field{var = <<"FORM_TYPE">>,
- type = hidden,
- values = [?NS_PUBSUB_SUB_AUTH]},
- #xdata_field{var = <<"pubsub#node">>,
- type = 'text-single',
- label = translate:translate(Lang, <<"Node ID">>),
- values = [Node]},
- #xdata_field{var = <<"pubsub#subscriber_jid">>,
- type = 'jid-single',
- label = translate:translate(Lang, <<"Subscriber Address">>),
- values = [jid:to_string(Subscriber)]},
- #xdata_field{var = <<"pubsub#allow">>,
- type = boolean,
- label = translate:translate(
- Lang,
- <<"Allow this Jabber ID to subscribe to "
- "this pubsub node?">>),
- values = [<<"false">>]}],
+ Fs = pubsub_subscribe_authorization:encode(
+ [{node, Node},
+ {subscriber_jid, Subscriber},
+ {allow, false}],
+ fun(T) -> translate:translate(Lang, T) end),
X = #xdata{type = form,
title = translate:translate(
Lang, <<"PubSub subscriber request">>),
@@ -1455,15 +1468,24 @@ send_authorization_request(#pubsub_node{nodeid = {Host, Node},
ejabberd_router:route(service_jid(Host), jid:make(Owner), Stanza)
end, node_owners_action(Host, Type, Nidx, O)).
--spec find_authorization_response(message()) -> undefined | xdata().
+-spec find_authorization_response(message()) -> undefined |
+ pubsub_subscribe_authorization:result() |
+ {error, stanza_error()}.
find_authorization_response(Packet) ->
case xmpp:get_subtag(Packet, #xdata{}) of
- #xdata{type = submit} = X ->
- case xmpp_util:get_xdata_values(<<"FORM_TYPE">>, X) of
- [?NS_PUBSUB_SUB_AUTH] -> X;
- _ -> undefined
+ #xdata{type = cancel} ->
+ undefined;
+ #xdata{type = submit, fields = Fs} ->
+ try pubsub_subscribe_authorization:decode(Fs) of
+ Result -> Result
+ catch _:{pubsub_subscribe_authorization, Why} ->
+ Lang = xmpp:get_lang(Packet),
+ Txt = pubsub_subscribe_authorization:format_error(Why),
+ {error, xmpp:err_bad_request(Txt, Lang)}
end;
- _ ->
+ #xdata{} ->
+ {error, xmpp:err_bad_request()};
+ false ->
undefined
end.
@@ -1477,43 +1499,33 @@ send_authorization_approval(Host, JID, SNode, Subscription) ->
Stanza = #message{sub_els = [Event]},
ejabberd_router:route(service_jid(Host), JID, Stanza).
--spec handle_authorization_response(binary(), jid(), jid(), message(), xdata()) -> ok.
-handle_authorization_response(Host, From, To, Packet, X) ->
+-spec handle_authorization_response(binary(), jid(), jid(), message(),
+ pubsub_subscribe_authorization:result()) -> ok.
+handle_authorization_response(Host, From, To, Packet, Response) ->
+ Node = proplists:get_value(node, Response),
+ Subscriber = proplists:get_value(subscriber_jid, Response),
+ Allow = proplists:get_value(allow, Response),
Lang = xmpp:get_lang(Packet),
- case {xmpp_util:get_xdata_values(<<"pubsub#node">>, X),
- xmpp_util:get_xdata_values(<<"pubsub#subscriber_jid">>, X),
- xmpp_util:get_xdata_values(<<"pubsub#allow">>, X)} of
- {[Node], [SSubscriber], [SAllow]} ->
- FromLJID = jid:tolower(jid:remove_resource(From)),
- Subscriber = jid:from_string(SSubscriber),
- Allow = case SAllow of
- <<"1">> -> true;
- <<"true">> -> true;
- _ -> false
- end,
- Action =
- fun(#pubsub_node{type = Type, id = Nidx, owners = O}) ->
- Owners = node_owners_call(Host, Type, Nidx, O),
- case lists:member(FromLJID, Owners) of
- true ->
- {result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]),
- update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs);
- false ->
- {error, xmpp:err_forbidden(<<"Owner privileges required">>, Lang)}
- end
- end,
- case transaction(Host, Node, Action, sync_dirty) of
- {error, Error} ->
- ejabberd_router:route_error(To, From, Packet, Error);
- {result, {_, _NewSubscription}} ->
- %% XXX: notify about subscription state change, section 12.11
- ok;
- _ ->
- Err = xmpp:err_internal_server_error(),
- ejabberd_router:route_error(To, From, Packet, Err)
- end;
+ FromLJID = jid:tolower(jid:remove_resource(From)),
+ Action =
+ fun(#pubsub_node{type = Type, id = Nidx, owners = O}) ->
+ Owners = node_owners_call(Host, Type, Nidx, O),
+ case lists:member(FromLJID, Owners) of
+ true ->
+ {result, Subs} = node_call(Host, Type, get_subscriptions, [Nidx, Subscriber]),
+ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs);
+ false ->
+ {error, xmpp:err_forbidden(<<"Owner privileges required">>, Lang)}
+ end
+ end,
+ case transaction(Host, Node, Action, sync_dirty) of
+ {error, Error} ->
+ ejabberd_router:route_error(To, From, Packet, Error);
+ {result, {_, _NewSubscription}} ->
+ %% XXX: notify about subscription state change, section 12.11
+ ok;
_ ->
- Err = xmpp:err_not_acceptable(<<"Incorrect data form">>, Lang),
+ Err = xmpp:err_internal_server_error(),
ejabberd_router:route_error(To, From, Packet, Err)
end.
@@ -1539,45 +1551,6 @@ update_auth(Host, Node, Type, Nidx, Subscriber, Allow, Subs) ->
{error, xmpp:err_unexpected_request(Txt, ?MYLANG)}
end.
--define(XFIELD(Type, Label, Var, Val),
- #xdata_field{type = Type,
- label = translate:translate(Lang, Label),
- var = Var,
- values = [Val]}).
-
--define(BOOLXFIELD(Label, Var, Val),
- ?XFIELD(boolean, Label, Var,
- case Val of
- true -> <<"1">>;
- _ -> <<"0">>
- end)).
-
--define(STRINGXFIELD(Label, Var, Val),
- ?XFIELD('text-single', Label, Var, Val)).
-
--define(STRINGMXFIELD(Label, Var, Vals),
- #xdata_field{type = 'text-multi',
- label = translate:translate(Lang, Label),
- var = Var,
- values = Vals}).
-
--define(XFIELDOPT(Type, Label, Var, Val, Opts),
- #xdata_field{type = Type,
- label = translate:translate(Lang, Label),
- var = Var,
- options = [#xdata_option{value = Opt} || Opt <- Opts],
- values = [Val]}).
-
--define(LISTXFIELD(Label, Var, Val, Opts),
- ?XFIELDOPT('list-single', Label, Var, Val, Opts)).
-
--define(LISTMXFIELD(Label, Var, Vals, Opts),
- #xdata_field{type = 'list-multi',
- label = translate:translate(Lang, Label),
- var = Var,
- options = [#xdata_option{value = Opt} || Opt <- Opts],
- values = Vals}).
-
%% @doc
Create new pubsub nodes
%%In addition to method-specific error conditions, there are several general reasons why the node creation request might fail:
%%