Comparison

plugins/muc/mod_muc.lua @ 6609:d2faaaca695d

Merge 0.10->trunk
author Matthew Wild <mwild1@gmail.com>
date Fri, 27 Mar 2015 22:24:57 +0000
parent 6479:d016437e01bf
child 6745:6728ad041761
comparison
equal deleted inserted replaced
6608:b6e558febb7a 6609:d2faaaca695d
4 -- 4 --
5 -- This project is MIT/X11 licensed. Please see the 5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information. 6 -- COPYING file in the source package for more information.
7 -- 7 --
8 8
9 local array = require "util.array";
10
11 if module:get_host_type() ~= "component" then 9 if module:get_host_type() ~= "component" then
12 error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0); 10 error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
13 end 11 end
14 12
15 local muc_host = module:get_host();
16 local muc_name = module:get_option("name");
17 if type(muc_name) ~= "string" then muc_name = "Prosody Chatrooms"; end
18 local restrict_room_creation = module:get_option("restrict_room_creation");
19 if restrict_room_creation then
20 if restrict_room_creation == true then
21 restrict_room_creation = "admin";
22 elseif restrict_room_creation ~= "admin" and restrict_room_creation ~= "local" then
23 restrict_room_creation = nil;
24 end
25 end
26 local lock_rooms = module:get_option_boolean("muc_room_locking", false);
27 local lock_room_timeout = module:get_option_number("muc_room_lock_timeout", 300);
28
29 local muclib = module:require "muc"; 13 local muclib = module:require "muc";
30 local muc_new_room = muclib.new_room; 14 room_mt = muclib.room_mt; -- Yes, global.
15 local iterators = require "util.iterators";
31 local jid_split = require "util.jid".split; 16 local jid_split = require "util.jid".split;
32 local jid_bare = require "util.jid".bare; 17 local jid_bare = require "util.jid".bare;
33 local st = require "util.stanza"; 18 local st = require "util.stanza";
34 local uuid_gen = require "util.uuid".generate;
35 local um_is_admin = require "core.usermanager".is_admin; 19 local um_is_admin = require "core.usermanager".is_admin;
36 local hosts = prosody.hosts; 20
37 21 local rooms = module:shared "rooms";
38 rooms = {};
39 local rooms = rooms;
40 local persistent_rooms_storage = module:open_store("persistent");
41 local persistent_rooms = persistent_rooms_storage:get() or {};
42 local room_configs = module:open_store("config");
43
44 -- Configurable options
45 muclib.set_max_history_length(module:get_option_number("max_history_messages"));
46 22
47 module:depends("disco"); 23 module:depends("disco");
48 module:add_identity("conference", "text", muc_name); 24 module:add_identity("conference", "text", module:get_option_string("name", "Prosody Chatrooms"));
49 module:add_feature("http://jabber.org/protocol/muc"); 25 module:add_feature("http://jabber.org/protocol/muc");
26 module:depends "muc_unique"
27 module:require "muc/lock";
50 28
51 local function is_admin(jid) 29 local function is_admin(jid)
52 return um_is_admin(jid, module.host); 30 return um_is_admin(jid, module.host);
53 end 31 end
54 32
55 room_mt = muclib.room_mt; -- Yes, global. 33 do -- Monkey patch to make server admins room owners
56 local _set_affiliation = room_mt.set_affiliation; 34 local _get_affiliation = room_mt.get_affiliation;
57 local _get_affiliation = room_mt.get_affiliation; 35 function room_mt:get_affiliation(jid)
58 function muclib.room_mt:get_affiliation(jid) 36 if is_admin(jid) then return "owner"; end
59 if is_admin(jid) then return "owner"; end 37 return _get_affiliation(self, jid);
60 return _get_affiliation(self, jid); 38 end
61 end 39
62 function muclib.room_mt:set_affiliation(actor, jid, affiliation, callback, reason) 40 local _set_affiliation = room_mt.set_affiliation;
63 if is_admin(jid) then return nil, "modify", "not-acceptable"; end 41 function room_mt:set_affiliation(actor, jid, ...)
64 return _set_affiliation(self, actor, jid, affiliation, callback, reason); 42 if is_admin(jid) then return nil, "modify", "not-acceptable"; end
65 end 43 return _set_affiliation(self, actor, jid, ...);
66 44 end
67 local function room_route_stanza(room, stanza) module:send(stanza); end 45 end
46
47 local persistent = module:require "muc/persistent";
48 local persistent_rooms_storage = module:open_store("persistent");
49 local persistent_rooms = module:open_store("persistent", "map");
50 local room_configs = module:open_store("config");
51
68 local function room_save(room, forced) 52 local function room_save(room, forced)
69 local node = jid_split(room.jid); 53 local node = jid_split(room.jid);
70 persistent_rooms[room.jid] = room._data.persistent; 54 local is_persistent = persistent.get(room);
71 if room._data.persistent then 55 persistent_rooms:set(nil, room.jid, is_persistent);
56 if is_persistent then
72 local history = room._data.history; 57 local history = room._data.history;
73 room._data.history = nil; 58 room._data.history = nil;
74 local data = { 59 local data = {
75 jid = room.jid; 60 jid = room.jid;
76 _data = room._data; 61 _data = room._data;
82 room_configs:set(node, nil); 67 room_configs:set(node, nil);
83 if not next(room._occupants) then -- Room empty 68 if not next(room._occupants) then -- Room empty
84 rooms[room.jid] = nil; 69 rooms[room.jid] = nil;
85 end 70 end
86 end 71 end
87 if forced then persistent_rooms_storage:set(nil, persistent_rooms); end 72 end
88 end 73
89 74 -- Automatically destroy empty non-persistent rooms
90 function create_room(jid) 75 module:hook("muc-occupant-left",function(event)
91 local room = muc_new_room(jid); 76 local room = event.room
92 room.route_stanza = room_route_stanza; 77 if not room:has_occupant() and not persistent.get(room) then -- empty, non-persistent room
78 module:fire_event("muc-room-destroyed", { room = room });
79 end
80 end);
81
82 function track_room(room)
83 rooms[room.jid] = room;
84 -- When room is created, over-ride 'save' method
93 room.save = room_save; 85 room.save = room_save;
94 rooms[jid] = room; 86 end
95 if lock_rooms then 87
96 room.locked = true; 88 local function restore_room(jid)
97 if lock_room_timeout and lock_room_timeout > 0 then
98 module:add_timer(lock_room_timeout, function ()
99 if room.locked then
100 room:destroy(); -- Not unlocked in time
101 end
102 end);
103 end
104 end
105 module:fire_event("muc-room-created", { room = room });
106 return room;
107 end
108
109 local persistent_errors = false;
110 for jid in pairs(persistent_rooms) do
111 local node = jid_split(jid); 89 local node = jid_split(jid);
112 local data = room_configs:get(node); 90 local data = room_configs:get(node);
113 if data then 91 if data then
114 local room = create_room(jid); 92 local room = muclib.new_room(jid);
115 room._data = data._data; 93 room._data = data._data;
116 room._affiliations = data._affiliations; 94 room._affiliations = data._affiliations;
117 else -- missing room data 95 track_room(room);
118 persistent_rooms[jid] = nil; 96 return room;
119 module:log("error", "Missing data for room '%s', removing from persistent room list", jid); 97 end
120 persistent_errors = true; 98 end
121 end 99
122 end 100 function forget_room(room)
123 if persistent_errors then persistent_rooms_storage:set(nil, persistent_rooms); end 101 local room_jid = room.jid;
124 102 local node = jid_split(room.jid);
125 local host_room = muc_new_room(muc_host); 103 rooms[room_jid] = nil;
126 host_room.route_stanza = room_route_stanza; 104 room_configs:set(node, nil);
127 host_room.save = room_save; 105 if persistent.get(room) then
106 persistent_rooms:set(nil, room_jid, nil);
107 end
108 end
109
110 function get_room_from_jid(room_jid)
111 local room = rooms[room_jid];
112 if room == nil then
113 -- Check if in persistent storage
114 if persistent_rooms:get(nil, room_jid) then
115 room = restore_room(room_jid);
116 if room == nil then
117 module:log("error", "Missing data for room '%s', removing from persistent room list", room_jid);
118 persistent_rooms:set(nil, room_jid, nil);
119 end
120 end
121 end
122 return room
123 end
124
125 function each_room(local_only)
126 if not local_only then
127 for room_jid in pairs(persistent_rooms_storage:get(nil) or {}) do
128 if rooms[room_jid] == nil then -- Don't restore rooms that already exist
129 local room = restore_room(room_jid);
130 if room == nil then
131 module:log("error", "Missing data for room '%s', omitting from iteration", room_jid);
132 end
133 end
134 end
135 end
136 return iterators.values(rooms);
137 end
128 138
129 module:hook("host-disco-items", function(event) 139 module:hook("host-disco-items", function(event)
130 local reply = event.reply; 140 local reply = event.reply;
131 module:log("debug", "host-disco-items called"); 141 module:log("debug", "host-disco-items called");
132 for jid, room in pairs(rooms) do 142 for room in each_room() do
133 if not room:get_hidden() then 143 if not room:get_hidden() then
134 reply:tag("item", {jid=jid, name=room:get_name()}):up(); 144 reply:tag("item", {jid=room.jid, name=room:get_name()}):up();
135 end 145 end
136 end 146 end
137 end); 147 end);
138 148
139 local function handle_to_domain(event) 149 module:hook("muc-room-pre-create", function(event)
140 local origin, stanza = event.origin, event.stanza; 150 track_room(event.room);
141 local type = stanza.attr.type; 151 end, -1000);
142 if type == "error" or type == "result" then return; end 152
143 if stanza.name == "iq" and type == "get" then 153 module:hook("muc-room-destroyed",function(event)
144 local xmlns = stanza.tags[1].attr.xmlns; 154 return forget_room(event.room);
145 local node = stanza.tags[1].attr.node; 155 end)
146 if xmlns == "http://jabber.org/protocol/muc#unique" then 156
147 origin.send(st.reply(stanza):tag("unique", {xmlns = xmlns}):text(uuid_gen())); -- FIXME Random UUIDs can theoretically have collisions 157 do
148 else 158 local restrict_room_creation = module:get_option("restrict_room_creation");
149 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc 159 if restrict_room_creation == true then
150 end 160 restrict_room_creation = "admin";
151 else 161 end
152 host_room:handle_stanza(origin, stanza); 162 if restrict_room_creation then
153 --origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it")); 163 local host_suffix = module.host:gsub("^[^%.]+%.", "");
154 end 164 module:hook("muc-room-pre-create", function(event)
155 return true; 165 local origin, stanza = event.origin, event.stanza;
156 end 166 local user_jid = stanza.attr.from;
157 167 if not is_admin(user_jid) and not (
158 function stanza_handler(event) 168 restrict_room_creation == "local" and
159 local origin, stanza = event.origin, event.stanza; 169 select(2, jid_split(user_jid)) == host_suffix
160 local bare = jid_bare(stanza.attr.to); 170 ) then
161 local room = rooms[bare]; 171 origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
162 if not room then 172 return true;
163 if stanza.name ~= "presence" then 173 end
164 origin.send(st.error_reply(stanza, "cancel", "item-not-found")); 174 end);
165 return true; 175 end
166 end 176 end
167 if not(restrict_room_creation) or 177
168 is_admin(stanza.attr.from) or 178 for event_name, method in pairs {
169 (restrict_room_creation == "local" and select(2, jid_split(stanza.attr.from)) == module.host:gsub("^[^%.]+%.", "")) then 179 -- Normal room interactions
170 room = create_room(bare); 180 ["iq-get/bare/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
171 end 181 ["iq-get/bare/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
172 end 182 ["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
173 if room then 183 ["iq-get/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
174 room:handle_stanza(origin, stanza); 184 ["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
175 if not next(room._occupants) and not persistent_rooms[room.jid] then -- empty, non-persistent room 185 ["iq-get/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
176 module:fire_event("muc-room-destroyed", { room = room }); 186 ["message/bare"] = "handle_message_to_room" ;
177 rooms[bare] = nil; -- discard room 187 ["presence/bare"] = "handle_presence_to_room" ;
178 end 188 -- Host room
179 else 189 ["iq-get/host/http://jabber.org/protocol/disco#info:query"] = "handle_disco_info_get_query" ;
180 origin.send(st.error_reply(stanza, "cancel", "not-allowed")); 190 ["iq-get/host/http://jabber.org/protocol/disco#items:query"] = "handle_disco_items_get_query" ;
181 end 191 ["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
182 return true; 192 ["iq-get/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_get_command" ;
183 end 193 ["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
184 module:hook("iq/bare", stanza_handler, -1); 194 ["iq-get/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_get_to_room" ;
185 module:hook("message/bare", stanza_handler, -1); 195 ["message/host"] = "handle_message_to_room" ;
186 module:hook("presence/bare", stanza_handler, -1); 196 ["presence/host"] = "handle_presence_to_room" ;
187 module:hook("iq/full", stanza_handler, -1); 197 -- Direct to occupant (normal rooms and host room)
188 module:hook("message/full", stanza_handler, -1); 198 ["presence/full"] = "handle_presence_to_occupant" ;
189 module:hook("presence/full", stanza_handler, -1); 199 ["iq/full"] = "handle_iq_to_occupant" ;
190 module:hook("iq/host", handle_to_domain, -1); 200 ["message/full"] = "handle_message_to_occupant" ;
191 module:hook("message/host", handle_to_domain, -1); 201 } do
192 module:hook("presence/host", handle_to_domain, -1); 202 module:hook(event_name, function (event)
193 203 local origin, stanza = event.origin, event.stanza;
194 hosts[module.host].send = function(stanza) -- FIXME do a generic fix 204 local room_jid = jid_bare(stanza.attr.to);
195 if stanza.attr.type == "result" or stanza.attr.type == "error" then 205 local room = get_room_from_jid(room_jid);
196 module:send(stanza); 206 if room == nil then
197 else error("component.send only supports result and error stanzas at the moment"); end 207 -- Watch presence to create rooms
198 end 208 if stanza.attr.type == nil and stanza.name == "presence" then
199 209 room = muclib.new_room(room_jid);
200 hosts[module:get_host()].muc = { rooms = rooms }; 210 else
201 211 origin.send(st.error_reply(stanza, "cancel", "not-allowed"));
202 local saved = false; 212 return true;
203 module.save = function() 213 end
204 saved = true; 214 end
205 return {rooms = rooms}; 215 return room[method](room, origin, stanza);
206 end 216 end, -2)
207 module.restore = function(data) 217 end
208 for jid, oldroom in pairs(data.rooms or {}) do 218
209 local room = create_room(jid);
210 room._jid_nick = oldroom._jid_nick;
211 room._occupants = oldroom._occupants;
212 room._data = oldroom._data;
213 room._affiliations = oldroom._affiliations;
214 end
215 hosts[module:get_host()].muc = { rooms = rooms };
216 end
217
218 function shutdown_room(room, stanza)
219 for nick, occupant in pairs(room._occupants) do
220 stanza.attr.from = nick;
221 for jid in pairs(occupant.sessions) do
222 stanza.attr.to = jid;
223 room:_route_stanza(stanza);
224 room._jid_nick[jid] = nil;
225 end
226 room._occupants[nick] = nil;
227 end
228 end
229 function shutdown_component() 219 function shutdown_component()
230 if not saved then 220 local x = st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user"})
231 local stanza = st.presence({type = "unavailable"}) 221 :tag("status", { code = "332"}):up();
232 :tag("x", {xmlns = "http://jabber.org/protocol/muc#user"}) 222 for room in each_room(true) do
233 :tag("item", { affiliation='none', role='none' }):up() 223 room:clear(x);
234 :tag("status", { code = "332"}):up(); 224 end
235 for roomjid, room in pairs(rooms) do 225 end
236 shutdown_room(room, stanza);
237 end
238 shutdown_room(host_room, stanza);
239 end
240 end
241 module.unload = shutdown_component;
242 module:hook_global("server-stopping", shutdown_component); 226 module:hook_global("server-stopping", shutdown_component);
243 227
244 -- Ad-hoc commands 228 do -- Ad-hoc commands
245 module:depends("adhoc") 229 module:depends "adhoc";
246 local t_concat = table.concat; 230 local t_concat = table.concat;
247 local keys = require "util.iterators".keys; 231 local adhoc_new = module:require "adhoc".new;
248 local adhoc_new = module:require "adhoc".new; 232 local adhoc_initial = require "util.adhoc".new_initial_data_form;
249 local adhoc_initial = require "util.adhoc".new_initial_data_form; 233 local array = require "util.array";
250 local dataforms_new = require "util.dataforms".new; 234 local dataforms_new = require "util.dataforms".new;
251 235
252 local destroy_rooms_layout = dataforms_new { 236 local destroy_rooms_layout = dataforms_new {
253 title = "Destroy rooms"; 237 title = "Destroy rooms";
254 instructions = "Select the rooms to destroy"; 238 instructions = "Select the rooms to destroy";
255 239
256 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" }; 240 { name = "FORM_TYPE", type = "hidden", value = "http://prosody.im/protocol/muc#destroy" };
257 { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"}; 241 { name = "rooms", type = "list-multi", required = true, label = "Rooms to destroy:"};
258 }; 242 };
259 243
260 local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function() 244 local destroy_rooms_handler = adhoc_initial(destroy_rooms_layout, function()
261 return { rooms = array.collect(keys(rooms)):sort() }; 245 return { rooms = array.collect(each_room()):pluck("jid"):sort(); };
262 end, function(fields, errors) 246 end, function(fields, errors)
263 if errors then 247 if errors then
264 local errmsg = {}; 248 local errmsg = {};
265 for name, err in pairs(errors) do 249 for name, err in pairs(errors) do
266 errmsg[#errmsg + 1] = name .. ": " .. err; 250 errmsg[#errmsg + 1] = name .. ": " .. err;
267 end 251 end
268 return { status = "completed", error = { message = t_concat(errmsg, "\n") } }; 252 return { status = "completed", error = { message = t_concat(errmsg, "\n") } };
269 end 253 end
270 for _, room in ipairs(fields.rooms) do 254 for _, room in ipairs(fields.rooms) do
271 rooms[room]:destroy(); 255 get_room_from_jid(room):destroy();
272 rooms[room] = nil; 256 end
273 end 257 return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") };
274 return { status = "completed", info = "The following rooms were destroyed:\n"..t_concat(fields.rooms, "\n") }; 258 end);
275 end); 259 local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin");
276 local destroy_rooms_desc = adhoc_new("Destroy Rooms", "http://prosody.im/protocol/muc#destroy", destroy_rooms_handler, "admin"); 260
277 261 module:provides("adhoc", destroy_rooms_desc);
278 module:provides("adhoc", destroy_rooms_desc); 262 end