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