|
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 "util.stanza" |
|
27 local b64 = require("mime").b64 |
|
28 local sha1 = require("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 return true; |
|
49 elseif item2.category < item1.category then return false; |
|
50 end |
|
51 if item1.type < item2.type then return true; |
|
52 elseif item2.type < item1.type then return false; |
|
53 end |
|
54 if (not item1['xml:lang'] and item2['xml:lang']) |
|
55 or (item2['xml:lang'] and item1['xml:lang'] < item2['xml:lang']) then |
|
56 return true |
|
57 end |
|
58 return false |
|
59 end |
|
60 |
|
61 local function cmp_feature(item1, item2) |
|
62 return item1.var < item2.var |
|
63 end |
|
64 |
|
65 local function calculate_hash() |
|
66 table.sort(bot.disco.info.identities, cmp_identity) |
|
67 table.sort(bot.disco.info.features, cmp_feature) |
|
68 local S = '' |
|
69 for key,identity in pairs(bot.disco.info.identities) do |
|
70 S = S .. string.format('%s/%s/%s/%s', identity.category, identity.type, |
|
71 identity['xml:lang'] or '', identity.name or '') |
|
72 .. '<' |
|
73 end |
|
74 for key,feature in pairs(bot.disco.info.features) do |
|
75 S = S .. feature.var |
|
76 .. '<' |
|
77 end |
|
78 -- FIXME: make sure S is utf8-encoded |
|
79 return (b64(sha1(S))) |
|
80 end |
|
81 |
|
82 setmetatable(bot.caps, |
|
83 { |
|
84 __call = function (...) -- vararg: allow calling as function or member |
|
85 -- retrieve the c stanza to insert into the |
|
86 -- presence stanza |
|
87 local hash = calculate_hash() |
|
88 return st.stanza('c', |
|
89 {xmlns = 'http://jabber.org/protocol/caps', |
|
90 hash = 'sha-1', |
|
91 node = bot.caps.node, |
|
92 ver = hash}) |
|
93 end}) |
|
94 |
|
95 bot:hook("iq/http://jabber.org/protocol/disco#info", |
|
96 function (event) |
|
97 local stanza = event.stanza |
|
98 if stanza.attr.type == 'get' then |
|
99 local query = stanza:child_with_name('query') |
|
100 if not query then return; end |
|
101 -- figure out what identities/features to send |
|
102 local identities |
|
103 local features |
|
104 if query.attr.node then |
|
105 local hash = calculate_hash() |
|
106 local node = bot.disco.nodes[query.attr.node] |
|
107 if node and node.info then |
|
108 identities = node.info.identities or {} |
|
109 features = node.info.identities or {} |
|
110 elseif query.attr.node == bot.caps.node..'#'..hash then |
|
111 -- matches caps hash, so use the main info |
|
112 identities = bot.disco.info.identities |
|
113 features = bot.disco.info.features |
|
114 else |
|
115 -- unknown node: give an error |
|
116 local response = st.stanza('iq', |
|
117 {to = stanza.attr.from, |
|
118 from = stanza.attr.to, |
|
119 id = stanza.attr.id, |
|
120 type = 'error'}) |
|
121 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#info'}):reset() |
|
122 response:tag('error',{type = 'cancel'}) |
|
123 :tag('item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}) |
|
124 bot:send(response) |
|
125 return true |
|
126 end |
|
127 else |
|
128 identities = bot.disco.info.identities |
|
129 features = bot.disco.info.features |
|
130 end |
|
131 -- construct the response |
|
132 local result = st.stanza('query', |
|
133 {xmlns = 'http://jabber.org/protocol/disco#info', |
|
134 node = query.attr.node}) |
|
135 for key,identity in pairs(identities) do |
|
136 result:tag('identity', identity):reset() |
|
137 end |
|
138 for key,feature in pairs(features) do |
|
139 result:tag('feature', feature):reset() |
|
140 end |
|
141 bot:send(st.stanza('iq', |
|
142 {to = stanza.attr.from, |
|
143 from = stanza.attr.to, |
|
144 id = stanza.attr.id, |
|
145 type = 'result'}) |
|
146 :add_child(result)) |
|
147 return true |
|
148 end |
|
149 end); |
|
150 |
|
151 bot:hook("iq/http://jabber.org/protocol/disco#items", |
|
152 function (event) |
|
153 local stanza = event.stanza |
|
154 if stanza.attr.type == 'get' then |
|
155 local query = stanza:child_with_name('query') |
|
156 if not query then return; end |
|
157 -- figure out what items to send |
|
158 local items |
|
159 if query.attr.node then |
|
160 local node = bot.disco.nodes[query.attr.node] |
|
161 if node then |
|
162 items = node.items or {} |
|
163 else |
|
164 -- unknown node: give an error |
|
165 local response = st.stanza('iq', |
|
166 {to = stanza.attr.from, |
|
167 from = stanza.attr.to, |
|
168 id = stanza.attr.id, |
|
169 type = 'error'}) |
|
170 response:tag('query',{xmlns = 'http://jabber.org/protocol/disco#items'}):reset() |
|
171 response:tag('error',{type = 'cancel'}) |
|
172 :tag('item-not-found',{xmlns = 'urn:ietf:params:xml:ns:xmpp-stanzas'}) |
|
173 bot:send(response) |
|
174 return true |
|
175 end |
|
176 else |
|
177 items = bot.disco.items |
|
178 end |
|
179 -- construct the response |
|
180 local result = st.stanza('query', |
|
181 {xmlns = 'http://jabber.org/protocol/disco#items', |
|
182 node = query.attr.node}) |
|
183 for key,item in pairs(items) do |
|
184 result:tag('item', item):reset() |
|
185 end |
|
186 bot:send(st.stanza('iq', |
|
187 {to = stanza.attr.from, |
|
188 from = stanza.attr.to, |
|
189 id = stanza.attr.id, |
|
190 type = 'result'}) |
|
191 :add_child(result)) |
|
192 return true |
|
193 end |
|
194 end); |
|
195 end |
|
196 |
|
197 -- end of disco.lua |