| 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 verse = require "verse"; |
| 10 | local b64 = require("mime").b64; |
| 11 | local sha1 = require("prosody.util.hashes").sha1; |
| 12 | local calculate_hash = require "prosody.util.caps".calculate_hash; |
| 13 | |
| 14 | local xmlns_caps = "http://jabber.org/protocol/caps"; |
| 15 | local xmlns_disco = "http://jabber.org/protocol/disco"; |
| 16 | local xmlns_disco_info = xmlns_disco.."#info"; |
| 17 | local xmlns_disco_items = xmlns_disco.."#items"; |
| 18 | |
| 19 | function verse.plugins.disco(stream) |
| 20 | stream:add_plugin("presence"); |
| 21 | local disco_info_mt = { |
| 22 | __index = function(t, k) |
| 23 | local node = { identities = {}, features = {} }; |
| 24 | if k == "identities" or k == "features" then |
| 25 | return t[false][k] |
| 26 | end |
| 27 | t[k] = node; |
| 28 | return node; |
| 29 | end, |
| 30 | }; |
| 31 | local disco_items_mt = { |
| 32 | __index = function(t, k) |
| 33 | local node = { }; |
| 34 | t[k] = node; |
| 35 | return node; |
| 36 | end, |
| 37 | }; |
| 38 | stream.disco = { |
| 39 | cache = {}, |
| 40 | info = setmetatable({ |
| 41 | [false] = { |
| 42 | identities = { |
| 43 | {category = 'client', type='pc', name='Verse'}, |
| 44 | }, |
| 45 | features = { |
| 46 | [xmlns_caps] = true, |
| 47 | [xmlns_disco_info] = true, |
| 48 | [xmlns_disco_items] = true, |
| 49 | }, |
| 50 | }, |
| 51 | }, disco_info_mt); |
| 52 | items = setmetatable({[false]={}}, disco_items_mt); |
| 53 | }; |
| 54 | |
| 55 | stream.caps = {} |
| 56 | stream.caps.node = 'http://code.matthewwild.co.uk/verse/' |
| 57 | |
| 58 | local function build_self_disco_info_stanza(query_node) |
| 59 | local node = stream.disco.info[query_node or false]; |
| 60 | if query_node and query_node == stream.caps.node .. "#" .. stream.caps.hash then |
| 61 | node = stream.disco.info[false]; |
| 62 | end |
| 63 | local identities, features = node.identities, node.features |
| 64 | |
| 65 | -- construct the response |
| 66 | local result = verse.stanza("query", { |
| 67 | xmlns = xmlns_disco_info, |
| 68 | node = query_node, |
| 69 | }); |
| 70 | for _,identity in pairs(identities) do |
| 71 | result:tag('identity', identity):up() |
| 72 | end |
| 73 | for feature in pairs(features) do |
| 74 | result:tag('feature', { var = feature }):up() |
| 75 | end |
| 76 | return result; |
| 77 | end |
| 78 | |
| 79 | setmetatable(stream.caps, { |
| 80 | __call = function (...) -- vararg: allow calling as function or member |
| 81 | -- retrieve the c stanza to insert into the |
| 82 | -- presence stanza |
| 83 | local hash = calculate_hash(build_self_disco_info_stanza()) |
| 84 | stream.caps.hash = hash; |
| 85 | -- TODO proper caching.... some day |
| 86 | return verse.stanza('c', { |
| 87 | xmlns = xmlns_caps, |
| 88 | hash = 'sha-1', |
| 89 | node = stream.caps.node, |
| 90 | ver = hash |
| 91 | }) |
| 92 | end |
| 93 | }) |
| 94 | |
| 95 | function stream:set_identity(identity, node) |
| 96 | self.disco.info[node or false].identities = { identity }; |
| 97 | stream:event("disco-info-changed"); |
| 98 | end |
| 99 | |
| 100 | function stream:add_identity(identity, node) |
| 101 | local identities = self.disco.info[node or false].identities; |
| 102 | identities[#identities + 1] = identity; |
| 103 | stream:event("disco-info-changed"); |
| 104 | end |
| 105 | |
| 106 | function stream:add_disco_feature(feature, node) |
| 107 | local feature = feature.var or feature; |
| 108 | self.disco.info[node or false].features[feature] = true; |
| 109 | stream:event("disco-info-changed"); |
| 110 | end |
| 111 | |
| 112 | function stream:remove_disco_feature(feature, node) |
| 113 | local feature = feature.var or feature; |
| 114 | self.disco.info[node or false].features[feature] = nil; |
| 115 | stream:event("disco-info-changed"); |
| 116 | end |
| 117 | |
| 118 | function stream:add_disco_item(item, node) |
| 119 | local items = self.disco.items[node or false]; |
| 120 | items[#items +1] = item; |
| 121 | end |
| 122 | |
| 123 | function stream:remove_disco_item(item, node) |
| 124 | local items = self.disco.items[node or false]; |
| 125 | for i=#items,1,-1 do |
| 126 | if items[i] == item then |
| 127 | table.remove(items, i); |
| 128 | end |
| 129 | end |
| 130 | end |
| 131 | |
| 132 | -- TODO Node? |
| 133 | function stream:jid_has_identity(jid, category, type) |
| 134 | local cached_disco = self.disco.cache[jid]; |
| 135 | if not cached_disco then |
| 136 | return nil, "no-cache"; |
| 137 | end |
| 138 | local identities = self.disco.cache[jid].identities; |
| 139 | if type then |
| 140 | return identities[category.."/"..type] or false; |
| 141 | end |
| 142 | -- Check whether we have any identities with this category instead |
| 143 | for identity in pairs(identities) do |
| 144 | if identity:match("^(.*)/") == category then |
| 145 | return true; |
| 146 | end |
| 147 | end |
| 148 | end |
| 149 | |
| 150 | function stream:jid_supports(jid, feature) |
| 151 | local cached_disco = self.disco.cache[jid]; |
| 152 | if not cached_disco or not cached_disco.features then |
| 153 | return nil, "no-cache"; |
| 154 | end |
| 155 | return cached_disco.features[feature] or false; |
| 156 | end |
| 157 | |
| 158 | function stream:get_local_services(category, type) |
| 159 | local host_disco = self.disco.cache[self.host]; |
| 160 | if not(host_disco) or not(host_disco.items) then |
| 161 | return nil, "no-cache"; |
| 162 | end |
| 163 | |
| 164 | local results = {}; |
| 165 | for _, service in ipairs(host_disco.items) do |
| 166 | if self:jid_has_identity(service.jid, category, type) then |
| 167 | table.insert(results, service.jid); |
| 168 | end |
| 169 | end |
| 170 | return results; |
| 171 | end |
| 172 | |
| 173 | function stream:disco_local_services(callback) |
| 174 | self:disco_items(self.host, nil, function (items) |
| 175 | if not items then |
| 176 | return callback({}); |
| 177 | end |
| 178 | local n_items = 0; |
| 179 | local function item_callback() |
| 180 | n_items = n_items - 1; |
| 181 | if n_items == 0 then |
| 182 | return callback(items); |
| 183 | end |
| 184 | end |
| 185 | |
| 186 | for _, item in ipairs(items) do |
| 187 | if item.jid then |
| 188 | n_items = n_items + 1; |
| 189 | self:disco_info(item.jid, nil, item_callback); |
| 190 | end |
| 191 | end |
| 192 | if n_items == 0 then |
| 193 | return callback(items); |
| 194 | end |
| 195 | end); |
| 196 | end |
| 197 | |
| 198 | function stream:disco_info(jid, node, callback) |
| 199 | local disco_request = verse.iq({ to = jid, type = "get" }) |
| 200 | :tag("query", { xmlns = xmlns_disco_info, node = node }); |
| 201 | self:send_iq(disco_request, function (result) |
| 202 | if result.attr.type == "error" then |
| 203 | return callback(nil, result:get_error()); |
| 204 | end |
| 205 | |
| 206 | local identities, features, extended = {}, {}, {}; |
| 207 | |
| 208 | for tag in result:get_child("query", xmlns_disco_info):childtags() do |
| 209 | if tag.name == "identity" then |
| 210 | identities[tag.attr.category.."/"..tag.attr.type] = tag.attr.name or true; |
| 211 | elseif tag.name == "feature" then |
| 212 | features[tag.attr.var] = true; |
| 213 | end |
| 214 | end |
| 215 | |
| 216 | for tag in result:get_child("query", xmlns_disco_info):childtags("x", "jabber:x:data") do |
| 217 | local form_type_field = tag:get_child_with_attr("field", nil, "var", "FORM_TYPE"); |
| 218 | local form_type = form_type_field and form_type_field:get_child_text("value"); |
| 219 | if form_type then |
| 220 | extended[form_type] = tag; |
| 221 | end |
| 222 | end |
| 223 | |
| 224 | if not self.disco.cache[jid] then |
| 225 | self.disco.cache[jid] = { nodes = {} }; |
| 226 | end |
| 227 | |
| 228 | if node then |
| 229 | if not self.disco.cache[jid].nodes[node] then |
| 230 | self.disco.cache[jid].nodes[node] = { nodes = {} }; |
| 231 | end |
| 232 | self.disco.cache[jid].nodes[node].identities = identities; |
| 233 | self.disco.cache[jid].nodes[node].features = features; |
| 234 | self.disco.cache[jid].nodes[node].extended = extended; |
| 235 | else |
| 236 | self.disco.cache[jid].identities = identities; |
| 237 | self.disco.cache[jid].features = features; |
| 238 | self.disco.cache[jid].extended = extended; |
| 239 | end |
| 240 | return callback(self.disco.cache[jid]); |
| 241 | end); |
| 242 | end |
| 243 | |
| 244 | function stream:disco_items(jid, node, callback) |
| 245 | local disco_request = verse.iq({ to = jid, type = "get" }) |
| 246 | :tag("query", { xmlns = xmlns_disco_items, node = node }); |
| 247 | self:send_iq(disco_request, function (result) |
| 248 | if result.attr.type == "error" then |
| 249 | return callback(nil, result:get_error()); |
| 250 | end |
| 251 | local disco_items = { }; |
| 252 | for tag in result:get_child("query", xmlns_disco_items):childtags() do |
| 253 | if tag.name == "item" then |
| 254 | table.insert(disco_items, { |
| 255 | name = tag.attr.name; |
| 256 | jid = tag.attr.jid; |
| 257 | node = tag.attr.node; |
| 258 | }); |
| 259 | end |
| 260 | end |
| 261 | |
| 262 | if not self.disco.cache[jid] then |
| 263 | self.disco.cache[jid] = { nodes = {} }; |
| 264 | end |
| 265 | |
| 266 | if node then |
| 267 | if not self.disco.cache[jid].nodes[node] then |
| 268 | self.disco.cache[jid].nodes[node] = { nodes = {} }; |
| 269 | end |
| 270 | self.disco.cache[jid].nodes[node].items = disco_items; |
| 271 | else |
| 272 | self.disco.cache[jid].items = disco_items; |
| 273 | end |
| 274 | return callback(disco_items); |
| 275 | end); |
| 276 | end |
| 277 | |
| 278 | stream:hook("iq/"..xmlns_disco_info, function (stanza) |
| 279 | local query = stanza.tags[1]; |
| 280 | if stanza.attr.type == 'get' and query.name == "query" then |
| 281 | local query_tag = build_self_disco_info_stanza(query.attr.node); |
| 282 | local result = verse.reply(stanza):add_child(query_tag); |
| 283 | stream:send(result); |
| 284 | return true |
| 285 | end |
| 286 | end); |
| 287 | |
| 288 | stream:hook("iq/"..xmlns_disco_items, function (stanza) |
| 289 | local query = stanza.tags[1]; |
| 290 | if stanza.attr.type == 'get' and query.name == "query" then |
| 291 | -- figure out what items to send |
| 292 | local items = stream.disco.items[query.attr.node or false]; |
| 293 | |
| 294 | -- construct the response |
| 295 | local result = verse.reply(stanza):tag('query',{ |
| 296 | xmlns = xmlns_disco_items, |
| 297 | node = query.attr.node |
| 298 | }) |
| 299 | for i=1,#items do |
| 300 | result:tag('item', items[i]):up() |
| 301 | end |
| 302 | stream:send(result); |
| 303 | return true |
| 304 | end |
| 305 | end); |
| 306 | |
| 307 | local initial_disco_started; |
| 308 | stream:hook("ready", function () |
| 309 | if initial_disco_started then return; end |
| 310 | initial_disco_started = true; |
| 311 | |
| 312 | -- Using the disco cache, fires events for each identity of a given JID |
| 313 | local function scan_identities_for_service(service_jid) |
| 314 | local service_disco_info = stream.disco.cache[service_jid]; |
| 315 | if service_disco_info then |
| 316 | for identity in pairs(service_disco_info.identities) do |
| 317 | local category, type = identity:match("^(.*)/(.*)$"); |
| 318 | stream:event("disco/service-discovered/"..category, { |
| 319 | type = type, jid = service_jid; |
| 320 | }); |
| 321 | end |
| 322 | end |
| 323 | end |
| 324 | |
| 325 | stream:disco_info(stream.host, nil, function () |
| 326 | scan_identities_for_service(stream.host); |
| 327 | end); |
| 328 | |
| 329 | stream:disco_local_services(function (services) |
| 330 | for _, service in ipairs(services) do |
| 331 | scan_identities_for_service(service.jid); |
| 332 | end |
| 333 | stream:event("ready"); |
| 334 | end); |
| 335 | return true; |
| 336 | end, 50); |
| 337 | |
| 338 | stream:hook("presence-out", function (presence) |
| 339 | presence:remove_children("c", xmlns_caps); |
| 340 | presence:reset():add_child(stream:caps()):reset(); |
| 341 | end, 10); |
| 342 | |
| 343 | stream:hook("disco-info-changed", function () |
| 344 | stream:resend_presence(); |
| 345 | end); |
| 346 | end |
| 347 | |
| 348 | -- end of disco.lua |