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

274 lines
10 KiB

local register_component = require "core.componentmanager".register_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 muc_domain = "conference."..module:get_host();
local muc_name = "MUCMUCMUC!!!";
-- 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
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 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();
broadcast_message(current_nick, room, subject or "", nil);
return true;
end
function broadcast_presence(type, from, room, code)
local data = rooms:get(room, from);
local stanza = st.presence({type=type, from=from})
:tag("x", {xmlns='http://jabber.org/protocol/muc#user'})
:tag("item", {affiliation=data.affiliation, role=data.role}):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 ~= from then
stanza.attr.to = o_data.jid;
core_route_stanza(component, stanza);
else
me = o_data.jid;
end
end
end
if me then
stanza:tag("status", {code='110'});
stanza.attr.to = me;
core_route_stanza(component, stanza);
end
end
function broadcast_message(from, room, subject, body)
local stanza = st.message({type='groupchat', from=from});
if subject then stanza:tag('subject'):text(subject):up(); end
if body then stanza:tag('body'):text(body):up(); end
local r = rooms:get(room);
if r then
for occupant, o_data in pairs(r) do
stanza.attr.to = o_data.jid;
core_route_stanza(component, stanza);
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;
if stanza.name == "presence" then
if type == "error" then -- error, kick em out!
local data = rooms:get(room, to);
data.role = 'none';
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>
rooms:remove(room, to);
jid_nick:remove(from, room);
elseif type == "unavailable" then -- unavailable
if current_nick == to then
local data = rooms:get(room, to);
data.role = 'none';
broadcast_presence('unavailable', to, room);
rooms:remove(room, to);
jid_nick:remove(from, room);
end -- TODO else do nothing?
elseif not type then -- available
if current_nick then
if current_nick == to then -- simple presence
-- TODO broadcast
else -- change nick
if rooms:get(room, to) then
origin.send(st.error_reply(stanza, "cancel", "conflict"));
else
local data = rooms:get(room, current_nick);
broadcast_presence('unavailable', current_nick, room, '303');
rooms:remove(room, current_nick);
rooms:set(room, to, data);
jid_nick:set(from, room, to);
broadcast_presence(nil, to, room);
end
end
else -- enter room
if rooms:get(room, to) then
origin.send(st.error_reply(stanza, "cancel", "conflict"));
else
local data;
if not rooms:get(room) and not rooms_info:get(room) then -- new room
data = {affiliation='owner', role='moderator', jid=from};
end
if not data then -- new occupant
data = {affiliation='none', role='participant', jid=from};
end
rooms:set(room, to, data);
jid_nick:set(from, room, to);
local r = rooms:get(room);
if r then
for occupant, o_data in pairs(r) do
if occupant ~= from then
local pres = st.presence({to=from, from=occupant})
: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
broadcast_presence(nil, to, room);
-- TODO send discussion history
if rooms_info:get(room, 'subject') then
core_route_stanza(component, st.message({type='groupchat', from=room, to=from}):tag("subject"):text(rooms_info:get(room, 'subject')));
end
end
end
elseif type ~= 'result' then -- bad type
origin.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME correct error?
end
elseif stanza.name == "message" and type == "groupchat" then
-- groupchat messages not allowed in PM
origin.send(st.error_reply(stanza, "modify", "bad-request"));
else
origin.send(st.error_reply(stanza, "cancel", "not-implemented", "Private stanzas not implemented")); -- TODO route private stanza
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 subject = getText(stanza, {"subject"});
if subject then
set_subject(current_nick, room, subject);
else
broadcast_message(current_nick, room, nil, getText(stanza, {"body"}));
-- TODO add to discussion history
end
end
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
component = register_component(muc_domain, function(origin, stanza)
local to_node, to_host, to_resource = jid_split(stanza.attr.to);
if stanza.name == "presence" and stanza.attr.type ~= nil and stanza.attr.type ~= "unavailable" then
if type == "error" or type == "result" then return; end
origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); -- FIXME what's appropriate?
elseif 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);