IMPORTANT: due to a drive failure, as of 13-Mar-2021, the Mercurial repository had to be re-mirrored, which changed every commit SHA. The old SHAs and trees are backed up in the vault branches. Please migrate to the new branches as soon as you can.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
prosody/plugins/mod_muc.lua

439 lines
17 KiB

-- Prosody IM v0.4
-- Copyright (C) 2008-2009 Matthew Wild
-- Copyright (C) 2008-2009 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local datamanager = require "util.datamanager";
local datetime = require "util.datetime";
local register_component = require "core.componentmanager".register_component;
local deregister_component = require "core.componentmanager".deregister_component;
local jid_split = require "util.jid".split;
local jid_bare = require "util.jid".bare;
local st = require "util.stanza";
local log = require "util.logger".init("mod_muc");
local multitable_new = require "util.multitable".new;
local t_insert, t_remove = table.insert, table.remove;
if module:get_host_type() ~= "component" then
error("MUC should be loaded as a component, please see http://prosody.im/doc/components", 0);
end
local muc_domain = module:get_host();
local muc_name = "Chatrooms";
local history_length = 20;
-- room_name -> room
-- occupant_room_nick -> data
-- affiliation = ...
-- role
-- jid = occupant's real jid
local rooms = multitable_new();
local jid_nick = multitable_new(); -- real jid -> room's jid -> room nick
-- room_name -> info
-- name - the room's friendly name
-- subject - the room's subject
-- non-anonymous = true|nil
-- persistent = true|nil
-- history = {preserialized stanzas}
local rooms_info = multitable_new();
local persist_list = datamanager.load(nil, muc_domain, 'room_list') or {};
for room in pairs(persist_list) do
rooms_info:set(room, datamanager.store(room, muc_domain, 'rooms') or nil);
end
local component;
function filter_xmlns_from_array(array, filters)
local count = 0;
for i=#array,1,-1 do
local attr = array[i].attr;
if filters[attr and attr.xmlns] then
t_remove(array, i);
count = count + 1;
end
end
return count;
end
function filter_xmlns_from_stanza(stanza, filters)
if filters then
if filter_xmlns_from_array(stanza.tags, filters) ~= 0 then
return stanza, filter_xmlns_from_array(stanza, filters);
end
end
return stanza, 0;
end
local presence_filters = {["http://jabber.org/protocol/muc"]=true;["http://jabber.org/protocol/muc#user"]=true};
function get_filtered_presence(stanza)
return filter_xmlns_from_stanza(st.clone(stanza), presence_filters);
end
local kickable_error_conditions = {
["gone"] = true;
["internal-server-error"] = true;
["item-not-found"] = true;
["jid-malformed"] = true;
["recipient-unavailable"] = true;
["redirect"] = true;
["remote-server-not-found"] = true;
["remote-server-timeout"] = true;
["service-unavailable"] = true;
};
function get_kickable_error(stanza)
for _, tag in ipairs(stanza.tags) do
if tag.name == "error" and tag.attr.xmlns == "jabber:client" then
for _, cond in ipairs(tag.tags) do
if cond.attr.xmlns == "urn:ietf:params:xml:ns:xmpp-stanzas" then
return kickable_error_conditions[cond.name] and cond.name;
end
end
return true; -- malformed error message
end
end
return true; -- malformed error message
end
function getUsingPath(stanza, path, getText)
local tag = stanza;
for _, name in ipairs(path) do
if type(tag) ~= 'table' then return; end
tag = tag:child_with_name(name);
end
if tag and getText then tag = table.concat(tag); end
return tag;
end
function getTag(stanza, path) return getUsingPath(stanza, path); end
function getText(stanza, path) return getUsingPath(stanza, path, true); end
function get_disco_info(stanza)
return st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
:tag("identity", {category='conference', type='text', name=muc_name}):up()
:tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
end
function get_disco_items(stanza)
local reply = st.iq({type='result', id=stanza.attr.id, from=muc_domain, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
for room in pairs(rooms_info:get()) do
reply:tag("item", {jid=room, name=rooms_info:get(room, "name")}):up();
end
return reply; -- TODO cache disco reply
end
function get_room_disco_info(stanza)
return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#info")
:tag("identity", {category='conference', type='text', name=rooms_info:get(stanza.attr.to, "name")}):up()
:tag("feature", {var="http://jabber.org/protocol/muc"}); -- TODO cache disco reply
end
function get_room_disco_items(stanza)
return st.iq({type='result', id=stanza.attr.id, from=stanza.attr.to, to=stanza.attr.from}):query("http://jabber.org/protocol/disco#items");
end -- TODO allow non-private rooms
function save_room(room)
local persistent = rooms_info:get(room, 'persistent');
if persistent then
datamanager.store(room, muc_domain, 'rooms', rooms_info:get(room));
end
if persistent ~= persist_list[room] then
if not persistent then
datamanager.store(room, muc_domain, 'rooms', nil);
end
persist_list[room] = persistent;
datamanager.store(nil, muc_domain, 'room_list', persist_list);
end
end
function set_subject(current_nick, room, subject)
-- TODO check nick's authority
if subject == "" then subject = nil; end
rooms_info:set(room, 'subject', subject);
save_room();
local msg = st.message({type='groupchat', from=current_nick})
:tag('subject'):text(subject):up();
broadcast_message_stanza(room, msg, false);
return true;
end
function broadcast_message_stanza(room, stanza, historic)
local r = rooms:get(room);
if r then
for occupant, o_data in pairs(r) do
for jid in pairs(o_data.sessions) do
stanza.attr.to = jid;
core_route_stanza(component, stanza);
end
end
if historic then -- add to history
local history = rooms_info:get(room, 'history');
if not history then history = {}; rooms_info:set(room, 'history', history); end
-- stanza = st.clone(stanza);
stanza:tag("delay", {xmlns = "urn:xmpp:delay", from = muc_domain, stamp = datetime.datetime()}):up(); -- XEP-0203
stanza:tag("x", {xmlns = "jabber:x:delay", from = muc_domain, stamp = datetime.legacy()}):up(); -- XEP-0091 (deprecated)
t_insert(history, st.clone(st.preserialize(stanza)));
while #history > history_length do t_remove(history, 1) end
end
end
end
function broadcast_presence_stanza(room, stanza, code, nick)
stanza = get_filtered_presence(stanza);
local data = rooms:get(room, stanza.attr.from);
stanza:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
:tag("item", {affiliation=data.affiliation, role=data.role, nick=nick}):up();
if code then
stanza:tag("status", {code=code}):up();
end
local me;
local r = rooms:get(room);
if r then
for occupant, o_data in pairs(r) do
if occupant ~= stanza.attr.from then
for jid in pairs(o_data.sessions) do
stanza.attr.to = jid;
core_route_stanza(component, stanza);
end
else
me = o_data;
end
end
end
if me then
stanza:tag("status", {code='110'});
for jid in pairs(me.sessions) do
stanza.attr.to = jid;
core_route_stanza(component, stanza);
end
end
end
function send_history(room, to)
local history = rooms_info:get(room, 'history'); -- send discussion history
if history then
for _, msg in ipairs(history) do
msg = st.deserialize(msg);
msg.attr.to=to;
core_route_stanza(component, msg);
end
end
if rooms_info:get(room, 'subject') then
core_route_stanza(component, st.message({type='groupchat', from=room, to=to}):tag("subject"):text(rooms_info:get(room, 'subject')));
end
end
function send_occupant_list(room, to)
local r = rooms:get(room);
if r then
local current_nick = jid_nick:get(to, room);
for occupant, o_data in pairs(r) do
if occupant ~= current_nick then
local pres = get_filtered_presence(o_data.sessions[o_data.jid]);
pres.attr.to, pres.attr.from = to, occupant;
pres:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
:tag("item", {affiliation=o_data.affiliation, role=o_data.role}):up();
core_route_stanza(component, pres);
end
end
end
end
function handle_to_occupant(origin, stanza) -- PM, vCards, etc
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
local current_nick = jid_nick:get(from, room);
local type = stanza.attr.type;
log("debug", "room: %s, current_nick: %s, stanza: %s", room or "nil", current_nick or "nil", stanza:top_tag());
if (select(2, jid_split(from)) == muc_domain) then error("Presence from the MUC itself!!!"); end
if stanza.name == "presence" then
local pr = get_filtered_presence(stanza);
pr.attr.from = current_nick;
if type == "error" then -- error, kick em out!
if current_nick then
log("debug", "kicking %s from %s", current_nick, room);
handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error presence')); -- send unavailable
end
elseif type == "unavailable" then -- unavailable
if current_nick then
log("debug", "%s leaving %s", current_nick, room);
local data = rooms:get(room, current_nick);
data.role = 'none';
broadcast_presence_stanza(room, pr);
rooms:remove(room, current_nick);
jid_nick:remove(from, room);
end
elseif not type then -- available
if current_nick then
--if #pr == #stanza or current_nick ~= to then -- commented because google keeps resending directed presence
if current_nick == to then -- simple presence
log("debug", "%s broadcasted presence", current_nick);
rooms:get(room, current_nick).sessions[from] = pr;
broadcast_presence_stanza(room, pr);
else -- change nick
if rooms:get(room, to) then
log("debug", "%s couldn't change nick", current_nick);
origin.send(st.error_reply(stanza, "cancel", "conflict"));
else
local data = rooms:get(room, current_nick);
local to_nick = select(3, jid_split(to));
if to_nick then
log("debug", "%s (%s) changing nick to %s", current_nick, data.jid, to);
local p = st.presence({type='unavailable', from=current_nick});
broadcast_presence_stanza(room, p, '303', to_nick);
rooms:remove(room, current_nick);
rooms:set(room, to, data);
jid_nick:set(from, room, to);
pr.attr.from = to;
rooms:get(room, to).sessions[from] = pr;
broadcast_presence_stanza(room, pr);
else
--TODO malformed-jid
end
end
end
--else -- possible rejoin
-- log("debug", "%s had connection replaced", current_nick);
-- handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('Replaced by new connection'):up()); -- send unavailable
-- handle_to_occupant(origin, stanza); -- resend available
--end
else -- enter room
local new_nick = to;
if rooms:get(room, to) then
new_nick = nil;
end
if not new_nick then
log("debug", "%s couldn't join due to nick conflict: %s", from, to);
origin.send(st.error_reply(stanza, "cancel", "conflict"));
else
log("debug", "%s joining as %s", from, to);
local data;
if not rooms:get(room) and not rooms_info:get(room) then -- new room
rooms_info:set(room, 'name', (jid_split(room)));
data = {affiliation='owner', role='moderator', jid=from, sessions={[from]=get_filtered_presence(stanza)}};
end
if not data then -- new occupant
data = {affiliation='none', role='participant', jid=from, sessions={[from]=get_filtered_presence(stanza)}};
end
rooms:set(room, to, data);
jid_nick:set(from, room, to);
send_occupant_list(room, from);
pr.attr.from = to;
broadcast_presence_stanza(room, pr);
send_history(room, from);
end
end
elseif type ~= 'result' then -- bad type
origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
end
elseif not current_nick and type ~= "error" and type ~= "result" then -- not in room
origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
elseif stanza.name == "message" and type == "groupchat" then -- groupchat messages not allowed in PM
origin.send(st.error_reply(stanza, "modify", "bad-request"));
elseif stanza.name == "message" and type == "error" and get_kickable_error(stanza) then
log("debug", "%s kicked from %s for sending an error message", current_nick, room);
handle_to_occupant(origin, st.presence({type='unavailable', from=from, to=to}):tag('status'):text('This participant is kicked from the room because he sent an error message to another occupant')); -- send unavailable
else -- private stanza
local o_data = rooms:get(room, to);
if o_data then
log("debug", "%s sent private stanza to %s (%s)", from, to, o_data.jid);
local jid = o_data.jid;
-- TODO if stanza.name=='iq' and type=='get' and stanza.tags[1].attr.xmlns == 'vcard-temp' then jid = jid_bare(jid); end
stanza.attr.to, stanza.attr.from = jid, current_nick;
core_route_stanza(component, stanza);
elseif type ~= "error" and type ~= "result" then -- recipient not in room
origin.send(st.error_reply(stanza, "cancel", "item-not-found", "Recipient not in room"));
end
end
end
function handle_to_room(origin, stanza) -- presence changes and groupchat messages, along with disco/etc
local type = stanza.attr.type;
if stanza.name == "iq" and type == "get" then -- disco requests
local xmlns = stanza.tags[1].attr.xmlns;
if xmlns == "http://jabber.org/protocol/disco#info" then
origin.send(get_room_disco_info(stanza));
elseif xmlns == "http://jabber.org/protocol/disco#items" then
origin.send(get_room_disco_items(stanza));
else
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
elseif stanza.name == "message" and type == "groupchat" then
local from, to = stanza.attr.from, stanza.attr.to;
local room = jid_bare(to);
local current_nick = jid_nick:get(from, room);
if not current_nick then -- not in room
origin.send(st.error_reply(stanza, "cancel", "not-acceptable"));
else
local from = stanza.attr.from;
stanza.attr.from = current_nick;
local subject = getText(stanza, {"subject"});
if subject then
set_subject(current_nick, room, subject); -- TODO use broadcast_message_stanza
else
broadcast_message_stanza(room, stanza, true);
end
end
elseif stanza.name == "presence" then -- hack - some buggy clients send presence updates to the room rather than their nick
local to = stanza.attr.to;
local current_nick = jid_nick:get(stanza.attr.from, to);
if current_nick then
stanza.attr.to = current_nick;
handle_to_occupant(origin, stanza);
stanza.attr.to = to;
elseif type ~= "error" and type ~= "result" then
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
elseif stanza.name == "message" and not stanza.attr.type and #stanza.tags == 1 and jid_nick:get(stanza.attr.from, stanza.attr.to)
and stanza.tags[1].name == "x" and stanza.tags[1].attr.xmlns == "http://jabber.org/protocol/muc#user" and #stanza.tags[1].tags == 1
and stanza.tags[1].tags[1].name == "invite" and stanza.tags[1].tags[1].attr.to then
local _from, _to = stanza.attr.from, stanza.attr.to;
local _invitee = stanza.tags[1].tags[1].attr.to;
stanza.attr.from, stanza.attr.to = _to, _invitee;
stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = _from, nil;
core_route_stanza(component, stanza);
stanza.tags[1].tags[1].attr.from, stanza.tags[1].tags[1].attr.to = nil, _invitee;
stanza.attr.from, stanza.attr.to = _from, _to;
else
if type == "error" or type == "result" then return; end
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
end
end
function handle_to_domain(origin, stanza)
local type = stanza.attr.type;
if type == "error" or type == "result" then return; end
if stanza.name == "iq" and type == "get" then
local xmlns = stanza.tags[1].attr.xmlns;
if xmlns == "http://jabber.org/protocol/disco#info" then
origin.send(get_disco_info(stanza));
elseif xmlns == "http://jabber.org/protocol/disco#items" then
origin.send(get_disco_items(stanza));
else
origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- TODO disco/etc
end
else
origin.send(st.error_reply(stanza, "cancel", "service-unavailable", "The muc server doesn't deal with messages and presence directed at it"));
end
end
register_component(muc_domain, function(origin, stanza)
local to_node, to_host, to_resource = jid_split(stanza.attr.to);
if to_resource and not to_node then
if type == "error" or type == "result" then return; end
origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- host/resource
elseif to_resource then
handle_to_occupant(origin, stanza);
elseif to_node then
handle_to_room(origin, stanza)
else -- to the main muc domain
if type == "error" or type == "result" then return; end
handle_to_domain(origin, stanza);
end
end);
module.unload = function()
deregister_component(muc_domain);
end
module.save = function()
return {rooms = rooms.data; jid_nick = jid_nick.data; rooms_info = rooms_info.data; persist_list = persist_list};
end
module.restore = function(data)
rooms.data, jid_nick.data, rooms_info.data, persist_list =
data.rooms or {}, data.jid_nick or {}, data.rooms_info or {}, data.persist_list or {};
end