Mercurial > prosody-modules
diff mod_pep_plus/pubsub.lib.lua @ 2801:cb2342cf3f3c
mod_pep_plus: Snapshot from Prosody trunk 910d3c3f60a6 including dependencies
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Wed, 18 Oct 2017 09:56:29 +0200 |
parents | |
children |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_pep_plus/pubsub.lib.lua Wed Oct 18 09:56:29 2017 +0200 @@ -0,0 +1,497 @@ +local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 +local time_now = os.time; + +local set = require "util.set"; +local st = require "util.stanza"; +local it = require "util.iterators"; +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"] = { "auth", "forbidden" }; + ["not-allowed"] = { "cancel", "not-allowed" }; +}; +local function pubsub_error_reply(stanza, error) + local e = pubsub_errors[error]; + local reply = st.error_reply(stanza, t_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 = dataform { + { + 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"; + }; + { + type = "boolean"; + name = "pubsub#persist_items"; + label = "Persist items to storage"; + }; +}; + +local service_method_feature_map = { + add_subscription = { "subscribe" }; + 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" }; + purge = { "purge-nodes" }; + retract = { "delete-items", "retract-items" }; + set_node_config = { "config-node" }; +}; +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 + + return supported_features; +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 item = items:get_child("item"); + local item_id = item and item.attr.id; + + if not node then + origin.send(pubsub_error_reply(stanza, "nodeid-required")); + return true; + end + local ok, results = service:get_items(node, stanza.attr.from, item_id); + if not ok then + origin.send(pubsub_error_reply(stanza, results)); + return true; + 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 + 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.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 not form_data then + origin.send(st.error_reply(stanza, "modify", "bad-request", err)); + return true; + end + config = { + ["max_items"] = tonumber(form_data["pubsub#max_items"]); + ["persist_items"] = form_data["pubsub#persist_items"]; + }; + 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; + 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 + 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 + 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); + else + reply = pubsub_error_reply(stanza, ret); + end + origin.send(reply); + 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 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 + 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 item = retract:get_child("item"); + local id = item and item.attr.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, notify = purge.attr.node, purge.attr.notify; + notify = (notify == "1") or (notify == "true"); + 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, notify); + 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 + + if not service:may(node, stanza.attr.from, "configure") 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 node_config = node_obj.config; + local pubsub_form_data = { + ["pubsub#max_items"] = tostring(node_config["max_items"]); + ["pubsub#persist_items"] = node_config["persist_items"] + } + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("configure", { node = node }) + :add_child(node_config_form:form(pubsub_form_data)); + 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 form_data, err = node_config_form:data(config_form); + if not form_data then + origin.send(st.error_reply(stanza, "modify", "bad-request", err)); + return true; + end + local new_config = { + ["max_items"] = tonumber(form_data["pubsub#max_items"]); + ["persist_items"] = form_data["pubsub#persist_items"]; + }; + 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 pubsub_form_data = { + ["pubsub#max_items"] = tostring(service.node_defaults["max_items"]); + ["pubsub#persist_items"] = service.node_defaults["persist_items"] + } + local reply = st.reply(stanza) + :tag("pubsub", { xmlns = xmlns_pubsub_owner }) + :tag("default") + :add_child(node_config_form:form(pubsub_form_data)); + origin.send(reply); + return true; +end + +local function create_encapsulating_item(id, payload) + local item = st.stanza("item", { id = id, xmlns = xmlns_pubsub }); + item:add_child(payload); + return item; +end + +local function archive_itemstore(archive, config, user, node) + module:log("debug", "Creation of itemstore for node %s with config %s", node, config); + local get_set = {}; + function get_set:items() -- luacheck: ignore 212/self + local data, err = archive:find(user, { + limit = tonumber(config["pubsub#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 it.reverse(function() + 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 + 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 + return setmetatable(get_set, archive); +end +_M.archive_itemstore = archive_itemstore; + +return _M;