Mercurial > prosody-modules
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 2800:8d9aed6d1f87 | 2801:cb2342cf3f3c |
|---|---|
| 1 local t_unpack = table.unpack or unpack; -- luacheck: ignore 113 | |
| 2 local time_now = os.time; | |
| 3 | |
| 4 local set = require "util.set"; | |
| 5 local st = require "util.stanza"; | |
| 6 local it = require "util.iterators"; | |
| 7 local uuid_generate = require "util.uuid".generate; | |
| 8 local dataform = require"util.dataforms".new; | |
| 9 | |
| 10 local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; | |
| 11 local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; | |
| 12 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner"; | |
| 13 | |
| 14 local _M = {}; | |
| 15 | |
| 16 local handlers = {}; | |
| 17 _M.handlers = handlers; | |
| 18 | |
| 19 local pubsub_errors = { | |
| 20 ["conflict"] = { "cancel", "conflict" }; | |
| 21 ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; | |
| 22 ["jid-required"] = { "modify", "bad-request", nil, "jid-required" }; | |
| 23 ["nodeid-required"] = { "modify", "bad-request", nil, "nodeid-required" }; | |
| 24 ["item-not-found"] = { "cancel", "item-not-found" }; | |
| 25 ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; | |
| 26 ["forbidden"] = { "auth", "forbidden" }; | |
| 27 ["not-allowed"] = { "cancel", "not-allowed" }; | |
| 28 }; | |
| 29 local function pubsub_error_reply(stanza, error) | |
| 30 local e = pubsub_errors[error]; | |
| 31 local reply = st.error_reply(stanza, t_unpack(e, 1, 3)); | |
| 32 if e[4] then | |
| 33 reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); | |
| 34 end | |
| 35 return reply; | |
| 36 end | |
| 37 _M.pubsub_error_reply = pubsub_error_reply; | |
| 38 | |
| 39 local node_config_form = dataform { | |
| 40 { | |
| 41 type = "hidden"; | |
| 42 name = "FORM_TYPE"; | |
| 43 value = "http://jabber.org/protocol/pubsub#node_config"; | |
| 44 }; | |
| 45 { | |
| 46 type = "text-single"; | |
| 47 name = "pubsub#max_items"; | |
| 48 label = "Max # of items to persist"; | |
| 49 }; | |
| 50 { | |
| 51 type = "boolean"; | |
| 52 name = "pubsub#persist_items"; | |
| 53 label = "Persist items to storage"; | |
| 54 }; | |
| 55 }; | |
| 56 | |
| 57 local service_method_feature_map = { | |
| 58 add_subscription = { "subscribe" }; | |
| 59 create = { "create-nodes", "instant-nodes", "item-ids", "create-and-configure" }; | |
| 60 delete = { "delete-nodes" }; | |
| 61 get_items = { "retrieve-items" }; | |
| 62 get_subscriptions = { "retrieve-subscriptions" }; | |
| 63 node_defaults = { "retrieve-default" }; | |
| 64 publish = { "publish" }; | |
| 65 purge = { "purge-nodes" }; | |
| 66 retract = { "delete-items", "retract-items" }; | |
| 67 set_node_config = { "config-node" }; | |
| 68 }; | |
| 69 local service_config_feature_map = { | |
| 70 autocreate_on_publish = { "auto-create" }; | |
| 71 }; | |
| 72 | |
| 73 function _M.get_feature_set(service) | |
| 74 local supported_features = set.new(); | |
| 75 | |
| 76 for method, features in pairs(service_method_feature_map) do | |
| 77 if service[method] then | |
| 78 for _, feature in ipairs(features) do | |
| 79 if feature then | |
| 80 supported_features:add(feature); | |
| 81 end | |
| 82 end | |
| 83 end | |
| 84 end | |
| 85 | |
| 86 for option, features in pairs(service_config_feature_map) do | |
| 87 if service.config[option] then | |
| 88 for _, feature in ipairs(features) do | |
| 89 if feature then | |
| 90 supported_features:add(feature); | |
| 91 end | |
| 92 end | |
| 93 end | |
| 94 end | |
| 95 | |
| 96 for affiliation in pairs(service.config.capabilities) do | |
| 97 if affiliation ~= "none" and affiliation ~= "owner" then | |
| 98 supported_features:add(affiliation.."-affiliation"); | |
| 99 end | |
| 100 end | |
| 101 | |
| 102 return supported_features; | |
| 103 end | |
| 104 | |
| 105 function _M.handle_pubsub_iq(event, service) | |
| 106 local origin, stanza = event.origin, event.stanza; | |
| 107 local pubsub_tag = stanza.tags[1]; | |
| 108 local action = pubsub_tag.tags[1]; | |
| 109 if not action then | |
| 110 return origin.send(st.error_reply(stanza, "cancel", "bad-request")); | |
| 111 end | |
| 112 local prefix = ""; | |
| 113 if pubsub_tag.attr.xmlns == xmlns_pubsub_owner then | |
| 114 prefix = "owner_"; | |
| 115 end | |
| 116 local handler = handlers[prefix..stanza.attr.type.."_"..action.name]; | |
| 117 if handler then | |
| 118 handler(origin, stanza, action, service); | |
| 119 return true; | |
| 120 end | |
| 121 end | |
| 122 | |
| 123 function handlers.get_items(origin, stanza, items, service) | |
| 124 local node = items.attr.node; | |
| 125 local item = items:get_child("item"); | |
| 126 local item_id = item and item.attr.id; | |
| 127 | |
| 128 if not node then | |
| 129 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 130 return true; | |
| 131 end | |
| 132 local ok, results = service:get_items(node, stanza.attr.from, item_id); | |
| 133 if not ok then | |
| 134 origin.send(pubsub_error_reply(stanza, results)); | |
| 135 return true; | |
| 136 end | |
| 137 | |
| 138 local data = st.stanza("items", { node = node }); | |
| 139 for _, id in ipairs(results) do | |
| 140 data:add_child(results[id]); | |
| 141 end | |
| 142 local reply; | |
| 143 if data then | |
| 144 reply = st.reply(stanza) | |
| 145 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
| 146 :add_child(data); | |
| 147 else | |
| 148 reply = pubsub_error_reply(stanza, "item-not-found"); | |
| 149 end | |
| 150 origin.send(reply); | |
| 151 return true; | |
| 152 end | |
| 153 | |
| 154 function handlers.get_subscriptions(origin, stanza, subscriptions, service) | |
| 155 local node = subscriptions.attr.node; | |
| 156 local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); | |
| 157 if not ok then | |
| 158 origin.send(pubsub_error_reply(stanza, ret)); | |
| 159 return true; | |
| 160 end | |
| 161 local reply = st.reply(stanza) | |
| 162 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
| 163 :tag("subscriptions"); | |
| 164 for _, sub in ipairs(ret) do | |
| 165 reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); | |
| 166 end | |
| 167 origin.send(reply); | |
| 168 return true; | |
| 169 end | |
| 170 | |
| 171 function handlers.set_create(origin, stanza, create, service) | |
| 172 local node = create.attr.node; | |
| 173 local ok, ret, reply; | |
| 174 local config; | |
| 175 local configure = stanza.tags[1]:get_child("configure"); | |
| 176 if configure then | |
| 177 local config_form = configure:get_child("x", "jabber:x:data"); | |
| 178 if not config_form then | |
| 179 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); | |
| 180 return true; | |
| 181 end | |
| 182 local form_data, err = node_config_form:data(config_form); | |
| 183 if not form_data then | |
| 184 origin.send(st.error_reply(stanza, "modify", "bad-request", err)); | |
| 185 return true; | |
| 186 end | |
| 187 config = { | |
| 188 ["max_items"] = tonumber(form_data["pubsub#max_items"]); | |
| 189 ["persist_items"] = form_data["pubsub#persist_items"]; | |
| 190 }; | |
| 191 end | |
| 192 if node then | |
| 193 ok, ret = service:create(node, stanza.attr.from, config); | |
| 194 if ok then | |
| 195 reply = st.reply(stanza); | |
| 196 else | |
| 197 reply = pubsub_error_reply(stanza, ret); | |
| 198 end | |
| 199 else | |
| 200 repeat | |
| 201 node = uuid_generate(); | |
| 202 ok, ret = service:create(node, stanza.attr.from, config); | |
| 203 until ok or ret ~= "conflict"; | |
| 204 if ok then | |
| 205 reply = st.reply(stanza) | |
| 206 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
| 207 :tag("create", { node = node }); | |
| 208 else | |
| 209 reply = pubsub_error_reply(stanza, ret); | |
| 210 end | |
| 211 end | |
| 212 origin.send(reply); | |
| 213 return true; | |
| 214 end | |
| 215 | |
| 216 function handlers.owner_set_delete(origin, stanza, delete, service) | |
| 217 local node = delete.attr.node; | |
| 218 | |
| 219 local reply; | |
| 220 if not node then | |
| 221 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 222 return true; | |
| 223 end | |
| 224 local ok, ret = service:delete(node, stanza.attr.from); | |
| 225 if ok then | |
| 226 reply = st.reply(stanza); | |
| 227 else | |
| 228 reply = pubsub_error_reply(stanza, ret); | |
| 229 end | |
| 230 origin.send(reply); | |
| 231 return true; | |
| 232 end | |
| 233 | |
| 234 function handlers.set_subscribe(origin, stanza, subscribe, service) | |
| 235 local node, jid = subscribe.attr.node, subscribe.attr.jid; | |
| 236 if not (node and jid) then | |
| 237 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); | |
| 238 return true; | |
| 239 end | |
| 240 --[[ | |
| 241 local options_tag, options = stanza.tags[1]:get_child("options"), nil; | |
| 242 if options_tag then | |
| 243 options = options_form:data(options_tag.tags[1]); | |
| 244 end | |
| 245 --]] | |
| 246 local options_tag, options; -- FIXME | |
| 247 local ok, ret = service:add_subscription(node, stanza.attr.from, jid, options); | |
| 248 local reply; | |
| 249 if ok then | |
| 250 reply = st.reply(stanza) | |
| 251 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
| 252 :tag("subscription", { | |
| 253 node = node, | |
| 254 jid = jid, | |
| 255 subscription = "subscribed" | |
| 256 }):up(); | |
| 257 if options_tag then | |
| 258 reply:add_child(options_tag); | |
| 259 end | |
| 260 else | |
| 261 reply = pubsub_error_reply(stanza, ret); | |
| 262 end | |
| 263 origin.send(reply); | |
| 264 end | |
| 265 | |
| 266 function handlers.set_unsubscribe(origin, stanza, unsubscribe, service) | |
| 267 local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; | |
| 268 if not (node and jid) then | |
| 269 origin.send(pubsub_error_reply(stanza, jid and "nodeid-required" or "invalid-jid")); | |
| 270 return true; | |
| 271 end | |
| 272 local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); | |
| 273 local reply; | |
| 274 if ok then | |
| 275 reply = st.reply(stanza); | |
| 276 else | |
| 277 reply = pubsub_error_reply(stanza, ret); | |
| 278 end | |
| 279 origin.send(reply); | |
| 280 return true; | |
| 281 end | |
| 282 | |
| 283 function handlers.set_publish(origin, stanza, publish, service) | |
| 284 local node = publish.attr.node; | |
| 285 if not node then | |
| 286 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 287 return true; | |
| 288 end | |
| 289 local item = publish:get_child("item"); | |
| 290 local id = (item and item.attr.id); | |
| 291 if not id then | |
| 292 id = uuid_generate(); | |
| 293 if item then | |
| 294 item.attr.id = id; | |
| 295 end | |
| 296 end | |
| 297 local ok, ret = service:publish(node, stanza.attr.from, id, item); | |
| 298 local reply; | |
| 299 if ok then | |
| 300 reply = st.reply(stanza) | |
| 301 :tag("pubsub", { xmlns = xmlns_pubsub }) | |
| 302 :tag("publish", { node = node }) | |
| 303 :tag("item", { id = id }); | |
| 304 else | |
| 305 reply = pubsub_error_reply(stanza, ret); | |
| 306 end | |
| 307 origin.send(reply); | |
| 308 return true; | |
| 309 end | |
| 310 | |
| 311 function handlers.set_retract(origin, stanza, retract, service) | |
| 312 local node, notify = retract.attr.node, retract.attr.notify; | |
| 313 notify = (notify == "1") or (notify == "true"); | |
| 314 local item = retract:get_child("item"); | |
| 315 local id = item and item.attr.id | |
| 316 if not (node and id) then | |
| 317 origin.send(pubsub_error_reply(stanza, node and "item-not-found" or "nodeid-required")); | |
| 318 return true; | |
| 319 end | |
| 320 local reply, notifier; | |
| 321 if notify then | |
| 322 notifier = st.stanza("retract", { id = id }); | |
| 323 end | |
| 324 local ok, ret = service:retract(node, stanza.attr.from, id, notifier); | |
| 325 if ok then | |
| 326 reply = st.reply(stanza); | |
| 327 else | |
| 328 reply = pubsub_error_reply(stanza, ret); | |
| 329 end | |
| 330 origin.send(reply); | |
| 331 return true; | |
| 332 end | |
| 333 | |
| 334 function handlers.owner_set_purge(origin, stanza, purge, service) | |
| 335 local node, notify = purge.attr.node, purge.attr.notify; | |
| 336 notify = (notify == "1") or (notify == "true"); | |
| 337 local reply; | |
| 338 if not node then | |
| 339 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 340 return true; | |
| 341 end | |
| 342 local ok, ret = service:purge(node, stanza.attr.from, notify); | |
| 343 if ok then | |
| 344 reply = st.reply(stanza); | |
| 345 else | |
| 346 reply = pubsub_error_reply(stanza, ret); | |
| 347 end | |
| 348 origin.send(reply); | |
| 349 return true; | |
| 350 end | |
| 351 | |
| 352 function handlers.owner_get_configure(origin, stanza, config, service) | |
| 353 local node = config.attr.node; | |
| 354 if not node then | |
| 355 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 356 return true; | |
| 357 end | |
| 358 | |
| 359 if not service:may(node, stanza.attr.from, "configure") then | |
| 360 origin.send(pubsub_error_reply(stanza, "forbidden")); | |
| 361 return true; | |
| 362 end | |
| 363 | |
| 364 local node_obj = service.nodes[node]; | |
| 365 if not node_obj then | |
| 366 origin.send(pubsub_error_reply(stanza, "item-not-found")); | |
| 367 return true; | |
| 368 end | |
| 369 | |
| 370 local node_config = node_obj.config; | |
| 371 local pubsub_form_data = { | |
| 372 ["pubsub#max_items"] = tostring(node_config["max_items"]); | |
| 373 ["pubsub#persist_items"] = node_config["persist_items"] | |
| 374 } | |
| 375 local reply = st.reply(stanza) | |
| 376 :tag("pubsub", { xmlns = xmlns_pubsub_owner }) | |
| 377 :tag("configure", { node = node }) | |
| 378 :add_child(node_config_form:form(pubsub_form_data)); | |
| 379 origin.send(reply); | |
| 380 return true; | |
| 381 end | |
| 382 | |
| 383 function handlers.owner_set_configure(origin, stanza, config, service) | |
| 384 local node = config.attr.node; | |
| 385 if not node then | |
| 386 origin.send(pubsub_error_reply(stanza, "nodeid-required")); | |
| 387 return true; | |
| 388 end | |
| 389 if not service:may(node, stanza.attr.from, "configure") then | |
| 390 origin.send(pubsub_error_reply(stanza, "forbidden")); | |
| 391 return true; | |
| 392 end | |
| 393 local config_form = config:get_child("x", "jabber:x:data"); | |
| 394 if not config_form then | |
| 395 origin.send(st.error_reply(stanza, "modify", "bad-request", "Missing dataform")); | |
| 396 return true; | |
| 397 end | |
| 398 local form_data, err = node_config_form:data(config_form); | |
| 399 if not form_data then | |
| 400 origin.send(st.error_reply(stanza, "modify", "bad-request", err)); | |
| 401 return true; | |
| 402 end | |
| 403 local new_config = { | |
| 404 ["max_items"] = tonumber(form_data["pubsub#max_items"]); | |
| 405 ["persist_items"] = form_data["pubsub#persist_items"]; | |
| 406 }; | |
| 407 local ok, err = service:set_node_config(node, stanza.attr.from, new_config); | |
| 408 if not ok then | |
| 409 origin.send(pubsub_error_reply(stanza, err)); | |
| 410 return true; | |
| 411 end | |
| 412 origin.send(st.reply(stanza)); | |
| 413 return true; | |
| 414 end | |
| 415 | |
| 416 function handlers.owner_get_default(origin, stanza, default, service) -- luacheck: ignore 212/default | |
| 417 local pubsub_form_data = { | |
| 418 ["pubsub#max_items"] = tostring(service.node_defaults["max_items"]); | |
| 419 ["pubsub#persist_items"] = service.node_defaults["persist_items"] | |
| 420 } | |
| 421 local reply = st.reply(stanza) | |
| 422 :tag("pubsub", { xmlns = xmlns_pubsub_owner }) | |
| 423 :tag("default") | |
| 424 :add_child(node_config_form:form(pubsub_form_data)); | |
| 425 origin.send(reply); | |
| 426 return true; | |
| 427 end | |
| 428 | |
| 429 local function create_encapsulating_item(id, payload) | |
| 430 local item = st.stanza("item", { id = id, xmlns = xmlns_pubsub }); | |
| 431 item:add_child(payload); | |
| 432 return item; | |
| 433 end | |
| 434 | |
| 435 local function archive_itemstore(archive, config, user, node) | |
| 436 module:log("debug", "Creation of itemstore for node %s with config %s", node, config); | |
| 437 local get_set = {}; | |
| 438 function get_set:items() -- luacheck: ignore 212/self | |
| 439 local data, err = archive:find(user, { | |
| 440 limit = tonumber(config["pubsub#max_items"]); | |
| 441 reverse = true; | |
| 442 }); | |
| 443 if not data then | |
| 444 module:log("error", "Unable to get items: %s", err); | |
| 445 return true; | |
| 446 end | |
| 447 module:log("debug", "Listed items %s", data); | |
| 448 return it.reverse(function() | |
| 449 local id, payload, when, publisher = data(); | |
| 450 if id == nil then | |
| 451 return; | |
| 452 end | |
| 453 local item = create_encapsulating_item(id, payload, publisher); | |
| 454 return id, item; | |
| 455 end); | |
| 456 end | |
| 457 function get_set:get(key) -- luacheck: ignore 212/self | |
| 458 local data, err = archive:find(user, { | |
| 459 key = key; | |
| 460 -- Get the last item with that key, if the archive doesn't deduplicate | |
| 461 reverse = true, | |
| 462 limit = 1; | |
| 463 }); | |
| 464 if not data then | |
| 465 module:log("error", "Unable to get item: %s", err); | |
| 466 return nil, err; | |
| 467 end | |
| 468 local id, payload, when, publisher = data(); | |
| 469 module:log("debug", "Get item %s (published at %s by %s)", id, when, publisher); | |
| 470 if id == nil then | |
| 471 return nil; | |
| 472 end | |
| 473 return create_encapsulating_item(id, payload, publisher); | |
| 474 end | |
| 475 function get_set:set(key, value) -- luacheck: ignore 212/self | |
| 476 local data, err; | |
| 477 if value ~= nil then | |
| 478 local publisher = value.attr.publisher; | |
| 479 local payload = value.tags[1]; | |
| 480 data, err = archive:append(user, key, payload, time_now(), publisher); | |
| 481 else | |
| 482 data, err = archive:delete(user, { key = key; }); | |
| 483 end | |
| 484 if not data then | |
| 485 module:log("error", "Unable to set item: %s", err); | |
| 486 return nil, err; | |
| 487 end | |
| 488 return data; | |
| 489 end | |
| 490 function get_set:clear() -- luacheck: ignore 212/self | |
| 491 return archive:delete(user); | |
| 492 end | |
| 493 return setmetatable(get_set, archive); | |
| 494 end | |
| 495 _M.archive_itemstore = archive_itemstore; | |
| 496 | |
| 497 return _M; |
