Software /
code /
prosody
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 |