From 860e165c3b562637da22441ae9cd51877dd3dfb6 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 1 Aug 2018 19:08:09 +0100 Subject: mod_pep -> mod_pep_simple, mod_pep_plus -> mod_pep --- plugins/mod_pep.lua | 631 ++++++++++++++++++++++++++++----------------- plugins/mod_pep_plus.lua | 500 +---------------------------------- plugins/mod_pep_simple.lua | 333 ++++++++++++++++++++++++ 3 files changed, 733 insertions(+), 731 deletions(-) create mode 100644 plugins/mod_pep_simple.lua diff --git a/plugins/mod_pep.lua b/plugins/mod_pep.lua index f0b5d7ef..89c1f820 100644 --- a/plugins/mod_pep.lua +++ b/plugins/mod_pep.lua @@ -1,333 +1,498 @@ --- Prosody IM --- Copyright (C) 2008-2010 Matthew Wild --- Copyright (C) 2008-2010 Waqas Hussain --- --- This project is MIT/X11 licensed. Please see the --- COPYING file in the source package for more information. --- - - +local pubsub = require "util.pubsub"; local jid_bare = require "util.jid".bare; local jid_split = require "util.jid".split; +local jid_join = require "util.jid".join; +local set_new = require "util.set".new; local st = require "util.stanza"; -local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local pairs = pairs; -local next = next; -local type = type; local calculate_hash = require "util.caps".calculate_hash; -local core_post_stanza = prosody.core_post_stanza; -local bare_sessions = prosody.bare_sessions; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local cache = require "util.cache"; +local set = require "util.set"; 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"; --- Used as canonical 'empty table' -local NULL = {}; --- data[user_bare_jid][node] = item_stanza -local data = {}; ---- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true +local lib_pubsub = module:require "pubsub"; + +local empty_set = set_new(); + +local services = {}; local recipients = {}; --- hash_map[hash][subscribed_nodes] = true local hash_map = {}; -module.save = function() - return { data = data, recipients = recipients, hash_map = hash_map }; +local host = module.host; + +local node_config = module:open_store("pep", "map"); +local known_nodes = module:open_store("pep"); + +function module.save() + return { services = services }; +end + +function module.restore(data) + services = data.services; end -module.restore = function(state) - data = state.data or {}; - recipients = state.recipients or {}; - hash_map = state.hash_map or {}; + +function is_item_stanza(item) + return st.is_stanza(item) and item.attr.xmlns == xmlns_pubsub and item.name == "item"; end -local function subscription_presence(user_bare, recipient) +local function subscription_presence(username, recipient) + local user_bare = jid_join(username, host); local recipient_bare = jid_bare(recipient); - if (recipient_bare == user_bare) then return true end - local username, host = jid_split(user_bare); + if (recipient_bare == user_bare) then return true; end return is_contact_subscribed(username, host, recipient_bare); end -module:hook("pep-publish-item", function (event) - local session, bare, node, id, item = event.session, event.user, event.node, event.id, event.item; - item.attr.xmlns = nil; - local disable = #item.tags ~= 1 or #item.tags[1] == 0; - if #item.tags == 0 then item.name = "retract"; end - local stanza = st.message({from=bare, type='headline'}) - :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) - :tag('items', {node=node}) - :add_child(item) - :up() - :up(); - - -- store for the future - local user_data = data[bare]; - if disable then - if user_data then - user_data[node] = nil; - if not next(user_data) then data[bare] = nil; end +local function nodestore(username) + -- luacheck: ignore 212/self + local store = {}; + function store:get(node) + local data, err = node_config:get(username, node) + if data == true then + -- COMPAT Previously stored only a boolean representing 'persist_items' + data = { + name = node; + config = {}; + subscribers = {}; + affiliations = {}; + }; end - else - if not user_data then user_data = {}; data[bare] = user_data; end - user_data[node] = {id, item}; + return data, err; + end + function store:set(node, data) + if data then + -- Save the data without subscriptions + -- TODO Save explicit subscriptions maybe? + data = { + name = data.name; + config = data.config; + affiliations = data.affiliations; + subscribers = {}; + }; + end + return node_config:set(username, node, data); end + function store:users() + return pairs(known_nodes:get(username) or {}); + end + return store; +end - -- broadcast - for recipient, notify in pairs(recipients[bare] or NULL) do - if notify[node] then - stanza.attr.to = recipient; - core_post_stanza(session, stanza); +local function simple_itemstore(username) + return function (config, node) + if config["persist_items"] then + module:log("debug", "Creating new persistent item store for user %s, node %q", username, node); + local archive = module:open_store("pep_"..node, "archive"); + return lib_pubsub.archive_itemstore(archive, config, username, node, false); + else + module:log("debug", "Creating new ephemeral item store for user %s, node %q", username, node); + return cache.new(tonumber(config["max_items"])); end end -end); +end + +local function get_broadcaster(username) + local user_bare = jid_join(username, host); + local function simple_broadcast(kind, node, jids, item) + local message = st.message({ from = user_bare, type = "headline" }) + :tag("event", { xmlns = xmlns_pubsub_event }) + :tag(kind, { node = node }); + if item then + item = st.clone(item); + item.attr.xmlns = nil; -- Clear the pubsub namespace + message:add_child(item); + end + for jid in pairs(jids) do + module:log("debug", "Sending notification to %s from %s: %s", jid, user_bare, tostring(item)); + message.attr.to = jid; + module:send(message); + end + end + return simple_broadcast; +end + +function get_pep_service(username) + module:log("debug", "get_pep_service(%q)", username); + local user_bare = jid_join(username, host); + local service = services[username]; + if service then + return service; + end + service = pubsub.new({ + capabilities = { + none = { + create = false; + publish = false; + retract = false; + get_nodes = false; + + subscribe = false; + unsubscribe = false; + get_subscription = false; + get_subscriptions = false; + get_items = false; -local function publish_all(user, recipient, session) - local d = data[user]; - local notify = recipients[user] and recipients[user][recipient]; - if d and notify then - for node in pairs(notify) do - if d[node] then - local id, item = unpack(d[node]); - session.send(st.message({from=user, to=recipient, type='headline'}) - :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) - :tag('items', {node=node}) - :add_child(item) - :up() - :up()); + subscribe_other = false; + unsubscribe_other = false; + get_subscription_other = false; + get_subscriptions_other = false; + + be_subscribed = true; + be_unsubscribed = true; + + set_affiliation = false; + }; + subscriber = { + 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; + }; + }; + + node_defaults = { + ["max_items"] = 1; + ["persist_items"] = true; + }; + + autocreate_on_publish = true; + autocreate_on_subscribe = true; + + nodestore = nodestore(username); + itemstore = simple_itemstore(username); + broadcaster = get_broadcaster(username); + itemcheck = is_item_stanza; + get_affiliation = function (jid) + if jid_bare(jid) == user_bare then + return "owner"; + elseif subscription_presence(username, jid) then + return "subscriber"; end + end; + + normalize_jid = jid_bare; + }); + local nodes, err = known_nodes:get(username); + if nodes then + module:log("debug", "Restoring nodes for user %s", username); + for node in pairs(nodes) do + module:log("debug", "Restoring node %q", node); + service:create(node, true); end + elseif err then + module:log("error", "Could not restore nodes for %s: %s", username, err); + else + module:log("debug", "No known nodes"); end + services[username] = service; + module:add_item("pep-service", { service = service, jid = user_bare }); + return service; end +function handle_pubsub_iq(event) + local origin, stanza = event.origin, event.stanza; + local service_name = origin.username; + if stanza.attr.to ~= nil then + service_name = jid_split(stanza.attr.to); + end + local service = get_pep_service(service_name); + + return lib_pubsub.handle_pubsub_iq(event, service) +end + +module:hook("iq/bare/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); +module:hook("iq/bare/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); + +module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody")); +module:add_feature("http://jabber.org/protocol/pubsub#publish"); + local function get_caps_hash_from_presence(stanza, current) local t = stanza.attr.type; if not t then - for _, child in pairs(stanza.tags) do - if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then - local attr = child.attr; - if attr.hash then -- new caps - if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end - else -- legacy caps - if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end + local child = stanza:get_child("c", "http://jabber.org/protocol/caps"); + if child then + local attr = child.attr; + if attr.hash then -- new caps + if attr.hash == 'sha-1' and attr.node and attr.ver then + return attr.ver, attr.node.."#"..attr.ver; + end + else -- legacy caps + if attr.node and attr.ver then + return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end - return; -- bad caps format end end + return; -- no or bad caps elseif t == "unavailable" or t == "error" then return; end return current; -- no caps, could mean caps optimization, so return current end +local function resend_last_item(jid, node, service) + local ok, id, item = service:get_last_item(node, jid); + if not ok then return; end + if not id then return; end + service.config.broadcaster("items", node, { [jid] = true }, item); +end + +local function update_subscriptions(recipient, service_name, nodes) + nodes = nodes or empty_set; + + local service_recipients = recipients[service_name]; + if not service_recipients then + service_recipients = {}; + recipients[service_name] = service_recipients; + end + + local current = service_recipients[recipient]; + if not current or type(current) ~= "table" then + current = empty_set; + end + + if (current == empty_set or current:empty()) and (nodes == empty_set or nodes:empty()) then + return; + end + + local service = get_pep_service(service_name); + for node in current - nodes do + service:remove_subscription(node, recipient, recipient); + end + + for node in nodes - current do + service:add_subscription(node, recipient, recipient); + resend_last_item(recipient, node, service); + end + + if nodes == empty_set or nodes:empty() then + nodes = nil; + end + + service_recipients[recipient] = nodes; +end + module:hook("presence/bare", function(event) -- inbound presence to bare JID received local origin, stanza = event.origin, event.stanza; - local user = stanza.attr.to or (origin.username..'@'..origin.host); local t = stanza.attr.type; - local self = not stanza.attr.to; - - -- Only cache subscriptions if user is online - if not bare_sessions[user] then return; end + local is_self = not stanza.attr.to; + local username = jid_split(stanza.attr.to); + local user_bare = jid_bare(stanza.attr.to); + if is_self then + username = origin.username; + user_bare = jid_join(username, host); + end if not t then -- available presence - if self or subscription_presence(user, stanza.attr.from) then + if is_self or subscription_presence(username, stanza.attr.from) then local recipient = stanza.attr.from; - local current = recipients[user] and recipients[user][recipient]; - local hash = get_caps_hash_from_presence(stanza, current); + local current = recipients[username] and recipients[username][recipient]; + local hash, query_node = get_caps_hash_from_presence(stanza, current); if current == hash or (current and current == hash_map[hash]) then return; end if not hash then - if recipients[user] then recipients[user][recipient] = nil; end + update_subscriptions(recipient, username); else - recipients[user] = recipients[user] or {}; + recipients[username] = recipients[username] or {}; if hash_map[hash] then - recipients[user][recipient] = hash_map[hash]; - publish_all(user, recipient, origin); + update_subscriptions(recipient, username, hash_map[hash]); else - recipients[user][recipient] = hash; + recipients[username][recipient] = hash; local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; - if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then - -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute + if is_self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then + -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute origin.send( - st.stanza("iq", {from=user, to=stanza.attr.from, id="disco", type="get"}) - :query("http://jabber.org/protocol/disco#info") + st.stanza("iq", {from=user_bare, to=stanza.attr.from, id="disco", type="get"}) + :tag("query", {xmlns = "http://jabber.org/protocol/disco#info", node = query_node}) ); end end end end elseif t == "unavailable" then - if recipients[user] then recipients[user][stanza.attr.from] = nil; end - elseif not self and t == "unsubscribe" then + update_subscriptions(stanza.attr.from, username); + elseif not is_self and t == "unsubscribe" then local from = jid_bare(stanza.attr.from); - local subscriptions = recipients[user]; + local subscriptions = recipients[username]; if subscriptions then for subscriber in pairs(subscriptions) do if jid_bare(subscriber) == from then - recipients[user][subscriber] = nil; + update_subscriptions(subscriber, username); end end end end end, 10); -module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) - local session, stanza = event.origin, event.stanza; - local payload = stanza.tags[1]; - - if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then - payload = payload.tags[1]; -- - if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then - local node = payload.attr.node; - payload = payload.tags[1]; - if payload and payload.name == "item" then -- - local id = payload.attr.id or "1"; - payload.attr.id = id; - session.send(st.reply(stanza)); - module:fire_event("pep-publish-item", { - node = node, user = jid_bare(session.full_jid), actor = session.jid, - id = id, session = session, item = st.clone(payload); - }); - return true; - else - module:log("debug", "Payload is missing the ", node); - end - else - module:log("debug", "Unhandled payload: %s", payload and payload:top_tag() or "(no payload)"); +module:hook("iq-result/bare/disco", function(event) + local origin, stanza = event.origin, event.stanza; + local disco = stanza:get_child("query", "http://jabber.org/protocol/disco#info"); + if not disco then + return; + end + + -- Process disco response + local is_self = stanza.attr.to == nil; + local user_bare = jid_bare(stanza.attr.to); + local username = jid_split(stanza.attr.to); + if is_self then + username = origin.username; + user_bare = jid_join(username, host); + end + local contact = stanza.attr.from; + local current = recipients[username] and recipients[username][contact]; + if type(current) ~= "string" then return; end -- check if waiting for recipient's response + local ver = current; + if not string.find(current, "#") then + ver = calculate_hash(disco.tags); -- calculate hash + end + local notify = set_new(); + for _, feature in pairs(disco.tags) do + if feature.name == "feature" and feature.attr.var then + local nfeature = feature.attr.var:match("^(.*)%+notify$"); + if nfeature then notify:add(nfeature); end end - elseif stanza.attr.type == 'get' then - local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host; - if subscription_presence(user, stanza.attr.from) then - local user_data = data[user]; - local node, requested_id; - payload = payload.tags[1]; - if payload and payload.name == 'items' then - node = payload.attr.node; - local item = payload.tags[1]; - if item and item.name == "item" then - requested_id = item.attr.id; - end - end - if node and user_data and user_data[node] then -- Send the last item - local id, item = unpack(user_data[node]); - if not requested_id or id == requested_id then - local reply_stanza = st.reply(stanza) - :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) - :tag('items', {node=node}) - :add_child(item) - :up() - :up(); - session.send(reply_stanza); - return true; - else -- requested item doesn't exist - local reply_stanza = st.reply(stanza) - :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) - :tag('items', {node=node}) - :up(); - session.send(reply_stanza); - return true; + end + hash_map[ver] = notify; -- update hash map + if is_self then + -- Optimization: Fiddle with other local users + for jid, item in pairs(origin.roster) do -- for all interested contacts + if jid then + local contact_node, contact_host = jid_split(jid); + if contact_host == host and (item.subscription == "both" or item.subscription == "from") then + update_subscriptions(user_bare, contact_node, notify); end - elseif node then -- node doesn't exist - session.send(st.error_reply(stanza, 'cancel', 'item-not-found')); - module:log("debug", "Item '%s' not found", node) - return true; - else --invalid request - session.send(st.error_reply(stanza, 'modify', 'bad-request')); - module:log("debug", "Invalid request: %s", tostring(payload)); - return true; end - else --no presence subscription - session.send(st.error_reply(stanza, 'auth', 'not-authorized') - :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'})); - module:log("debug", "Unauthorized request: %s", tostring(payload)); - return true; end end + update_subscriptions(contact, username, notify); end); -module:hook("iq-result/bare/disco", function(event) - local session, stanza = event.origin, event.stanza; - if stanza.attr.type == "result" then - local disco = stanza.tags[1]; - if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then - -- Process disco response - local self = not stanza.attr.to; - local user = stanza.attr.to or (session.username..'@'..session.host); - local contact = stanza.attr.from; - local current = recipients[user] and recipients[user][contact]; - if type(current) ~= "string" then return; end -- check if waiting for recipient's response - local ver = current; - if not string.find(current, "#") then - ver = calculate_hash(disco.tags); -- calculate hash - end - local notify = {}; - for _, feature in pairs(disco.tags) do - if feature.name == "feature" and feature.attr.var then - local nfeature = feature.attr.var:match("^(.*)%+notify$"); - if nfeature then notify[nfeature] = true; end - end - end - hash_map[ver] = notify; -- update hash map - if self then - for jid, item in pairs(session.roster) do -- for all interested contacts - if item.subscription == "both" or item.subscription == "from" then - if not recipients[jid] then recipients[jid] = {}; end - recipients[jid][contact] = notify; - publish_all(jid, contact, session); - end - end - end - recipients[user][contact] = notify; -- set recipient's data to calculated data - -- send messages to recipient - publish_all(user, contact, session); - end +module:hook("account-disco-info-node", function(event) + local stanza, origin = event.stanza, event.origin; + local service_name = origin.username; + if stanza.attr.to ~= nil then + service_name = jid_split(stanza.attr.to); end + local service = get_pep_service(service_name); + return lib_pubsub.handle_disco_info_node(event, service); end); module:hook("account-disco-info", function(event) - local reply = event.reply; + local origin, reply = event.origin, event.reply; + reply:tag('identity', {category='pubsub', type='pep'}):up(); - reply:tag('feature', {var=xmlns_pubsub}):up(); - local features = { + + local username = jid_split(reply.attr.from) or origin.username; + local service = get_pep_service(username); + + local supported_features = lib_pubsub.get_feature_set(service) + set.new{ + -- Features not covered by the above "access-presence", - "auto-create", "auto-subscribe", "filtered-notifications", - "item-ids", "last-published", + "persistent-items", "presence-notifications", "presence-subscribe", - "publish", - "retract-items", - "retrieve-items", }; - for _, feature in ipairs(features) do + + for feature in supported_features do reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up(); end end); -module:hook("account-disco-items", function(event) - local reply = event.reply; - local bare = reply.attr.to; - local user_data = data[bare]; - - if user_data then - for node, _ in pairs(user_data) do - reply:tag('item', {jid=bare, node=node}):up(); - end +module:hook("account-disco-items-node", function(event) + local stanza, origin = event.stanza, event.origin; + local is_self = stanza.attr.to == nil; + local username = jid_split(stanza.attr.to); + if is_self then + username = origin.username; end + local service = get_pep_service(username); + return lib_pubsub.handle_disco_items_node(event, service); end); -module:hook("account-disco-info-node", function (event) - local stanza, node = event.stanza, event.node; - local user = stanza.attr.to; - local user_data = data[user]; - if user_data and user_data[node] then - event.exists = true; - event.reply:tag('identity', {category='pubsub', type='leaf'}):up(); +module:hook("account-disco-items", function(event) + local reply, stanza, origin = event.reply, event.stanza, event.origin; + + local is_self = stanza.attr.to == nil; + local user_bare = jid_bare(stanza.attr.to); + local username = jid_split(stanza.attr.to); + if is_self then + username = origin.username; + user_bare = jid_join(username, host); end -end); + local service = get_pep_service(username); + + local ok, ret = service:get_nodes(jid_bare(stanza.attr.from)); + if not ok then return; end -module:hook("resource-unbind", function (event) - local user_bare_jid = event.session.username.."@"..event.session.host; - if not bare_sessions[user_bare_jid] then -- User went offline - -- We don't need this info cached anymore, clear it. - recipients[user_bare_jid] = nil; + for node, node_obj in pairs(ret) do + reply:tag("item", { jid = user_bare, node = node, name = node_obj.config.name }):up(); end end); diff --git a/plugins/mod_pep_plus.lua b/plugins/mod_pep_plus.lua index 89c1f820..503315d2 100644 --- a/plugins/mod_pep_plus.lua +++ b/plugins/mod_pep_plus.lua @@ -1,498 +1,2 @@ -local pubsub = require "util.pubsub"; -local jid_bare = require "util.jid".bare; -local jid_split = require "util.jid".split; -local jid_join = require "util.jid".join; -local set_new = require "util.set".new; -local st = require "util.stanza"; -local calculate_hash = require "util.caps".calculate_hash; -local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local cache = require "util.cache"; -local set = require "util.set"; - -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 lib_pubsub = module:require "pubsub"; - -local empty_set = set_new(); - -local services = {}; -local recipients = {}; -local hash_map = {}; - -local host = module.host; - -local node_config = module:open_store("pep", "map"); -local known_nodes = module:open_store("pep"); - -function module.save() - return { services = services }; -end - -function module.restore(data) - services = data.services; -end - -function is_item_stanza(item) - return st.is_stanza(item) and item.attr.xmlns == xmlns_pubsub and item.name == "item"; -end - -local function subscription_presence(username, recipient) - local user_bare = jid_join(username, host); - local recipient_bare = jid_bare(recipient); - if (recipient_bare == user_bare) then return true; end - return is_contact_subscribed(username, host, recipient_bare); -end - -local function nodestore(username) - -- luacheck: ignore 212/self - local store = {}; - function store:get(node) - local data, err = node_config:get(username, node) - if data == true then - -- COMPAT Previously stored only a boolean representing 'persist_items' - data = { - name = node; - config = {}; - subscribers = {}; - affiliations = {}; - }; - end - return data, err; - end - function store:set(node, data) - if data then - -- Save the data without subscriptions - -- TODO Save explicit subscriptions maybe? - data = { - name = data.name; - config = data.config; - affiliations = data.affiliations; - subscribers = {}; - }; - end - return node_config:set(username, node, data); - end - function store:users() - return pairs(known_nodes:get(username) or {}); - end - return store; -end - -local function simple_itemstore(username) - return function (config, node) - if config["persist_items"] then - module:log("debug", "Creating new persistent item store for user %s, node %q", username, node); - local archive = module:open_store("pep_"..node, "archive"); - return lib_pubsub.archive_itemstore(archive, config, username, node, false); - else - module:log("debug", "Creating new ephemeral item store for user %s, node %q", username, node); - return cache.new(tonumber(config["max_items"])); - end - end -end - -local function get_broadcaster(username) - local user_bare = jid_join(username, host); - local function simple_broadcast(kind, node, jids, item) - local message = st.message({ from = user_bare, type = "headline" }) - :tag("event", { xmlns = xmlns_pubsub_event }) - :tag(kind, { node = node }); - if item then - item = st.clone(item); - item.attr.xmlns = nil; -- Clear the pubsub namespace - message:add_child(item); - end - for jid in pairs(jids) do - module:log("debug", "Sending notification to %s from %s: %s", jid, user_bare, tostring(item)); - message.attr.to = jid; - module:send(message); - end - end - return simple_broadcast; -end - -function get_pep_service(username) - module:log("debug", "get_pep_service(%q)", username); - local user_bare = jid_join(username, host); - local service = services[username]; - if service then - return service; - end - service = pubsub.new({ - capabilities = { - none = { - create = false; - publish = false; - retract = false; - get_nodes = false; - - subscribe = false; - unsubscribe = false; - get_subscription = false; - get_subscriptions = false; - get_items = false; - - subscribe_other = false; - unsubscribe_other = false; - get_subscription_other = false; - get_subscriptions_other = false; - - be_subscribed = true; - be_unsubscribed = true; - - set_affiliation = false; - }; - subscriber = { - 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; - }; - }; - - node_defaults = { - ["max_items"] = 1; - ["persist_items"] = true; - }; - - autocreate_on_publish = true; - autocreate_on_subscribe = true; - - nodestore = nodestore(username); - itemstore = simple_itemstore(username); - broadcaster = get_broadcaster(username); - itemcheck = is_item_stanza; - get_affiliation = function (jid) - if jid_bare(jid) == user_bare then - return "owner"; - elseif subscription_presence(username, jid) then - return "subscriber"; - end - end; - - normalize_jid = jid_bare; - }); - local nodes, err = known_nodes:get(username); - if nodes then - module:log("debug", "Restoring nodes for user %s", username); - for node in pairs(nodes) do - module:log("debug", "Restoring node %q", node); - service:create(node, true); - end - elseif err then - module:log("error", "Could not restore nodes for %s: %s", username, err); - else - module:log("debug", "No known nodes"); - end - services[username] = service; - module:add_item("pep-service", { service = service, jid = user_bare }); - return service; -end - -function handle_pubsub_iq(event) - local origin, stanza = event.origin, event.stanza; - local service_name = origin.username; - if stanza.attr.to ~= nil then - service_name = jid_split(stanza.attr.to); - end - local service = get_pep_service(service_name); - - return lib_pubsub.handle_pubsub_iq(event, service) -end - -module:hook("iq/bare/"..xmlns_pubsub..":pubsub", handle_pubsub_iq); -module:hook("iq/bare/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq); - -module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody")); -module:add_feature("http://jabber.org/protocol/pubsub#publish"); - -local function get_caps_hash_from_presence(stanza, current) - local t = stanza.attr.type; - if not t then - local child = stanza:get_child("c", "http://jabber.org/protocol/caps"); - if child then - local attr = child.attr; - if attr.hash then -- new caps - if attr.hash == 'sha-1' and attr.node and attr.ver then - return attr.ver, attr.node.."#"..attr.ver; - end - else -- legacy caps - if attr.node and attr.ver then - return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; - end - end - end - return; -- no or bad caps - elseif t == "unavailable" or t == "error" then - return; - end - return current; -- no caps, could mean caps optimization, so return current -end - -local function resend_last_item(jid, node, service) - local ok, id, item = service:get_last_item(node, jid); - if not ok then return; end - if not id then return; end - service.config.broadcaster("items", node, { [jid] = true }, item); -end - -local function update_subscriptions(recipient, service_name, nodes) - nodes = nodes or empty_set; - - local service_recipients = recipients[service_name]; - if not service_recipients then - service_recipients = {}; - recipients[service_name] = service_recipients; - end - - local current = service_recipients[recipient]; - if not current or type(current) ~= "table" then - current = empty_set; - end - - if (current == empty_set or current:empty()) and (nodes == empty_set or nodes:empty()) then - return; - end - - local service = get_pep_service(service_name); - for node in current - nodes do - service:remove_subscription(node, recipient, recipient); - end - - for node in nodes - current do - service:add_subscription(node, recipient, recipient); - resend_last_item(recipient, node, service); - end - - if nodes == empty_set or nodes:empty() then - nodes = nil; - end - - service_recipients[recipient] = nodes; -end - -module:hook("presence/bare", function(event) - -- inbound presence to bare JID received - local origin, stanza = event.origin, event.stanza; - local t = stanza.attr.type; - local is_self = not stanza.attr.to; - local username = jid_split(stanza.attr.to); - local user_bare = jid_bare(stanza.attr.to); - if is_self then - username = origin.username; - user_bare = jid_join(username, host); - end - - if not t then -- available presence - if is_self or subscription_presence(username, stanza.attr.from) then - local recipient = stanza.attr.from; - local current = recipients[username] and recipients[username][recipient]; - local hash, query_node = get_caps_hash_from_presence(stanza, current); - if current == hash or (current and current == hash_map[hash]) then return; end - if not hash then - update_subscriptions(recipient, username); - else - recipients[username] = recipients[username] or {}; - if hash_map[hash] then - update_subscriptions(recipient, username, hash_map[hash]); - else - recipients[username][recipient] = hash; - local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; - if is_self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then - -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute - origin.send( - st.stanza("iq", {from=user_bare, to=stanza.attr.from, id="disco", type="get"}) - :tag("query", {xmlns = "http://jabber.org/protocol/disco#info", node = query_node}) - ); - end - end - end - end - elseif t == "unavailable" then - update_subscriptions(stanza.attr.from, username); - elseif not is_self and t == "unsubscribe" then - local from = jid_bare(stanza.attr.from); - local subscriptions = recipients[username]; - if subscriptions then - for subscriber in pairs(subscriptions) do - if jid_bare(subscriber) == from then - update_subscriptions(subscriber, username); - end - end - end - end -end, 10); - -module:hook("iq-result/bare/disco", function(event) - local origin, stanza = event.origin, event.stanza; - local disco = stanza:get_child("query", "http://jabber.org/protocol/disco#info"); - if not disco then - return; - end - - -- Process disco response - local is_self = stanza.attr.to == nil; - local user_bare = jid_bare(stanza.attr.to); - local username = jid_split(stanza.attr.to); - if is_self then - username = origin.username; - user_bare = jid_join(username, host); - end - local contact = stanza.attr.from; - local current = recipients[username] and recipients[username][contact]; - if type(current) ~= "string" then return; end -- check if waiting for recipient's response - local ver = current; - if not string.find(current, "#") then - ver = calculate_hash(disco.tags); -- calculate hash - end - local notify = set_new(); - for _, feature in pairs(disco.tags) do - if feature.name == "feature" and feature.attr.var then - local nfeature = feature.attr.var:match("^(.*)%+notify$"); - if nfeature then notify:add(nfeature); end - end - end - hash_map[ver] = notify; -- update hash map - if is_self then - -- Optimization: Fiddle with other local users - for jid, item in pairs(origin.roster) do -- for all interested contacts - if jid then - local contact_node, contact_host = jid_split(jid); - if contact_host == host and (item.subscription == "both" or item.subscription == "from") then - update_subscriptions(user_bare, contact_node, notify); - end - end - end - end - update_subscriptions(contact, username, notify); -end); - -module:hook("account-disco-info-node", function(event) - local stanza, origin = event.stanza, event.origin; - local service_name = origin.username; - if stanza.attr.to ~= nil then - service_name = jid_split(stanza.attr.to); - end - local service = get_pep_service(service_name); - return lib_pubsub.handle_disco_info_node(event, service); -end); - -module:hook("account-disco-info", function(event) - local origin, reply = event.origin, event.reply; - - reply:tag('identity', {category='pubsub', type='pep'}):up(); - - local username = jid_split(reply.attr.from) or origin.username; - local service = get_pep_service(username); - - local supported_features = lib_pubsub.get_feature_set(service) + set.new{ - -- Features not covered by the above - "access-presence", - "auto-subscribe", - "filtered-notifications", - "last-published", - "persistent-items", - "presence-notifications", - "presence-subscribe", - }; - - for feature in supported_features do - reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up(); - end -end); - -module:hook("account-disco-items-node", function(event) - local stanza, origin = event.stanza, event.origin; - local is_self = stanza.attr.to == nil; - local username = jid_split(stanza.attr.to); - if is_self then - username = origin.username; - end - local service = get_pep_service(username); - return lib_pubsub.handle_disco_items_node(event, service); -end); - -module:hook("account-disco-items", function(event) - local reply, stanza, origin = event.reply, event.stanza, event.origin; - - local is_self = stanza.attr.to == nil; - local user_bare = jid_bare(stanza.attr.to); - local username = jid_split(stanza.attr.to); - if is_self then - username = origin.username; - user_bare = jid_join(username, host); - end - local service = get_pep_service(username); - - local ok, ret = service:get_nodes(jid_bare(stanza.attr.from)); - if not ok then return; end - - for node, node_obj in pairs(ret) do - reply:tag("item", { jid = user_bare, node = node, name = node_obj.config.name }):up(); - end -end); +module:log("error", "mod_pep_plus has been renamed to mod_pep, please update your config file. Auto-loading mod_pep..."); +module:depends("pep"); diff --git a/plugins/mod_pep_simple.lua b/plugins/mod_pep_simple.lua new file mode 100644 index 00000000..f0b5d7ef --- /dev/null +++ b/plugins/mod_pep_simple.lua @@ -0,0 +1,333 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- + + +local jid_bare = require "util.jid".bare; +local jid_split = require "util.jid".split; +local st = require "util.stanza"; +local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; +local pairs = pairs; +local next = next; +local type = type; +local calculate_hash = require "util.caps".calculate_hash; +local core_post_stanza = prosody.core_post_stanza; +local bare_sessions = prosody.bare_sessions; + +local xmlns_pubsub = "http://jabber.org/protocol/pubsub"; + +-- Used as canonical 'empty table' +local NULL = {}; +-- data[user_bare_jid][node] = item_stanza +local data = {}; +--- recipients[user_bare_jid][contact_full_jid][subscribed_node] = true +local recipients = {}; +-- hash_map[hash][subscribed_nodes] = true +local hash_map = {}; + +module.save = function() + return { data = data, recipients = recipients, hash_map = hash_map }; +end +module.restore = function(state) + data = state.data or {}; + recipients = state.recipients or {}; + hash_map = state.hash_map or {}; +end + +local function subscription_presence(user_bare, recipient) + local recipient_bare = jid_bare(recipient); + if (recipient_bare == user_bare) then return true end + local username, host = jid_split(user_bare); + return is_contact_subscribed(username, host, recipient_bare); +end + +module:hook("pep-publish-item", function (event) + local session, bare, node, id, item = event.session, event.user, event.node, event.id, event.item; + item.attr.xmlns = nil; + local disable = #item.tags ~= 1 or #item.tags[1] == 0; + if #item.tags == 0 then item.name = "retract"; end + local stanza = st.message({from=bare, type='headline'}) + :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) + :tag('items', {node=node}) + :add_child(item) + :up() + :up(); + + -- store for the future + local user_data = data[bare]; + if disable then + if user_data then + user_data[node] = nil; + if not next(user_data) then data[bare] = nil; end + end + else + if not user_data then user_data = {}; data[bare] = user_data; end + user_data[node] = {id, item}; + end + + -- broadcast + for recipient, notify in pairs(recipients[bare] or NULL) do + if notify[node] then + stanza.attr.to = recipient; + core_post_stanza(session, stanza); + end + end +end); + +local function publish_all(user, recipient, session) + local d = data[user]; + local notify = recipients[user] and recipients[user][recipient]; + if d and notify then + for node in pairs(notify) do + if d[node] then + local id, item = unpack(d[node]); + session.send(st.message({from=user, to=recipient, type='headline'}) + :tag('event', {xmlns='http://jabber.org/protocol/pubsub#event'}) + :tag('items', {node=node}) + :add_child(item) + :up() + :up()); + end + end + end +end + +local function get_caps_hash_from_presence(stanza, current) + local t = stanza.attr.type; + if not t then + for _, child in pairs(stanza.tags) do + if child.name == "c" and child.attr.xmlns == "http://jabber.org/protocol/caps" then + local attr = child.attr; + if attr.hash then -- new caps + if attr.hash == 'sha-1' and attr.node and attr.ver then return attr.ver, attr.node.."#"..attr.ver; end + else -- legacy caps + if attr.node and attr.ver then return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver; end + end + return; -- bad caps format + end + end + elseif t == "unavailable" or t == "error" then + return; + end + return current; -- no caps, could mean caps optimization, so return current +end + +module:hook("presence/bare", function(event) + -- inbound presence to bare JID received + local origin, stanza = event.origin, event.stanza; + local user = stanza.attr.to or (origin.username..'@'..origin.host); + local t = stanza.attr.type; + local self = not stanza.attr.to; + + -- Only cache subscriptions if user is online + if not bare_sessions[user] then return; end + + if not t then -- available presence + if self or subscription_presence(user, stanza.attr.from) then + local recipient = stanza.attr.from; + local current = recipients[user] and recipients[user][recipient]; + local hash = get_caps_hash_from_presence(stanza, current); + if current == hash or (current and current == hash_map[hash]) then return; end + if not hash then + if recipients[user] then recipients[user][recipient] = nil; end + else + recipients[user] = recipients[user] or {}; + if hash_map[hash] then + recipients[user][recipient] = hash_map[hash]; + publish_all(user, recipient, origin); + else + recipients[user][recipient] = hash; + local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host; + if self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then + -- COMPAT from ~= stanza.attr.to because OneTeam and Asterisk 1.8 can't deal with missing from attribute + origin.send( + st.stanza("iq", {from=user, to=stanza.attr.from, id="disco", type="get"}) + :query("http://jabber.org/protocol/disco#info") + ); + end + end + end + end + elseif t == "unavailable" then + if recipients[user] then recipients[user][stanza.attr.from] = nil; end + elseif not self and t == "unsubscribe" then + local from = jid_bare(stanza.attr.from); + local subscriptions = recipients[user]; + if subscriptions then + for subscriber in pairs(subscriptions) do + if jid_bare(subscriber) == from then + recipients[user][subscriber] = nil; + end + end + end + end +end, 10); + +module:hook("iq/bare/http://jabber.org/protocol/pubsub:pubsub", function(event) + local session, stanza = event.origin, event.stanza; + local payload = stanza.tags[1]; + + if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then + payload = payload.tags[1]; -- + if payload and (payload.name == 'publish' or payload.name == 'retract') and payload.attr.node then + local node = payload.attr.node; + payload = payload.tags[1]; + if payload and payload.name == "item" then -- + local id = payload.attr.id or "1"; + payload.attr.id = id; + session.send(st.reply(stanza)); + module:fire_event("pep-publish-item", { + node = node, user = jid_bare(session.full_jid), actor = session.jid, + id = id, session = session, item = st.clone(payload); + }); + return true; + else + module:log("debug", "Payload is missing the ", node); + end + else + module:log("debug", "Unhandled payload: %s", payload and payload:top_tag() or "(no payload)"); + end + elseif stanza.attr.type == 'get' then + local user = stanza.attr.to and jid_bare(stanza.attr.to) or session.username..'@'..session.host; + if subscription_presence(user, stanza.attr.from) then + local user_data = data[user]; + local node, requested_id; + payload = payload.tags[1]; + if payload and payload.name == 'items' then + node = payload.attr.node; + local item = payload.tags[1]; + if item and item.name == "item" then + requested_id = item.attr.id; + end + end + if node and user_data and user_data[node] then -- Send the last item + local id, item = unpack(user_data[node]); + if not requested_id or id == requested_id then + local reply_stanza = st.reply(stanza) + :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) + :tag('items', {node=node}) + :add_child(item) + :up() + :up(); + session.send(reply_stanza); + return true; + else -- requested item doesn't exist + local reply_stanza = st.reply(stanza) + :tag('pubsub', {xmlns='http://jabber.org/protocol/pubsub'}) + :tag('items', {node=node}) + :up(); + session.send(reply_stanza); + return true; + end + elseif node then -- node doesn't exist + session.send(st.error_reply(stanza, 'cancel', 'item-not-found')); + module:log("debug", "Item '%s' not found", node) + return true; + else --invalid request + session.send(st.error_reply(stanza, 'modify', 'bad-request')); + module:log("debug", "Invalid request: %s", tostring(payload)); + return true; + end + else --no presence subscription + session.send(st.error_reply(stanza, 'auth', 'not-authorized') + :tag('presence-subscription-required', {xmlns='http://jabber.org/protocol/pubsub#errors'})); + module:log("debug", "Unauthorized request: %s", tostring(payload)); + return true; + end + end +end); + +module:hook("iq-result/bare/disco", function(event) + local session, stanza = event.origin, event.stanza; + if stanza.attr.type == "result" then + local disco = stanza.tags[1]; + if disco and disco.name == "query" and disco.attr.xmlns == "http://jabber.org/protocol/disco#info" then + -- Process disco response + local self = not stanza.attr.to; + local user = stanza.attr.to or (session.username..'@'..session.host); + local contact = stanza.attr.from; + local current = recipients[user] and recipients[user][contact]; + if type(current) ~= "string" then return; end -- check if waiting for recipient's response + local ver = current; + if not string.find(current, "#") then + ver = calculate_hash(disco.tags); -- calculate hash + end + local notify = {}; + for _, feature in pairs(disco.tags) do + if feature.name == "feature" and feature.attr.var then + local nfeature = feature.attr.var:match("^(.*)%+notify$"); + if nfeature then notify[nfeature] = true; end + end + end + hash_map[ver] = notify; -- update hash map + if self then + for jid, item in pairs(session.roster) do -- for all interested contacts + if item.subscription == "both" or item.subscription == "from" then + if not recipients[jid] then recipients[jid] = {}; end + recipients[jid][contact] = notify; + publish_all(jid, contact, session); + end + end + end + recipients[user][contact] = notify; -- set recipient's data to calculated data + -- send messages to recipient + publish_all(user, contact, session); + end + end +end); + +module:hook("account-disco-info", function(event) + local reply = event.reply; + reply:tag('identity', {category='pubsub', type='pep'}):up(); + reply:tag('feature', {var=xmlns_pubsub}):up(); + local features = { + "access-presence", + "auto-create", + "auto-subscribe", + "filtered-notifications", + "item-ids", + "last-published", + "presence-notifications", + "presence-subscribe", + "publish", + "retract-items", + "retrieve-items", + }; + for _, feature in ipairs(features) do + reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up(); + end +end); + +module:hook("account-disco-items", function(event) + local reply = event.reply; + local bare = reply.attr.to; + local user_data = data[bare]; + + if user_data then + for node, _ in pairs(user_data) do + reply:tag('item', {jid=bare, node=node}):up(); + end + end +end); + +module:hook("account-disco-info-node", function (event) + local stanza, node = event.stanza, event.node; + local user = stanza.attr.to; + local user_data = data[user]; + if user_data and user_data[node] then + event.exists = true; + event.reply:tag('identity', {category='pubsub', type='leaf'}):up(); + end +end); + +module:hook("resource-unbind", function (event) + local user_bare_jid = event.session.username.."@"..event.session.host; + if not bare_sessions[user_bare_jid] then -- User went offline + -- We don't need this info cached anymore, clear it. + recipients[user_bare_jid] = nil; + end +end); -- cgit v1.2.3