plugins/roster.lua

1
local verse = require "verse";
2
local bare_jid = require "prosody.util.jid".bare;
3
 
4
local xmlns_roster = "jabber:iq:roster";
5
local xmlns_rosterver = "urn:xmpp:features:rosterver";
6
local t_insert = table.insert;
7
 
8
function verse.plugins.roster(stream)
9
	local ver_supported = false;
10
	local roster = {
11
		items = {};
12
		ver = "";
13
		-- TODO:
14
		-- groups = {};
15
	};
16
	stream.roster = roster;
17
 
18
	stream:hook("stream-features", function(features_stanza)
19
		if features_stanza:get_child("ver", xmlns_rosterver) then
20
			ver_supported = true;
21
		end
22
	end);
23
 
24
	local function item_lua2xml(item_table)
25
		local xml_item = verse.stanza("item", { xmlns = xmlns_roster });
26
		for k, v in pairs(item_table) do
27
			if k ~= "groups" then
28
				xml_item.attr[k] = v;
29
			else
30
				for i = 1,#v do
31
					xml_item:tag("group"):text(v[i]):up();
32
				end
33
			end
34
		end
35
		return xml_item;
36
	end
37
 
38
	local function item_xml2lua(xml_item)
39
		local item_table = { };
40
		local groups = {};
41
		item_table.groups = groups;
42
 
43
		for k, v in pairs(xml_item.attr) do
44
			if k ~= "xmlns" then
45
				item_table[k] = v
46
			end
47
		end
48
 
49
		for group in xml_item:childtags("group") do
50
			t_insert(groups, group:get_text())
51
		end
52
		return item_table;
53
	end
54
 
55
	function roster:load(r)
56
		roster.ver, roster.items = r.ver, r.items;
57
	end
58
 
59
	function roster:dump()
60
		return {
61
			ver = roster.ver,
62
			items = roster.items,
63
		};
64
	end
65
 
66
	-- should this be add_contact(item, callback) instead?
67
	function roster:add_contact(jid, name, groups, callback)
68
		local item = { jid = jid, name = name, groups = groups };
69
		local stanza = verse.iq({ type = "set" })
70
			:tag("query", { xmlns = xmlns_roster })
71
				:add_child(item_lua2xml(item));
72
		stream:send_iq(stanza, function (reply)
73
			if not callback then return end
74
			if reply.attr.type == "result" then
75
				callback(true);
76
			else
77
				callback(nil, reply);
78
			end
79
		end);
80
	end
81
	-- What about subscriptions?
82
 
83
	function roster:delete_contact(jid, callback)
84
		jid = (type(jid) == "table" and jid.jid) or jid;
85
		local item = { jid = jid, subscription = "remove" }
86
		if not roster.items[jid] then return false, "item-not-found"; end
87
		stream:send_iq(verse.iq({ type = "set" })
88
			:tag("query", { xmlns = xmlns_roster })
89
				:add_child(item_lua2xml(item)),
90
			function (reply)
91
				if not callback then return end
92
				if reply.attr.type == "result" then
93
					callback(true);
94
				else
95
					callback(nil, reply);
96
				end
97
			end);
98
	end
99
 
100
	local function add_item(item) -- Takes one roster <item/>
101
		local roster_item = item_xml2lua(item);
102
		roster.items[roster_item.jid] = roster_item;
103
	end
104
 
105
	-- Private low level
106
	local function delete_item(jid)
107
		local deleted_item = roster.items[jid];
108
		roster.items[jid] = nil;
109
		return deleted_item;
110
	end
111
 
112
	function roster:fetch(callback)
113
		stream:send_iq(verse.iq({type="get"}):tag("query", { xmlns = xmlns_roster, ver = ver_supported and roster.ver or nil }),
114
			function (result)
115
				if result.attr.type == "result" then
116
					local query = result:get_child("query", xmlns_roster);
117
					if query then
118
						roster.items = {};
119
						for item in query:childtags("item") do
120
							add_item(item)
121
						end
122
						roster.ver = query.attr.ver or "";
123
					end
124
					callback(roster);
125
				else
126
					callback(nil, result);
127
				end
128
			end);
129
	end
130
 
131
	stream:hook("iq/"..xmlns_roster, function(stanza)
132
		local type, from = stanza.attr.type, stanza.attr.from;
133
		if type == "set" and (not from or from == bare_jid(stream.jid)) then
134
			local query = stanza:get_child("query", xmlns_roster);
135
			local item = query and query:get_child("item");
136
			if item then
137
				local event, target;
138
				local jid = item.attr.jid;
139
				if item.attr.subscription == "remove" then
140
					event = "removed"
141
					target = delete_item(jid);
142
				else
143
					event = roster.items[jid] and "changed" or "added";
144
					add_item(item)
145
					target = roster.items[jid];
146
				end
147
				roster.ver = query.attr.ver;
148
				if target then
149
					stream:event("roster/item-"..event, target);
150
				end
151
			-- TODO else return error? Events?
152
			end
153
			stream:send(verse.reply(stanza))
154
			return true;
155
		end
156
	end);
157
end