local time_now = os.time ;
local jid_prep = require " prosody.util.jid " . prep ;
local set = require " prosody.util.set " ;
local st = require " prosody.util.stanza " ;
local it = require " prosody.util.iterators " ;
local uuid_generate = require " prosody.util.uuid " . generate ;
local dataform = require " prosody.util.dataforms " . new ;
local errors = require " prosody.util.error " ;
local xmlns_pubsub = " http://jabber.org/protocol/pubsub " ;
local xmlns_pubsub_errors = " http://jabber.org/protocol/pubsub#errors " ;
local xmlns_pubsub_owner = " http://jabber.org/protocol/pubsub#owner " ;
local _M = { } ;
local handlers = { } ;
_M.handlers = handlers ;
local pubsub_errors = errors.init ( " pubsub " , xmlns_pubsub_errors , {
[ " conflict " ] = { " cancel " , " conflict " } ;
[ " invalid-jid " ] = { " modify " , " bad-request " , nil , " invalid-jid " } ;
[ " jid-required " ] = { " modify " , " bad-request " , nil , " jid-required " } ;
[ " nodeid-required " ] = { " modify " , " bad-request " , nil , " nodeid-required " } ;
[ " item-not-found " ] = { " cancel " , " item-not-found " } ;
[ " not-subscribed " ] = { " modify " , " unexpected-request " , nil , " not-subscribed " } ;
[ " invalid-options " ] = { " modify " , " bad-request " , nil , " invalid-options " } ;
[ " forbidden " ] = { " auth " , " forbidden " } ;
[ " not-allowed " ] = { " cancel " , " not-allowed " } ;
[ " not-acceptable " ] = { " modify " , " not-acceptable " } ;
[ " internal-server-error " ] = { " wait " , " internal-server-error " } ;
[ " precondition-not-met " ] = { " cancel " , " conflict " , nil , " precondition-not-met " } ;
[ " invalid-item " ] = { " modify " , " bad-request " , " invalid item " } ;
[ " persistent-items-unsupported " ] = { " cancel " , " feature-not-implemented " , nil , " persistent-items " } ;
} ) ;
local function pubsub_error_reply ( stanza , error , context )
local err = pubsub_errors.wrap ( error , context ) ;
if error == " precondition-not-met " and type ( context ) == " table " and type ( context.field ) == " string " then
err.text = " Field does not match: " .. context.field ;
end
local reply = st.error_reply ( stanza , err ) ;
return reply ;
end
_M.pubsub_error_reply = pubsub_error_reply ;
local function dataform_error_message ( err ) -- ({ string : string }) -> string?
local out = { } ;
for field , errmsg in pairs ( err ) do
table.insert ( out , ( " %s: %s " ) : format ( field , errmsg ) )
end
return table.concat ( out , " ; " ) ;
end
-- Note: If any config options are added that are of complex types,
-- (not simply strings/numbers) then the publish-options code will
-- need to be revisited
local node_config_form = dataform {
{
type = " hidden " ;
var = " FORM_TYPE " ;
value = " http://jabber.org/protocol/pubsub#node_config " ;
} ;
{
type = " text-single " ;
name = " title " ;
var = " pubsub#title " ;
label = " Title " ;
} ;
{
type = " text-single " ;
name = " description " ;
var = " pubsub#description " ;
label = " Description " ;
} ;
{
type = " text-single " ;
name = " payload_type " ;
var = " pubsub#type " ;
label = " The type of node data, usually specified by the namespace of the payload (if any) " ;
} ;
{
type = " text-single " ;
datatype = " pubsub:integer-or-max " ;
name = " max_items " ;
range_min = 1 ;
var = " pubsub#max_items " ;
label = " Max # of items to persist " ;
} ;
{
type = " boolean " ;
name = " persist_items " ;
var = " pubsub#persist_items " ;
label = " Persist items to storage " ;
} ;
{
type = " list-single " ;
name = " access_model " ;
var = " pubsub#access_model " ;
label = " Specify the subscriber model " ;
options = {
" authorize " ,
" open " ,
" presence " ,
" roster " ,
" whitelist " ,
} ;
} ;
{
type = " list-multi " ; -- TODO some way to inject options
name = " roster_groups_allowed " ;
var = " pubsub#roster_groups_allowed " ;
label = " Roster groups allowed to subscribe " ;
} ;
{
type = " list-single " ;
name = " publish_model " ;
var = " pubsub#publish_model " ;
label = " Specify the publisher model " ;
options = {
" publishers " ;
" subscribers " ;
" open " ;
} ;
} ;
{
type = " list-single " ;
var = " pubsub#send_last_published_item " ;
name = " send_last_published_item " ;
options = { " never " ; " on_sub " ; " on_sub_and_presence " } ;
} ;
{
type = " boolean " ;
value = true ;
label = " Whether to deliver event notifications " ;
name = " notify_items " ;
var = " pubsub#deliver_notifications " ;
} ;
{
type = " boolean " ;
value = true ;
label = " Whether to deliver payloads with event notifications " ;
name = " include_payload " ;
var = " pubsub#deliver_payloads " ;
} ;
{
type = " list-single " ;
name = " notification_type " ;
var = " pubsub#notification_type " ;
label = " Specify the delivery style for notifications " ;
options = {
{ label = " Messages of type normal " , value = " normal " } ,
{ label = " Messages of type headline " , value = " headline " , default = true } ,
} ;
} ;
{
type = " boolean " ;
label = " Whether to notify subscribers when the node is deleted " ;
name = " notify_delete " ;
var = " pubsub#notify_delete " ;
value = true ;
} ;
{
type = " boolean " ;
label = " Whether to notify subscribers when items are removed from the node " ;
name = " notify_retract " ;
var = " pubsub#notify_retract " ;
value = true ;
} ;
{
type = " list-single " ;
label = " Specify whose JID to include as the publisher of items " ;
name = " itemreply " ;
var = " pubsub#itemreply " ;
options = {
{ label = " Include the node owner's JID " , value = " owner " } ;
{ label = " Include the item publisher's JID " , value = " publisher " } ;
{ label = " Don't include any JID with items " , value = " none " , default = true } ;
} ;
} ;
} ;
_M.node_config_form = node_config_form ;
local subscribe_options_form = dataform {
{
type = " hidden " ;
var = " FORM_TYPE " ;
value = " http://jabber.org/protocol/pubsub#subscribe_options " ;
} ;
{
type = " boolean " ;
name = " pubsub#include_body " ;
label = " Receive message body in addition to payload? " ;
} ;
} ;
_M.subscribe_options_form = subscribe_options_form ;
local node_metadata_form = dataform {
{
type = " hidden " ;
var = " FORM_TYPE " ;
value = " http://jabber.org/protocol/pubsub#meta-data " ;
} ;
{
type = " text-single " ;
name = " title " ;
var = " pubsub#title " ;
} ;
{
type = " text-single " ;
name = " description " ;
var = " pubsub#description " ;
} ;
{
type = " text-single " ;
name = " payload_type " ;
var = " pubsub#type " ;
} ;
{
type = " text-single " ;
name = " access_model " ;
var = " pubsub#access_model " ;
} ;
{
type = " text-single " ;
name = " publish_model " ;
var = " pubsub#publish_model " ;
} ;
} ;
_M.node_metadata_form = node_metadata_form ;
local service_method_feature_map = {
add_subscription = { " subscribe " , " subscription-options " } ;
create = { " create-nodes " , " instant-nodes " , " item-ids " , " create-and-configure " } ;
delete = { " delete-nodes " } ;
get_items = { " retrieve-items " } ;
get_subscriptions = { " retrieve-subscriptions " } ;
node_defaults = { " retrieve-default " } ;
publish = { " publish " , " multi-items " , " publish-options " } ;
purge = { " purge-nodes " } ;
retract = { " delete-items " , " retract-items " } ;
set_node_config = { " config-node " , " meta-data " } ;
set_affiliation = { " modify-affiliations " } ;
} ;
local service_config_feature_map = {
autocreate_on_publish = { " auto-create " } ;
} ;
function _M . get_feature_set ( service )
local supported_features = set.new ( ) ;
for method , features in pairs ( service_method_feature_map ) do
if service [ method ] then
for _ , feature in ipairs ( features ) do
if feature then
supported_features : add ( feature ) ;
end
end
end
end
for option , features in pairs ( service_config_feature_map ) do
if service.config [ option ] then
for _ , feature in ipairs ( features ) do
if feature then
supported_features : add ( feature ) ;
end
end
end
end
for affiliation in pairs ( service.config . capabilities ) do
if affiliation ~= " none " and affiliation ~= " owner " then
supported_features : add ( affiliation .. " -affiliation " ) ;
end
end
if service.node_defaults . access_model then
supported_features : add ( " access- " .. service.node_defaults . access_model ) ;
end
if service.node_defaults . send_last_published_item ~= " never " then
supported_features : add ( " last-published " ) ;
end
if rawget ( service.config , " itemstore " ) and rawget ( service.config , " nodestore " ) then
supported_features : add ( " persistent-items " ) ;
end
if true --[[ node_metadata_form[max_items].datatype == "pubsub:integer-or-max" ]] then
supported_features : add ( " config-node-max " ) ;
end
return supported_features ;
end
function _M . handle_disco_info_node ( event , service )
local stanza , reply , node = event.stanza , event.reply , event.node ;
local ok , meta = service : get_node_metadata ( node , stanza.attr . from ) ;
if not ok then
event.origin . send ( pubsub_error_reply ( stanza , meta ) ) ;
return true ;
end
event.exists = true ;
reply : tag ( " identity " , { category = " pubsub " , type = " leaf " } ) : up ( ) ;
reply : add_child ( node_metadata_form : form ( meta , " result " ) ) ;
end
function _M . handle_disco_items_node ( event , service )
local stanza , reply , node = event.stanza , event.reply , event.node ;
local ok , ret = service : get_items ( node , stanza.attr . from ) ;
if not ok then
event.origin . send ( pubsub_error_reply ( stanza , ret ) ) ;
return true ;
end
for _ , id in ipairs ( ret ) do
reply : tag ( " item " , { jid = service.config . jid or module.host , name = id } ) : up ( ) ;
end
event.exists = true ;
end
function _M . handle_pubsub_iq ( event , service )
local origin , stanza = event.origin , event.stanza ;
local pubsub_tag = stanza.tags [ 1 ] ;
local action = pubsub_tag.tags [ 1 ] ;
if not action then
return origin.send ( st.error_reply ( stanza , " cancel " , " bad-request " ) ) ;
end
local prefix = " " ;
if pubsub_tag.attr . xmlns == xmlns_pubsub_owner then
prefix = " owner_ " ;
end
local handler = handlers [ prefix .. stanza.attr . type .. " _ " .. action.name ] ;
if handler then
handler ( origin , stanza , action , service ) ;
return true ;
end
end
function handlers . get_items ( origin , stanza , items , service )
local node = items.attr . node ;
local requested_items = { } ;
for item in items : childtags ( " item " ) do
table.insert ( requested_items , item.attr . id ) ;
end
if requested_items [ 1 ] == nil then
requested_items = nil ;
end
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
local node_obj = service.nodes [ node ] ;
if not node_obj then
origin.send ( pubsub_error_reply ( stanza , " item-not-found " ) ) ;
return true ;
end
local resultspec ; -- TODO rsm.get()
if items.attr . max_items then
resultspec = { max = tonumber ( items.attr . max_items ) } ;
end
local ok , results = service : get_items ( node , stanza.attr . from , requested_items , resultspec ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , results ) ) ;
return true ;
end
local expose_publisher = service.config . expose_publisher ;
if expose_publisher == nil and node_obj.config . itemreply == " publisher " then
expose_publisher = true ;
end
local data = st.stanza ( " items " , { node = node } ) ;
local iter , v , i = ipairs ( results ) ;
if not requested_items then
-- XXX Hack to preserve order of explicitly requested items.
iter , v , i = it.reverse ( iter , v , i ) ;
end
for _ , id in iter , v , i do
local item = results [ id ] ;
if not expose_publisher then
item = st.clone ( item ) ;
item.attr . publisher = nil ;
end
data : add_child ( item ) ;
end
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: add_child ( data ) ;
origin.send ( reply ) ;
return true ;
end
function handlers . get_subscriptions ( origin , stanza , subscriptions , service )
local node = subscriptions.attr . node ;
local ok , ret = service : get_subscriptions ( node , stanza.attr . from , stanza.attr . from ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , ret ) ) ;
return true ;
end
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " subscriptions " ) ;
for _ , sub in ipairs ( ret ) do
reply : tag ( " subscription " , { node = sub.node , jid = sub.jid , subscription = ' subscribed ' } ) : up ( ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_get_subscriptions ( origin , stanza , subscriptions , service )
local node = subscriptions.attr . node ;
local ok , ret = service : get_subscriptions ( node , stanza.attr . from ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , ret ) ) ;
return true ;
end
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub_owner } )
: tag ( " subscriptions " ) ;
for _ , sub in ipairs ( ret ) do
reply : tag ( " subscription " , { node = sub.node , jid = sub.jid , subscription = ' subscribed ' } ) : up ( ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_set_subscriptions ( origin , stanza , subscriptions , service )
local node = subscriptions.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
if not service : may ( node , stanza.attr . from , " subscribe_other " ) then
origin.send ( pubsub_error_reply ( stanza , " forbidden " ) ) ;
return true ;
end
local node_obj = service.nodes [ node ] ;
if not node_obj then
origin.send ( pubsub_error_reply ( stanza , " item-not-found " ) ) ;
return true ;
end
for subscription_tag in subscriptions : childtags ( " subscription " ) do
if subscription_tag.attr . subscription == ' subscribed ' then
local ok , err = service : add_subscription ( node , stanza.attr . from , subscription_tag.attr . jid ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , err ) ) ;
return true ;
end
elseif subscription_tag.attr . subscription == ' none ' then
local ok , err = service : remove_subscription ( node , stanza.attr . from , subscription_tag.attr . jid ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , err ) ) ;
return true ;
end
end
end
local reply = st.reply ( stanza ) ;
origin.send ( reply ) ;
return true ;
end
function handlers . set_create ( origin , stanza , create , service )
local node = create.attr . node ;
local ok , ret , reply ;
local config ;
local configure = stanza.tags [ 1 ] : get_child ( " configure " ) ;
if configure then
local config_form = configure : get_child ( " x " , " jabber:x:data " ) ;
if not config_form then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , " Missing dataform " ) ) ;
return true ;
end
local form_data , err = node_config_form : data ( config_form ) ;
if err then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , dataform_error_message ( err ) ) ) ;
return true ;
end
config = form_data ;
end
if node then
ok , ret = service : create ( node , stanza.attr . from , config ) ;
if ok then
reply = st.reply ( stanza ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
else
repeat
node = uuid_generate ( ) ;
ok , ret = service : create ( node , stanza.attr . from , config ) ;
until ok or ret ~= " conflict " ;
if ok then
reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " create " , { node = node } ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_set_delete ( origin , stanza , delete , service )
local node = delete.attr . node ;
local reply ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
local ok , ret = service : delete ( node , stanza.attr . from ) ;
if ok then
reply = st.reply ( stanza ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . set_subscribe ( origin , stanza , subscribe , service )
local node , jid = subscribe.attr . node , subscribe.attr . jid ;
jid = jid_prep ( jid ) ;
if not ( node and jid ) then
origin.send ( pubsub_error_reply ( stanza , jid and " nodeid-required " or " invalid-jid " ) ) ;
return true ;
end
local options_tag , options = stanza.tags [ 1 ] : get_child ( " options " ) , nil ;
if options_tag then
-- FIXME form parsing errors ignored here, why?
local err
options , err = subscribe_options_form : data ( options_tag.tags [ 1 ] ) ;
if err then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , dataform_error_message ( err ) ) ) ;
return true
end
end
local ok , ret = service : add_subscription ( node , stanza.attr . from , jid , options ) ;
local reply ;
if ok then
reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " subscription " , {
node = node ,
jid = jid ,
subscription = " subscribed "
} ) : up ( ) ;
if options_tag then
reply : add_child ( options_tag ) ;
end
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
origin.send ( reply ) ;
local ok , config = service : get_node_config ( node , true ) ;
if ok and config.send_last_published_item ~= " never " then
local ok , id , item = service : get_last_item ( node , jid ) ;
if not ( ok and id ) then return ; end
service.config . broadcaster ( " items " , node , { [ jid ] = true } , item ) ;
end
end
function handlers . set_unsubscribe ( origin , stanza , unsubscribe , service )
local node , jid = unsubscribe.attr . node , unsubscribe.attr . jid ;
jid = jid_prep ( jid ) ;
if not ( node and jid ) then
origin.send ( pubsub_error_reply ( stanza , jid and " nodeid-required " or " invalid-jid " ) ) ;
return true ;
end
local ok , ret = service : remove_subscription ( node , stanza.attr . from , jid ) ;
local reply ;
if ok then
reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " subscription " , {
node = node ,
jid = jid ,
subscription = " none "
} ) : up ( ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . get_options ( origin , stanza , options , service )
local node , jid = options.attr . node , options.attr . jid ;
jid = jid_prep ( jid ) ;
if not ( node and jid ) then
origin.send ( pubsub_error_reply ( stanza , jid and " nodeid-required " or " invalid-jid " ) ) ;
return true ;
end
local ok , ret = service : get_subscription ( node , stanza.attr . from , jid ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , " not-subscribed " ) ) ;
return true ;
end
if ret == true then ret = { } end
origin.send ( st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " options " , { node = node , jid = jid } )
: add_child ( subscribe_options_form : form ( ret ) ) ) ;
return true ;
end
function handlers . set_options ( origin , stanza , options , service )
local node , jid = options.attr . node , options.attr . jid ;
jid = jid_prep ( jid ) ;
if not ( node and jid ) then
origin.send ( pubsub_error_reply ( stanza , jid and " nodeid-required " or " invalid-jid " ) ) ;
return true ;
end
local ok , ret = service : get_subscription ( node , stanza.attr . from , jid ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , ret ) ) ;
return true ;
elseif not ret then
origin.send ( pubsub_error_reply ( stanza , " not-subscribed " ) ) ;
return true ;
end
local old_subopts = ret ;
local new_subopts , err = subscribe_options_form : data ( options.tags [ 1 ] , old_subopts ) ;
if err then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , dataform_error_message ( err ) ) ) ;
return true ;
end
local ok , err = service : add_subscription ( node , stanza.attr . from , jid , new_subopts ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , err ) ) ;
return true ;
end
origin.send ( st.reply ( stanza ) ) ;
return true ;
end
function handlers . set_publish ( origin , stanza , publish , service )
local node = publish.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
local required_config = nil ;
local publish_options = stanza.tags [ 1 ] : get_child ( " publish-options " ) ;
if publish_options then
-- Ensure that the node configuration matches the values in publish-options
local publish_options_form = publish_options : get_child ( " x " , " jabber:x:data " ) ;
local err ;
required_config , err = node_config_form : data ( publish_options_form ) ;
if err then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , dataform_error_message ( err ) ) ) ;
return true
end
end
local item = publish : get_child ( " item " ) ;
local id = ( item and item.attr . id ) ;
if not id then
id = uuid_generate ( ) ;
if item then
item.attr . id = id ;
end
end
if item then
item.attr . publisher = service.config . normalize_jid ( stanza.attr . from ) ;
end
local ok , ret , context = service : publish ( node , stanza.attr . from , id , item , required_config ) ;
local reply ;
if ok then
if type ( ok ) == " string " then
id = ok ;
end
reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub } )
: tag ( " publish " , { node = node } )
: tag ( " item " , { id = id } ) ;
else
reply = pubsub_error_reply ( stanza , ret , context ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . set_retract ( origin , stanza , retract , service )
local node , notify = retract.attr . node , retract.attr . notify ;
notify = ( notify == " 1 " ) or ( notify == " true " ) ;
local id = retract : get_child_attr ( " item " , nil , " id " ) ;
if not ( node and id ) then
origin.send ( pubsub_error_reply ( stanza , node and " item-not-found " or " nodeid-required " ) ) ;
return true ;
end
local reply , notifier ;
if notify then
notifier = st.stanza ( " retract " , { id = id } ) ;
end
local ok , ret = service : retract ( node , stanza.attr . from , id , notifier ) ;
if ok then
reply = st.reply ( stanza ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_set_purge ( origin , stanza , purge , service )
local node = purge.attr . node ;
local reply ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
local ok , ret = service : purge ( node , stanza.attr . from , true ) ;
if ok then
reply = st.reply ( stanza ) ;
else
reply = pubsub_error_reply ( stanza , ret ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_get_configure ( origin , stanza , config , service )
local node = config.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
local ok , node_config = service : get_node_config ( node , stanza.attr . from ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , node_config ) ) ;
return true ;
end
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub_owner } )
: tag ( " configure " , { node = node } )
: add_child ( node_config_form : form ( node_config ) ) ;
origin.send ( reply ) ;
return true ;
end
function handlers . owner_set_configure ( origin , stanza , config , service )
local node = config.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
if not service : may ( node , stanza.attr . from , " configure " ) then
origin.send ( pubsub_error_reply ( stanza , " forbidden " ) ) ;
return true ;
end
local config_form = config : get_child ( " x " , " jabber:x:data " ) ;
if not config_form then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , " Missing dataform " ) ) ;
return true ;
end
local ok , old_config = service : get_node_config ( node , stanza.attr . from ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , old_config ) ) ;
return true ;
end
local new_config , err = node_config_form : data ( config_form , old_config ) ;
if err then
origin.send ( st.error_reply ( stanza , " modify " , " bad-request " , dataform_error_message ( err ) ) ) ;
return true ;
end
local ok , err = service : set_node_config ( node , stanza.attr . from , new_config ) ;
if not ok then
origin.send ( pubsub_error_reply ( stanza , err ) ) ;
return true ;
end
origin.send ( st.reply ( stanza ) ) ;
return true ;
end
function handlers . owner_get_default ( origin , stanza , default , service ) -- luacheck: ignore 212/default
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub_owner } )
: tag ( " default " )
: add_child ( node_config_form : form ( service.node_defaults ) ) ;
origin.send ( reply ) ;
return true ;
end
function handlers . owner_get_affiliations ( origin , stanza , affiliations , service )
local node = affiliations.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
if not service : may ( node , stanza.attr . from , " set_affiliation " ) then
origin.send ( pubsub_error_reply ( stanza , " forbidden " ) ) ;
return true ;
end
local node_obj = service.nodes [ node ] ;
if not node_obj then
origin.send ( pubsub_error_reply ( stanza , " item-not-found " ) ) ;
return true ;
end
local reply = st.reply ( stanza )
: tag ( " pubsub " , { xmlns = xmlns_pubsub_owner } )
: tag ( " affiliations " , { node = node } ) ;
for jid , affiliation in pairs ( node_obj.affiliations ) do
reply : tag ( " affiliation " , { jid = jid , affiliation = affiliation } ) : up ( ) ;
end
origin.send ( reply ) ;
return true ;
end
function handlers . owner_set_affiliations ( origin , stanza , affiliations , service )
local node = affiliations.attr . node ;
if not node then
origin.send ( pubsub_error_reply ( stanza , " nodeid-required " ) ) ;
return true ;
end
if not service : may ( node , stanza.attr . from , " set_affiliation " ) then
origin.send ( pubsub_error_reply ( stanza , " forbidden " ) ) ;
return true ;
end
local node_obj = service.nodes [ node ] ;
if not node_obj then
origin.send ( pubsub_error_reply ( stanza , " item-not-found " ) ) ;
return true ;
end
for affiliation_tag in affiliations : childtags ( " affiliation " ) do
local jid = affiliation_tag.attr . jid ;
local affiliation = affiliation_tag.attr . affiliation ;
jid = jid_prep ( jid ) ;
if affiliation == " none " then affiliation = nil ; end
local ok , err = service : set_affiliation ( node , stanza.attr . from , jid , affiliation ) ;
if not ok then
-- FIXME Incomplete error handling,
-- see XEP 60 8.9.2.4 Multiple Simultaneous Modifications
origin.send ( pubsub_error_reply ( stanza , err ) ) ;
return true ;
end
end
local reply = st.reply ( stanza ) ;
origin.send ( reply ) ;
return true ;
end
local function create_encapsulating_item ( id , payload , publisher )
local item = st.stanza ( " item " , { id = id , publisher = publisher , xmlns = xmlns_pubsub } ) ;
item : add_child ( payload ) ;
return item ;
end
local function archive_itemstore ( archive , max_items , user , node )
module : log ( " debug " , " Creation of archive itemstore for node %s with limit %d " , node , max_items ) ;
local get_set = { } ;
function get_set : items ( ) -- luacheck: ignore 212/self
local data , err = archive : find ( user , {
limit = tonumber ( max_items ) ;
reverse = true ;
} ) ;
if not data then
module : log ( " error " , " Unable to get items: %s " , err ) ;
return true ;
end
module : log ( " debug " , " Listed items %s " , data ) ;
return function ( )
-- luacheck: ignore 211/when
local id , payload , when , publisher = data ( ) ;
if id == nil then
return ;
end
local item = create_encapsulating_item ( id , payload , publisher ) ;
return id , item ;
end ;
end
function get_set : get ( key ) -- luacheck: ignore 212/self
local data , err = archive : find ( user , {
key = key ;
-- Get the last item with that key, if the archive doesn't deduplicate
reverse = true ,
limit = 1 ;
} ) ;
if not data then
module : log ( " error " , " Unable to get item: %s " , err ) ;
return nil , err ;
end
local id , payload , when , publisher = data ( ) ;
module : log ( " debug " , " Get item %s (published at %s by %s) " , id , when , publisher ) ;
if id == nil then
return nil ;
end
return create_encapsulating_item ( id , payload , publisher ) ;
end
function get_set : set ( key , value ) -- luacheck: ignore 212/self
local data , err ;
if value ~= nil then
local publisher = value.attr . publisher ;
local payload = value.tags [ 1 ] ;
data , err = archive : append ( user , key , payload , time_now ( ) , publisher ) ;
else
data , err = archive : delete ( user , { key = key ; } ) ;
end
-- TODO archive support for maintaining maximum items
archive : delete ( user , {
truncate = max_items ;
} ) ;
if not data then
module : log ( " error " , " Unable to set item: %s " , err ) ;
return nil , err ;
end
return data ;
end
function get_set : clear ( ) -- luacheck: ignore 212/self
return archive : delete ( user ) ;
end
function get_set : resize ( size ) -- luacheck: ignore 212/self
max_items = size ;
return archive : delete ( user , {
truncate = size ;
} ) ;
end
function get_set : head ( )
-- This should conveniently return the most recent item
local item = self : get ( nil ) ;
if item then
return item.attr . id , item ;
end
end
return get_set ;
end
_M.archive_itemstore = archive_itemstore ;
return _M ;