Mercurial > prosody-modules
comparison mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua @ 5803:f55e65315ba0
mod_pubsub_serverinfo: implemented all basic features
This commit replaces the earlier proof-of-concept to a solution that:
- reports on remotely-connected domains
- uses disco/info to detect if those domains opt-in
- publishes domain names for remote domains that do so
- caches the disco/info response
author | Guus der Kinderen <guus.der.kinderen@gmail.com> |
---|---|
date | Wed, 03 Jan 2024 23:05:14 +0100 |
parents | 73887dcb2129 |
children | c3eeeb968403 |
comparison
equal
deleted
inserted
replaced
5802:f6a2602129c8 | 5803:f55e65315ba0 |
---|---|
1 module:add_feature("urn:xmpp:serverinfo:0"); | 1 local st = require "util.stanza"; |
2 local new_id = require"util.id".medium; | |
3 local dataform = require "util.dataforms".new; | |
4 | |
5 local local_domain = module:get_host(); | |
6 local service = module:get_option(module.name .. "_service") or "pubsub." .. local_domain; | |
7 local node = module:get_option(module.name .. "_node") or "serverinfo"; | |
8 local actor = module.host .. "/modules/" .. module.name; | |
9 local publication_interval = module:get_option(module.name .. "_publication_interval") or 300; | |
10 | |
11 local opt_in_reports | |
12 | |
13 function module.load() | |
14 -- Will error out with a 'conflict' if the node already exists. TODO: create the node only when it's missing. | |
15 create_node():next() | |
16 | |
17 module:add_feature("urn:xmpp:serverinfo:0"); | |
18 | |
19 module:add_extension(dataform { | |
20 { name = "FORM_TYPE", type = "hidden", value = "http://jabber.org/network/serverinfo" }, | |
21 { name = "serverinfo-pubsub-node", type = "text-single" }, | |
22 }:form({ ["serverinfo-pubsub-node"] = ("xmpp:%s?;node=%s"):format(service, node) }, "result")); | |
23 | |
24 module:add_timer(10, publish_serverinfo); | |
25 end | |
26 | |
27 function module.unload() | |
28 -- This removes all subscribers, which may or may not be desirable, depending on the reason for the unload. | |
29 delete_node(); -- Should this block, to delay unload() until the node is deleted? | |
30 end | |
31 | |
32 -- Returns a promise | |
33 function create_node() | |
34 local request = st.iq({ type = "set", to = service, from = actor, id = new_id() }) | |
35 :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) | |
36 :tag("create", { node = node }):up() | |
37 :tag("configure") | |
38 :tag("x", { xmlns = "jabber:x:data", type = "submit" }) | |
39 :tag("field", { var = "FORM_TYPE", type = "hidden"}) | |
40 :text_tag("value", "http://jabber.org/protocol/pubsub#node_config") | |
41 :up() | |
42 :tag("field", { var = "pubsub#max_items" }) | |
43 :text_tag("value", "1") | |
44 :up() | |
45 :tag("field", { var = "pubsub#persist_items" }) | |
46 :text_tag("value", "0") | |
47 return module:send_iq(request); | |
48 end | |
49 | |
50 -- Returns a promise | |
51 function delete_node() | |
52 local request = st.iq({ type = "set", to = service, from = actor, id = new_id() }) | |
53 :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) | |
54 :tag("delete", { node = node }); | |
55 | |
56 return module:send_iq(request); | |
57 end | |
58 | |
59 function publish_serverinfo() | |
60 -- Iterate over s2s sessions, adding them to a multimap, where the key is the local domain name, | |
61 -- mapped to a collection of remote domain names. De-duplicate all remote domain names by using | |
62 -- them as an index in a table. | |
63 local domains_by_host = {} | |
64 for session, _ in pairs(prosody.incoming_s2s) do | |
65 if session ~= nil and session.from_host ~= nil and local_domain == session.to_host then | |
66 local sessions = domains_by_host[session.to_host] | |
67 if sessions == nil then sessions = {} end; -- instantiate a new entry if none existed | |
68 sessions[session.from_host] = true | |
69 domains_by_host[session.to_host] = sessions | |
70 end | |
71 end | |
72 | |
73 -- At an earlier stage, the code iterated voer all prosody.hosts - but that turned out to be to noisy. | |
74 -- for host, data in pairs(prosody.hosts) do | |
75 local host = local_domain | |
76 local data = prosody.hosts[host] | |
77 if data ~= nil then | |
78 local sessions = domains_by_host[host] | |
79 if sessions == nil then sessions = {} end; -- instantiate a new entry if none existed | |
80 if data.s2sout ~= nil then | |
81 for _, session in pairs(data.s2sout) do | |
82 if session.to_host ~= nil then | |
83 sessions[session.to_host] = true | |
84 domains_by_host[host] = sessions | |
85 end | |
86 end | |
87 end | |
88 end | |
89 | |
90 -- Build the publication stanza. | |
91 local request = st.iq({ type = "set", to = service, from = actor, id = new_id() }) | |
92 :tag("pubsub", { xmlns = "http://jabber.org/protocol/pubsub" }) | |
93 :tag("publish", { node = node, xmlns = "http://jabber.org/protocol/pubsub" }) | |
94 :tag("item", { id = "current", xmlns = "http://jabber.org/protocol/pubsub" }) | |
95 :tag("serverinfo", { xmlns = "urn:xmpp:serverinfo:0" }) | |
96 | |
97 request:tag("domain", { name = local_domain }) | |
98 :tag("federation") | |
99 | |
100 local remotes = domains_by_host[host] | |
101 | |
102 if remotes ~= nil then | |
103 for remote, _ in pairs(remotes) do | |
104 -- include a domain name for remote domains, but only if they advertise support. | |
105 if does_opt_in(remote) then | |
106 request:tag("remote-domain", { name = remote }):up() | |
107 else | |
108 request:tag("remote-domain"):up() | |
109 end | |
110 end | |
111 end | |
112 | |
113 request:up():up() | |
114 | |
115 module:send_iq(request):next() | |
116 | |
117 return publication_interval; | |
118 end | |
119 | |
120 local opt_in_cache = {} | |
121 | |
122 function does_opt_in(remoteDomain) | |
123 | |
124 -- try to read answer from cache. | |
125 local cached_value = opt_in_cache[remoteDomain] | |
126 if cached_value ~= nil and os.difftime(cached_value.expires, os.time()) > 0 then | |
127 return cached_value.opt_in; | |
128 end | |
129 | |
130 -- TODO worry about not having multiple requests in flight to the same domain.cached_value | |
131 | |
132 -- Cache could not provide an answer. Perform service discovery. | |
133 local discoRequest = st.iq({ type = "get", to = remoteDomain, from = actor, id = new_id() }) | |
134 :tag("query", { xmlns = "http://jabber.org/protocol/disco#info" }) | |
135 | |
136 module:send_iq(discoRequest):next( | |
137 function(response) | |
138 if response.stanza ~= nil and response.stanza.attr.type == "result" then | |
139 local query = response.stanza:get_child("query", "http://jabber.org/protocol/disco#info") | |
140 if query ~= nil then | |
141 for feature in query:childtags("feature", "http://jabber.org/protocol/disco#info") do | |
142 if feature.attr.var == 'urn:xmpp:serverinfo:0' then | |
143 opt_in_cache[remoteDomain] = { | |
144 opt_in = true; | |
145 expires = os.time() + 3600; | |
146 } | |
147 return; -- prevent 'false' to be cached, down below. | |
148 end | |
149 end | |
150 end | |
151 end | |
152 opt_in_cache[remoteDomain] = { | |
153 opt_in = false; | |
154 expires = os.time() + 3600; | |
155 } | |
156 end, | |
157 function(response) | |
158 opt_in_cache[remoteDomain] = { | |
159 opt_in = false; | |
160 expires = os.time() + 3600; | |
161 } | |
162 end | |
163 ); | |
164 | |
165 -- return 'false' for now. Better luck next time... | |
166 return false; | |
167 | |
168 end |