Fixed: User resources not disconnected correctly on account delete
[prosody.git] / plugins / mod_muc.lua
1
2
3 local register_component = require "core.componentmanager".register_component;
4 local deregister_component = require "core.componentmanager".deregister_component;
5 local jid_split = require "util.jid".split;
6 local jid_bare = require "util.jid".bare;
7 local st = require "util.stanza";
8 local log = require "util.logger".init("mod_muc");
9 local multitable_new = require "util.multitable".new;
10 local t_insert, t_remove = table.insert, table.remove;
11
12 if module:get_host_type() ~= "component" then
13         error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
14 end
15
16 local muc_domain = module:get_host();
17 local muc_name = "MUCMUCMUC!!!";
18 local history_length = 20;
19
20 -- room_name -> room
21         -- occupant_room_nick -> data
22                 -- affiliation = ...
23                 -- role
24                 -- jid = occupant's real jid
25 local rooms = multitable_new();
26
27 local jid_nick = multitable_new(); -- real jid -> room's jid -> room nick
28
29 -- room_name -> info
30         -- name - the room's friendly name
31         -- subject - the room's subject
32         -- non-anonymous = true|nil
33         -- persistent = true|nil
34         -- history = {preserialized stanzas}
35 local rooms_info = multitable_new();
36
37 local persist_list = datamanager.load(nil, muc_domain, 'room_list') or {};
38 for room in pairs(persist_list) do
39         rooms_info:set(room, datamanager.store(room, muc_domain, 'rooms') or nil);
40 end
41
42 local component;
43
44 function getUsingPath(stanza, path, getText)
45         local tag = stanza;
46         for _, name in ipairs(path) do
47                 if type(tag) ~= 'table' then return; end
48                 tag = tag:child_with_name(name);
49         end
50         if tag and getText then tag = table.concat(tag); end
51         return tag;
52 end
53 function getTag(stanza, path) return getUsingPath(stanza, path); end
54 function getText(stanza, path) return getUsingPath(stanza, path, true); end
55
56 function get_disco_info(stanza)
57         return st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
58                 :tag("identity", {category='conference', type='text', name=muc_name}):up()
59                 :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
60 end
61 function get_disco_items(stanza)
62         local reply = st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
63         for room in pairs(rooms_info:get()) do
64                 reply:tag("item", {jid=room, name=rooms_info:get(room, "name")}):up();
65         end
66         return reply; -- TODO cache disco reply
67 end
68 function get_room_disco_info(stanza)
69         return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
70                 :tag("identity", {category='conference', type='text', name=rooms_info:get(stanza.attr.to, "name")}):up()
71                 :tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
72 end
73 function get_room_disco_items(stanza)
74         return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
75 end -- TODO allow non-private rooms
76
77 function save_room(room)
78         local persistent = rooms_info:get(room, 'persistent');
79         if persistent then
80                 datamanager.store(room, muc_domain, 'rooms', rooms_info:get(room));
81         end
82         if persistent ~= persist_list[room] then
83                 if not persistent then
84                         datamanager.store(room, muc_domain, 'rooms', nil);
85                 end
86                 persist_list[room] = persistent;
87                 datamanager.store(nil, muc_domain, 'room_list', persist_list);
88         end
89 end
90
91 function set_subject(current_nick, room, subject)
92         -- TODO check nick's authority
93         if subject == "" then subject = nil; end
94         rooms_info:set(room, 'subject', subject);
95         save_room();
96         broadcast_message(current_nick, room, subject or "", nil);
97         return true;
98 end
99
100 function broadcast_presence(type, from, room, code, newnick)
101         local data = rooms:get(room, from);
102         local stanza = st.presence({type=type, from=from})
103                 :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
104                 :tag("item", {affiliation=data.affiliation, role=data.role, nick = newnick}):up();
105         if code then
106                 stanza:tag("status", {code=code}):up();
107         end
108         local me;
109         local r = rooms:get(room);
110         if r then
111                 for occupant, o_data in pairs(r) do
112                         if occupant ~= from then
113                                 stanza.attr.to = o_data.jid;
114                                 core_route_stanza(component, stanza);
115                         else
116                                 me = o_data.jid;
117                         end
118                 end
119         end
120         if me then
121                 stanza:tag("status", {code='110'});
122                 stanza.attr.to = me;
123                 core_route_stanza(component, stanza);
124         end
125 end
126 function broadcast_message(from, room, subject, body)
127         local stanza = st.message({type='groupchat', from=from});
128         if subject then stanza:tag('subject'):text(subject):up(); end
129         if body then stanza:tag('body'):text(body):up(); end
130         local r = rooms:get(room);
131         if r then
132                 for occupant, o_data in pairs(r) do
133                         stanza.attr.to = o_data.jid;
134                         core_route_stanza(component, stanza);
135                 end
136                 if not subject and body then -- add to history
137                         local history = rooms_info:get(room, 'history');
138                         if not history then history = {}; rooms_info:set(room, 'history', history); end
139                         -- stanza = st.deserialize(st.preserialize(stanza));
140                         stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
141                         stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
142                         t_insert(history, st.preserialize(stanza));
143                         while #history > history_length do t_remove(history, 1) end
144                 end
145         end
146 end
147
148 function handle_to_occupant(origin, stanza) -- PM, vCards, etc
149         local from, to = stanza.attr.from, stanza.attr.to;
150         local room = jid_bare(to);
151         local current_nick = jid_nick:get(from, room);
152         local type = stanza.attr.type;
153         if stanza.name == "presence" then
154                 if type == "error" then -- error, kick em out!
155                         local data = rooms:get(room, to);
156                         data.role = 'none';
157                         broadcast_presence('unavailable', to, room); -- TODO also add <status>This participant is kicked from the room because he sent an error presence: badformed error stanza</status>
158                         rooms:remove(room, to);
159                         jid_nick:remove(from, room);
160                 elseif type == "unavailable" then -- unavailable
161                         if current_nick == to then
162                                 local data = rooms:get(room, to);
163                                 data.role = 'none';
164                                 broadcast_presence('unavailable', to, room);
165                                 rooms:remove(room, to);
166                                 jid_nick:remove(from, room);
167                         end -- TODO else do nothing?
168                 elseif not type then -- available
169                         if current_nick then
170                                 if current_nick == to then -- simple presence
171                                         -- TODO broadcast
172                                 else -- change nick
173                                         if rooms:get(room, to) then
174                                                 origin.send(st.error_reply(stanza, "cancel", "conflict"));
175                                         else
176                                                 local data = rooms:get(room, current_nick);
177                                                 local to_nick = select(3, jid_split(to));
178                                                 if to_nick then
179                                                         broadcast_presence('unavailable', current_nick, room, '303', to_nick);
180                                                         rooms:remove(room, current_nick);
181                                                         rooms:set(room, to, data);
182                                                         jid_nick:set(from, room, to);
183                                                         broadcast_presence(nil, to, room, nil);
184                                                 else
185                                                         --TODO: malformed-jid
186                                                 end
187                                         end
188                                 end
189                         else -- enter room
190                                 if rooms:get(room, to) then
191                                         origin.send(st.error_reply(stanza, "cancel", "conflict"));
192                                 else
193                                         local data;
194                                         if not rooms:get(room) and not rooms_info:get(room) then -- new room
195                                                 data = {affiliation='owner', role='moderator', jid=from};
196                                         end
197                                         if not data then -- new occupant
198                                                 data = {affiliation='none', role='participant', jid=from};
199                                         end
200                                         rooms:set(room, to, data);
201                                         jid_nick:set(from, room, to);
202                                         local r = rooms:get(room);
203                                         if r then
204                                                 for occupant, o_data in pairs(r) do
205                                                         if occupant ~= from then
206                                                                 local pres = st.presence({to=from, from=occupant})
207                                                                         :tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
208                                                                         :tag("item", {affiliation=o_data.affiliation, role=o_data.role}):up();
209                                                                 core_route_stanza(component, pres);
210                                                         end
211                                                 end
212                                         end
213                                         broadcast_presence(nil, to, room);
214                                         local history = rooms_info:get(room, 'history'); -- send discussion history
215                                         if history then
216                                                 for _, msg in ipairs(history) do
217                                                         msg = st.deserialize(msg);
218                                                         msg.attr.to=from;
219                                                         core_route_stanza(component, msg);
220                                                 end
221                                         end
222                                         if rooms_info:get(room, 'subject') then
223                                                 core_route_stanza(component, st.message({type='groupchat', from=room, to=from}):tag("subject"):text(rooms_info:get(room, 'subject')));
224                                         end
225                                 end
226                         end
227                 elseif type ~= 'result' then -- bad type
228                         origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
229                 end
230         elseif stanza.name == "message" and type == "groupchat" then
231                 -- groupchat messages not allowed in PM
232                 origin.send(st.error_reply(stanza, "modify", "bad-request"));
233         else
234                 origin.send(st.error_reply(stanza, "cancel", "not-implemented", "Private stanzas not implemented")); -- TODO route private stanza
235         end
236 end
237
238 function handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
239         local type = stanza.attr.type;
240         if stanza.name == "iq" and type == "get" then -- disco requests
241                 local xmlns = stanza.tags[1].attr.xmlns;
242                 if xmlns == "http://jabber.org/protocol/disco#info" then
243                         origin.send(get_room_disco_info(stanza));
244                 elseif xmlns == "http://jabber.org/protocol/disco#items" then
245                         origin.send(get_room_disco_items(stanza));
246                 else
247                         origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
248                 end
249         elseif stanza.name == "message" and type == "groupchat" then
250                 local from, to = stanza.attr.from, stanza.attr.to;
251                 local room = jid_bare(to);
252                 local current_nick = jid_nick:get(from, room);
253                 if not current_nick then -- not in room
254                         origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
255                 else
256                         local subject = getText(stanza, {"subject"});
257                         if subject then
258                                 set_subject(current_nick, room, subject);
259                         else
260                                 broadcast_message(current_nick, room, nil, getText(stanza, {"body"}));
261                                 -- TODO add to discussion history
262                         end
263                 end
264         else
265                 if type == "error" or type == "result" then return; end
266                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
267         end
268 end
269
270 function handle_to_domain(origin, stanza)
271         local type = stanza.attr.type;
272         if type == "error" or type == "result" then return; end
273         if stanza.name == "iq" and type == "get" then
274                 local xmlns = stanza.tags[1].attr.xmlns;
275                 if xmlns == "http://jabber.org/protocol/disco#info" then
276                         origin.send(get_disco_info(stanza));
277                 elseif xmlns == "http://jabber.org/protocol/disco#items" then
278                         origin.send(get_disco_items(stanza));
279                 else
280                         origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
281                 end
282         else
283                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it"));
284         end
285 end
286
287 register_component(muc_domain, function(origin, stanza)
288         local to_node, to_host, to_resource = jid_split(stanza.attr.to);
289         if stanza.name == "presence" and stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" then
290                 if type == "error" or type == "result" then return; end
291                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME what's appropriate?
292         elseif to_resource and not to_node then
293                 if type == "error" or type == "result" then return; end
294                 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- host/resource
295         elseif to_resource then
296                 handle_to_occupant(origin, stanza);
297         elseif to_node then
298                 handle_to_room(origin, stanza)
299         else -- to the main muc domain
300                 if type == "error" or type == "result" then return; end
301                 handle_to_domain(origin, stanza);
302         end
303 end);
304
305 module.unload = function()
306         deregister_component(muc_domain);
307 end
308 module.save = function()
309         return {rooms = rooms.data; jid_nick = jid_nick.data; rooms_info = rooms_info.data; persist_list = persist_list};
310 end
311 module.restore = function(data)
312         rooms.data, jid_nick.data, rooms_info.data, persist_list =
313         data.rooms or {}, data.jid_nick or {}, data.rooms_info or {}, data.persist_list or {};
314 end