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