Software / code / prosody-modules
Comparison
mod_pubsub_serverinfo/mod_pubsub_serverinfo.lua @ 5826: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 |
| parent | 5824:73887dcb2129 |
| child | 5827:c3eeeb968403 |
comparison
equal
deleted
inserted
replaced
| 5825:f6a2602129c8 | 5826: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 |