1 local pubsub
= 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";
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";
16 local lib_pubsub
= module
:require
"pubsub";
18 local empty_set
= set_new();
20 -- username -> util.pubsub service object
23 -- username -> recipient -> set of nodes
24 local recipients
= {};
26 -- caps hash -> set of nodes
29 local host
= module
.host
;
31 local node_config
= module
:open_store("pep", "map");
32 local known_nodes
= module
:open_store("pep");
34 local max_max_items
= module
:get_option_number("pep_max_items", 256);
36 function module
.save()
39 recipients
= recipients
;
43 function module
.restore(data
)
44 services
= data
.services
;
45 recipients
= data
.recipients
;
46 for username
, service
in pairs(services
) do
47 local user_bare
= jid_join(username
, host
);
48 module
:add_item("pep-service", { service
= service
, jid
= user_bare
});
52 function is_item_stanza(item
)
53 return st
.is_stanza(item
) and item
.attr
.xmlns
== xmlns_pubsub
and item
.name
== "item";
56 function check_node_config(node
, actor
, new_config
) -- luacheck: ignore 212/node 212/actor
57 if (new_config
["max_items"] or 1) > max_max_items
then
60 if new_config
["access_model"] ~= "presence"
61 and new_config
["access_model"] ~= "whitelist"
62 and new_config
["access_model"] ~= "open" then
68 local function subscription_presence(username
, recipient
)
69 local user_bare
= jid_join(username
, host
);
70 local recipient_bare
= jid_bare(recipient
);
71 if (recipient_bare
== user_bare
) then return true; end
72 return is_contact_subscribed(username
, host
, recipient_bare
);
75 local function nodestore(username
)
76 -- luacheck: ignore 212/self
78 function store
:get(node
)
79 local data
, err
= node_config
:get(username
, node
)
81 -- COMPAT Previously stored only a boolean representing 'persist_items'
91 function store
:set(node
, data
)
93 -- Save the data without subscriptions
94 local subscribers
= {};
95 for jid
, sub
in pairs(data
.subscribers
) do
96 if type(sub
) ~= "table" or not sub
.presence
then
97 subscribers
[jid
] = sub
;
102 config
= data
.config
;
103 affiliations
= data
.affiliations
;
104 subscribers
= subscribers
;
107 return node_config
:set(username
, node
, data
);
109 function store
:users()
110 return pairs(known_nodes
:get(username
) or {});
115 local function simple_itemstore(username
)
116 return function (config
, node
)
117 if config
["persist_items"] then
118 module
:log("debug", "Creating new persistent item store for user %s, node %q", username
, node
);
119 local archive
= module
:open_store("pep_"..node
, "archive");
120 return lib_pubsub
.archive_itemstore(archive
, config
, username
, node
, false);
122 module
:log("debug", "Creating new ephemeral item store for user %s, node %q", username
, node
);
123 return cache
.new(tonumber(config
["max_items"]));
128 local function get_broadcaster(username
)
129 local user_bare
= jid_join(username
, host
);
130 local function simple_broadcast(kind
, node
, jids
, item
, _
, node_obj
)
132 if node_obj
.config
["notify_"..kind
] == false then
136 if kind
== "retract" then
137 kind
= "items"; -- XEP-0060 signals retraction in an <items> container
139 local message
= st
.message({ from
= user_bare
, type = "headline" })
140 :tag("event", { xmlns
= xmlns_pubsub_event
})
141 :tag(kind
, { node
= node
});
143 item
= st
.clone(item
);
144 item
.attr
.xmlns
= nil; -- Clear the pubsub namespace
145 if kind
== "items" then
146 if node_obj
and node_obj
.config
.include_payload
== false then
147 item
:maptags(function () return nil; end);
150 message
:add_child(item
);
152 for jid
in pairs(jids
) do
153 module
:log("debug", "Sending notification to %s from %s: %s", jid
, user_bare
, tostring(item
));
154 message
.attr
.to
= jid
;
155 module
:send(message
);
158 return simple_broadcast
;
161 local function on_node_creation(event
)
162 local service
= event
.service
;
163 local node
= event
.node
;
164 local username
= service
.config
.pep_username
;
166 local service_recipients
= recipients
[username
];
167 if not service_recipients
then return; end
169 for recipient
, nodes
in pairs(service_recipients
) do
170 if nodes
:contains(node
) then
171 service
:add_subscription(node
, recipient
, recipient
, { presence
= true });
176 function get_pep_service(username
)
177 module
:log("debug", "get_pep_service(%q)", username
);
178 local user_bare
= jid_join(username
, host
);
179 local service
= services
[username
];
183 service
= pubsub
.new({
184 pep_username
= username
;
187 ["persist_items"] = true;
188 ["access_model"] = "presence";
191 autocreate_on_publish
= true;
192 autocreate_on_subscribe
= false;
194 nodestore
= nodestore(username
);
195 itemstore
= simple_itemstore(username
);
196 broadcaster
= get_broadcaster(username
);
197 itemcheck
= is_item_stanza
;
198 get_affiliation
= function (jid
)
199 if jid_bare(jid
) == user_bare
then
205 presence
= function (jid
)
206 if subscription_presence(username
, jid
) then
213 normalize_jid
= jid_bare
;
215 check_node_config
= check_node_config
;
217 local nodes
, err
= known_nodes
:get(username
);
219 module
:log("debug", "Restoring nodes for user %s", username
);
220 for node
in pairs(nodes
) do
221 module
:log("debug", "Restoring node %q", node
);
222 service
:create(node
, true);
225 module
:log("error", "Could not restore nodes for %s: %s", username
, err
);
227 module
:log("debug", "No known nodes");
229 services
[username
] = service
;
230 module
:add_item("pep-service", { service
= service
, jid
= user_bare
});
234 module
:hook("item-added/pep-service", function (event
)
235 local service
= event
.item
.service
;
236 module
:hook_object_event(service
.events
, "node-created", on_node_creation
);
239 function handle_pubsub_iq(event
)
240 local origin
, stanza
= event
.origin
, event
.stanza
;
241 local service_name
= origin
.username
;
242 if stanza
.attr
.to
~= nil then
243 service_name
= jid_split(stanza
.attr
.to
);
245 local service
= get_pep_service(service_name
);
247 return lib_pubsub
.handle_pubsub_iq(event
, service
)
250 module
:hook("iq/bare/"..xmlns_pubsub
..":pubsub", handle_pubsub_iq
);
251 module
:hook("iq/bare/"..xmlns_pubsub_owner
..":pubsub", handle_pubsub_iq
);
253 module
:add_identity("pubsub", "pep", module
:get_option_string("name", "Prosody"));
254 module
:add_feature("http://jabber.org/protocol/pubsub#publish");
256 local function get_caps_hash_from_presence(stanza
, current
)
257 local t
= stanza
.attr
.type;
259 local child
= stanza
:get_child("c", "http://jabber.org/protocol/caps");
261 local attr
= child
.attr
;
262 if attr
.hash
then -- new caps
263 if attr
.hash
== 'sha-1' and attr
.node
and attr
.ver
then
264 return attr
.ver
, attr
.node
.."#"..attr
.ver
;
267 if attr
.node
and attr
.ver
then
268 return attr
.node
.."#"..attr
.ver
.."#"..(attr
.ext
or ""), attr
.node
.."#"..attr
.ver
;
272 return; -- no or bad caps
273 elseif t
== "unavailable" or t
== "error" then
276 return current
; -- no caps, could mean caps optimization, so return current
279 local function resend_last_item(jid
, node
, service
)
280 local ok
, id
, item
= service
:get_last_item(node
, jid
);
281 if not (ok
and id
) then return; end
282 service
.config
.broadcaster("items", node
, { [jid
] = true }, item
);
285 local function update_subscriptions(recipient
, service_name
, nodes
)
286 nodes
= nodes
or empty_set
;
288 local service_recipients
= recipients
[service_name
];
289 if not service_recipients
then
290 service_recipients
= {};
291 recipients
[service_name
] = service_recipients
;
294 local current
= service_recipients
[recipient
];
299 if (current
== empty_set
or current
:empty()) and (nodes
== empty_set
or nodes
:empty()) then
303 local service
= get_pep_service(service_name
);
304 for node
in current
- nodes
do
305 service
:remove_subscription(node
, recipient
, recipient
);
308 for node
in nodes
- current
do
309 if service
:add_subscription(node
, recipient
, recipient
, { presence
= true }) then
310 resend_last_item(recipient
, node
, service
);
314 if nodes
== empty_set
or nodes
:empty() then
318 service_recipients
[recipient
] = nodes
;
321 module
:hook("presence/bare", function(event
)
322 -- inbound presence to bare JID received
323 local origin
, stanza
= event
.origin
, event
.stanza
;
324 local t
= stanza
.attr
.type;
325 local is_self
= not stanza
.attr
.to
;
326 local username
= jid_split(stanza
.attr
.to
);
327 local user_bare
= jid_bare(stanza
.attr
.to
);
329 username
= origin
.username
;
330 user_bare
= jid_join(username
, host
);
333 if not t
then -- available presence
334 if is_self
or subscription_presence(username
, stanza
.attr
.from
) then
335 local recipient
= stanza
.attr
.from
;
336 local current
= recipients
[username
] and recipients
[username
][recipient
];
337 local hash
, query_node
= get_caps_hash_from_presence(stanza
, current
);
338 if current
== hash
or (current
and current
== hash_map
[hash
]) then return; end
340 update_subscriptions(recipient
, username
);
342 recipients
[username
] = recipients
[username
] or {};
343 if hash_map
[hash
] then
344 update_subscriptions(recipient
, username
, hash_map
[hash
]);
346 -- COMPAT from ~= stanza.attr.to because OneTeam can't deal with missing from attribute
348 st
.stanza("iq", {from
=user_bare
, to
=stanza
.attr
.from
, id
="disco", type="get"})
349 :tag("query", {xmlns
= "http://jabber.org/protocol/disco#info", node
= query_node
})
354 elseif t
== "unavailable" then
355 update_subscriptions(stanza
.attr
.from
, username
);
356 elseif not is_self
and t
== "unsubscribe" then
357 local from
= jid_bare(stanza
.attr
.from
);
358 local subscriptions
= recipients
[username
];
359 if subscriptions
then
360 for subscriber
in pairs(subscriptions
) do
361 if jid_bare(subscriber
) == from
then
362 update_subscriptions(subscriber
, username
);
369 module
:hook("iq-result/bare/disco", function(event
)
370 local origin
, stanza
= event
.origin
, event
.stanza
;
371 local disco
= stanza
:get_child("query", "http://jabber.org/protocol/disco#info");
376 -- Process disco response
377 local is_self
= stanza
.attr
.to
== nil;
378 local user_bare
= jid_bare(stanza
.attr
.to
);
379 local username
= jid_split(stanza
.attr
.to
);
381 username
= origin
.username
;
382 user_bare
= jid_join(username
, host
);
384 local contact
= stanza
.attr
.from
;
385 local ver
= calculate_hash(disco
.tags
); -- calculate hash
386 local notify
= set_new();
387 for _
, feature
in pairs(disco
.tags
) do
388 if feature
.name
== "feature" and feature
.attr
.var
then
389 local nfeature
= feature
.attr
.var
:match("^(.*)%+notify$");
390 if nfeature
then notify
:add(nfeature
); end
393 hash_map
[ver
] = notify
; -- update hash map
395 -- Optimization: Fiddle with other local users
396 for jid
, item
in pairs(origin
.roster
) do -- for all interested contacts
398 local contact_node
, contact_host
= jid_split(jid
);
399 if contact_host
== host
and (item
.subscription
== "both" or item
.subscription
== "from") then
400 update_subscriptions(user_bare
, contact_node
, notify
);
405 update_subscriptions(contact
, username
, notify
);
408 module
:hook("account-disco-info-node", function(event
)
409 local stanza
, origin
= event
.stanza
, event
.origin
;
410 local service_name
= origin
.username
;
411 if stanza
.attr
.to
~= nil then
412 service_name
= jid_split(stanza
.attr
.to
);
414 local service
= get_pep_service(service_name
);
415 return lib_pubsub
.handle_disco_info_node(event
, service
);
418 module
:hook("account-disco-info", function(event
)
419 local origin
, reply
= event
.origin
, event
.reply
;
421 reply
:tag('identity', {category
='pubsub', type='pep'}):up();
423 local username
= jid_split(reply
.attr
.from
) or origin
.username
;
424 local service
= get_pep_service(username
);
426 local supported_features
= lib_pubsub
.get_feature_set(service
) + set
.new
{
427 -- Features not covered by the above
429 "filtered-notifications",
431 "presence-notifications",
432 "presence-subscribe",
435 for feature
in supported_features
do
436 reply
:tag('feature', {var
=xmlns_pubsub
.."#"..feature
}):up();
440 module
:hook("account-disco-items-node", function(event
)
441 local stanza
, origin
= event
.stanza
, event
.origin
;
442 local is_self
= stanza
.attr
.to
== nil;
443 local username
= jid_split(stanza
.attr
.to
);
445 username
= origin
.username
;
447 local service
= get_pep_service(username
);
448 return lib_pubsub
.handle_disco_items_node(event
, service
);
451 module
:hook("account-disco-items", function(event
)
452 local reply
, stanza
, origin
= event
.reply
, event
.stanza
, event
.origin
;
454 local is_self
= stanza
.attr
.to
== nil;
455 local user_bare
= jid_bare(stanza
.attr
.to
);
456 local username
= jid_split(stanza
.attr
.to
);
458 username
= origin
.username
;
459 user_bare
= jid_join(username
, host
);
461 local service
= get_pep_service(username
);
463 local ok
, ret
= service
:get_nodes(jid_bare(stanza
.attr
.from
));
464 if not ok
then return; end
466 for node
, node_obj
in pairs(ret
) do
467 reply
:tag("item", { jid
= user_bare
, node
= node
, name
= node_obj
.config
.title
}):up();