diff options
Diffstat (limited to 'plugins/mod_pubsub')
-rw-r--r-- | plugins/mod_pubsub/mod_pubsub.lua | 229 | ||||
-rw-r--r-- | plugins/mod_pubsub/pubsub.lib.lua | 290 |
2 files changed, 519 insertions, 0 deletions
diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua new file mode 100644 index 00000000..8c777f54 --- /dev/null +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -0,0 +1,229 @@ +local pubsub = require "util.pubsub"; +local st = require "util.stanza"; +local jid_bare = require "util.jid".bare; +local usermanager = require "core.usermanager"; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; +local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; +local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; + +local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); +local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); +local pubsub_disco_name = module:get_option("name"); +if type(pubsub_disco_name) ~= "string" then pubsub_disco_name = "Prosody PubSub Service"; end + +local service; + +local lib_pubsub = module:require "pubsub"; +local handlers = lib_pubsub.handlers; +local pubsub_error_reply = lib_pubsub.pubsub_error_reply; + +module:depends("disco"); +module:add_identity("pubsub", "service", pubsub_disco_name); +module:add_feature("http://jabber.org/protocol/pubsub"); + +function handle_pubsub_iq(event) + local origin, stanza = event.origin, event.stanza; + local pubsub = stanza.tags[1]; + local action = pubsub.tags[1]; + if not action then + return origin.send(st.error_reply(stanza, "cancel", "bad-request")); + end + local handler = handlers[stanza.attr.type.."_"..action.name]; + if handler then + handler(origin, stanza, action, service); + return true; + end +end + +function simple_broadcast(kind, node, jids, item) + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + end + local message = st.message({ from = module.host, type = "headline" }) + :tag("event", { xmlns = xmlns_pubsub_event }) + :tag(kind, { node = node }) + :add_child(item); + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s", jid); + message.attr.to = jid; + module:send(message); + end +end + +module:hook("iq/host/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); +module:hook("iq/host/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); + +local feature_map = { + create = { "create-nodes", "instant-nodes", "item-ids" }; + retract = { "delete-items", "retract-items" }; + purge = { "purge-nodes" }; + publish = { "publish", autocreate_on_publish and "auto-create" }; + delete = { "delete-nodes" }; + get_items = { "retrieve-items" }; + add_subscription = { "subscribe" }; + get_subscriptions = { "retrieve-subscriptions" }; + set_configure = { "config-node" }; + get_default = { "retrieve-default" }; +}; + +local function add_disco_features_from_service(service) + for method, features in pairs(feature_map) do + if service[method] then + for _, feature in ipairs(features) do + if feature then + module:add_feature(xmlns_pubsub.."#"..feature); + end + end + end + end + for affiliation in pairs(service.config.capabilities) do + if affiliation ~= "none" and affiliation ~= "owner" then + module:add_feature(xmlns_pubsub.."#"..affiliation.."-affiliation"); + end + end +end + +module:hook("host-disco-info-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_nodes(stanza.attr.from); + if not ok or not ret[node] then + return; + end + event.exists = true; + reply:tag("identity", { category = "pubsub", type = "leaf" }); +end); + +module:hook("host-disco-items-node", function (event) + local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; + local ok, ret = service:get_items(node, stanza.attr.from); + if not ok then + return; + end + + for _, id in ipairs(ret) do + reply:tag("item", { jid = module.host, name = id }):up(); + end + event.exists = true; +end); + + +module:hook("host-disco-items", function (event) + local stanza, origin, reply = event.stanza, event.origin, event.reply; + local ok, ret = service:get_nodes(event.stanza.attr.from); + if not ok then + return; + end + for node, node_obj in pairs(ret) do + reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); + end +end); + +local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +local function get_affiliation(jid) + local bare_jid = jid_bare(jid); + if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + return admin_aff; + end +end + +function set_service(new_service) + service = new_service; + module.environment.service = service; + add_disco_features_from_service(service); +end + +function module.save() + return { service = service }; +end + +function module.restore(data) + set_service(data.service); +end + +function module.load() + if module.reloading then return; end + + set_service(pubsub.new({ + capabilities = { + none = { + create = false; + publish = false; + retract = false; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + publisher = { + create = false; + publish = true; + retract = true; + get_nodes = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + owner = { + create = true; + publish = true; + retract = true; + delete = true; + get_nodes = true; + configure = true; + + subscribe = true; + unsubscribe = true; + get_subscription = true; + get_subscriptions = true; + get_items = true; + + + subscribe_other = true; + unsubscribe_other = true; + get_subscription_other = true; + get_subscriptions_other = true; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = true; + }; + }; + + autocreate_on_publish = autocreate_on_publish; + autocreate_on_subscribe = autocreate_on_subscribe; + + broadcaster = simple_broadcast; + get_affiliation = get_affiliation; + + normalize_jid = jid_bare; + })); +end diff --git a/plugins/mod_pubsub/pubsub.lib.lua b/plugins/mod_pubsub/pubsub.lib.lua new file mode 100644 index 00000000..5c6054d1 --- /dev/null +++ b/plugins/mod_pubsub/pubsub.lib.lua @@ -0,0 +1,290 @@ +local st = require "util.stanza"; +local uuid_generate = require "util.uuid".generate; +local dataform = require"util.dataforms".new; + +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 = { + ["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" }; + ["forbidden"] = { "cancel", "forbidden" }; + ["not-allowed"] = { "cancel", "not-allowed" }; +}; +local function pubsub_error_reply(stanza, error) + local e = pubsub_errors[error]; + local reply = st.error_reply(stanza, unpack(e, 1, 3)); + if e[4] then + reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); + end + return reply; +end +_M.pubsub_error_reply = pubsub_error_reply; + +local node_config_form = require"util.dataforms".new { + { + type = "hidden"; + name = "FORM_TYPE"; + value = "http://jabber.org/protocol/pubsub#node_config"; + }; + { + type = "text-single"; + name = "pubsub#max_items"; + label = "Max # of items to persist"; + }; +}; + +function handlers.get_items(origin, stanza, items, service) + local node = items.attr.node; + local item = items:get_child("item"); + local id = item and item.attr.id; + + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, results = service:get_items(node, stanza.attr.from, id); + if not ok then + return origin.send(pubsub_error_reply(stanza, results)); + end + + local data = st.stanza("items", { node = node }); + for _, id in ipairs(results) do + data:add_child(results[id]); + end + local reply; + if data then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :add_child(data); + else + reply = pubsub_error_reply(stanza, "item-not-found"); + end + return origin.send(reply); +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 + return origin.send(pubsub_error_reply(stanza, ret)); + 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 + return origin.send(reply); +end + +function handlers.set_create(origin, stanza, create, service) + local node = create.attr.node; + local ok, ret, reply; + if node then + ok, ret = service:create(node, stanza.attr.from); + 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); + 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 + return origin.send(reply); +end + +function handlers.set_delete(origin, stanza, delete, service) + local node = delete.attr.node; + + local reply, notifier; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + 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 + return origin.send(reply); +end + +function handlers.set_subscribe(origin, stanza, subscribe, service) + local node, jid = subscribe.attr.node, subscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + --[[ + local options_tag, options = stanza.tags[1]:get_child("options"), nil; + if options_tag then + options = options_form:data(options_tag.tags[1]); + end + --]] + local options_tag, options; -- FIXME + 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); +end + +function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) + local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; + if not (node and jid) then + return origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); + end + local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); + local reply; + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.set_publish(origin, stanza, publish, service) + local node = publish.attr.node; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + 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 + local ok, ret = service:publish(node, stanza.attr.from, id, item); + local reply; + if ok then + reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub }) + :tag("publish", { node = node }) + :tag("item", { id = id }); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +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 item = retract:get_child("item"); + local id = item and item.attr.id + if not (node and id) then + return origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); + 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 + return origin.send(reply); +end + +function handlers.set_purge(origin, stanza, purge, service) + local node, notify = purge.attr.node, purge.attr.notify; + notify = (notify == "1") or (notify == "true"); + local reply; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + local ok, ret = service:purge(node, stanza.attr.from, notify); + if ok then + reply = st.reply(stanza); + else + reply = pubsub_error_reply(stanza, ret); + end + return origin.send(reply); +end + +function handlers.get_configure(origin, stanza, config, service) + local node = config.attr.node; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + + if not service:may(node, stanza.attr.from, "configure") then + return origin.send(pubsub_error_reply(stanza, "forbidden")); + end + + local node_obj = service.nodes[node]; + if not node_obj then + return origin.send(pubsub_error_reply(stanza, "item-not-found")); + end + + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("configure", { node = node }) + :add_child(node_config_form:form(node_obj.config)); + return origin.send(reply); +end + +function handlers.set_configure(origin, stanza, config, service) + local node = config.attr.node; + if not node then + return origin.send(pubsub_error_reply(stanza, "nodeid-required")); + end + if not service:may(node, stanza.attr.from, "configure") then + return origin.send(pubsub_error_reply(stanza, "forbidden")); + end + local new_config, err = node_config_form:data(config.tags[1]); + if not new_config then + return origin.send(st.error_reply(stanza, "modify", "bad-request", err)); + end + local ok, err = service:set_node_config(node, stanza.attr.from, new_config); + if not ok then + return origin.send(pubsub_error_reply(stanza, err)); + end + return origin.send(st.reply(stanza)); +end + +function handlers.get_default(origin, stanza, default, service) + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("default") + :add_child(node_config_form:form(service.node_defaults)); + return origin.send(reply); +end + +return _M; |