feat: A/V moderation (prosody module) (#9106)
* feat(prosody-modules): Moves a function for getting room to util. * feat: Audio/Video moderation. * squash: Fix docs. * squash: Changes a field name in the message for adding jid to whitelist. * squash: Moves to boolean from boolean string. * squash: Only moderators get whitelist on join. * squash: Check whether in room and moderator. * squash: Send to participants only message about approval. Skips sending the whole list. * feat: Separates enable/disable by media type. Adds actor to the messages to inform who enabled it. * squash: Fixes reporting disable of the feature. * squash: Fixes init of av_moderation_actors. * squash: Fixes av_moderation_actor jid to be room jid. * squash: Fixes comments. * squash: Fixes warning about shadowing definition. * squash: Updates ljm. * fix: Fixes auto-granting from jicofo. * squash: Further simplify...
This commit is contained in:
parent
6d15bcc719
commit
5c08b1ec5b
|
@ -35,6 +35,7 @@ VirtualHost "jitmeet.example.com"
|
|||
key = "/etc/prosody/certs/jitmeet.example.com.key";
|
||||
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
|
||||
}
|
||||
av_moderation_component = "avmoderation.jitmeet.example.com"
|
||||
speakerstats_component = "speakerstats.jitmeet.example.com"
|
||||
conference_duration_component = "conferenceduration.jitmeet.example.com"
|
||||
-- we need bosh
|
||||
|
@ -46,6 +47,7 @@ VirtualHost "jitmeet.example.com"
|
|||
"external_services";
|
||||
"conference_duration";
|
||||
"muc_lobby_rooms";
|
||||
"av_moderation";
|
||||
}
|
||||
c2s_require_encryption = false
|
||||
lobby_muc = "lobby.jitmeet.example.com"
|
||||
|
@ -86,6 +88,9 @@ Component "speakerstats.jitmeet.example.com" "speakerstats_component"
|
|||
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
Component "lobby.jitmeet.example.com" "muc"
|
||||
storage = "memory"
|
||||
restrict_room_creation = true
|
||||
|
|
|
@ -11058,8 +11058,8 @@
|
|||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
|
||||
"from": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
|
||||
"version": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
|
||||
"from": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "1.0.2",
|
||||
"@jitsi/sdp-interop": "1.0.3",
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#923aa449c4e6e46823dc990b19041898e09263ca",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#88560a8a5ed1ce6e7b829d7fc8e460b4c963f119",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
local formdecode = require 'util.http'.formdecode;
|
||||
|
||||
local avmoderation_component = module:get_option_string('av_moderation_component', 'avmoderation'..module.host);
|
||||
|
||||
-- Advertise AV Moderation so client can pick up the address and use it
|
||||
module:add_identity('component', 'av_moderation', avmoderation_component);
|
||||
|
||||
-- Extract 'room' param from URL when session is created
|
||||
function update_session(event)
|
||||
local session = event.session;
|
||||
|
||||
if session.jitsi_web_query_room then
|
||||
-- no need for an update
|
||||
return;
|
||||
end
|
||||
|
||||
local query = event.request.url.query;
|
||||
if query ~= nil then
|
||||
local params = formdecode(query);
|
||||
-- The room name and optional prefix from the web query
|
||||
session.jitsi_web_query_room = params.room;
|
||||
session.jitsi_web_query_prefix = params.prefix or '';
|
||||
end
|
||||
end
|
||||
module:hook_global('bosh-session', update_session);
|
||||
module:hook_global('websocket-session', update_session);
|
|
@ -0,0 +1,250 @@
|
|||
local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
|
||||
local is_healthcheck_room = module:require 'util'.is_healthcheck_room;
|
||||
local json = require 'util.json';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
module:log('info', 'Starting av_moderation for %s', muc_component_host);
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
function send_json_message(to_jid, json_message)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
-- Notifies that av moderation has been enabled or disabled
|
||||
-- @param jid the jid to notify, if missing will notify all occupants
|
||||
-- @param enable whether it is enabled or disabled
|
||||
-- @param room the room
|
||||
-- @param actorJid the jid that is performing the enable/disable operation (the muc jid)
|
||||
-- @param mediaType the media type for the moderation
|
||||
function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.enabled = enable;
|
||||
body_json.room = room.jid;
|
||||
body_json.actor = actorJid;
|
||||
body_json.mediaType = mediaType;
|
||||
local body_json_str = json.encode(body_json);
|
||||
|
||||
if jid then
|
||||
send_json_message(jid, body_json_str)
|
||||
else
|
||||
for _, occupant in room:each_occupant() do
|
||||
send_json_message(occupant.jid, body_json_str)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Notifies about a jid added to the whitelist. Notifies all moderators and admin and the jid itself
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param moderators whether to notify all moderators in the room
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType used only when a participant is approved (not sent to moderators)
|
||||
function notify_whitelist_change(jid, moderators, room, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = room.jid;
|
||||
body_json.whitelists = room.av_moderation;
|
||||
local moderators_body_json_str = json.encode(body_json);
|
||||
body_json.whitelists = nil;
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
body_json.mediaType = mediaType;
|
||||
local participant_body_json_str = json.encode(body_json);
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
if moderators and occupant.role == 'moderator' then
|
||||
send_json_message(occupant.jid, moderators_body_json_str);
|
||||
elseif occupant.jid == jid then
|
||||
send_json_message(occupant.jid, participant_body_json_str);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local moderation_command = event.stanza:get_child('av_moderation');
|
||||
|
||||
if moderation_command then
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local mediaType = moderation_command.attr.mediaType;
|
||||
if mediaType then
|
||||
if mediaType ~= 'audio' and mediaType ~= 'video' then
|
||||
module:log('warn', 'Wrong mediaType %s for %s', mediaType, room.jid);
|
||||
return false;
|
||||
end
|
||||
else
|
||||
module:log('warn', 'Missing mediaType for %s', room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if moderation_command.attr.enable ~= nil then
|
||||
local enabled;
|
||||
if moderation_command.attr.enable == 'true' then
|
||||
enabled = true;
|
||||
if room.av_moderation and room.av_moderation[mediaType] then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
room.av_moderation[mediaType] = {};
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
else
|
||||
enabled = false;
|
||||
if not room.av_moderation or not room.av_moderation[mediaType] then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
room.av_moderation[mediaType] = nil;
|
||||
room.av_moderation_actors[mediaType] = nil;
|
||||
|
||||
-- clears room.av_moderation if empty
|
||||
local is_empty = false;
|
||||
for key,_ in pairs(room.av_moderation) do
|
||||
if room.av_moderation[key] then
|
||||
is_empty = true;
|
||||
end
|
||||
end
|
||||
if is_empty then
|
||||
room.av_moderation = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist and room.av_moderation then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
-- check if jid is in the room, if so add it to whitelist
|
||||
-- inform all moderators and admins and the jid
|
||||
local occupant_to_add = room:get_occupant_by_nick(occupant_jid);
|
||||
|
||||
if not occupant_to_add then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if not whitelist then
|
||||
whitelist = {};
|
||||
room.av_moderation[mediaType] = whitelist;
|
||||
end
|
||||
table.insert(whitelist, occupant_jid);
|
||||
|
||||
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType);
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- return error
|
||||
return false
|
||||
end
|
||||
|
||||
-- handles new occupants to inform them about the state enabled/disabled, new moderators also get and the whitelist
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_occupants_enable(
|
||||
occupant.jid, true, room, room.av_moderation_actors[mediaType], mediaType);
|
||||
end
|
||||
end
|
||||
|
||||
-- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
|
||||
-- from allowners module) but iterating over room occupants returns the correct role
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if moderator send the whitelist
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
notify_whitelist_change(room_occupant.jid, false, room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- when a occupant was granted moderator we need to update him with the whitelist
|
||||
function occupant_affiliation_changed(event)
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
if event.actor and event.affiliation == 'owner' and event.room.av_moderation then
|
||||
local room = event.room;
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
notify_whitelist_change(occupant.jid, false, room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
||||
|
||||
-- executed on every host added internally in prosody, including components
|
||||
function process_host(host)
|
||||
if host == muc_component_host then -- the conference muc component
|
||||
module:log('info','Hook to muc events on %s', host);
|
||||
|
||||
local muc_module = module:context(host);
|
||||
muc_module:hook('muc-occupant-joined', occupant_joined, -2); -- make sure it runs after allowners or similar
|
||||
muc_module:hook('muc-set-affiliation', occupant_affiliation_changed, -1);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[muc_component_host] == nil then
|
||||
module:log('info', 'No muc component found, will listen for it: %s', muc_component_host);
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler('host-activated', process_host);
|
||||
else
|
||||
process_host(muc_component_host);
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
local bare = require "util.jid".bare;
|
||||
local get_room_from_jid = module:require "util".get_room_from_jid;
|
||||
local get_room_by_name_and_subdomain = module:require "util".get_room_by_name_and_subdomain;
|
||||
local jid = require "util.jid";
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
@ -39,21 +39,6 @@ local disableTokenVerification
|
|||
|
||||
-- poltergaist management functions
|
||||
|
||||
-- Returns the room if available, work and in multidomain mode
|
||||
-- @param room_name the name of the room
|
||||
-- @param group name of the group (optional)
|
||||
-- @return returns room if found or nil
|
||||
function get_room(room_name, group)
|
||||
local room_address = jid.join(room_name, module:get_host());
|
||||
-- if there is a group we are in multidomain mode and that group is not
|
||||
-- our parent host
|
||||
if group and group ~= "" and group ~= parentHostName then
|
||||
room_address = "["..group.."]"..room_address;
|
||||
end
|
||||
|
||||
return get_room_from_jid(room_address);
|
||||
end
|
||||
|
||||
--- Verifies room name, domain name with the values in the token
|
||||
-- @param token the token we received
|
||||
-- @param room_name the room name
|
||||
|
@ -105,7 +90,7 @@ end
|
|||
prosody.events.add_handler("pre-jitsi-authentication", function(session)
|
||||
|
||||
if (session.jitsi_meet_context_user) then
|
||||
local room = get_room(
|
||||
local room = get_room_by_name_and_subdomain(
|
||||
session.jitsi_web_query_room,
|
||||
session.jitsi_web_query_prefix);
|
||||
|
||||
|
@ -194,7 +179,7 @@ function handle_create_poltergeist (event)
|
|||
|
||||
-- If the provided room conference doesn't exist then we
|
||||
-- can't add a poltergeist to it.
|
||||
local room = get_room(room_name, group);
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
|
@ -257,7 +242,7 @@ function handle_update_poltergeist (event)
|
|||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room(room_name, group);
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
|
@ -299,7 +284,7 @@ function handle_remove_poltergeist (event)
|
|||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room(room_name, group);
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
|
|
|
@ -8,23 +8,19 @@ local http_headers = {
|
|||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
|
||||
};
|
||||
|
||||
local muc_domain_prefix
|
||||
= module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
-- defaults to module.host, the module that uses the utility
|
||||
local muc_domain_base
|
||||
= module:get_option_string("muc_mapper_domain_base", module.host);
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base", module.host);
|
||||
|
||||
-- The "real" MUC domain that we are proxying to
|
||||
local muc_domain = module:get_option_string(
|
||||
"muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
|
||||
local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
|
||||
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
|
||||
-- The pattern used to extract the target subdomain
|
||||
-- (e.g. extract 'foo' from 'conference.foo.example.com')
|
||||
local target_subdomain_pattern
|
||||
= "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
|
||||
local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
|
||||
|
||||
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc compoent
|
||||
local roomless_iqs = {};
|
||||
|
@ -120,6 +116,23 @@ function get_room_from_jid(room_jid)
|
|||
end
|
||||
end
|
||||
|
||||
-- Returns the room if available, work and in multidomain mode
|
||||
-- @param room_name the name of the room
|
||||
-- @param group name of the group (optional)
|
||||
-- @return returns room if found or nil
|
||||
function get_room_by_name_and_subdomain(room_name, subdomain)
|
||||
local room_address;
|
||||
|
||||
-- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
|
||||
if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
else
|
||||
room_address = jid.join(room_name, muc_domain);
|
||||
end
|
||||
|
||||
return get_room_from_jid(room_address);
|
||||
end
|
||||
|
||||
function async_handler_wrapper(event, handler)
|
||||
if not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
|
@ -347,6 +360,7 @@ return {
|
|||
is_feature_allowed = is_feature_allowed;
|
||||
is_healthcheck_room = is_healthcheck_room;
|
||||
get_room_from_jid = get_room_from_jid;
|
||||
get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
|
||||
async_handler_wrapper = async_handler_wrapper;
|
||||
presence_check_status = presence_check_status;
|
||||
room_jid_match_rewrite = room_jid_match_rewrite;
|
||||
|
|
Loading…
Reference in New Issue