mod_pep_plus/mod_pep_plus.lua
changeset 2857 a844d1535c4d
parent 2856 668447566edf
child 2858 687b19cad4f5
equal deleted inserted replaced
2856:668447566edf 2857:a844d1535c4d
     1 local pubsub = module:require "util_pubsub";
       
     2 local jid_bare = require "util.jid".bare;
       
     3 local jid_split = require "util.jid".split;
       
     4 local jid_join = require "util.jid".join;
       
     5 local set_new = require "util.set".new;
       
     6 local st = require "util.stanza";
       
     7 local calculate_hash = require "util.caps".calculate_hash;
       
     8 local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed;
       
     9 local cache = require "util.cache";
       
    10 local set = require "util.set";
       
    11 
       
    12 local xmlns_pubsub = "http://jabber.org/protocol/pubsub";
       
    13 local xmlns_pubsub_event = "http://jabber.org/protocol/pubsub#event";
       
    14 local xmlns_pubsub_owner = "http://jabber.org/protocol/pubsub#owner";
       
    15 
       
    16 local lib_pubsub = module:require "pubsub";
       
    17 
       
    18 local empty_set = set_new();
       
    19 
       
    20 local services = {};
       
    21 local recipients = {};
       
    22 local hash_map = {};
       
    23 
       
    24 local host = module.host;
       
    25 
       
    26 local known_nodes_map = module:open_store("pep", "map");
       
    27 local known_nodes = module:open_store("pep");
       
    28 
       
    29 function module.save()
       
    30 	return { services = services };
       
    31 end
       
    32 
       
    33 function module.restore(data)
       
    34 	services = data.services;
       
    35 end
       
    36 
       
    37 local function subscription_presence(username, recipient)
       
    38 	local user_bare = jid_join(username, host);
       
    39 	local recipient_bare = jid_bare(recipient);
       
    40 	if (recipient_bare == user_bare) then return true; end
       
    41 	return is_contact_subscribed(username, host, recipient_bare);
       
    42 end
       
    43 
       
    44 local function simple_itemstore(username)
       
    45 	return function (config, node)
       
    46 		if config["persist_items"] then
       
    47 			module:log("debug", "Creating new persistent item store for user %s, node %q", username, node);
       
    48 			known_nodes_map:set(username, node, true);
       
    49 			local archive = module:open_store("pep_"..node, "archive");
       
    50 			return lib_pubsub.archive_itemstore(archive, config, username, node, false);
       
    51 		else
       
    52 			module:log("debug", "Creating new ephemeral item store for user %s, node %q", username, node);
       
    53 			known_nodes_map:set(username, node, nil);
       
    54 			return cache.new(tonumber(config["max_items"]));
       
    55 		end
       
    56 	end
       
    57 end
       
    58 
       
    59 local function get_broadcaster(username)
       
    60 	local user_bare = jid_join(username, host);
       
    61 	local function simple_broadcast(kind, node, jids, item)
       
    62 		if item then
       
    63 			item = st.clone(item);
       
    64 			item.attr.xmlns = nil; -- Clear the pubsub namespace
       
    65 		end
       
    66 		local message = st.message({ from = user_bare, type = "headline" })
       
    67 			:tag("event", { xmlns = xmlns_pubsub_event })
       
    68 				:tag(kind, { node = node })
       
    69 					:add_child(item);
       
    70 		for jid in pairs(jids) do
       
    71 			module:log("debug", "Sending notification to %s from %s: %s", jid, user_bare, tostring(item));
       
    72 			message.attr.to = jid;
       
    73 			module:send(message);
       
    74 		end
       
    75 	end
       
    76 	return simple_broadcast;
       
    77 end
       
    78 
       
    79 function get_pep_service(username)
       
    80 	module:log("debug", "get_pep_service(%q)", username);
       
    81 	local user_bare = jid_join(username, host);
       
    82 	local service = services[username];
       
    83 	if service then
       
    84 		return service;
       
    85 	end
       
    86 	service = pubsub.new({
       
    87 		capabilities = {
       
    88 			none = {
       
    89 				create = false;
       
    90 				publish = false;
       
    91 				retract = false;
       
    92 				get_nodes = false;
       
    93 
       
    94 				subscribe = false;
       
    95 				unsubscribe = false;
       
    96 				get_subscription = false;
       
    97 				get_subscriptions = false;
       
    98 				get_items = false;
       
    99 
       
   100 				subscribe_other = false;
       
   101 				unsubscribe_other = false;
       
   102 				get_subscription_other = false;
       
   103 				get_subscriptions_other = false;
       
   104 
       
   105 				be_subscribed = true;
       
   106 				be_unsubscribed = true;
       
   107 
       
   108 				set_affiliation = false;
       
   109 			};
       
   110 			subscriber = {
       
   111 				create = false;
       
   112 				publish = false;
       
   113 				retract = false;
       
   114 				get_nodes = true;
       
   115 
       
   116 				subscribe = true;
       
   117 				unsubscribe = true;
       
   118 				get_subscription = true;
       
   119 				get_subscriptions = true;
       
   120 				get_items = true;
       
   121 
       
   122 				subscribe_other = false;
       
   123 				unsubscribe_other = false;
       
   124 				get_subscription_other = false;
       
   125 				get_subscriptions_other = false;
       
   126 
       
   127 				be_subscribed = true;
       
   128 				be_unsubscribed = true;
       
   129 
       
   130 				set_affiliation = false;
       
   131 			};
       
   132 			publisher = {
       
   133 				create = false;
       
   134 				publish = true;
       
   135 				retract = true;
       
   136 				get_nodes = true;
       
   137 
       
   138 				subscribe = true;
       
   139 				unsubscribe = true;
       
   140 				get_subscription = true;
       
   141 				get_subscriptions = true;
       
   142 				get_items = true;
       
   143 
       
   144 				subscribe_other = false;
       
   145 				unsubscribe_other = false;
       
   146 				get_subscription_other = false;
       
   147 				get_subscriptions_other = false;
       
   148 
       
   149 				be_subscribed = true;
       
   150 				be_unsubscribed = true;
       
   151 
       
   152 				set_affiliation = false;
       
   153 			};
       
   154 			owner = {
       
   155 				create = true;
       
   156 				publish = true;
       
   157 				retract = true;
       
   158 				delete = true;
       
   159 				get_nodes = true;
       
   160 				configure = true;
       
   161 
       
   162 				subscribe = true;
       
   163 				unsubscribe = true;
       
   164 				get_subscription = true;
       
   165 				get_subscriptions = true;
       
   166 				get_items = true;
       
   167 
       
   168 
       
   169 				subscribe_other = true;
       
   170 				unsubscribe_other = true;
       
   171 				get_subscription_other = true;
       
   172 				get_subscriptions_other = true;
       
   173 
       
   174 				be_subscribed = true;
       
   175 				be_unsubscribed = true;
       
   176 
       
   177 				set_affiliation = true;
       
   178 			};
       
   179 		};
       
   180 
       
   181 		node_defaults = {
       
   182 			["max_items"] = 1;
       
   183 			["persist_items"] = true;
       
   184 		};
       
   185 
       
   186 		autocreate_on_publish = true;
       
   187 		autocreate_on_subscribe = true;
       
   188 
       
   189 		itemstore = simple_itemstore(username);
       
   190 		broadcaster = get_broadcaster(username);
       
   191 		get_affiliation = function (jid)
       
   192 			if jid_bare(jid) == user_bare then
       
   193 				return "owner";
       
   194 			elseif subscription_presence(username, jid) then
       
   195 				return "subscriber";
       
   196 			end
       
   197 		end;
       
   198 
       
   199 		normalize_jid = jid_bare;
       
   200 	});
       
   201 	local nodes, err = known_nodes:get(username);
       
   202 	if nodes then
       
   203 		module:log("debug", "Restoring nodes for user %s", username);
       
   204 		for node in pairs(nodes) do
       
   205 			module:log("debug", "Restoring node %q", node);
       
   206 			service:create(node, true);
       
   207 		end
       
   208 	elseif err then
       
   209 		module:log("error", "Could not restore nodes for %s: %s", username, err);
       
   210 	else
       
   211 		module:log("debug", "No known nodes");
       
   212 	end
       
   213 	services[username] = service;
       
   214 	module:add_item("pep-service", { service = service, jid = user_bare });
       
   215 	return service;
       
   216 end
       
   217 
       
   218 function handle_pubsub_iq(event)
       
   219 	local origin, stanza = event.origin, event.stanza;
       
   220 	local service_name = origin.username;
       
   221 	if stanza.attr.to ~= nil then
       
   222 		service_name = jid_split(stanza.attr.to);
       
   223 	end
       
   224 	local service = get_pep_service(service_name);
       
   225 
       
   226 	return lib_pubsub.handle_pubsub_iq(event, service)
       
   227 end
       
   228 
       
   229 module:hook("iq/bare/"..xmlns_pubsub..":pubsub", handle_pubsub_iq);
       
   230 module:hook("iq/bare/"..xmlns_pubsub_owner..":pubsub", handle_pubsub_iq);
       
   231 
       
   232 module:add_identity("pubsub", "pep", module:get_option_string("name", "Prosody"));
       
   233 module:add_feature("http://jabber.org/protocol/pubsub#publish");
       
   234 
       
   235 local function get_caps_hash_from_presence(stanza, current)
       
   236 	local t = stanza.attr.type;
       
   237 	if not t then
       
   238 		local child = stanza:get_child("c", "http://jabber.org/protocol/caps");
       
   239 		if child then
       
   240 			local attr = child.attr;
       
   241 			if attr.hash then -- new caps
       
   242 				if attr.hash == 'sha-1' and attr.node and attr.ver then
       
   243 					return attr.ver, attr.node.."#"..attr.ver;
       
   244 				end
       
   245 			else -- legacy caps
       
   246 				if attr.node and attr.ver then
       
   247 					return attr.node.."#"..attr.ver.."#"..(attr.ext or ""), attr.node.."#"..attr.ver;
       
   248 				end
       
   249 			end
       
   250 		end
       
   251 		return; -- no or bad caps
       
   252 	elseif t == "unavailable" or t == "error" then
       
   253 		return;
       
   254 	end
       
   255 	return current; -- no caps, could mean caps optimization, so return current
       
   256 end
       
   257 
       
   258 local function resend_last_item(jid, node, service)
       
   259 	local ok, items = service:get_items(node, jid);
       
   260 	if not ok then return; end
       
   261 	for _, id in ipairs(items) do
       
   262 		service.config.broadcaster("items", node, { [jid] = true }, items[id]);
       
   263 	end
       
   264 end
       
   265 
       
   266 local function update_subscriptions(recipient, service_name, nodes)
       
   267 	local service = get_pep_service(service_name);
       
   268 	nodes = nodes or empty_set;
       
   269 
       
   270 	local service_recipients = recipients[service_name];
       
   271 	if not service_recipients then
       
   272 		service_recipients = {};
       
   273 		recipients[service_name] = service_recipients;
       
   274 	end
       
   275 
       
   276 	local current = service_recipients[recipient];
       
   277 	if not current or type(current) ~= "table" then
       
   278 		current = empty_set;
       
   279 	end
       
   280 
       
   281 	if (current == empty_set or current:empty()) and (nodes == empty_set or nodes:empty()) then
       
   282 		return;
       
   283 	end
       
   284 
       
   285 	for node in current - nodes do
       
   286 		service:remove_subscription(node, recipient, recipient);
       
   287 	end
       
   288 
       
   289 	for node in nodes - current do
       
   290 		service:add_subscription(node, recipient, recipient);
       
   291 		resend_last_item(recipient, node, service);
       
   292 	end
       
   293 
       
   294 	if nodes == empty_set or nodes:empty() then
       
   295 		nodes = nil;
       
   296 	end
       
   297 
       
   298 	service_recipients[recipient] = nodes;
       
   299 end
       
   300 
       
   301 module:hook("presence/bare", function(event)
       
   302 	-- inbound presence to bare JID recieved
       
   303 	local origin, stanza = event.origin, event.stanza;
       
   304 	local t = stanza.attr.type;
       
   305 	local is_self = not stanza.attr.to;
       
   306 	local username = jid_split(stanza.attr.to);
       
   307 	local user_bare = jid_bare(stanza.attr.to);
       
   308 	if is_self then
       
   309 		username = origin.username;
       
   310 		user_bare = jid_join(username, host);
       
   311 	end
       
   312 
       
   313 	if not t then -- available presence
       
   314 		if is_self or subscription_presence(username, stanza.attr.from) then
       
   315 			local recipient = stanza.attr.from;
       
   316 			local current = recipients[username] and recipients[username][recipient];
       
   317 			local hash, query_node = get_caps_hash_from_presence(stanza, current);
       
   318 			if current == hash or (current and current == hash_map[hash]) then return; end
       
   319 			if not hash then
       
   320 				update_subscriptions(recipient, username);
       
   321 			else
       
   322 				recipients[username] = recipients[username] or {};
       
   323 				if hash_map[hash] then
       
   324 					update_subscriptions(recipient, username, hash_map[hash]);
       
   325 				else
       
   326 					recipients[username][recipient] = hash;
       
   327 					local from_bare = origin.type == "c2s" and origin.username.."@"..origin.host;
       
   328 					if is_self or origin.type ~= "c2s" or (recipients[from_bare] and recipients[from_bare][origin.full_jid]) ~= hash then
       
   329 						-- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute
       
   330 						origin.send(
       
   331 							st.stanza("iq", {from=user_bare, to=stanza.attr.from, id="disco", type="get"})
       
   332 								:tag("query", {xmlns = "http://jabber.org/protocol/disco#info", node = query_node})
       
   333 						);
       
   334 					end
       
   335 				end
       
   336 			end
       
   337 		end
       
   338 	elseif t == "unavailable" then
       
   339 		update_subscriptions(stanza.attr.from, username);
       
   340 	elseif not is_self and t == "unsubscribe" then
       
   341 		local from = jid_bare(stanza.attr.from);
       
   342 		local subscriptions = recipients[username];
       
   343 		if subscriptions then
       
   344 			for subscriber in pairs(subscriptions) do
       
   345 				if jid_bare(subscriber) == from then
       
   346 					update_subscriptions(subscriber, username);
       
   347 				end
       
   348 			end
       
   349 		end
       
   350 	end
       
   351 end, 10);
       
   352 
       
   353 module:hook("iq-result/bare/disco", function(event)
       
   354 	local origin, stanza = event.origin, event.stanza;
       
   355 	local disco = stanza:get_child("query", "http://jabber.org/protocol/disco#info");
       
   356 	if not disco then
       
   357 		return;
       
   358 	end
       
   359 
       
   360 	-- Process disco response
       
   361 	local is_self = stanza.attr.to == nil;
       
   362 	local user_bare = jid_bare(stanza.attr.to);
       
   363 	local username = jid_split(stanza.attr.to);
       
   364 	if is_self then
       
   365 		username = origin.username;
       
   366 		user_bare = jid_join(username, host);
       
   367 	end
       
   368 	local contact = stanza.attr.from;
       
   369 	local current = recipients[username] and recipients[username][contact];
       
   370 	if type(current) ~= "string" then return; end -- check if waiting for recipient's response
       
   371 	local ver = current;
       
   372 	if not string.find(current, "#") then
       
   373 		ver = calculate_hash(disco.tags); -- calculate hash
       
   374 	end
       
   375 	local notify = set_new();
       
   376 	for _, feature in pairs(disco.tags) do
       
   377 		if feature.name == "feature" and feature.attr.var then
       
   378 			local nfeature = feature.attr.var:match("^(.*)%+notify$");
       
   379 			if nfeature then notify:add(nfeature); end
       
   380 		end
       
   381 	end
       
   382 	hash_map[ver] = notify; -- update hash map
       
   383 	if is_self then
       
   384 		-- Optimization: Fiddle with other local users
       
   385 		for jid, item in pairs(origin.roster) do -- for all interested contacts
       
   386 			if jid then
       
   387 				local contact_node, contact_host = jid_split(jid);
       
   388 				if contact_host == host and item.subscription == "both" or item.subscription == "from" then
       
   389 					update_subscriptions(user_bare, contact_node, notify);
       
   390 				end
       
   391 			end
       
   392 		end
       
   393 	end
       
   394 	update_subscriptions(contact, username, notify);
       
   395 end);
       
   396 
       
   397 module:hook("account-disco-info-node", function(event)
       
   398 	local reply, stanza, origin = event.reply, event.stanza, event.origin;
       
   399 	local service_name = origin.username;
       
   400 	if stanza.attr.to ~= nil then
       
   401 		service_name = jid_split(stanza.attr.to);
       
   402 	end
       
   403 	local service = get_pep_service(service_name);
       
   404 	local node = event.node;
       
   405 	local ok = service:get_items(node, jid_bare(stanza.attr.from) or true);
       
   406 	if not ok then return; end
       
   407 	event.exists = true;
       
   408 	reply:tag('identity', {category='pubsub', type='leaf'}):up();
       
   409 end);
       
   410 
       
   411 module:hook("account-disco-info", function(event)
       
   412 	local origin, reply = event.origin, event.reply;
       
   413 
       
   414 	reply:tag('identity', {category='pubsub', type='pep'}):up();
       
   415 
       
   416 	local username = jid_split(reply.attr.from) or origin.username;
       
   417 	local service = get_pep_service(username);
       
   418 
       
   419 	local suppored_features = lib_pubsub.get_feature_set(service) + set.new{
       
   420 		-- Features not covered by the above
       
   421 		"access-presence",
       
   422 		"auto-subscribe",
       
   423 		"filtered-notifications",
       
   424 		"last-published",
       
   425 		"persistent-items",
       
   426 		"presence-notifications",
       
   427 		"presence-subscribe",
       
   428 	};
       
   429 
       
   430 	for feature in suppored_features do
       
   431 		reply:tag('feature', {var=xmlns_pubsub.."#"..feature}):up();
       
   432 	end
       
   433 end);
       
   434 
       
   435 module:hook("account-disco-items-node", function(event)
       
   436 	local reply, stanza, origin = event.reply, event.stanza, event.origin;
       
   437 	local node = event.node;
       
   438 	local is_self = stanza.attr.to == nil;
       
   439 	local user_bare = jid_bare(stanza.attr.to);
       
   440 	local username = jid_split(stanza.attr.to);
       
   441 	if is_self then
       
   442 		username = origin.username;
       
   443 		user_bare = jid_join(username, host);
       
   444 	end
       
   445 	local service = get_pep_service(username);
       
   446 	local ok, ret = service:get_items(node, jid_bare(stanza.attr.from) or true);
       
   447 	if not ok then return; end
       
   448 	event.exists = true;
       
   449 	for _, id in ipairs(ret) do
       
   450 		reply:tag("item", { jid = user_bare, name = id }):up();
       
   451 	end
       
   452 end);
       
   453 
       
   454 module:hook("account-disco-items", function(event)
       
   455 	local reply, stanza, origin = event.reply, event.stanza, event.origin;
       
   456 
       
   457 	local is_self = stanza.attr.to == nil;
       
   458 	local user_bare = jid_bare(stanza.attr.to);
       
   459 	local username = jid_split(stanza.attr.to);
       
   460 	if is_self then
       
   461 		username = origin.username;
       
   462 		user_bare = jid_join(username, host);
       
   463 	end
       
   464 	local service = get_pep_service(username);
       
   465 
       
   466 	local ok, ret = service:get_nodes(jid_bare(stanza.attr.from));
       
   467 	if not ok then return; end
       
   468 
       
   469 	for node, node_obj in pairs(ret) do
       
   470 		reply:tag("item", { jid = user_bare, node = node, name = node_obj.config.name }):up();
       
   471 	end
       
   472 end);