| 1 | -- disco.lua |
| 2 | |
| 3 | -- Responds to service discovery queries (XEP-0030), and calculates the entity |
| 4 | -- capabilities hash (XEP-0115). |
| 5 | |
| 6 | -- Fill the bot.disco.info.identities, bot.disco.info.features, and |
| 7 | -- bot.disco.items tables with the relevant disco data. It comes pre-populated |
| 8 | -- to advertise support for disco#info, disco#items, and entity capabilities, |
| 9 | -- and to identify itself as Riddim. |
| 10 | |
| 11 | -- If you want to advertise a node, add entries to the bot.disco.nodes table |
| 12 | -- with the relevant data. The bot.disco.nodes table should have the same |
| 13 | -- format as bot.disco (without the nodes element). The nodes are NOT |
| 14 | -- automatically added to the base disco items, so you will need to add them |
| 15 | -- yourself. |
| 16 | |
| 17 | -- To property implement Entity Capabilities, you should make sure that you |
| 18 | -- send a "c" element within presence stanzas that are sent. The correct "c" |
| 19 | -- element can be obtained by calling bot.caps() (or bot:caps()). |
| 20 | |
| 21 | -- Hubert Chathi <hubert@uhoreg.ca> |
| 22 | |
| 23 | -- This file is hereby placed in the public domain. Feel free to modify and |
| 24 | -- redistribute it at will |
| 25 | |
| 26 | local st = require "prosody.util.stanza" |
| 27 | local b64 = require("mime").b64 |
| 28 | local sha1 = require("prosody.util.hashes").sha1 |
| 29 | |
| 30 | function riddim.plugins.disco(bot) |
| 31 | bot.disco = {} |
| 32 | bot.disco.info = {} |
| 33 | bot.disco.info.identities = { |
| 34 | {category = 'client', type='bot', name='Riddim'}, |
| 35 | } |
| 36 | bot.disco.info.features = { |
| 37 | {var = 'http://jabber.org/protocol/caps'}, |
| 38 | {var = 'http://jabber.org/protocol/disco#info'}, |
| 39 | {var = 'http://jabber.org/protocol/disco#items'}, |
| 40 | } |
| 41 | bot.disco.items = {} |
| 42 | bot.disco.nodes = {} |
| 43 | |
| 44 | bot.caps = {} |
| 45 | bot.caps.node = 'http://code.matthewwild.co.uk/riddim/' |
| 46 | |
| 47 | local function cmp_identity(item1, item2) |
| 48 | if item1.category < item2.category then |
| 49 | return true; |
| 50 | elseif item2.category < item1.category then |
| 51 | return false; |
| 52 | end |
| 53 | if item1.type < item2.type then |
| 54 | return true; |
| 55 | elseif item2.type < item1.type then |
| 56 | return false; |
| 57 | end |
| 58 | if (not item1['xml:lang'] and item2['xml:lang']) or |
| 59 | (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then |
| 60 | return true |
| 61 | end |
| 62 | return false |
| 63 | end |
| 64 | |
| 65 | local function cmp_feature(item1, item2) |
| 66 | return item1.var < item2.var |
| 67 | end |
| 68 | |
| 69 | local function calculate_hash() |
| 70 | table.sort(bot.disco.info.identities, cmp_identity) |
| 71 | table.sort(bot.disco.info.features, cmp_feature) |
| 72 | local S = '' |
| 73 | for key,identity in pairs(bot.disco.info.identities) do |
| 74 | S = S .. string.format( |
| 75 | '%s/%s/%s/%s', identity.category, identity.type, |
| 76 | identity['xml:lang'] or '', identity.name or '' |
| 77 | ) .. '<' |
| 78 | end |
| 79 | for key,feature in pairs(bot.disco.info.features) do |
| 80 | S = S .. feature.var .. '<' |
| 81 | end |
| 82 | -- FIXME: make sure S is utf8-encoded |
| 83 | return (b64(sha1(S))) |
| 84 | end |
| 85 | |
| 86 | setmetatable(bot.caps, { |
| 87 | __call = function (...) -- vararg: allow calling as function or member |
| 88 | -- retrieve the c stanza to insert into the |
| 89 | -- presence stanza |
| 90 | local hash = calculate_hash() |
| 91 | return st.stanza('c', { |
| 92 | xmlns = 'http://jabber.org/protocol/caps', |
| 93 | hash = 'sha-1', |
| 94 | node = bot.caps.node, |
| 95 | ver = hash |
| 96 | }) |
| 97 | end |
| 98 | }) |
| 99 | |
| 100 | bot:hook("iq/http://jabber.org/protocol/disco#info", function (event) |
| 101 | local stanza = event.stanza |
| 102 | if stanza.attr.type == 'get' then |
| 103 | local query = stanza:child_with_name('query') |
| 104 | if not query then return; end |
| 105 | -- figure out what identities/features to send |
| 106 | local identities |
| 107 | local features |
| 108 | if query.attr.node then |
| 109 | local hash = calculate_hash() |
| 110 | local node = bot.disco.nodes[query.attr.node] |
| 111 | if node and node.info then |
| 112 | identities = node.info.identities or {} |
| 113 | features = node.info.identities or {} |
| 114 | elseif query.attr.node == bot.caps.node..'#'..hash then |
| 115 | -- matches caps hash, so use the main info |
| 116 | identities = bot.disco.info.identities |
| 117 | features = bot.disco.info.features |
| 118 | else |
| 119 | -- unknown node: give an error |
| 120 | local response = st.stanza('iq',{ |
| 121 | to = stanza.attr.from, |
| 122 | from = stanza.attr.to, |
| 123 | id = stanza.attr.id, |
| 124 | type = 'error' |
| 125 | }) |
| 126 | response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#info'}):reset() |
| 127 | response:tag('error',{type = 'cancel'}):tag( |
| 128 | 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} |
| 129 | ) |
| 130 | bot:send(response) |
| 131 | return true |
| 132 | end |
| 133 | else |
| 134 | identities = bot.disco.info.identities |
| 135 | features = bot.disco.info.features |
| 136 | end |
| 137 | -- construct the response |
| 138 | local result = st.stanza('query',{ |
| 139 | xmlns = 'http://jabber.org/protocol/disco#info', |
| 140 | node = query.attr.node |
| 141 | }) |
| 142 | for key,identity in pairs(identities) do |
| 143 | result:tag('identity', identity):reset() |
| 144 | end |
| 145 | for key,feature in pairs(features) do |
| 146 | result:tag('feature', feature):reset() |
| 147 | end |
| 148 | bot:send(st.stanza('iq',{ |
| 149 | to = stanza.attr.from, |
| 150 | from = stanza.attr.to, |
| 151 | id = stanza.attr.id, |
| 152 | type = 'result' |
| 153 | }):add_child(result)) |
| 154 | return true |
| 155 | end |
| 156 | end); |
| 157 | |
| 158 | bot:hook("iq/http://jabber.org/protocol/disco#items", function (event) |
| 159 | local stanza = event.stanza |
| 160 | if stanza.attr.type == 'get' then |
| 161 | local query = stanza:child_with_name('query') |
| 162 | if not query then return; end |
| 163 | -- figure out what items to send |
| 164 | local items |
| 165 | if query.attr.node then |
| 166 | local node = bot.disco.nodes[query.attr.node] |
| 167 | if node then |
| 168 | items = node.items or {} |
| 169 | else |
| 170 | -- unknown node: give an error |
| 171 | local response = st.stanza('iq',{ |
| 172 | to = stanza.attr.from, |
| 173 | from = stanza.attr.to, |
| 174 | id = stanza.attr.id, |
| 175 | type = 'error' |
| 176 | }) |
| 177 | response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#items'}):reset() |
| 178 | response:tag('error',{type = 'cancel'}):tag( |
| 179 | 'item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'} |
| 180 | ) |
| 181 | bot:send(response) |
| 182 | return true |
| 183 | end |
| 184 | else |
| 185 | items = bot.disco.items |
| 186 | end |
| 187 | -- construct the response |
| 188 | local result = st.stanza('query',{ |
| 189 | xmlns = 'http://jabber.org/protocol/disco#items', |
| 190 | node = query.attr.node |
| 191 | }) |
| 192 | for key,item in pairs(items) do |
| 193 | result:tag('item', item):reset() |
| 194 | end |
| 195 | bot:send(st.stanza('iq',{ |
| 196 | to = stanza.attr.from, |
| 197 | from = stanza.attr.to, |
| 198 | id = stanza.attr.id, |
| 199 | type = 'result' |
| 200 | }):add_child(result)) |
| 201 | return true |
| 202 | end |
| 203 | end); |
| 204 | end |
| 205 | |
| 206 | -- end of disco.lua |