mod_pep -> mod_pep_simple, mod_pep_plus -> mod_pep

This commit is contained in:
Matthew Wild 2018-08-01 19:08:09 +01:00
parent 5d94b20a62
commit 860e165c3b
3 changed files with 741 additions and 739 deletions

View file

@ -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 };
end
module.restore = function(state)
data = state.data or {};
recipients = state.recipients or {};
hash_map = state.hash_map or {};
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
local function subscription_presence(user_bare, recipient)
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
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
-- broadcast
for recipient, notify in pairs(recipients[bare] or NULL) do
if notify[node] then
stanza.attr.to = recipient;
core_post_stanza(session, stanza);
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
end);
function store:users()
return pairs(known_nodes:get(username) or {});
end
return store;
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
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
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];
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
if stanza.attr.type == 'set' and (not stanza.attr.to or jid_bare(stanza.attr.from) == stanza.attr.to) then
payload = payload.tags[1]; -- <publish node='http://jabber.org/protocol/tune'>
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 -- <item>
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 <item>", 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;
-- 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("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-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 = event.reply;
local bare = reply.attr.to;
local user_data = data[bare];
local reply, stanza, origin = event.reply, event.stanza, event.origin;
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;
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);

View file

@ -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");

333
plugins/mod_pep_simple.lua Normal file
View file

@ -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]; -- <publish node='http://jabber.org/protocol/tune'>
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 -- <item>
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 <item>", 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);