From 2e6f14f87201b301a8cd13637a9b0353432aace1 Mon Sep 17 00:00:00 2001 From: Shawn Chin Date: Tue, 27 Sep 2022 20:59:30 +0100 Subject: [PATCH] feat(reservations) start lobby and set password from reservation (#12215) * feat(reservations) support enabling lobby and password based on reservations data * Add warning about unhandled use case * feat(lobby) Support automated activation of lobby --- .../prosody-plugins/mod_muc_lobby_rooms.lua | 26 ++- .../prosody-plugins/mod_persistent_lobby.lua | 174 ++++++++++++++++++ .../prosody-plugins/mod_reservations.lua | 98 ++++++++-- 3 files changed, 280 insertions(+), 18 deletions(-) create mode 100644 resources/prosody-plugins/mod_persistent_lobby.lua diff --git a/resources/prosody-plugins/mod_muc_lobby_rooms.lua b/resources/prosody-plugins/mod_muc_lobby_rooms.lua index 64a591d31..b3535d7ce 100644 --- a/resources/prosody-plugins/mod_muc_lobby_rooms.lua +++ b/resources/prosody-plugins/mod_muc_lobby_rooms.lua @@ -30,6 +30,8 @@ local jid_bare = require 'util.jid'.bare; local json = require 'util.json'; local filters = require 'util.filters'; local st = require 'util.stanza'; +local muc_util = module:require "muc/util"; +local valid_affiliations = muc_util.valid_affiliations; local MUC_NS = 'http://jabber.org/protocol/muc'; local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info'; local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required'; @@ -436,8 +438,30 @@ end); function handle_create_lobby(event) local room = event.room; + + -- since this is called by backend rather than triggered by UI, we need to handle a few additional things: + -- 1. Make sure existing participants are already members or they will get kicked out when set_members_only(true) + -- 2. Trigger a 104 (config change) status message so UI state is properly updated for existing users + + -- make sure all existing occupants are members + for _, occupant in room:each_occupant() do + local affiliation = room:get_affiliation(occupant.bare_jid); + if valid_affiliations[affiliation or "none"] < valid_affiliations.member then + room:set_affiliation(true, occupant.bare_jid, 'member'); + end + end + -- Now it is safe to set the room to members only room:set_members_only(true); - attach_lobby_room(room) + + -- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig + room:broadcast_message( + st.message({ type='groupchat', from=room.jid }) + :tag('x', { xmlns='http://jabber.org/protocol/muc#user' }) + :tag('status', { code='104' }) + ); + + -- Attach the lobby room. + attach_lobby_room(room); end function handle_destroy_lobby(event) diff --git a/resources/prosody-plugins/mod_persistent_lobby.lua b/resources/prosody-plugins/mod_persistent_lobby.lua new file mode 100644 index 000000000..2ce0f2c42 --- /dev/null +++ b/resources/prosody-plugins/mod_persistent_lobby.lua @@ -0,0 +1,174 @@ +-- This module allows lobby room to be created even when the main room is empty. +-- Without this module, the empty main room will get deleted after grace period +-- which triggers lobby room deletion even if there are still people in the lobby. +-- +-- This module should be added to the main virtual host domain. +-- It assumes you have properly configured the muc_lobby_rooms module and lobby muc component. +-- +-- To trigger creation of lobby room: +-- prosody.events.fire_event("create-persistent-lobby-room", { room = room; }); +-- + +local util = module:require "util"; +local is_healthcheck_room = util.is_healthcheck_room; +local main_muc_component_host = module:get_option_string('main_muc'); +local lobby_muc_component_host = module:get_option_string('lobby_muc'); + + +if main_muc_component_host == nil then + module:log('error', 'main_muc not configured. Cannot proceed.'); + return; +end + +if lobby_muc_component_host == nil then + module:log('error', 'lobby not enabled missing lobby_muc config'); + return; +end + + +-- Helper function to wait till a component is loaded before running the given callback +local function run_when_component_loaded(component_host_name, callback) + local function trigger_callback() + module:log('info', 'Component loaded %s', component_host_name); + callback(module:context(component_host_name), component_host_name); + end + + if prosody.hosts[component_host_name] == nil then + module:log('debug', 'Host %s not yet loaded. Will trigger when it is loaded.', component_host_name); + prosody.events.add_handler('host-activated', function (host) + if host == component_host_name then + trigger_callback(); + end + end); + else + trigger_callback(); + end +end + +-- Helper function to wait till a component's muc module is loaded before running the given callback +local function run_when_muc_module_loaded(component_host_module, component_host_name, callback) + local function trigger_callback() + module:log('info', 'MUC module loaded for %s', component_host_name); + callback(prosody.hosts[component_host_name].modules.muc, component_host_module); + end + + if prosody.hosts[component_host_name].modules.muc == nil then + module:log('debug', 'MUC module for %s not yet loaded. Will trigger when it is loaded.', component_host_name); + prosody.hosts[component_host_name].events.add_handler('module-loaded', function(event) + if (event.module == 'muc') then + trigger_callback(); + end + end); + else + trigger_callback() + end +end + + +local lobby_muc_service; +local main_muc_service; +local main_muc_module; + + +-- Helper methods to track rooms that have persistent lobby +local function set_persistent_lobby(room) + room._data.persist_lobby = true; +end + +local function has_persistent_lobby(room) + if room._data.persist_lobby == true then + return true; + else + return false; + end +end + + +-- Helper method to trigger main room destroy if room is persistent (no auto-delete) and destroy not yet triggered +local function trigger_room_destroy(room) + if room.get_persistent(room) and room._data.room_destroy_triggered == nil then + room._data.room_destroy_triggered = true; + main_muc_module:fire_event("muc-room-destroyed", { room = room; }); + end +end + + +-- For rooms with persistent lobby, we need to trigger deletion ourselves when both the main room +-- and the lobby room are empty. This will be checked each time an occupant leaves the main room +-- of if someone drops off the lobby. + + +-- Handle events on main muc module +run_when_component_loaded(main_muc_component_host, function(host_module, host_name) + run_when_muc_module_loaded(host_module, host_name, function (main_muc, main_module) + main_muc_service = main_muc; -- so it can be accessed from lobby muc event handlers + main_muc_module = main_module; + + main_module:hook("muc-occupant-left", function(event) + -- Check if room should be destroyed when someone leaves the main room + + local main_room = event.room; + if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then + return; + end + + local lobby_room_jid = main_room._data.lobbyroom; + + -- If occupant leaving results in main room being empty, we trigger room destroy if + -- a) lobby exists and is not empty + -- b) lobby does not exist (possible for lobby to be disabled manually by moderator in meeting) + -- + -- (main room destroy also triggers lobby room destroy in muc_lobby_rooms) + if not main_room:has_occupant() then + if lobby_room_jid == nil then -- lobby disabled + trigger_room_destroy(main_room); + else -- lobby exists + local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid); + if lobby_room and not lobby_room:has_occupant() then + trigger_room_destroy(main_room); + end + end + end + end); + + end); +end); + + +-- Handle events on lobby muc module +run_when_component_loaded(lobby_muc_component_host, function(host_module, host_name) + run_when_muc_module_loaded(host_module, host_name, function (lobby_muc, lobby_module) + lobby_muc_service = lobby_muc; -- so it can be accessed from main muc event handlers + + lobby_module:hook("muc-occupant-left", function(event) + -- Check if room should be destroyed when someone leaves the lobby + + local lobby_room = event.room; + local main_room = lobby_room.main_room; + + if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then + return; + end + + -- If both lobby room and main room are empty, we destroy main room. + -- (main room destroy also triggers lobby room destroy in muc_lobby_rooms) + if not lobby_room:has_occupant() and main_room and not main_room:has_occupant() then + trigger_room_destroy(main_room); + end + + end); + end); +end); + + +function handle_create_persistent_lobby(event) + local room = event.room; + prosody.events.fire_event("create-lobby-room", { room = room; }); + + set_persistent_lobby(room); + room:set_persistent(true); +end + + +module:hook_global('create-persistent-lobby-room', handle_create_persistent_lobby); + diff --git a/resources/prosody-plugins/mod_reservations.lua b/resources/prosody-plugins/mod_reservations.lua index 3146914a9..e47f95b9e 100644 --- a/resources/prosody-plugins/mod_reservations.lua +++ b/resources/prosody-plugins/mod_reservations.lua @@ -33,8 +33,15 @@ -- returns true if API call should be retried. By default, retries are done for 5XX -- responses. Timeouts are never retried, and HTTP call failures are always retried. -- * set "reservations_enable_max_occupants" to true to enable integration with --- mod_muc_max_occupants. Setting thia will allow optional "max_occupants" +-- mod_muc_max_occupants. Setting thia will allow optional "max_occupants" (integer) -- payload from API to influence max occupants allowed for a given room. +-- * set "reservations_enable_lobby_support" to true to enable integration +-- with "muc_lobby_rooms". Setting this will allow optional "lobby" (boolean) +-- fields in API payload. If set to true, Lobby will be enabled for the room. +-- "persistent_lobby" module must also be enabled for this to work. +-- * set "reservations_enable_password_support" to allow optional "password" (string) +-- field in API payload. If set and not empty, then room password will be set +-- to the given string. -- -- -- Example config: @@ -56,7 +63,10 @@ -- return code >= 500 or code == 408 -- end -- - +-- reservations_enable_max_occupants = true -- support "max_occupants" field +-- reservations_enable_lobby_support = true -- support "lobby" field +-- reservations_enable_password_support = true -- support "password" field +-- local jid = require 'util.jid'; local http = require "net.http"; @@ -75,6 +85,8 @@ local api_timeout = module:get_option("reservations_api_timeout", 20); local api_retry_count = tonumber(module:get_option("reservations_api_retry_count", 3)); local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 3)); local max_occupants_enabled = module:get_option("reservations_enable_max_occupants", false); +local lobby_support_enabled = module:get_option("reservations_enable_lobby_support", false); +local password_support_enabled = module:get_option("reservations_enable_password_support", false); -- Option for user to control HTTP response codes that will result in a retry. @@ -248,7 +260,7 @@ function RoomReservation:enqueue_or_route_event(event) end --- Updates status and initiates event routing. Called internally when API call complete. -function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, max_occupants) +function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, data) module:log("info", "Reservation created successfully for %s", self.room_jid); self.meta = { status = STATUS.SUCCESS; @@ -259,8 +271,14 @@ function RoomReservation:set_status_success(start_time, duration, mail_owner, co error_text = nil; error_code = nil; } - if max_occupants_enabled and max_occupants then - self.meta.max_occupants = max_occupants + if max_occupants_enabled and data.max_occupants then + self.meta.max_occupants = data.max_occupants + end + if lobby_support_enabled and data.lobby then + self.meta.lobby = data.lobby + end + if password_support_enabled and data.password then + self.meta.password = data.password end self:route_pending_events() end @@ -400,7 +418,7 @@ function RoomReservation:parse_conference_response(response_body) end data.duration = duration; - -- if optional max_occupants field set, cast to number + -- if optional "max_occupants" field set, cast to number if data.max_occupants ~= nil then local max_occupants = tonumber(data.max_occupants) if max_occupants == nil or max_occupants < 1 then @@ -411,6 +429,24 @@ function RoomReservation:parse_conference_response(response_body) data.max_occupants = max_occupants end + -- if optional "lobby" field set, accept boolean true or "true" + if data.lobby ~= nil then + if (type(data.lobby) == "boolean" and data.lobby) or data.lobby == "true" then + data.lobby = true + else + data.lobby = false + end + end + + -- if optional "password" field set, it has to be string + if data.password ~= nil then + if type(data.password) ~= "string" then + -- N.B. invalid "password" rejected even if reservations_enable_password_support=false + module:log("error", "Invalid type for password - string expected"); + return; + end + end + local start_time = datetime.parse(data.start_time); -- N.B. we lose milliseconds portion of the date if start_time == nil then module:log("error", "Missing or invalid start_time - %s", data.start_time); @@ -458,7 +494,7 @@ function RoomReservation:handler_conference_data_returned_from_api(response_body module:log("error", "API returned success code but invalid payload"); self:set_status_failed(500, 'Invalid response from reservation server'); else - self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data.max_occupants) + self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data) end end @@ -591,30 +627,58 @@ local function room_destroyed(event) end end ---- If max_occupants_enabled, update room max_occupants if returned by API + local function room_created(event) - local res; local room = event.room - if max_occupants_enabled and not is_healthcheck_room(room.jid) then - res = reservations[room.jid] + if is_healthcheck_room(room.jid) then + return; + end - if res and res.meta.max_occupants ~= nil then - module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid); - room._data.max_occupants = res.meta.max_occupants - end + local res = reservations[room.jid] + + if res and max_occupants_enabled and res.meta.max_occupants ~= nil then + module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid); + room._data.max_occupants = res.meta.max_occupants + end + + if res and password_support_enabled and res.meta.password ~= nil then + module:log("info", "Setting password for room %s", room.jid); + room:set_password(res.meta.password); end end + +local function room_pre_create(event) + local room = event.room + + if is_healthcheck_room(room.jid) then + return; + end + + local res = reservations[room.jid] + + if res and lobby_support_enabled and res.meta.lobby then + module:log("info", "Enabling lobby for room %s", room.jid); + prosody.events.fire_event("create-persistent-lobby-room", { room = room; }); + end +end + + function process_host(host) if host == muc_component_host then -- the conference muc component module:log("info", "Hook to muc-room-destroyed on %s", host); module:context(host):hook("muc-room-destroyed", room_destroyed, -1); - if max_occupants_enabled then - module:log("info", "Hook to muc-room-created on %s (mod_muc_max_occupants integration enabled)", host); + if max_occupants_enabled or password_support_enabled then + module:log("info", "Hook to muc-room-created on %s (max_occupants or password integration enabled)", host); module:context(host):hook("muc-room-created", room_created); end + + if lobby_support_enabled then + module:log("info", "Hook to muc-room-pre-create on %s (lobby integration enabled)", host); + module:context(host):hook("muc-room-pre-create", room_pre_create); + end end end