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
This commit is contained in:
Shawn Chin 2022-09-27 20:59:30 +01:00 committed by GitHub
parent 90b17046f6
commit 2e6f14f872
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 280 additions and 18 deletions

View File

@ -30,6 +30,8 @@ local jid_bare = require 'util.jid'.bare;
local json = require 'util.json'; local json = require 'util.json';
local filters = require 'util.filters'; local filters = require 'util.filters';
local st = require 'util.stanza'; 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 MUC_NS = 'http://jabber.org/protocol/muc';
local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info'; local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info';
local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required'; local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required';
@ -436,8 +438,30 @@ end);
function handle_create_lobby(event) function handle_create_lobby(event)
local room = event.room; 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); 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 end
function handle_destroy_lobby(event) function handle_destroy_lobby(event)

View File

@ -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);

View File

@ -33,8 +33,15 @@
-- returns true if API call should be retried. By default, retries are done for 5XX -- 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. -- responses. Timeouts are never retried, and HTTP call failures are always retried.
-- * set "reservations_enable_max_occupants" to true to enable integration with -- * 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. -- 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: -- Example config:
@ -56,7 +63,10 @@
-- return code >= 500 or code == 408 -- return code >= 500 or code == 408
-- end -- 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 jid = require 'util.jid';
local http = require "net.http"; 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_count = tonumber(module:get_option("reservations_api_retry_count", 3));
local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 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 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. -- 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 end
--- Updates status and initiates event routing. Called internally when API call complete. --- 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); module:log("info", "Reservation created successfully for %s", self.room_jid);
self.meta = { self.meta = {
status = STATUS.SUCCESS; status = STATUS.SUCCESS;
@ -259,8 +271,14 @@ function RoomReservation:set_status_success(start_time, duration, mail_owner, co
error_text = nil; error_text = nil;
error_code = nil; error_code = nil;
} }
if max_occupants_enabled and max_occupants then if max_occupants_enabled and data.max_occupants then
self.meta.max_occupants = max_occupants 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 end
self:route_pending_events() self:route_pending_events()
end end
@ -400,7 +418,7 @@ function RoomReservation:parse_conference_response(response_body)
end end
data.duration = duration; 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 if data.max_occupants ~= nil then
local max_occupants = tonumber(data.max_occupants) local max_occupants = tonumber(data.max_occupants)
if max_occupants == nil or max_occupants < 1 then 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 data.max_occupants = max_occupants
end 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 local start_time = datetime.parse(data.start_time); -- N.B. we lose milliseconds portion of the date
if start_time == nil then if start_time == nil then
module:log("error", "Missing or invalid start_time - %s", data.start_time); 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"); module:log("error", "API returned success code but invalid payload");
self:set_status_failed(500, 'Invalid response from reservation server'); self:set_status_failed(500, 'Invalid response from reservation server');
else 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
end end
@ -591,30 +627,58 @@ local function room_destroyed(event)
end end
end end
--- If max_occupants_enabled, update room max_occupants if returned by API
local function room_created(event) local function room_created(event)
local res;
local room = event.room local room = event.room
if max_occupants_enabled and not is_healthcheck_room(room.jid) then if is_healthcheck_room(room.jid) then
res = reservations[room.jid] return;
end
if res and res.meta.max_occupants ~= nil then local res = reservations[room.jid]
module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid);
room._data.max_occupants = res.meta.max_occupants if res and max_occupants_enabled and res.meta.max_occupants ~= nil then
end 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
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) function process_host(host)
if host == muc_component_host then -- the conference muc component if host == muc_component_host then -- the conference muc component
module:log("info", "Hook to muc-room-destroyed on %s", host); module:log("info", "Hook to muc-room-destroyed on %s", host);
module:context(host):hook("muc-room-destroyed", room_destroyed, -1); module:context(host):hook("muc-room-destroyed", room_destroyed, -1);
if max_occupants_enabled then if max_occupants_enabled or password_support_enabled then
module:log("info", "Hook to muc-room-created on %s (mod_muc_max_occupants integration enabled)", host); 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); module:context(host):hook("muc-room-created", room_created);
end 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
end end