plugins/disco.lua

changeset 16
ae69cea97598
parent 6
b0fec41e695b
equal deleted inserted replaced
15:22e6c003a83a 16:ae69cea97598
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

mercurial