plugins/disco.lua

changeset 6
b0fec41e695b
child 16
ae69cea97598
equal deleted inserted replaced
5:d9ed6e7d9936 6:b0fec41e695b
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

mercurial