| 1 | local pubsub = require "util.pubsub"; |
| 2 | local st = require "util.stanza"; |
| 3 | local jid_bare = require "util.jid".bare; |
| 4 | local uuid_generate = require "util.uuid".generate; |
| 5 | |
| 6 | require "core.modulemanager".load(module.host, "iq"); |
| 7 | |
| 8 | local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; |
| 9 | local xmlns_pubsub_errors = "http://jabber.org/protocol/pubsub#errors"; |
| 10 | local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event"; |
| 11 | |
| 12 | local autocreate_on_publish = module:get_option_boolean("autocreate_on_publish", false); |
| 13 | local autocreate_on_subscribe = module:get_option_boolean("autocreate_on_subscribe", false); |
| 14 | |
| 15 | local service; |
| 16 | |
| 17 | local handlers = {}; |
| 18 | |
| 19 | function handle_pubsub_iq(event) |
| 20 | local origin, stanza = event.origin, event.stanza; |
| 21 | local pubsub = stanza.tags[1]; |
| 22 | local action = pubsub.tags[1]; |
| 23 | local handler = handlers[stanza.attr.type.."_"..action.name]; |
| 24 | if handler then |
| 25 | handler(origin, stanza, action); |
| 26 | return true; |
| 27 | end |
| 28 | end |
| 29 | |
| 30 | local pubsub_errors = { |
| 31 | ["conflict"] = { "cancel", "conflict" }; |
| 32 | ["invalid-jid"] = { "modify", "bad-request", nil, "invalid-jid" }; |
| 33 | ["item-not-found"] = { "cancel", "item-not-found" }; |
| 34 | ["not-subscribed"] = { "modify", "unexpected-request", nil, "not-subscribed" }; |
| 35 | ["forbidden"] = { "cancel", "forbidden" }; |
| 36 | }; |
| 37 | function pubsub_error_reply(stanza, error) |
| 38 | local e = pubsub_errors[error]; |
| 39 | local reply = st.error_reply(stanza, unpack(e, 1, 3)); |
| 40 | if e[4] then |
| 41 | reply:tag(e[4], { xmlns = xmlns_pubsub_errors }):up(); |
| 42 | end |
| 43 | return reply; |
| 44 | end |
| 45 | |
| 46 | function handlers.get_items(origin, stanza, items) |
| 47 | local node = items.attr.node; |
| 48 | local item = items:get_child("item"); |
| 49 | local id = item and item.attr.id; |
| 50 | |
| 51 | local ok, results = service:get_items(node, stanza.attr.from, id); |
| 52 | if not ok then |
| 53 | return origin.send(pubsub_error_reply(stanza, results)); |
| 54 | end |
| 55 | |
| 56 | local data = st.stanza("items", { node = node }); |
| 57 | for _, entry in pairs(results) do |
| 58 | data:add_child(entry); |
| 59 | end |
| 60 | if data then |
| 61 | reply = st.reply(stanza) |
| 62 | :tag("pubsub", { xmlns = xmlns_pubsub }) |
| 63 | :add_child(data); |
| 64 | else |
| 65 | reply = pubsub_error_reply(stanza, "item-not-found"); |
| 66 | end |
| 67 | return origin.send(reply); |
| 68 | end |
| 69 | |
| 70 | function handlers.get_subscriptions(origin, stanza, subscriptions) |
| 71 | local node = subscriptions.attr.node; |
| 72 | local ok, ret = service:get_subscriptions(node, stanza.attr.from, stanza.attr.from); |
| 73 | if not ok then |
| 74 | return origin.send(pubsub_error_reply(stanza, ret)); |
| 75 | end |
| 76 | local reply = st.reply(stanza) |
| 77 | :tag("pubsub", { xmlns = xmlns_pubsub }) |
| 78 | :tag("subscriptions"); |
| 79 | for _, sub in ipairs(ret) do |
| 80 | reply:tag("subscription", { node = sub.node, jid = sub.jid, subscription = 'subscribed' }):up(); |
| 81 | end |
| 82 | return origin.send(reply); |
| 83 | end |
| 84 | |
| 85 | function handlers.set_create(origin, stanza, create) |
| 86 | local node = create.attr.node; |
| 87 | local ok, ret, reply; |
| 88 | if node then |
| 89 | ok, ret = service:create(node, stanza.attr.from); |
| 90 | if ok then |
| 91 | reply = st.reply(stanza); |
| 92 | else |
| 93 | reply = pubsub_error_reply(stanza, ret); |
| 94 | end |
| 95 | else |
| 96 | repeat |
| 97 | node = uuid_generate(); |
| 98 | ok, ret = service:create(node, stanza.attr.from); |
| 99 | until ok or ret ~= "conflict"; |
| 100 | if ok then |
| 101 | reply = st.reply(stanza) |
| 102 | :tag("pubsub", { xmlns = xmlns_pubsub }) |
| 103 | :tag("create", { node = node }); |
| 104 | else |
| 105 | reply = pubsub_error_reply(stanza, ret); |
| 106 | end |
| 107 | end |
| 108 | return origin.send(reply); |
| 109 | end |
| 110 | |
| 111 | function handlers.set_subscribe(origin, stanza, subscribe) |
| 112 | local node, jid = subscribe.attr.node, subscribe.attr.jid; |
| 113 | if jid_bare(jid) ~= jid_bare(stanza.attr.from) then |
| 114 | return origin.send(pubsub_error_reply(stanza, "invalid-jid")); |
| 115 | end |
| 116 | local ok, ret = service:add_subscription(node, stanza.attr.from, jid); |
| 117 | local reply; |
| 118 | if ok then |
| 119 | reply = st.reply(stanza) |
| 120 | :tag("pubsub", { xmlns = xmlns_pubsub }) |
| 121 | :tag("subscription", { |
| 122 | node = node, |
| 123 | jid = jid, |
| 124 | subscription = "subscribed" |
| 125 | }); |
| 126 | else |
| 127 | reply = pubsub_error_reply(stanza, ret); |
| 128 | end |
| 129 | return origin.send(reply); |
| 130 | end |
| 131 | |
| 132 | function handlers.set_unsubscribe(origin, stanza, unsubscribe) |
| 133 | local node, jid = unsubscribe.attr.node, unsubscribe.attr.jid; |
| 134 | if jid_bare(jid) ~= jid_bare(stanza.attr.from) then |
| 135 | return origin.send(pubsub_error_reply(stanza, "invalid-jid")); |
| 136 | end |
| 137 | local ok, ret = service:remove_subscription(node, stanza.attr.from, jid); |
| 138 | local reply; |
| 139 | if ok then |
| 140 | reply = st.reply(stanza); |
| 141 | else |
| 142 | reply = pubsub_error_reply(stanza, ret); |
| 143 | end |
| 144 | return origin.send(reply); |
| 145 | end |
| 146 | |
| 147 | function handlers.set_publish(origin, stanza, publish) |
| 148 | local node = publish.attr.node; |
| 149 | local item = publish:get_child("item"); |
| 150 | local id = (item and item.attr.id) or uuid_generate(); |
| 151 | local ok, ret = service:publish(node, stanza.attr.from, id, item); |
| 152 | local reply; |
| 153 | if ok then |
| 154 | reply = st.reply(stanza) |
| 155 | :tag("pubsub", { xmlns = xmlns_pubsub }) |
| 156 | :tag("publish", { node = node }) |
| 157 | :tag("item", { id = id }); |
| 158 | else |
| 159 | reply = pubsub_error_reply(stanza, ret); |
| 160 | end |
| 161 | return origin.send(reply); |
| 162 | end |
| 163 | |
| 164 | function handlers.set_retract(origin, stanza, retract) |
| 165 | local node, notify = retract.attr.node, retract.attr.notify; |
| 166 | notify = (notify == "1") or (notify == "true"); |
| 167 | local item = retract:get_child("item"); |
| 168 | local id = item and item.attr.id |
| 169 | local reply, notifier; |
| 170 | if notify then |
| 171 | notifier = st.stanza("retract", { id = id }); |
| 172 | end |
| 173 | local ok, ret = service:retract(node, stanza.attr.from, id, notifier); |
| 174 | if ok then |
| 175 | reply = st.reply(stanza); |
| 176 | else |
| 177 | reply = pubsub_error_reply(stanza, ret); |
| 178 | end |
| 179 | return origin.send(reply); |
| 180 | end |
| 181 | |
| 182 | function simple_broadcast(node, jids, item) |
| 183 | item = st.clone(item); |
| 184 | item.attr.xmlns = nil; -- Clear the pubsub namespace |
| 185 | local message = st.message({ from = module.host, type = "headline" }) |
| 186 | :tag("event", { xmlns = xmlns_pubsub_event }) |
| 187 | :tag("items", { node = node }) |
| 188 | :add_child(item); |
| 189 | for jid in pairs(jids) do |
| 190 | module:log("debug", "Sending notification to %s", jid); |
| 191 | message.attr.to = jid; |
| 192 | core_post_stanza(hosts[module.host], message); |
| 193 | end |
| 194 | end |
| 195 | |
| 196 | module:hook("iq/host/http://jabber.org/protocol/pubsub:pubsub", handle_pubsub_iq); |
| 197 | |
| 198 | local disco_info; |
| 199 | |
| 200 | local feature_map = { |
| 201 | create = { "create-nodes", autocreate_on_publish and "instant-nodes", "item-ids" }; |
| 202 | retract = { "delete-items", "retract-items" }; |
| 203 | publish = { "publish" }; |
| 204 | get_items = { "retrieve-items" }; |
| 205 | add_subscription = { "subscribe" }; |
| 206 | get_subscriptions = { "retrieve-subscriptions" }; |
| 207 | }; |
| 208 | |
| 209 | local function add_disco_features_from_service(disco, service) |
| 210 | for method, features in pairs(feature_map) do |
| 211 | if service[method] then |
| 212 | for _, feature in ipairs(features) do |
| 213 | if feature then |
| 214 | disco:tag("feature", { var = xmlns_pubsub.."#"..feature }):up(); |
| 215 | end |
| 216 | end |
| 217 | end |
| 218 | end |
| 219 | for affiliation in pairs(service.config.capabilities) do |
| 220 | if affiliation ~= "none" and affiliation ~= "owner" then |
| 221 | disco:tag("feature", { var = xmlns_pubsub.."#"..affiliation.."-affiliation" }):up(); |
| 222 | end |
| 223 | end |
| 224 | end |
| 225 | |
| 226 | local function build_disco_info(service) |
| 227 | local disco_info = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" }) |
| 228 | :tag("identity", { category = "pubsub", type = "service" }):up() |
| 229 | :tag("feature", { var = "http://jabber.org/protocol/pubsub" }):up(); |
| 230 | add_disco_features_from_service(disco_info, service); |
| 231 | return disco_info; |
| 232 | end |
| 233 | |
| 234 | module:hook("iq-get/host/http://jabber.org/protocol/disco#info:query", function (event) |
| 235 | local origin, stanza = event.origin, event.stanza; |
| 236 | local node = stanza.tags[1].attr.node; |
| 237 | if not node then |
| 238 | return origin.send(st.reply(stanza):add_child(disco_info)); |
| 239 | else |
| 240 | local ok, ret = service:get_nodes(stanza.attr.from); |
| 241 | if ok and not ret[node] then |
| 242 | ok, ret = false, "item-not-found"; |
| 243 | end |
| 244 | if not ok then |
| 245 | return origin.send(pubsub_error_reply(stanza, ret)); |
| 246 | end |
| 247 | local reply = st.reply(stanza) |
| 248 | :tag("query", { xmlns = "http://jabber.org/protocol/disco#info", node = node }) |
| 249 | :tag("identity", { category = "pubsub", type = "leaf" }); |
| 250 | return origin.send(reply); |
| 251 | end |
| 252 | end); |
| 253 | |
| 254 | local function handle_disco_items_on_node(event) |
| 255 | local stanza, origin = event.stanza, event.origin; |
| 256 | local query = stanza.tags[1]; |
| 257 | local node = query.attr.node; |
| 258 | local ok, ret = service:get_items(node, stanza.attr.from); |
| 259 | if not ok then |
| 260 | return origin.send(pubsub_error_reply(stanza, ret)); |
| 261 | end |
| 262 | |
| 263 | local reply = st.reply(stanza) |
| 264 | :tag("query", { xmlns = "http://jabber.org/protocol/disco#items", node = node }); |
| 265 | |
| 266 | for id, item in pairs(ret) do |
| 267 | reply:tag("item", { jid = module.host, name = id }):up(); |
| 268 | end |
| 269 | |
| 270 | return origin.send(reply); |
| 271 | end |
| 272 | |
| 273 | |
| 274 | module:hook("iq-get/host/http://jabber.org/protocol/disco#items:query", function (event) |
| 275 | if event.stanza.tags[1].attr.node then |
| 276 | return handle_disco_items_on_node(event); |
| 277 | end |
| 278 | local ok, ret = service:get_nodes(event.stanza.attr.from); |
| 279 | if not ok then |
| 280 | event.origin.send(pubsub_error_reply(stanza, ret)); |
| 281 | else |
| 282 | local reply = st.reply(event.stanza) |
| 283 | :tag("query", { xmlns = "http://jabber.org/protocol/disco#items" }); |
| 284 | for node, node_obj in pairs(ret) do |
| 285 | reply:tag("item", { jid = module.host, node = node, name = node_obj.config.name }):up(); |
| 286 | end |
| 287 | event.origin.send(reply); |
| 288 | end |
| 289 | return true; |
| 290 | end); |
| 291 | |
| 292 | local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); |
| 293 | local function get_affiliation(jid) |
| 294 | local bare_jid = jid_bare(jid); |
| 295 | if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then |
| 296 | return admin_aff; |
| 297 | end |
| 298 | end |
| 299 | |
| 300 | function set_service(new_service) |
| 301 | service = new_service; |
| 302 | module.environment.service = service; |
| 303 | disco_info = build_disco_info(service); |
| 304 | end |
| 305 | |
| 306 | function module.save() |
| 307 | return { service = service }; |
| 308 | end |
| 309 | |
| 310 | function module.restore(data) |
| 311 | set_service(data.service); |
| 312 | end |
| 313 | |
| 314 | set_service(pubsub.new({ |
| 315 | capabilities = { |
| 316 | none = { |
| 317 | create = false; |
| 318 | publish = false; |
| 319 | retract = false; |
| 320 | get_nodes = true; |
| 321 | |
| 322 | subscribe = true; |
| 323 | unsubscribe = true; |
| 324 | get_subscription = true; |
| 325 | get_subscriptions = true; |
| 326 | get_items = true; |
| 327 | |
| 328 | subscribe_other = false; |
| 329 | unsubscribe_other = false; |
| 330 | get_subscription_other = false; |
| 331 | get_subscriptions_other = false; |
| 332 | |
| 333 | be_subscribed = true; |
| 334 | be_unsubscribed = true; |
| 335 | |
| 336 | set_affiliation = false; |
| 337 | }; |
| 338 | owner = { |
| 339 | create = true; |
| 340 | publish = true; |
| 341 | retract = true; |
| 342 | get_nodes = true; |
| 343 | |
| 344 | subscribe = true; |
| 345 | unsubscribe = true; |
| 346 | get_subscription = true; |
| 347 | get_subscriptions = true; |
| 348 | get_items = true; |
| 349 | |
| 350 | |
| 351 | subscribe_other = true; |
| 352 | unsubscribe_other = true; |
| 353 | get_subscription_other = true; |
| 354 | get_subscriptions_other = true; |
| 355 | |
| 356 | be_subscribed = true; |
| 357 | be_unsubscribed = true; |
| 358 | |
| 359 | set_affiliation = true; |
| 360 | }; |
| 361 | }; |
| 362 | |
| 363 | autocreate_on_publish = autocreate_on_publish; |
| 364 | autocreate_on_subscribe = autocreate_on_subscribe; |
| 365 | |
| 366 | broadcaster = simple_broadcast; |
| 367 | get_affiliation = get_affiliation; |
| 368 | |
| 369 | normalize_jid = jid_bare; |
| 370 | })); |