Files
jitsi-meet/resources/prosody-plugins/mod_av_moderation_component.lua
damencho a76f9d548b feat: Move to use cjson everywhere.
We were using prosody,util.json and cjson at the same time, but the latter is more performant.
Adds some error handling which were missing with the prosody util json one.
2024-04-05 11:26:51 -05:00

332 lines
13 KiB
Lua

local util = module:require 'util';
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
local is_healthcheck_room = util.is_healthcheck_room;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local process_host_module = util.process_host_module;
local array = require "util.array";
local json = require 'cjson.safe';
local st = require 'util.stanza';
local muc_component_host = module:get_option_string('muc_component');
if muc_component_host == nil then
module:log('error', 'No muc_component specified. No muc to operate on!');
return;
end
module:log('info', 'Starting av_moderation for %s', muc_component_host);
-- Returns the index of the given element in the table
-- @param table in which to look
-- @param elem the element for which to find the index
function get_index_in_table(table, elem)
for index, value in pairs(table) do
if value == elem then
return index
end
end
end
-- 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 = internal_room_jid_match_rewrite(room.jid);
body_json.actor = actorJid;
body_json.mediaType = mediaType;
local body_json_str, error = json.encode(body_json);
if not body_json_str then
module:log('error', 'error encoding json room:%s error:%s', room.jid, error);
return;
end
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 change 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)
-- @param removed whether the jid is removed or added
function notify_whitelist_change(jid, moderators, room, mediaType, removed)
local body_json = {};
body_json.type = 'av_moderation';
body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.whitelists = room.av_moderation;
if removed then
body_json.removed = true;
end
body_json.mediaType = mediaType;
local moderators_body_json_str, error = json.encode(body_json);
if not moderators_body_json_str then
module:log('error', 'error encoding moderator json room:%s error:%s', room.jid, error);
return;
end
body_json.whitelists = nil;
if not removed then
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
end
local participant_body_json_str, error = json.encode(body_json);
if not participant_body_json_str then
module:log('error', 'error encoding participant json room:%s error:%s', room.jid, error);
return;
end
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
-- if the occupant is not moderator we send him that it is approved
-- if it is moderator we update him with the list, this is moderator joining or grant moderation was executed
if occupant.role == 'moderator' then
send_json_message(occupant.jid, moderators_body_json_str);
else
send_json_message(occupant.jid, participant_body_json_str);
end
end
end
end
-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
-- @param jid the jid to notify about the change
-- @param from the jid that triggered this
-- @param room the room where to send it
-- @param mediaType the mediaType it was approved for
function notify_jid_approved(jid, from, room, mediaType)
local body_json = {};
body_json.type = 'av_moderation';
body_json.room = internal_room_jid_match_rewrite(room.jid);
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
body_json.mediaType = mediaType;
body_json.from = from;
local json_message, error = json.encode(body_json);
if not json_message then
module:log('error', 'skip sending json message to:%s error:%s', jid, error);
return;
end
send_json_message(jid, json_message);
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
module:log('warn', 'No occupant %s found for %s', from, room.jid);
return false;
end
if occupant.role ~= 'moderator' then
module: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
if not room.av_moderation then
room.av_moderation = {};
room.av_moderation_actors = {};
end
room.av_moderation[mediaType] = array{};
room.av_moderation_actors[mediaType] = occupant.nick;
end
else
enabled = false;
if not room.av_moderation 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 = true;
for key,_ in pairs(room.av_moderation) do
if room.av_moderation[key] then
is_empty = false;
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 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(room_jid_match_rewrite(occupant_jid));
if not occupant_to_add then
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
return false;
end
if room.av_moderation then
local whitelist = room.av_moderation[mediaType];
if not whitelist then
whitelist = array{};
room.av_moderation[mediaType] = whitelist;
end
whitelist:push(occupant_jid);
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
return true;
else
-- this is a moderator asking the jid to unmute without enabling av moderation
-- let's just send the event
notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
end
elseif moderation_command.attr.jidToBlacklist then
local occupant_jid = moderation_command.attr.jidToBlacklist;
-- check if jid is in the room, if so remove it from the whitelist
-- inform all moderators and admins
local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
if not occupant_to_remove then
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
return false;
end
if room.av_moderation then
local whitelist = room.av_moderation[mediaType];
if whitelist then
local index = get_index_in_table(whitelist, occupant_jid)
if(index) then
whitelist:pop(index);
notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
end
end
return true;
end
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);
process_host_module(muc_component_host, function(host_module, host)
module:log('info','Hook to muc events on %s', host);
host_module:hook('muc-occupant-joined', occupant_joined, -2); -- make sure it runs after allowners or similar
host_module:hook('muc-set-affiliation', occupant_affiliation_changed, -1);
end);