"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7329/resources/prosody-plugins/mod_reservations.lua" (9 Jun 2023, 27933 Bytes) of package /linux/misc/jitsi-meet-7329.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Lua source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 --- This is a port of Jicofo's Reservation System as a prosody module
    2 --  ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
    3 --
    4 --  We try to retain the same behaviour and interfaces where possible, but there
    5 --  is some difference:
    6 --    * In the event that the DELETE call fails, Jicofo's reservation
    7 --      system retains reservation data and allows re-creation of room if requested by
    8 --      the same creator without making further call to the API; this module does not
    9 --      offer this behaviour. Re-creation of a closed room will behave like a new meeting
   10 --      and trigger a new API call to validate the reservation.
   11 --    * Jicofo's reservation system expect int-based conflict_id. We take any sensible string.
   12 --
   13 --  In broad strokes, this module works by intercepting Conference IQs sent to focus component
   14 --  and buffers it until reservation is confirmed (by calling the provided API endpoint).
   15 --  The IQ events are routed on to focus component if reservation is valid, or error
   16 --  response is sent back to the origin if reservation is denied. Events are routed as usual
   17 --  if the room already exists.
   18 --
   19 --
   20 --  Installation:
   21 --  =============
   22 --
   23 --  Under domain config,
   24 --   1. add "reservations" to modules_enabled.
   25 --   2. Specify URL base for your API endpoint using "reservations_api_prefix" (required)
   26 --   3. Optional config:
   27 --      * set "reservations_api_timeout" to change API call timeouts (defaults to 20 seconds)
   28 --      * set "reservations_api_headers" to specify custom HTTP headers included in
   29 --        all API calls e.g. to provide auth tokens.
   30 --      * set "reservations_api_retry_count" to the number of times API call failures are retried (defaults to 3)
   31 --      * set "reservations_api_retry_delay" seconds to wait between retries (defaults to 3s)
   32 --      * set "reservations_api_should_retry_for_code" to a function that takes an HTTP response code and
   33 --        returns true if API call should be retried. By default, retries are done for 5XX
   34 --        responses. Timeouts are never retried, and HTTP call failures are always retried.
   35 --      * set "reservations_enable_max_occupants" to true to enable integration with
   36 --        mod_muc_max_occupants. Setting thia will allow optional "max_occupants" (integer)
   37 --        payload from API to influence max occupants allowed for a given room.
   38 --      * set "reservations_enable_lobby_support" to true to enable integration
   39 --        with "muc_lobby_rooms". Setting this will allow optional "lobby" (boolean)
   40 --        fields in API payload. If set to true, Lobby will be enabled for the room.
   41 --        "persistent_lobby" module must also be enabled for this to work.
   42 --      * set "reservations_enable_password_support" to allow optional "password" (string)
   43 --        field in API payload. If set and not empty, then room password will be set
   44 --        to the given string.
   45 --      * By default, reservation checks are skipped for breakout rooms. You can subject
   46 --        breakout rooms to the same checks by setting "reservations_skip_breakout_rooms" to false.
   47 --
   48 --
   49 --  Example config:
   50 --
   51 --    VirtualHost "jitmeet.example.com"
   52 --        modules_enabled = {
   53 --            "reservations";
   54 --        }
   55 --        reservations_api_prefix = "http://reservation.example.com"
   56 --
   57 --        --- The following are all optional
   58 --        reservations_api_headers = {
   59 --            ["Authorization"] = "Bearer TOKEN-237958623045";
   60 --        }
   61 --        reservations_api_timeout = 10  -- timeout if API does not respond within 10s
   62 --        reservations_api_retry_count = 5  -- retry up to 5 times
   63 --        reservations_api_retry_delay = 1  -- wait 1s between retries
   64 --        reservations_api_should_retry_for_code = function (code)
   65 --            return code >= 500 or code == 408
   66 --        end
   67 --
   68 --        reservations_enable_max_occupants = true  -- support "max_occupants" field
   69 --        reservations_enable_lobby_support = true  -- support "lobby" field
   70 --        reservations_enable_password_support = true  -- support "password" field
   71 --
   72 
   73 local jid = require 'util.jid';
   74 local http = require "net.http";
   75 local json = require "util.json";
   76 local st = require "util.stanza";
   77 local timer = require 'util.timer';
   78 local datetime = require 'util.datetime';
   79 
   80 local get_room_from_jid = module:require "util".get_room_from_jid;
   81 local is_healthcheck_room = module:require "util".is_healthcheck_room;
   82 local room_jid_match_rewrite = module:require "util".room_jid_match_rewrite;
   83 
   84 local api_prefix = module:get_option("reservations_api_prefix");
   85 local api_headers = module:get_option("reservations_api_headers");
   86 local api_timeout = module:get_option("reservations_api_timeout", 20);
   87 local api_retry_count = tonumber(module:get_option("reservations_api_retry_count", 3));
   88 local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 3));
   89 local max_occupants_enabled = module:get_option("reservations_enable_max_occupants", false);
   90 local lobby_support_enabled = module:get_option("reservations_enable_lobby_support", false);
   91 local password_support_enabled = module:get_option("reservations_enable_password_support", false);
   92 local skip_breakout_room = module:get_option("reservations_skip_breakout_rooms", true);
   93 
   94 
   95 -- Option for user to control HTTP response codes that will result in a retry.
   96 -- Defaults to returning true on any 5XX code or 0
   97 local api_should_retry_for_code = module:get_option("reservations_api_should_retry_for_code", function (code)
   98    return code >= 500;
   99 end)
  100 
  101 
  102 local muc_component_host = module:get_option_string("main_muc");
  103 local breakout_muc_component_host = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
  104 
  105 
  106 -- How often to check and evict expired reservation data
  107 local expiry_check_period = 60;
  108 
  109 
  110 -- Cannot proceed if "reservations_api_prefix" not configured
  111 if not api_prefix then
  112     module:log("error", "reservations_api_prefix not specified. Disabling %s", module:get_name());
  113     return;
  114 end
  115 
  116 
  117 -- get/infer focus component hostname so we can intercept IQ bound for it
  118 local focus_component_host = module:get_option_string("focus_component");
  119 if not focus_component_host then
  120     local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
  121     if not muc_domain_base then
  122         module:log("error", "Could not infer focus domain. Disabling %s", module:get_name());
  123         return;
  124     end
  125     focus_component_host = 'focus.'..muc_domain_base;
  126 end
  127 
  128 -- common HTTP headers added to all API calls
  129 local http_headers = {
  130     ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")";
  131 };
  132 if api_headers then -- extra headers from config
  133     for key, value in pairs(api_headers) do
  134        http_headers[key] = value;
  135     end
  136 end
  137 
  138 
  139 --- Utils
  140 
  141 --- Converts int timestamp to datetime string compatible with Java SimpleDateFormat
  142 -- @param t timestamps in seconds. Supports int (as returned by os.time()) or higher
  143 --          precision (as returned by socket.gettime())
  144 -- @return formatted datetime string (yyyy-MM-dd'T'HH:mm:ss.SSSX)
  145 local function to_java_date_string(t)
  146     local t_secs, mantissa = math.modf(t);
  147     local ms_str = (mantissa == 0) and '.000' or tostring(mantissa):sub(2,5);
  148     local date_str = os.date("!%Y-%m-%dT%H:%M:%S", t_secs);
  149     return date_str..ms_str..'Z';
  150 end
  151 
  152 
  153 --- Start non-blocking HTTP call
  154 -- @param url URL to call
  155 -- @param options options table as expected by net.http where we provide optional headers, body or method.
  156 -- @param callback if provided, called with callback(response_body, response_code) when call complete.
  157 -- @param timeout_callback if provided, called without args when request times out.
  158 -- @param retries how many times to retry on failure; 0 means no retries.
  159 local function async_http_request(url, options, callback, timeout_callback, retries)
  160     local completed = false;
  161     local timed_out = false;
  162     local retries = retries or api_retry_count;
  163 
  164     local function cb_(response_body, response_code)
  165         if not timed_out then  -- request completed before timeout
  166             completed = true;
  167             if (response_code == 0 or api_should_retry_for_code(response_code)) and retries > 0 then
  168                 module:log("warn", "API Response code %d. Will retry after %ds", response_code, api_retry_delay);
  169                 timer.add_task(api_retry_delay, function()
  170                     async_http_request(url, options, callback, timeout_callback, retries - 1)
  171                 end)
  172                 return;
  173             end
  174 
  175             if callback then
  176                 callback(response_body, response_code)
  177             end
  178         end
  179     end
  180 
  181     local request = http.request(url, options, cb_);
  182 
  183     timer.add_task(api_timeout, function ()
  184         timed_out = true;
  185 
  186         if not completed then
  187             http.destroy_request(request);
  188             if timeout_callback then
  189                 timeout_callback()
  190             end
  191         end
  192     end);
  193 
  194 end
  195 
  196 --- Returns current timestamp
  197 local function now()
  198     -- Don't really need higher precision of socket.gettime(). Besides, we loose
  199     -- milliseconds precision when converting back to timestamp from date string
  200     -- when we use datetime.parse(t), so let's be consistent.
  201     return os.time();
  202 end
  203 
  204 --- Start RoomReservation implementation
  205 
  206 -- Status enums used in RoomReservation:meta.status
  207 local STATUS = {
  208     PENDING = 0;
  209     SUCCESS = 1;
  210     FAILED  = -1;
  211 }
  212 
  213 local RoomReservation = {};
  214 RoomReservation.__index = RoomReservation;
  215 
  216 function newRoomReservation(room_jid, creator_jid)
  217     return setmetatable({
  218         room_jid = room_jid;
  219 
  220         -- Reservation metadata. store as table so we can set and read atomically.
  221         -- N.B. This should always be updated using self.set_status_*
  222         meta = {
  223             status = STATUS.PENDING;
  224             mail_owner = jid.bare(creator_jid);
  225             conflict_id = nil;
  226             start_time = now();  -- timestamp, in seconds
  227             expires_at = nil;  -- timestamp, in seconds
  228             error_text = nil;
  229             error_code = nil;
  230         };
  231 
  232         -- Array of pending events that we need to route once API call is complete
  233         pending_events = {};
  234 
  235         -- Set true when API call trigger has been triggered (by enqueue of first event)
  236         api_call_triggered = false;
  237     }, RoomReservation);
  238 end
  239 
  240 
  241 --- Extracts room name from room jid
  242 function RoomReservation:get_room_name()
  243     return jid.node(self.room_jid);
  244 end
  245 
  246 --- Checks if reservation data is expires and should be evicted from store
  247 function RoomReservation:is_expired()
  248     return self.meta.expires_at ~= nil and now() > self.meta.expires_at;
  249 end
  250 
  251 --- Main entry point for handing and routing events.
  252 function RoomReservation:enqueue_or_route_event(event)
  253     if self.meta.status == STATUS.PENDING then
  254         table.insert(self.pending_events, event)
  255         if self.api_call_triggered ~= true then
  256             self:call_api_create_conference();
  257         end
  258     else
  259         -- API call already complete. Immediately route without enqueueing.
  260         -- This could happen if request comes in between the time reservation approved
  261         -- and when Jicofo actually creates the room.
  262         module:log("debug", "Reservation details already stored. Skipping queue for %s", self.room_jid);
  263         self:route_event(event);
  264     end
  265 end
  266 
  267 --- Updates status and initiates event routing. Called internally when API call complete.
  268 function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, data)
  269     module:log("info", "Reservation created successfully for %s", self.room_jid);
  270     self.meta = {
  271         status = STATUS.SUCCESS;
  272         mail_owner = mail_owner or self.meta.mail_owner;
  273         conflict_id = conflict_id;
  274         start_time = start_time;
  275         expires_at = start_time + duration;
  276         error_text = nil;
  277         error_code = nil;
  278     }
  279     if max_occupants_enabled and data.max_occupants then
  280         self.meta.max_occupants = data.max_occupants
  281     end
  282     if lobby_support_enabled and data.lobby then
  283         self.meta.lobby = data.lobby
  284     end
  285     if password_support_enabled and data.password then
  286         self.meta.password = data.password
  287     end
  288     self:route_pending_events()
  289 end
  290 
  291 --- Updates status and initiates error response to pending events. Called internally when API call complete.
  292 function RoomReservation:set_status_failed(error_code, error_text)
  293     module:log("info", "Reservation creation failed for %s - (%s) %s", self.room_jid, error_code, error_text);
  294     self.meta = {
  295         status = STATUS.FAILED;
  296         mail_owner = self.meta.mail_owner;
  297         conflict_id = nil;
  298         start_time = self.meta.start_time;
  299         -- Retain reservation rejection for a short while so we have time to report failure to
  300         -- existing clients and not trigger a re-query too soon.
  301         -- N.B. Expiry could take longer since eviction happens periodically.
  302         expires_at = now() + 30;
  303         error_text = error_text;
  304         error_code = error_code;
  305     }
  306     self:route_pending_events()
  307 end
  308 
  309 --- Triggers routing of all enqueued events
  310 function RoomReservation:route_pending_events()
  311     if self.meta.status == STATUS.PENDING then  -- should never be called while PENDING. check just in case.
  312         return;
  313     end
  314 
  315     module:log("debug", "Routing all pending events for %s", self.room_jid);
  316     local event;
  317 
  318     while #self.pending_events ~= 0 do
  319         event = table.remove(self.pending_events);
  320         self:route_event(event)
  321     end
  322 end
  323 
  324 --- Event routing implementation
  325 function RoomReservation:route_event(event)
  326     -- this should only be called after API call complete and status no longer PENDING
  327     assert(self.meta.status ~= STATUS.PENDING, "Attempting to route event while API call still PENDING")
  328 
  329     local meta = self.meta;
  330     local origin, stanza = event.origin, event.stanza;
  331 
  332     if meta.status == STATUS.FAILED then
  333         module:log("debug", "Route: Sending reservation error to %s", stanza.attr.from);
  334         self:reply_with_error(event, meta.error_code, meta.error_text);
  335     else
  336         if meta.status == STATUS.SUCCESS then
  337             if self:is_expired() then
  338                 module:log("debug", "Route: Sending reservation expiry to %s", stanza.attr.from);
  339                 self:reply_with_error(event, 419, "Reservation expired");
  340             else
  341                 module:log("debug", "Route: Forwarding on event from %s", stanza.attr.from);
  342                 prosody.core_post_stanza(origin, stanza, false); -- route iq to intended target (focus)
  343             end
  344         else
  345             -- this should never happen unless dev made a mistake. Block by default just in case.
  346             module:log("error", "Reservation for %s has invalid state %s. Rejecting request.", self.room_jid, meta.status);
  347             self:reply_with_error(event, 500, "Failed to determine reservation state");
  348         end
  349     end
  350 end
  351 
  352 --- Generates reservation-error stanza and sends to event origin.
  353 function RoomReservation:reply_with_error(event, error_code, error_text)
  354     local stanza = event.stanza;
  355     local id = stanza.attr.id;
  356     local to = stanza.attr.from;
  357     local from = stanza.attr.to;
  358 
  359     event.origin.send(
  360         st.iq({ type="error", to=to, from=from, id=id })
  361             :tag("error", { type="cancel" })
  362                 :tag("service-unavailable", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):up()
  363                 :tag("text", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):text(error_text):up()
  364                 :tag("reservation-error", { xmlns="http://jitsi.org/protocol/focus", ["error-code"]=tostring(error_code) })
  365     );
  366 end
  367 
  368 --- Initiates non-blocking API call to validate reservation
  369 function RoomReservation:call_api_create_conference()
  370     self.api_call_triggered = true;
  371 
  372     local url = api_prefix..'/conference';
  373     local request_data = {
  374         name = self:get_room_name();
  375         start_time = to_java_date_string(self.meta.start_time);
  376         mail_owner = self.meta.mail_owner;
  377     }
  378 
  379     local http_options = {
  380         body = http.formencode(request_data);  -- because Jicofo reservation encodes as form data instead JSON
  381         method = 'POST';
  382         headers = http_headers;
  383     }
  384 
  385     module:log("debug", "Sending POST /conference for %s", self.room_jid);
  386     async_http_request(url, http_options, function (response_body, response_code)
  387         self:on_api_create_conference_complete(response_body, response_code);
  388     end, function ()
  389         self:on_api_call_timeout();
  390     end);
  391 end
  392 
  393 --- Parses and validates HTTP response body for conference payload
  394 --  Ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
  395 --  @return nil if invalid, or table with payload parsed from JSON response
  396 function RoomReservation:parse_conference_response(response_body)
  397     local data = json.decode(response_body);
  398 
  399     if data == nil then  -- invalid JSON payload
  400         module:log("error", "Invalid JSON response from API - %s", response_body);
  401         return;
  402     end
  403 
  404     if data.name == nil or data.name:lower() ~= self:get_room_name() then
  405         module:log("error", "Missing or mismatching room name - %s", data.name);
  406         return;
  407     end
  408 
  409     if data.id == nil then
  410         module:log("error", "Missing id");
  411         return;
  412     end
  413 
  414     if data.mail_owner == nil then
  415         module:log("error", "Missing mail_owner");
  416         return;
  417     end
  418 
  419     local duration = tonumber(data.duration);
  420     if duration == nil then
  421         module:log("error", "Missing or invalid duration - %s", data.duration);
  422         return;
  423     end
  424     data.duration = duration;
  425 
  426     -- if optional "max_occupants" field set, cast to number
  427     if data.max_occupants ~= nil then
  428         local max_occupants = tonumber(data.max_occupants)
  429         if max_occupants == nil or max_occupants < 1 then
  430             -- N.B. invalid max_occupants rejected even if max_occupants_enabled=false
  431             module:log("error", "Invalid value for max_occupants - %s", data.max_occupants);
  432             return;
  433         end
  434         data.max_occupants = max_occupants
  435     end
  436 
  437     -- if optional "lobby" field set, accept boolean true or "true"
  438     if data.lobby ~= nil then
  439         if (type(data.lobby) == "boolean" and data.lobby) or data.lobby == "true" then
  440             data.lobby = true
  441         else
  442             data.lobby = false
  443         end
  444     end
  445 
  446     -- if optional "password" field set, it has to be string
  447     if data.password ~= nil then
  448         if type(data.password) ~= "string" then
  449             -- N.B. invalid "password" rejected even if reservations_enable_password_support=false
  450             module:log("error", "Invalid type for password - string expected");
  451             return;
  452         end
  453     end
  454 
  455     local start_time = datetime.parse(data.start_time);  -- N.B. we lose milliseconds portion of the date
  456     if start_time == nil then
  457         module:log("error", "Missing or invalid start_time - %s", data.start_time);
  458         return;
  459     end
  460     data.start_time = start_time;
  461 
  462     return data;
  463 end
  464 
  465 --- Parses and validates HTTP error response body for API call.
  466 --  Expect JSON with a "message" field.
  467 --  @return message string, or generic error message if invalid payload.
  468 function RoomReservation:parse_error_message_from_response(response_body)
  469     local data = json.decode(response_body);
  470     if data ~= nil and data.message ~= nil then
  471         module:log("debug", "Invalid error response body. Will use generic error message.");
  472         return data.message;
  473     else
  474         return "Rejected by reservation server";
  475     end
  476 end
  477 
  478 --- callback on API timeout
  479 function RoomReservation:on_api_call_timeout()
  480     self:set_status_failed(500, 'Reservation lookup timed out');
  481 end
  482 
  483 --- callback on API response
  484 function RoomReservation:on_api_create_conference_complete(response_body, response_code)
  485     if response_code == 200 or response_code == 201 then
  486         self:handler_conference_data_returned_from_api(response_body);
  487     elseif response_code == 409 then
  488         self:handle_conference_already_exist(response_body);
  489     elseif response_code == nil then  -- warrants a retry, but this should be done automatically by the http call method.
  490         self:set_status_failed(500, 'Could not contact reservation server');
  491     else
  492         self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
  493     end
  494 end
  495 
  496 function RoomReservation:handler_conference_data_returned_from_api(response_body)
  497     local data = self:parse_conference_response(response_body);
  498     if not data then  -- invalid response from API
  499         module:log("error", "API returned success code but invalid payload");
  500         self:set_status_failed(500, 'Invalid response from reservation server');
  501     else
  502         self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data)
  503     end
  504 end
  505 
  506 function RoomReservation:handle_conference_already_exist(response_body)
  507     local data = json.decode(response_body);
  508     if data == nil or data.conflict_id == nil then
  509         -- yes, in the case of 409, API expected to return "id" as "conflict_id".
  510         self:set_status_failed(409, 'Invalid response from reservation server');
  511     else
  512         local url = api_prefix..'/conference/'..data.conflict_id;
  513         local http_options = {
  514             method = 'GET';
  515             headers = http_headers;
  516         }
  517 
  518         async_http_request(url, http_options, function(response_body, response_code)
  519             if response_code == 200 then
  520                 self:handler_conference_data_returned_from_api(response_body);
  521             else
  522                 self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
  523             end
  524         end, function ()
  525             self:on_api_call_timeout();
  526         end);
  527     end
  528 end
  529 
  530 --- End RoomReservation
  531 
  532 --- Store reservations lookups that are still pending or with room still active
  533 local reservations = {}
  534 
  535 local function get_or_create_reservations(room_jid, creator_jid)
  536     if reservations[room_jid] == nil then
  537         module:log("debug", "Creating new reservation data for %s", room_jid);
  538         reservations[room_jid] = newRoomReservation(room_jid, creator_jid);
  539     end
  540 
  541     return reservations[room_jid];
  542 end
  543 
  544 local function evict_expired_reservations()
  545     local expired = {}
  546 
  547     -- first, gather jids of expired rooms. So we don't remove from table while iterating.
  548     for room_jid, res in pairs(reservations) do
  549         if res:is_expired() then
  550             table.insert(expired, room_jid);
  551         end
  552     end
  553 
  554     local room;
  555     for _, room_jid in ipairs(expired) do
  556         room = get_room_from_jid(room_jid);
  557         if room then
  558             -- Close room if still active (reservation duration exceeded)
  559             module:log("info", "Room exceeded reservation duration. Terminating %s", room_jid);
  560             room:destroy(nil, "Scheduled conference duration exceeded.");
  561             -- Rely on room_destroyed to calls DELETE /conference and drops reservation[room_jid]
  562         else
  563             module:log("error", "Reservation references expired room that is no longer active. Dropping %s", room_jid);
  564             -- This should not happen unless evict_expired_reservations somehow gets triggered
  565             -- between the time room is destroyed and room_destroyed callback is called. (Possible?)
  566             -- But just in case, we drop the reservation to avoid repeating this path on every pass.
  567             reservations[room_jid] = nil;
  568         end
  569     end
  570 end
  571 
  572 timer.add_task(expiry_check_period, function()
  573     evict_expired_reservations();
  574     return expiry_check_period;
  575 end)
  576 
  577 
  578 --- Intercept conference IQ to Jicofo handle reservation checks before allowing normal event flow
  579 module:log("info", "Hook to global pre-iq/host");
  580 module:hook("pre-iq/host", function(event)
  581     local stanza = event.stanza;
  582 
  583     if stanza.name ~= "iq" or stanza.attr.to ~= focus_component_host or stanza.attr.type ~= 'set' then
  584         return;  -- not IQ for jicofo. Ignore this event.
  585     end
  586 
  587     local conference = stanza:get_child('conference', 'http://jitsi.org/protocol/focus');
  588     if conference == nil then
  589         return; -- not Conference IQ. Ignore.
  590     end
  591 
  592     local room_jid = room_jid_match_rewrite(conference.attr.room);
  593 
  594     if get_room_from_jid(room_jid) ~= nil then
  595         module:log("debug", "Skip reservation check for existing room %s", room_jid);
  596         return;  -- room already exists. Continue with normal flow
  597     end
  598 
  599     if skip_breakout_room then
  600         local _, host = jid.split(room_jid);
  601         if host == breakout_muc_component_host then
  602             module:log("debug", "Skip reservation check for breakout room %s", room_jid);
  603             return;
  604         end
  605     end
  606 
  607     local res = get_or_create_reservations(room_jid, stanza.attr.from);
  608     res:enqueue_or_route_event(event);  -- hand over to reservation obj to route event
  609     return true;
  610 
  611 end);
  612 
  613 
  614 --- Forget reservation details once room destroyed so query is repeated if room re-created
  615 local function room_destroyed(event)
  616     local res;
  617     local room = event.room
  618 
  619     if not is_healthcheck_room(room.jid) then
  620         res = reservations[room.jid]
  621 
  622         -- drop reservation data for this room
  623         reservations[room.jid] = nil
  624 
  625         if res then  -- just in case event triggered more than once?
  626             module:log("info", "Dropped reservation data for destroyed room %s", room.jid);
  627 
  628             local conflict_id = res.meta.conflict_id
  629             if conflict_id then
  630                 local url = api_prefix..'/conference/'..conflict_id;
  631                 local http_options = {
  632                     method = 'DELETE';
  633                     headers = http_headers;
  634                 }
  635 
  636                 module:log("debug", "Sending DELETE /conference/%s", conflict_id);
  637                 async_http_request(url, http_options);
  638             end
  639         end
  640     end
  641 end
  642 
  643 
  644 local function room_created(event)
  645     local room = event.room
  646 
  647     if is_healthcheck_room(room.jid) then
  648         return;
  649     end
  650 
  651     local res = reservations[room.jid]
  652 
  653     if res and max_occupants_enabled and res.meta.max_occupants ~= nil then
  654         module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid);
  655         room._data.max_occupants = res.meta.max_occupants
  656     end
  657 
  658     if res and password_support_enabled and res.meta.password ~= nil then
  659         module:log("info", "Setting password for room %s", room.jid);
  660         room:set_password(res.meta.password);
  661     end
  662 end
  663 
  664 
  665 local function room_pre_create(event)
  666     local room = event.room
  667 
  668     if is_healthcheck_room(room.jid) then
  669         return;
  670     end
  671 
  672     local res = reservations[room.jid]
  673 
  674     if res and lobby_support_enabled and res.meta.lobby then
  675         module:log("info", "Enabling lobby for room %s", room.jid);
  676         prosody.events.fire_event("create-persistent-lobby-room", { room = room; });
  677     end
  678 end
  679 
  680 
  681 function process_host(host)
  682     if host == muc_component_host then -- the conference muc component
  683         module:log("info", "Hook to muc-room-destroyed on %s", host);
  684         module:context(host):hook("muc-room-destroyed", room_destroyed, -1);
  685 
  686         if max_occupants_enabled or password_support_enabled then
  687             module:log("info", "Hook to muc-room-created on %s (max_occupants or password integration enabled)", host);
  688             module:context(host):hook("muc-room-created", room_created);
  689         end
  690 
  691         if lobby_support_enabled then
  692             module:log("info", "Hook to muc-room-pre-create on %s (lobby integration enabled)", host);
  693             module:context(host):hook("muc-room-pre-create", room_pre_create);
  694         end
  695     end
  696 end
  697 
  698 if prosody.hosts[muc_component_host] == nil then
  699     module:log("info", "No muc component found, will listen for it: %s", muc_component_host)
  700     prosody.events.add_handler("host-activated", process_host);
  701 else
  702     process_host(muc_component_host);
  703 end