mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
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.
696 lines
27 KiB
Lua
696 lines
27 KiB
Lua
--- This is a port of Jicofo's Reservation System as a prosody module
|
|
-- ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
|
--
|
|
-- We try to retain the same behaviour and interfaces where possible, but there
|
|
-- is some difference:
|
|
-- * In the event that the DELETE call fails, Jicofo's reservation
|
|
-- system retains reservation data and allows re-creation of room if requested by
|
|
-- the same creator without making further call to the API; this module does not
|
|
-- offer this behaviour. Re-creation of a closed room will behave like a new meeting
|
|
-- and trigger a new API call to validate the reservation.
|
|
-- * Jicofo's reservation system expect int-based conflict_id. We take any sensible string.
|
|
--
|
|
-- In broad strokes, this module works by intercepting Conference IQs sent to focus component
|
|
-- and buffers it until reservation is confirmed (by calling the provided API endpoint).
|
|
-- The IQ events are routed on to focus component if reservation is valid, or error
|
|
-- response is sent back to the origin if reservation is denied. Events are routed as usual
|
|
-- if the room already exists.
|
|
--
|
|
--
|
|
-- Installation:
|
|
-- =============
|
|
--
|
|
-- Under domain config,
|
|
-- 1. add "reservations" to modules_enabled.
|
|
-- 2. Specify URL base for your API endpoint using "reservations_api_prefix" (required)
|
|
-- 3. Optional config:
|
|
-- * set "reservations_api_timeout" to change API call timeouts (defaults to 20 seconds)
|
|
-- * set "reservations_api_headers" to specify custom HTTP headers included in
|
|
-- all API calls e.g. to provide auth tokens.
|
|
-- * set "reservations_api_retry_count" to the number of times API call failures are retried (defaults to 3)
|
|
-- * set "reservations_api_retry_delay" seconds to wait between retries (defaults to 3s)
|
|
-- * set "reservations_api_should_retry_for_code" to a function that takes an HTTP response code and
|
|
-- 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" (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.
|
|
-- * By default, reservation checks are skipped for breakout rooms. You can subject
|
|
-- breakout rooms to the same checks by setting "reservations_skip_breakout_rooms" to false.
|
|
--
|
|
--
|
|
-- Example config:
|
|
--
|
|
-- VirtualHost "jitmeet.example.com"
|
|
-- modules_enabled = {
|
|
-- "reservations";
|
|
-- }
|
|
-- reservations_api_prefix = "http://reservation.example.com"
|
|
--
|
|
-- --- The following are all optional
|
|
-- reservations_api_headers = {
|
|
-- ["Authorization"] = "Bearer TOKEN-237958623045";
|
|
-- }
|
|
-- reservations_api_timeout = 10 -- timeout if API does not respond within 10s
|
|
-- reservations_api_retry_count = 5 -- retry up to 5 times
|
|
-- reservations_api_retry_delay = 1 -- wait 1s between retries
|
|
-- reservations_api_should_retry_for_code = function (code)
|
|
-- 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";
|
|
local json = require 'cjson.safe';
|
|
local st = require "util.stanza";
|
|
local timer = require 'util.timer';
|
|
local datetime = require 'util.datetime';
|
|
|
|
local util = module:require "util";
|
|
local get_room_from_jid = util.get_room_from_jid;
|
|
local is_healthcheck_room = util.is_healthcheck_room;
|
|
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
|
local process_host_module = util.process_host_module;
|
|
|
|
local api_prefix = module:get_option("reservations_api_prefix");
|
|
local api_headers = module:get_option("reservations_api_headers");
|
|
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);
|
|
local skip_breakout_room = module:get_option("reservations_skip_breakout_rooms", true);
|
|
|
|
|
|
-- Option for user to control HTTP response codes that will result in a retry.
|
|
-- Defaults to returning true on any 5XX code or 0
|
|
local api_should_retry_for_code = module:get_option("reservations_api_should_retry_for_code", function (code)
|
|
return code >= 500;
|
|
end)
|
|
|
|
|
|
local muc_component_host = module:get_option_string("main_muc");
|
|
local breakout_muc_component_host = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
|
|
|
|
|
|
-- How often to check and evict expired reservation data
|
|
local expiry_check_period = 60;
|
|
|
|
|
|
-- Cannot proceed if "reservations_api_prefix" not configured
|
|
if not api_prefix then
|
|
module:log("error", "reservations_api_prefix not specified. Disabling %s", module:get_name());
|
|
return;
|
|
end
|
|
|
|
|
|
-- get/infer focus component hostname so we can intercept IQ bound for it
|
|
local focus_component_host = module:get_option_string("focus_component");
|
|
if not focus_component_host then
|
|
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
|
if not muc_domain_base then
|
|
module:log("error", "Could not infer focus domain. Disabling %s", module:get_name());
|
|
return;
|
|
end
|
|
focus_component_host = 'focus.'..muc_domain_base;
|
|
end
|
|
|
|
-- common HTTP headers added to all API calls
|
|
local http_headers = {
|
|
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")";
|
|
};
|
|
if api_headers then -- extra headers from config
|
|
for key, value in pairs(api_headers) do
|
|
http_headers[key] = value;
|
|
end
|
|
end
|
|
|
|
|
|
--- Utils
|
|
|
|
--- Converts int timestamp to datetime string compatible with Java SimpleDateFormat
|
|
-- @param t timestamps in seconds. Supports int (as returned by os.time()) or higher
|
|
-- precision (as returned by socket.gettime())
|
|
-- @return formatted datetime string (yyyy-MM-dd'T'HH:mm:ss.SSSX)
|
|
local function to_java_date_string(t)
|
|
local t_secs, mantissa = math.modf(t);
|
|
local ms_str = (mantissa == 0) and '.000' or tostring(mantissa):sub(2,5);
|
|
local date_str = os.date("!%Y-%m-%dT%H:%M:%S", t_secs);
|
|
return date_str..ms_str..'Z';
|
|
end
|
|
|
|
|
|
--- Start non-blocking HTTP call
|
|
-- @param url URL to call
|
|
-- @param options options table as expected by net.http where we provide optional headers, body or method.
|
|
-- @param callback if provided, called with callback(response_body, response_code) when call complete.
|
|
-- @param timeout_callback if provided, called without args when request times out.
|
|
-- @param retries how many times to retry on failure; 0 means no retries.
|
|
local function async_http_request(url, options, callback, timeout_callback, retries)
|
|
local completed = false;
|
|
local timed_out = false;
|
|
local retries = retries or api_retry_count;
|
|
|
|
local function cb_(response_body, response_code)
|
|
if not timed_out then -- request completed before timeout
|
|
completed = true;
|
|
if (response_code == 0 or api_should_retry_for_code(response_code)) and retries > 0 then
|
|
module:log("warn", "API Response code %d. Will retry after %ds", response_code, api_retry_delay);
|
|
timer.add_task(api_retry_delay, function()
|
|
async_http_request(url, options, callback, timeout_callback, retries - 1)
|
|
end)
|
|
return;
|
|
end
|
|
|
|
if callback then
|
|
callback(response_body, response_code)
|
|
end
|
|
end
|
|
end
|
|
|
|
local request = http.request(url, options, cb_);
|
|
|
|
timer.add_task(api_timeout, function ()
|
|
timed_out = true;
|
|
|
|
if not completed then
|
|
http.destroy_request(request);
|
|
if timeout_callback then
|
|
timeout_callback()
|
|
end
|
|
end
|
|
end);
|
|
|
|
end
|
|
|
|
--- Returns current timestamp
|
|
local function now()
|
|
-- Don't really need higher precision of socket.gettime(). Besides, we loose
|
|
-- milliseconds precision when converting back to timestamp from date string
|
|
-- when we use datetime.parse(t), so let's be consistent.
|
|
return os.time();
|
|
end
|
|
|
|
--- Start RoomReservation implementation
|
|
|
|
-- Status enums used in RoomReservation:meta.status
|
|
local STATUS = {
|
|
PENDING = 0;
|
|
SUCCESS = 1;
|
|
FAILED = -1;
|
|
}
|
|
|
|
local RoomReservation = {};
|
|
RoomReservation.__index = RoomReservation;
|
|
|
|
function newRoomReservation(room_jid, creator_jid)
|
|
return setmetatable({
|
|
room_jid = room_jid;
|
|
|
|
-- Reservation metadata. store as table so we can set and read atomically.
|
|
-- N.B. This should always be updated using self.set_status_*
|
|
meta = {
|
|
status = STATUS.PENDING;
|
|
mail_owner = jid.bare(creator_jid);
|
|
conflict_id = nil;
|
|
start_time = now(); -- timestamp, in seconds
|
|
expires_at = nil; -- timestamp, in seconds
|
|
error_text = nil;
|
|
error_code = nil;
|
|
};
|
|
|
|
-- Array of pending events that we need to route once API call is complete
|
|
pending_events = {};
|
|
|
|
-- Set true when API call trigger has been triggered (by enqueue of first event)
|
|
api_call_triggered = false;
|
|
}, RoomReservation);
|
|
end
|
|
|
|
|
|
--- Extracts room name from room jid
|
|
function RoomReservation:get_room_name()
|
|
return jid.node(self.room_jid);
|
|
end
|
|
|
|
--- Checks if reservation data is expires and should be evicted from store
|
|
function RoomReservation:is_expired()
|
|
return self.meta.expires_at ~= nil and now() > self.meta.expires_at;
|
|
end
|
|
|
|
--- Main entry point for handing and routing events.
|
|
function RoomReservation:enqueue_or_route_event(event)
|
|
if self.meta.status == STATUS.PENDING then
|
|
table.insert(self.pending_events, event)
|
|
if self.api_call_triggered ~= true then
|
|
self:call_api_create_conference();
|
|
end
|
|
else
|
|
-- API call already complete. Immediately route without enqueueing.
|
|
-- This could happen if request comes in between the time reservation approved
|
|
-- and when Jicofo actually creates the room.
|
|
module:log("debug", "Reservation details already stored. Skipping queue for %s", self.room_jid);
|
|
self:route_event(event);
|
|
end
|
|
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, data)
|
|
module:log("info", "Reservation created successfully for %s", self.room_jid);
|
|
self.meta = {
|
|
status = STATUS.SUCCESS;
|
|
mail_owner = mail_owner or self.meta.mail_owner;
|
|
conflict_id = conflict_id;
|
|
start_time = start_time;
|
|
expires_at = start_time + duration;
|
|
error_text = nil;
|
|
error_code = nil;
|
|
}
|
|
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
|
|
|
|
--- Updates status and initiates error response to pending events. Called internally when API call complete.
|
|
function RoomReservation:set_status_failed(error_code, error_text)
|
|
module:log("info", "Reservation creation failed for %s - (%s) %s", self.room_jid, error_code, error_text);
|
|
self.meta = {
|
|
status = STATUS.FAILED;
|
|
mail_owner = self.meta.mail_owner;
|
|
conflict_id = nil;
|
|
start_time = self.meta.start_time;
|
|
-- Retain reservation rejection for a short while so we have time to report failure to
|
|
-- existing clients and not trigger a re-query too soon.
|
|
-- N.B. Expiry could take longer since eviction happens periodically.
|
|
expires_at = now() + 30;
|
|
error_text = error_text;
|
|
error_code = error_code;
|
|
}
|
|
self:route_pending_events()
|
|
end
|
|
|
|
--- Triggers routing of all enqueued events
|
|
function RoomReservation:route_pending_events()
|
|
if self.meta.status == STATUS.PENDING then -- should never be called while PENDING. check just in case.
|
|
return;
|
|
end
|
|
|
|
module:log("debug", "Routing all pending events for %s", self.room_jid);
|
|
local event;
|
|
|
|
while #self.pending_events ~= 0 do
|
|
event = table.remove(self.pending_events);
|
|
self:route_event(event)
|
|
end
|
|
end
|
|
|
|
--- Event routing implementation
|
|
function RoomReservation:route_event(event)
|
|
-- this should only be called after API call complete and status no longer PENDING
|
|
assert(self.meta.status ~= STATUS.PENDING, "Attempting to route event while API call still PENDING")
|
|
|
|
local meta = self.meta;
|
|
local origin, stanza = event.origin, event.stanza;
|
|
|
|
if meta.status == STATUS.FAILED then
|
|
module:log("debug", "Route: Sending reservation error to %s", stanza.attr.from);
|
|
self:reply_with_error(event, meta.error_code, meta.error_text);
|
|
else
|
|
if meta.status == STATUS.SUCCESS then
|
|
if self:is_expired() then
|
|
module:log("debug", "Route: Sending reservation expiry to %s", stanza.attr.from);
|
|
self:reply_with_error(event, 419, "Reservation expired");
|
|
else
|
|
module:log("debug", "Route: Forwarding on event from %s", stanza.attr.from);
|
|
prosody.core_post_stanza(origin, stanza, false); -- route iq to intended target (focus)
|
|
end
|
|
else
|
|
-- this should never happen unless dev made a mistake. Block by default just in case.
|
|
module:log("error", "Reservation for %s has invalid state %s. Rejecting request.", self.room_jid, meta.status);
|
|
self:reply_with_error(event, 500, "Failed to determine reservation state");
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Generates reservation-error stanza and sends to event origin.
|
|
function RoomReservation:reply_with_error(event, error_code, error_text)
|
|
local stanza = event.stanza;
|
|
local id = stanza.attr.id;
|
|
local to = stanza.attr.from;
|
|
local from = stanza.attr.to;
|
|
|
|
event.origin.send(
|
|
st.iq({ type="error", to=to, from=from, id=id })
|
|
:tag("error", { type="cancel" })
|
|
:tag("service-unavailable", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):up()
|
|
:tag("text", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):text(error_text):up()
|
|
:tag("reservation-error", { xmlns="http://jitsi.org/protocol/focus", ["error-code"]=tostring(error_code) })
|
|
);
|
|
end
|
|
|
|
--- Initiates non-blocking API call to validate reservation
|
|
function RoomReservation:call_api_create_conference()
|
|
self.api_call_triggered = true;
|
|
|
|
local url = api_prefix..'/conference';
|
|
local request_data = {
|
|
name = self:get_room_name();
|
|
start_time = to_java_date_string(self.meta.start_time);
|
|
mail_owner = self.meta.mail_owner;
|
|
}
|
|
|
|
local http_options = {
|
|
body = http.formencode(request_data); -- because Jicofo reservation encodes as form data instead JSON
|
|
method = 'POST';
|
|
headers = http_headers;
|
|
}
|
|
|
|
module:log("debug", "Sending POST /conference for %s", self.room_jid);
|
|
async_http_request(url, http_options, function (response_body, response_code)
|
|
self:on_api_create_conference_complete(response_body, response_code);
|
|
end, function ()
|
|
self:on_api_call_timeout();
|
|
end);
|
|
end
|
|
|
|
--- Parses and validates HTTP response body for conference payload
|
|
-- Ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
|
-- @return nil if invalid, or table with payload parsed from JSON response
|
|
function RoomReservation:parse_conference_response(response_body)
|
|
local data, error = json.decode(response_body);
|
|
|
|
if data == nil then -- invalid JSON payload
|
|
module:log("error", "Invalid JSON response from API - %s error:%s", response_body, error);
|
|
return;
|
|
end
|
|
|
|
if data.name == nil or data.name:lower() ~= self:get_room_name() then
|
|
module:log("error", "Missing or mismatching room name - %s", data.name);
|
|
return;
|
|
end
|
|
|
|
if data.id == nil then
|
|
module:log("error", "Missing id");
|
|
return;
|
|
end
|
|
|
|
if data.mail_owner == nil then
|
|
module:log("error", "Missing mail_owner");
|
|
return;
|
|
end
|
|
|
|
local duration = tonumber(data.duration);
|
|
if duration == nil then
|
|
module:log("error", "Missing or invalid duration - %s", data.duration);
|
|
return;
|
|
end
|
|
data.duration = duration;
|
|
|
|
-- 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
|
|
-- N.B. invalid max_occupants rejected even if max_occupants_enabled=false
|
|
module:log("error", "Invalid value for max_occupants - %s", data.max_occupants);
|
|
return;
|
|
end
|
|
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);
|
|
return;
|
|
end
|
|
data.start_time = start_time;
|
|
|
|
return data;
|
|
end
|
|
|
|
--- Parses and validates HTTP error response body for API call.
|
|
-- Expect JSON with a "message" field.
|
|
-- @return message string, or generic error message if invalid payload.
|
|
function RoomReservation:parse_error_message_from_response(response_body)
|
|
local data = json.decode(response_body);
|
|
if data ~= nil and data.message ~= nil then
|
|
module:log("debug", "Invalid error response body. Will use generic error message.");
|
|
return data.message;
|
|
else
|
|
return "Rejected by reservation server";
|
|
end
|
|
end
|
|
|
|
--- callback on API timeout
|
|
function RoomReservation:on_api_call_timeout()
|
|
self:set_status_failed(500, 'Reservation lookup timed out');
|
|
end
|
|
|
|
--- callback on API response
|
|
function RoomReservation:on_api_create_conference_complete(response_body, response_code)
|
|
if response_code == 200 or response_code == 201 then
|
|
self:handler_conference_data_returned_from_api(response_body);
|
|
elseif response_code == 409 then
|
|
self:handle_conference_already_exist(response_body);
|
|
elseif response_code == nil then -- warrants a retry, but this should be done automatically by the http call method.
|
|
self:set_status_failed(500, 'Could not contact reservation server');
|
|
else
|
|
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
|
end
|
|
end
|
|
|
|
function RoomReservation:handler_conference_data_returned_from_api(response_body)
|
|
local data = self:parse_conference_response(response_body);
|
|
if not data then -- invalid response from API
|
|
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)
|
|
end
|
|
end
|
|
|
|
function RoomReservation:handle_conference_already_exist(response_body)
|
|
local data = json.decode(response_body);
|
|
if data == nil or data.conflict_id == nil then
|
|
-- yes, in the case of 409, API expected to return "id" as "conflict_id".
|
|
self:set_status_failed(409, 'Invalid response from reservation server');
|
|
else
|
|
local url = api_prefix..'/conference/'..data.conflict_id;
|
|
local http_options = {
|
|
method = 'GET';
|
|
headers = http_headers;
|
|
}
|
|
|
|
async_http_request(url, http_options, function(response_body, response_code)
|
|
if response_code == 200 then
|
|
self:handler_conference_data_returned_from_api(response_body);
|
|
else
|
|
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
|
end
|
|
end, function ()
|
|
self:on_api_call_timeout();
|
|
end);
|
|
end
|
|
end
|
|
|
|
--- End RoomReservation
|
|
|
|
--- Store reservations lookups that are still pending or with room still active
|
|
local reservations = {}
|
|
|
|
local function get_or_create_reservations(room_jid, creator_jid)
|
|
if reservations[room_jid] == nil then
|
|
module:log("debug", "Creating new reservation data for %s", room_jid);
|
|
reservations[room_jid] = newRoomReservation(room_jid, creator_jid);
|
|
end
|
|
|
|
return reservations[room_jid];
|
|
end
|
|
|
|
local function evict_expired_reservations()
|
|
local expired = {}
|
|
|
|
-- first, gather jids of expired rooms. So we don't remove from table while iterating.
|
|
for room_jid, res in pairs(reservations) do
|
|
if res:is_expired() then
|
|
table.insert(expired, room_jid);
|
|
end
|
|
end
|
|
|
|
local room;
|
|
for _, room_jid in ipairs(expired) do
|
|
room = get_room_from_jid(room_jid);
|
|
if room then
|
|
-- Close room if still active (reservation duration exceeded)
|
|
module:log("info", "Room exceeded reservation duration. Terminating %s", room_jid);
|
|
room:destroy(nil, "Scheduled conference duration exceeded.");
|
|
-- Rely on room_destroyed to calls DELETE /conference and drops reservation[room_jid]
|
|
else
|
|
module:log("error", "Reservation references expired room that is no longer active. Dropping %s", room_jid);
|
|
-- This should not happen unless evict_expired_reservations somehow gets triggered
|
|
-- between the time room is destroyed and room_destroyed callback is called. (Possible?)
|
|
-- But just in case, we drop the reservation to avoid repeating this path on every pass.
|
|
reservations[room_jid] = nil;
|
|
end
|
|
end
|
|
end
|
|
|
|
timer.add_task(expiry_check_period, function()
|
|
evict_expired_reservations();
|
|
return expiry_check_period;
|
|
end)
|
|
|
|
|
|
--- Intercept conference IQ to Jicofo handle reservation checks before allowing normal event flow
|
|
module:log("info", "Hook to global pre-iq/host");
|
|
module:hook("pre-iq/host", function(event)
|
|
local stanza = event.stanza;
|
|
|
|
if stanza.name ~= "iq" or stanza.attr.to ~= focus_component_host or stanza.attr.type ~= 'set' then
|
|
return; -- not IQ for jicofo. Ignore this event.
|
|
end
|
|
|
|
local conference = stanza:get_child('conference', 'http://jitsi.org/protocol/focus');
|
|
if conference == nil then
|
|
return; -- not Conference IQ. Ignore.
|
|
end
|
|
|
|
local room_jid = room_jid_match_rewrite(conference.attr.room);
|
|
|
|
if get_room_from_jid(room_jid) ~= nil then
|
|
module:log("debug", "Skip reservation check for existing room %s", room_jid);
|
|
return; -- room already exists. Continue with normal flow
|
|
end
|
|
|
|
if skip_breakout_room then
|
|
local _, host = jid.split(room_jid);
|
|
if host == breakout_muc_component_host then
|
|
module:log("debug", "Skip reservation check for breakout room %s", room_jid);
|
|
return;
|
|
end
|
|
end
|
|
|
|
local res = get_or_create_reservations(room_jid, stanza.attr.from);
|
|
res:enqueue_or_route_event(event); -- hand over to reservation obj to route event
|
|
return true;
|
|
|
|
end);
|
|
|
|
|
|
--- Forget reservation details once room destroyed so query is repeated if room re-created
|
|
local function room_destroyed(event)
|
|
local res;
|
|
local room = event.room
|
|
|
|
if not is_healthcheck_room(room.jid) then
|
|
res = reservations[room.jid]
|
|
|
|
-- drop reservation data for this room
|
|
reservations[room.jid] = nil
|
|
|
|
if res then -- just in case event triggered more than once?
|
|
module:log("info", "Dropped reservation data for destroyed room %s", room.jid);
|
|
|
|
local conflict_id = res.meta.conflict_id
|
|
if conflict_id then
|
|
local url = api_prefix..'/conference/'..conflict_id;
|
|
local http_options = {
|
|
method = 'DELETE';
|
|
headers = http_headers;
|
|
}
|
|
|
|
module:log("debug", "Sending DELETE /conference/%s", conflict_id);
|
|
async_http_request(url, http_options);
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
local function room_created(event)
|
|
local room = event.room
|
|
|
|
if is_healthcheck_room(room.jid) then
|
|
return;
|
|
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
|
|
|
|
process_host_module(muc_component_host, function(host_module, host)
|
|
module:log("info", "Hook to muc-room-destroyed on %s", host);
|
|
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
|
|
|
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);
|
|
host_module: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);
|
|
host_module:hook("muc-room-pre-create", room_pre_create);
|
|
end
|
|
end);
|