Mercurial > prosody-hg
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 |
| parents | d016437e01bf |
| children | 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 |
