Software / code / verse
Comparison
plugins/disco.lua @ 99:0f5a8d530fcd
verse.plugins.disco: Add disco plugin originally developed by Hubert Chathi for Riddim, but here adapted for Verse with new APIs added to allow disco'ing the local server and remote entities
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Sat, 21 Aug 2010 14:51:36 +0100 |
| child | 109:60a03b2cabec |
comparison
equal
deleted
inserted
replaced
| 98:1dccff7df2d5 | 99:0f5a8d530fcd |
|---|---|
| 1 -- Verse XMPP Library | |
| 2 -- Copyright (C) 2010 Hubert Chathi <hubert@uhoreg.ca> | |
| 3 -- Copyright (C) 2010 Matthew Wild <mwild1@gmail.com> | |
| 4 -- | |
| 5 -- This project is MIT/X11 licensed. Please see the | |
| 6 -- COPYING file in the source package for more information. | |
| 7 -- | |
| 8 | |
| 9 local st = require "util.stanza" | |
| 10 local b64 = require("mime").b64 | |
| 11 -- NOTE: The b64 routine in LuaSocket 2.0.2 and below | |
| 12 -- contains a bug regarding handling \0, it's advisable | |
| 13 -- that you use another base64 routine, or a patched | |
| 14 -- version of LuaSocket. | |
| 15 -- You can borrow Prosody's (binary) util.encodings lib: | |
| 16 --local b64 = require("util.encodings").base64.encode | |
| 17 | |
| 18 local sha1 = require("util.sha1").sha1 | |
| 19 | |
| 20 local xmlns_disco = "http://jabber.org/protocol/disco"; | |
| 21 local xmlns_disco_info = xmlns_disco.."#info"; | |
| 22 local xmlns_disco_items = xmlns_disco.."#items"; | |
| 23 | |
| 24 function verse.plugins.disco(stream) | |
| 25 stream.disco = { cache = {}, info = {} } | |
| 26 stream.disco.info.identities = { | |
| 27 {category = 'client', type='pc', name='Verse'}, | |
| 28 } | |
| 29 stream.disco.info.features = { | |
| 30 {var = 'http://jabber.org/protocol/caps'}, | |
| 31 {var = 'http://jabber.org/protocol/disco#info'}, | |
| 32 {var = 'http://jabber.org/protocol/disco#items'}, | |
| 33 } | |
| 34 stream.disco.items = {} | |
| 35 stream.disco.nodes = {} | |
| 36 | |
| 37 stream.caps = {} | |
| 38 stream.caps.node = 'http://code.matthewwild.co.uk/verse/' | |
| 39 | |
| 40 local function cmp_identity(item1, item2) | |
| 41 if item1.category < item2.category then | |
| 42 return true; | |
| 43 elseif item2.category < item1.category then | |
| 44 return false; | |
| 45 end | |
| 46 if item1.type < item2.type then | |
| 47 return true; | |
| 48 elseif item2.type < item1.type then | |
| 49 return false; | |
| 50 end | |
| 51 if (not item1['xml:lang'] and item2['xml:lang']) or | |
| 52 (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then | |
| 53 return true | |
| 54 end | |
| 55 return false | |
| 56 end | |
| 57 | |
| 58 local function cmp_feature(item1, item2) | |
| 59 return item1.var < item2.var | |
| 60 end | |
| 61 | |
| 62 local function calculate_hash() | |
| 63 table.sort(stream.disco.info.identities, cmp_identity) | |
| 64 table.sort(stream.disco.info.features, cmp_feature) | |
| 65 local S = '' | |
| 66 for key,identity in pairs(stream.disco.info.identities) do | |
| 67 S = S .. string.format( | |
| 68 '%s/%s/%s/%s', identity.category, identity.type, | |
| 69 identity['xml:lang'] or '', identity.name or '' | |
| 70 ) .. '<' | |
| 71 end | |
| 72 for key,feature in pairs(stream.disco.info.features) do | |
| 73 S = S .. feature.var .. '<' | |
| 74 end | |
| 75 -- FIXME: make sure S is utf8-encoded | |
| 76 --stream:debug("Computed hash string: "..S); | |
| 77 --stream:debug("Computed hash string (sha1): "..sha1(S, true)); | |
| 78 --stream:debug("Computed hash string (sha1+b64): "..b64(sha1(S))); | |
| 79 return (b64(sha1(S))) | |
| 80 end | |
| 81 | |
| 82 setmetatable(stream.caps, { | |
| 83 __call = function (...) -- vararg: allow calling as function or member | |
| 84 -- retrieve the c stanza to insert into the | |
| 85 -- presence stanza | |
| 86 local hash = calculate_hash() | |
| 87 return st.stanza('c', { | |
| 88 xmlns = 'http://jabber.org/protocol/caps', | |
| 89 hash = 'sha-1', | |
| 90 node = stream.caps.node, | |
| 91 ver = hash | |
| 92 }) | |
| 93 end | |
| 94 }) | |
| 95 | |
| 96 function stream:add_disco_feature(feature) | |
| 97 table.insert(self.disco.info.features, {var=feature}); | |
| 98 end | |
| 99 | |
| 100 function stream:jid_has_identity(jid, category, type) | |
| 101 local cached_disco = self.disco.cache[jid]; | |
| 102 if not cached_disco then | |
| 103 return nil, "no-cache"; | |
| 104 end | |
| 105 local identities = self.disco.cache[jid].identities; | |
| 106 if type then | |
| 107 return identities[category.."/"..type] or false; | |
| 108 end | |
| 109 -- Check whether we have any identities with this category instead | |
| 110 for identity in pairs(identities) do | |
| 111 if identity:match("^(.*)/") == category then | |
| 112 return true; | |
| 113 end | |
| 114 end | |
| 115 end | |
| 116 | |
| 117 function stream:jid_supports(jid, feature) | |
| 118 local cached_disco = self.disco.cache[jid]; | |
| 119 if not cached_disco or not cached_disco.features then | |
| 120 return nil, "no-cache"; | |
| 121 end | |
| 122 return cached_disco.features[feature] or false; | |
| 123 end | |
| 124 | |
| 125 function stream:get_local_services(category, type) | |
| 126 local host_disco = self.disco.cache[self.host]; | |
| 127 if not(host_disco) or not(host_disco.items) then | |
| 128 return nil, "no-cache"; | |
| 129 end | |
| 130 | |
| 131 local results = {}; | |
| 132 for _, service in ipairs(host_disco.items) do | |
| 133 if self:jid_has_identity(service.jid, category, type) then | |
| 134 table.insert(results, service.jid); | |
| 135 end | |
| 136 end | |
| 137 return results; | |
| 138 end | |
| 139 | |
| 140 function stream:disco_local_services(callback) | |
| 141 self:disco_items(self.host, nil, function (items) | |
| 142 local n_items = 0; | |
| 143 local function item_callback() | |
| 144 n_items = n_items - 1; | |
| 145 if n_items == 0 then | |
| 146 return callback(items); | |
| 147 end | |
| 148 end | |
| 149 | |
| 150 for _, item in ipairs(items) do | |
| 151 if item.jid then | |
| 152 n_items = n_items + 1; | |
| 153 self:disco_info(item.jid, nil, item_callback); | |
| 154 end | |
| 155 end | |
| 156 if n_items == 0 then | |
| 157 return callback(items); | |
| 158 end | |
| 159 end); | |
| 160 end | |
| 161 | |
| 162 function stream:disco_info(jid, node, callback) | |
| 163 local disco_request = verse.iq({ to = jid, type = "get" }) | |
| 164 :tag("query", { xmlns = xmlns_disco_info, node = node }); | |
| 165 self:send_iq(disco_request, function (result) | |
| 166 if result.attr.type == "error" then | |
| 167 return callback(nil, result:get_error()); | |
| 168 end | |
| 169 | |
| 170 local identities, features = {}, {}; | |
| 171 | |
| 172 for tag in result:get_child("query", xmlns_disco_info):childtags() do | |
| 173 if tag.name == "identity" then | |
| 174 identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true; | |
| 175 elseif tag.name == "feature" then | |
| 176 features[tag.attr.var] = true; | |
| 177 end | |
| 178 end | |
| 179 | |
| 180 | |
| 181 if not self.disco.cache[jid] then | |
| 182 self.disco.cache[jid] = { nodes = {} }; | |
| 183 end | |
| 184 | |
| 185 if node then | |
| 186 if not self.disco.cache.nodes[node] then | |
| 187 self.disco.cache.nodes[node] = { nodes = {} }; | |
| 188 end | |
| 189 self.disco.cache[jid].nodes[node].identities = identities; | |
| 190 self.disco.cache[jid].nodes[node].features = features; | |
| 191 else | |
| 192 self.disco.cache[jid].identities = identities; | |
| 193 self.disco.cache[jid].features = features; | |
| 194 end | |
| 195 return callback(self.disco.cache[jid]); | |
| 196 end); | |
| 197 end | |
| 198 | |
| 199 function stream:disco_items(jid, node, callback) | |
| 200 local disco_request = verse.iq({ to = jid, type = "get" }) | |
| 201 :tag("query", { xmlns = xmlns_disco_items, node = node }); | |
| 202 self:send_iq(disco_request, function (result) | |
| 203 if result.attr.type == "error" then | |
| 204 return callback(nil, result:get_error()); | |
| 205 end | |
| 206 local disco_items = { }; | |
| 207 for tag in result:get_child("query", xmlns_disco_items):childtags() do | |
| 208 if tag.name == "item" then | |
| 209 table.insert(disco_items, { | |
| 210 name = tag.attr.name; | |
| 211 jid = tag.attr.jid; | |
| 212 }); | |
| 213 end | |
| 214 end | |
| 215 | |
| 216 if not self.disco.cache[jid] then | |
| 217 self.disco.cache[jid] = { nodes = {} }; | |
| 218 end | |
| 219 | |
| 220 if node then | |
| 221 if not self.disco.cache.nodes[node] then | |
| 222 self.disco.cache.nodes[node] = { nodes = {} }; | |
| 223 end | |
| 224 self.disco.cache.nodes[node].items = disco_items; | |
| 225 else | |
| 226 self.disco.cache[jid].items = disco_items; | |
| 227 end | |
| 228 return callback(disco_items); | |
| 229 end); | |
| 230 end | |
| 231 | |
| 232 stream:hook("iq/http://jabber.org/protocol/disco#info", function (stanza) | |
| 233 if stanza.attr.type == 'get' then | |
| 234 local query = stanza:child_with_name('query') | |
| 235 if not query then return; end | |
| 236 -- figure out what identities/features to send | |
| 237 local identities | |
| 238 local features | |
| 239 if query.attr.node then | |
| 240 local hash = calculate_hash() | |
| 241 local node = stream.disco.nodes[query.attr.node] | |
| 242 if node and node.info then | |
| 243 identities = node.info.identities or {} | |
| 244 features = node.info.identities or {} | |
| 245 elseif query.attr.node == stream.caps.node..'#'..hash then | |
| 246 -- matches caps hash, so use the main info | |
| 247 identities = stream.disco.info.identities | |
| 248 features = stream.disco.info.features | |
| 249 else | |
| 250 -- unknown node: give an error | |
| 251 local response = st.stanza('iq',{ | |
| 252 to = stanza.attr.from, | |
| 253 from = stanza.attr.to, | |
| 254 id = stanza.attr.id, | |
| 255 type = 'error' | |
| 256 }) | |
| 257 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#info'}):reset() | |
| 258 response:tag('error',{type = 'cancel'}):tag( | |
| 259 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} | |
| 260 ) | |
| 261 stream:send(response) | |
| 262 return true | |
| 263 end | |
| 264 else | |
| 265 identities = stream.disco.info.identities | |
| 266 features = stream.disco.info.features | |
| 267 end | |
| 268 -- construct the response | |
| 269 local result = st.stanza('query',{ | |
| 270 xmlns = 'http://jabber.org/protocol/disco#info', | |
| 271 node = query.attr.node | |
| 272 }) | |
| 273 for key,identity in pairs(identities) do | |
| 274 result:tag('identity', identity):reset() | |
| 275 end | |
| 276 for key,feature in pairs(features) do | |
| 277 result:tag('feature', feature):reset() | |
| 278 end | |
| 279 stream:send(st.stanza('iq',{ | |
| 280 to = stanza.attr.from, | |
| 281 from = stanza.attr.to, | |
| 282 id = stanza.attr.id, | |
| 283 type = 'result' | |
| 284 }):add_child(result)) | |
| 285 return true | |
| 286 end | |
| 287 end); | |
| 288 | |
| 289 stream:hook("iq/http://jabber.org/protocol/disco#items", function (stanza) | |
| 290 if stanza.attr.type == 'get' then | |
| 291 local query = stanza:child_with_name('query') | |
| 292 if not query then return; end | |
| 293 -- figure out what items to send | |
| 294 local items | |
| 295 if query.attr.node then | |
| 296 local node = stream.disco.nodes[query.attr.node] | |
| 297 if node then | |
| 298 items = node.items or {} | |
| 299 else | |
| 300 -- unknown node: give an error | |
| 301 local response = st.stanza('iq',{ | |
| 302 to = stanza.attr.from, | |
| 303 from = stanza.attr.to, | |
| 304 id = stanza.attr.id, | |
| 305 type = 'error' | |
| 306 }) | |
| 307 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#items'}):reset() | |
| 308 response:tag('error',{type = 'cancel'}):tag( | |
| 309 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} | |
| 310 ) | |
| 311 stream:send(response) | |
| 312 return true | |
| 313 end | |
| 314 else | |
| 315 items = stream.disco.items | |
| 316 end | |
| 317 -- construct the response | |
| 318 local result = st.stanza('query',{ | |
| 319 xmlns = 'http://jabber.org/protocol/disco#items', | |
| 320 node = query.attr.node | |
| 321 }) | |
| 322 for key,item in pairs(items) do | |
| 323 result:tag('item', item):reset() | |
| 324 end | |
| 325 stream:send(st.stanza('iq',{ | |
| 326 to = stanza.attr.from, | |
| 327 from = stanza.attr.to, | |
| 328 id = stanza.attr.id, | |
| 329 type = 'result' | |
| 330 }):add_child(result)) | |
| 331 return true | |
| 332 end | |
| 333 end); | |
| 334 | |
| 335 stream:hook("ready", function () | |
| 336 stream:disco_local_services(function (services) | |
| 337 for _, service in ipairs(services) do | |
| 338 for identity in pairs(stream.disco.cache[service.jid].identities) do | |
| 339 local category, type = identity:match("^(.*)/(.*)$"); | |
| 340 stream:event("disco/service-discovered/"..category, { | |
| 341 type = type, jid = service.jid; | |
| 342 }); | |
| 343 end | |
| 344 end | |
| 345 end); | |
| 346 end, 5); | |
| 347 end | |
| 348 | |
| 349 -- end of disco.lua |