diff --git a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example index 8d445c68bc..0d9389eb28 100644 --- a/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example +++ b/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example @@ -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 diff --git a/package-lock.json b/package-lock.json index 080ed6291f..1ec002cdff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 34cdf64d59..797b676d2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/resources/prosody-plugins/mod_av_moderation.lua b/resources/prosody-plugins/mod_av_moderation.lua new file mode 100644 index 0000000000..a41a977556 --- /dev/null +++ b/resources/prosody-plugins/mod_av_moderation.lua @@ -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); diff --git a/resources/prosody-plugins/mod_av_moderation_component.lua b/resources/prosody-plugins/mod_av_moderation_component.lua new file mode 100644 index 0000000000..65b3db17ad --- /dev/null +++ b/resources/prosody-plugins/mod_av_moderation_component.lua @@ -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 diff --git a/resources/prosody-plugins/mod_muc_poltergeist.lua b/resources/prosody-plugins/mod_muc_poltergeist.lua index fbb8fc1d22..39c4bcc4ad 100644 --- a/resources/prosody-plugins/mod_muc_poltergeist.lua +++ b/resources/prosody-plugins/mod_muc_poltergeist.lua @@ -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; }; diff --git a/resources/prosody-plugins/util.lib.lua b/resources/prosody-plugins/util.lib.lua index 2cfcea68e9..23efbc20b4 100644 --- a/resources/prosody-plugins/util.lib.lua +++ b/resources/prosody-plugins/util.lib.lua @@ -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;