plugins/disco.lua

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