659 lines
30 KiB
Lua
659 lines
30 KiB
Lua
-- Test-only module. Never load in production.
|
|
-- Serves HTTP endpoints for test assertions. Must be loaded on the main VirtualHost
|
|
-- (not the MUC component) so that plain HTTP on port 5280 is reachable with
|
|
-- Host: localhost — the VirtualHost's domain.
|
|
--
|
|
-- Data is supplied by mod_test_observer (loaded on the MUC component) via
|
|
-- module:shared. Load order does not matter; shared tables are created lazily.
|
|
--
|
|
-- Also serves a mock access manager endpoint for mod_muc_auth_ban tests:
|
|
-- GET /test-observer/access-manager — called by Prosody; returns configured response
|
|
-- POST /test-observer/access-manager — called by tests to configure the response
|
|
|
|
local json = require "cjson.safe";
|
|
local io = require "io";
|
|
local jid_lib = require "util.jid";
|
|
|
|
-- Mock access-manager state for mod_muc_auth_ban tests.
|
|
-- access: true → return {"access": true} (user allowed)
|
|
-- access: false → return {"access": false} (user banned)
|
|
-- status: any non-200 value → return that HTTP status code with no JSON body
|
|
-- (simulates HTTP errors; mod_muc_auth_ban fails open on non-200)
|
|
-- non_json: true → return a 200 with a plain-text body (not valid JSON)
|
|
-- (tests the nil-check bug: json.decode returns nil, r['access'] would crash)
|
|
-- Reset to the default (allow, 200) between tests via POST /test-observer/access-manager.
|
|
local access_manager_state = { access = true, status = 200, non_json = false };
|
|
|
|
-- Mock OTLP trace receiver for mod_trace tests. Stores every exported
|
|
-- resource_spans payload POSTed by the module's Exporter so tests can assert
|
|
-- a span was (or was not) exported without needing a real OTLP collector.
|
|
local otlp_traces = {};
|
|
|
|
-- ASAP public key servers: serve test RSA public keys so that Prosody can
|
|
-- fetch them when verifying RS256 tokens signed by the matching private keys.
|
|
-- util.lib.lua constructs the URL as: <asap_key_server>/<sha256hex(kid)>.pem
|
|
--
|
|
-- Two separate key pairs are used:
|
|
-- Login tokens (kid "test-asap-key"): signed by the login key pair,
|
|
-- served at /test-observer/asap-keys/
|
|
-- used by mod_auth_token (VirtualHost "localhost")
|
|
-- System tokens (kid "test-system-asap-key"): signed by the system key pair,
|
|
-- served at /test-observer/system-asap-keys/
|
|
-- used by mod_muc_end_meeting and similar HTTP API modules
|
|
local ASAP_KEY_PATH = "/opt/prosody-jitsi-plugins/test-asap-public.pem";
|
|
local ASAP_KID_SHA256 = "dc6983da8e703a3f51d4c1cb92b52c982f7853ce3d5ba20c782fcd13616f6dfc";
|
|
|
|
local SYSTEM_ASAP_KEY_PATH = "/opt/prosody-jitsi-plugins/test-system-asap-public.pem";
|
|
local SYSTEM_ASAP_KID_SHA256 = "e76ed986a75a90756e5add6e8b56efc3d3f027764436d2744d29a33f0ec24fea";
|
|
|
|
local function load_pem(path)
|
|
local f = io.open(path, "r");
|
|
if not f then return nil; end
|
|
local data = f:read("*all");
|
|
f:close();
|
|
return data;
|
|
end
|
|
|
|
local asap_public_key = load_pem(ASAP_KEY_PATH);
|
|
if asap_public_key then
|
|
module:log("info", "Loaded test ASAP public key from %s", ASAP_KEY_PATH);
|
|
else
|
|
module:log("warn", "Test ASAP public key not found at %s; login ASAP key-server routes will 404", ASAP_KEY_PATH);
|
|
end
|
|
|
|
local system_asap_public_key = load_pem(SYSTEM_ASAP_KEY_PATH);
|
|
if system_asap_public_key then
|
|
module:log("info", "Loaded test system ASAP public key from %s", SYSTEM_ASAP_KEY_PATH);
|
|
else
|
|
module:log("warn", "Test system ASAP public key not found at %s; system ASAP key-server routes will 404", SYSTEM_ASAP_KEY_PATH);
|
|
end
|
|
|
|
-- session-info: capture mod_jitsi_session / mod_auth_token fields after
|
|
-- resource-bind so tests can assert URL query params and JWT claims were
|
|
-- stored correctly on the session object.
|
|
local session_info = {}; -- full_jid -> field snapshot
|
|
|
|
local function capture_session_info(event)
|
|
local session = event.session;
|
|
local jid = session.full_jid;
|
|
if jid then
|
|
session_info[jid] = {
|
|
previd = session.previd,
|
|
customusername = session.customusername,
|
|
jitsi_web_query_room = session.jitsi_web_query_room,
|
|
jitsi_web_query_prefix = session.jitsi_web_query_prefix,
|
|
auth_token = session.auth_token,
|
|
user_region = session.user_region,
|
|
user_agent_header = session.user_agent_header,
|
|
-- Fields set by mod_auth_token after JWT verification:
|
|
jitsi_meet_room = session.jitsi_meet_room,
|
|
jitsi_meet_context_user = session.jitsi_meet_context_user,
|
|
jitsi_meet_context_group = session.jitsi_meet_context_group,
|
|
jitsi_meet_context_features = session.jitsi_meet_context_features,
|
|
};
|
|
end
|
|
end
|
|
|
|
-- resource-bind fires on each VirtualHost's own event bus, not globally.
|
|
-- Register on every host already active at module load time, and on any host
|
|
-- that activates later (e.g. hs256.localhost loads after localhost since
|
|
-- VirtualHosts are initialized in config order).
|
|
local function register_on_host(host_obj)
|
|
if host_obj and host_obj.events then
|
|
host_obj.events.add_handler("resource-bind", capture_session_info, 10);
|
|
end
|
|
end
|
|
|
|
for _, host_obj in pairs(prosody.hosts) do
|
|
register_on_host(host_obj);
|
|
end
|
|
|
|
-- host-activated passes the hostname string directly (not a table).
|
|
module:hook_global("host-activated", function(host)
|
|
register_on_host(prosody.hosts[host]);
|
|
end);
|
|
|
|
-- /conference.localhost/mod_test_observer is the absolute path for the shared
|
|
-- table created by mod_test_observer running on conference.localhost.
|
|
local MUC_HOST = module:get_option_string("muc_mapper_domain_base", "localhost");
|
|
local MUC_COMPONENT = module:get_option_string("muc_mapper_domain_prefix", "conference") .. "." .. MUC_HOST;
|
|
local shared = module:shared("/" .. MUC_COMPONENT .. "/mod_test_observer");
|
|
|
|
local function urldecode(s)
|
|
return (s:gsub("%%(%x%x)", function(hex)
|
|
return string.char(tonumber(hex, 16));
|
|
end));
|
|
end
|
|
|
|
local function parse_query(q)
|
|
local params = {};
|
|
for k, v in (q or ""):gmatch("([^&=]+)=([^&]*)") do
|
|
params[urldecode(k)] = urldecode(v);
|
|
end
|
|
return params;
|
|
end
|
|
|
|
module:provides("http", {
|
|
default_path = "/test-observer";
|
|
route = {
|
|
-- GET /test-observer/asap-keys/<sha256hex(kid)>.pem
|
|
-- Returns the login RSA public key PEM so mod_auth_token can verify RS256 login tokens.
|
|
-- kid must be "test-asap-key" (its SHA256 hex is the filename).
|
|
["GET /asap-keys/"..ASAP_KID_SHA256..".pem"] = function()
|
|
if not asap_public_key then
|
|
return { status_code = 404; body = "key not found" };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/x-pem-file" };
|
|
body = asap_public_key;
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/system-asap-keys/<sha256hex(kid)>.pem
|
|
-- Returns the system RSA public key PEM for mod_muc_end_meeting and similar
|
|
-- system HTTP API modules that use prosody_password_public_key_repo_url.
|
|
-- kid must be "test-system-asap-key" (its SHA256 hex is the filename).
|
|
["GET /system-asap-keys/"..SYSTEM_ASAP_KID_SHA256..".pem"] = function()
|
|
if not system_asap_public_key then
|
|
return { status_code = 404; body = "key not found" };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/x-pem-file" };
|
|
body = system_asap_public_key;
|
|
};
|
|
end;
|
|
|
|
["GET /events"] = function()
|
|
local events = shared.events or {};
|
|
-- cjson encodes an empty Lua table as {} (object); force array literal.
|
|
local body = #events == 0 and "[]" or json.encode(events);
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = body;
|
|
};
|
|
end;
|
|
|
|
["DELETE /events"] = function()
|
|
-- Replace with a fresh table; mod_test_observer always reads shared.events
|
|
-- via the shared reference so it will see the new table immediately.
|
|
shared.events = {};
|
|
return { status_code = 204 };
|
|
end;
|
|
|
|
-- GET /test-observer/jibri-iqs
|
|
-- Returns the list of Jibri IQs that reached the MUC (i.e. passed mod_filter_iq_jibri).
|
|
["GET /jibri-iqs"] = function()
|
|
local iqs = shared.jibri_iqs or {};
|
|
local body = #iqs == 0 and "[]" or json.encode(iqs);
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = body;
|
|
};
|
|
end;
|
|
|
|
-- DELETE /test-observer/jibri-iqs
|
|
-- Clears the recorded Jibri IQ list. Call before each test.
|
|
["DELETE /jibri-iqs"] = function()
|
|
shared.jibri_iqs = {};
|
|
return { status_code = 204 };
|
|
end;
|
|
|
|
-- GET /test-observer/dial-iqs
|
|
-- Returns the list of Rayo dial IQs that reached the MUC (i.e. passed mod_filter_iq_rayo).
|
|
["GET /dial-iqs"] = function()
|
|
local iqs = shared.dial_iqs or {};
|
|
local body = #iqs == 0 and "[]" or json.encode(iqs);
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = body;
|
|
};
|
|
end;
|
|
|
|
-- DELETE /test-observer/dial-iqs
|
|
-- Clears the recorded Rayo dial IQ list. Call before each test.
|
|
["DELETE /dial-iqs"] = function()
|
|
shared.dial_iqs = {};
|
|
return { status_code = 204 };
|
|
end;
|
|
|
|
-- POST /test-observer/sessions/context
|
|
-- Body: { "jid": "node@localhost/resource", "user_id": "...", "features": { "flip": true } }
|
|
-- Sets jitsi_meet_context_user / jitsi_meet_context_features on the c2s session so that
|
|
-- mod_muc_flip (and other JWT-aware modules) see the same context as they would with a
|
|
-- real token — without needing a JWT auth module in the test setup.
|
|
["POST /sessions/context"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local full_jid = data.jid;
|
|
local user_id = data.user_id;
|
|
local features = data.features or {};
|
|
if not full_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid"}' };
|
|
end
|
|
local node, host, resource = jid_lib.split(full_jid);
|
|
local host_obj = host and prosody.hosts[host];
|
|
if not host_obj then
|
|
return { status_code = 404; body = '{"error":"host not found"}' };
|
|
end
|
|
local user_obj = host_obj.sessions and host_obj.sessions[node];
|
|
if not user_obj then
|
|
return { status_code = 404; body = '{"error":"user not found"}' };
|
|
end
|
|
local session = user_obj.sessions and user_obj.sessions[resource];
|
|
if not session then
|
|
return { status_code = 404; body = '{"error":"session not found"}' };
|
|
end
|
|
if user_id then
|
|
session.jitsi_meet_context_user = { id = user_id };
|
|
end
|
|
session.jitsi_meet_context_features = features;
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = '{"ok":true}';
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/sessions/features?jid=user@localhost/resource
|
|
-- Returns the live jitsi_meet_context_features for the session.
|
|
-- Reading live (not a snapshot) captures side-effects from modules such as
|
|
-- mod_jitsi_permissions, which may set or update features after resource-bind.
|
|
-- Returns 200 {"features": <object|null>} or 404.
|
|
["GET /sessions/features"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local full_jid = params["jid"];
|
|
if not full_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local node, host, resource = jid_lib.split(full_jid);
|
|
local host_obj = host and prosody.hosts[host];
|
|
if not host_obj then
|
|
return { status_code = 404; body = '{"error":"host not found"}' };
|
|
end
|
|
local user_obj = host_obj.sessions and host_obj.sessions[node];
|
|
if not user_obj then
|
|
return { status_code = 404; body = '{"error":"user not found"}' };
|
|
end
|
|
local session = user_obj.sessions and user_obj.sessions[resource];
|
|
if not session then
|
|
return { status_code = 404; body = '{"error":"session not found"}' };
|
|
end
|
|
local features = session.jitsi_meet_context_features;
|
|
local body = features and json.encode({ features = features }) or '{"features":null}';
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = body;
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/rooms/participants?jid=room@conference.localhost
|
|
-- Returns: { participants_details: { userId: fullNick }, kicked_participant_nick?, flip_participant_nick? }
|
|
-- Exposes mod_muc_flip's per-room tracking tables for test assertions.
|
|
["GET /rooms/participants"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local room_jid = params["jid"];
|
|
if not room_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
local result = { participants_details = room._data.participants_details or {} };
|
|
if room._data.kicked_participant_nick ~= nil then
|
|
result.kicked_participant_nick = room._data.kicked_participant_nick;
|
|
end
|
|
if room._data.flip_participant_nick ~= nil then
|
|
result.flip_participant_nick = room._data.flip_participant_nick;
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode(result);
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/rooms/hide-display-name
|
|
-- Body: { "jid": "room@conference.localhost", "hidden": true|false }
|
|
-- Sets room._data.hideDisplayNameForGuests for mod_muc_displayname tests.
|
|
["POST /rooms/hide-display-name"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local room_jid = data.jid;
|
|
local hidden = data.hidden;
|
|
if room_jid == nil or hidden == nil then
|
|
return { status_code = 400; body = '{"error":"missing jid or hidden"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
room._data.hideDisplayNameForGuests = hidden;
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = '{"ok":true}';
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/rooms/breakout-rooms-active
|
|
-- Body: { "jid": "room@conference.localhost", "active": true|false }
|
|
-- Sets room._data.breakout_rooms_active, which mod_muc_cleanup_backend_services
|
|
-- checks to skip the destroy-timer logic when breakout rooms are running.
|
|
["POST /rooms/breakout-rooms-active"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local room_jid = data.jid;
|
|
local active = data.active;
|
|
if room_jid == nil or active == nil then
|
|
return { status_code = 400; body = '{"error":"missing jid or active"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
room._data.breakout_rooms_active = active;
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = '{"ok":true}';
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/rooms/max-occupants
|
|
-- Body: { "jid": "room@conference.localhost", "max_occupants": 4 }
|
|
-- Sets room._data.max_occupants so per-room limit tests can override the
|
|
-- global muc_max_occupants without restarting Prosody.
|
|
["POST /rooms/max-occupants"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local room_jid = data.jid;
|
|
local max = tonumber(data.max_occupants);
|
|
if not room_jid or not max then
|
|
return { status_code = 400; body = '{"error":"missing jid or max_occupants"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
room._data.max_occupants = max;
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = '{"ok":true}';
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/otlp-traces
|
|
-- Called by mod_trace's Exporter. Body is the raw OTLP
|
|
-- ExportTraceServiceRequest JSON; stored verbatim for test assertions.
|
|
["POST /otlp-traces"] = function(event)
|
|
local data = json.decode(event.request.body or "{}");
|
|
if data then
|
|
table.insert(otlp_traces, data);
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = "{}";
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/otlp-traces
|
|
-- Returns every ExportTraceServiceRequest body received so far.
|
|
["GET /otlp-traces"] = function()
|
|
local body = #otlp_traces == 0 and "[]" or json.encode(otlp_traces);
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = body;
|
|
};
|
|
end;
|
|
|
|
-- DELETE /test-observer/otlp-traces
|
|
-- Clears the recorded export list. Call before each test.
|
|
["DELETE /otlp-traces"] = function()
|
|
otlp_traces = {};
|
|
return { status_code = 204 };
|
|
end;
|
|
|
|
-- GET /test-observer/access-manager
|
|
-- Mock access-manager endpoint called by mod_muc_auth_ban.
|
|
-- Returns {"access": true} or {"access": false} based on the current
|
|
-- access_manager_state, or a non-200 status to simulate HTTP errors.
|
|
-- The real access manager receives `Authorization: Bearer <token>` but
|
|
-- this mock ignores it and returns the globally configured response.
|
|
["GET /access-manager"] = function()
|
|
if access_manager_state.status ~= 200 then
|
|
return { status_code = access_manager_state.status; body = "error" };
|
|
end
|
|
if access_manager_state.non_json then
|
|
-- Return a 200 with a non-JSON body to exercise the nil-check
|
|
-- bug in mod_muc_auth_ban: json.decode returns nil, and
|
|
-- r['access'] would crash if the module doesn't guard for nil.
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "text/plain" };
|
|
body = "not-valid-json";
|
|
};
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode({ access = access_manager_state.access });
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/access-manager
|
|
-- Body: { "access": true|false, "status": <http-status-code>, "non_json": true|false }
|
|
-- Configures what the mock access manager returns.
|
|
-- Call this from tests before connecting the client under test.
|
|
-- Call with { "access": true, "status": 200 } to reset to the default.
|
|
["POST /access-manager"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
if data.access ~= nil then
|
|
access_manager_state.access = data.access;
|
|
end
|
|
if data.status ~= nil then
|
|
access_manager_state.status = tonumber(data.status) or 200;
|
|
end
|
|
if data.non_json ~= nil then
|
|
access_manager_state.non_json = data.non_json;
|
|
end
|
|
return { status_code = 204 };
|
|
end;
|
|
|
|
-- GET /test-observer/session-info?jid=user@localhost/resource
|
|
-- Returns the mod_jitsi_session fields captured at resource-bind time.
|
|
["GET /session-info"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local jid = params["jid"];
|
|
if not jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local info = session_info[jid];
|
|
if not info then
|
|
return { status_code = 404; body = '{"error":"session not found"}' };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode(info);
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/session-live?jid=user@localhost/resource
|
|
-- Returns the CURRENT values of the JWT-derived fields on the live c2s
|
|
-- session, read straight from prosody.full_sessions. Unlike
|
|
-- /session-info (a snapshot taken at resource-bind) this reflects
|
|
-- changes made after the session was established -- notably the claim
|
|
-- refresh performed by mod_auth_token on XEP-0198 stream resumption,
|
|
-- which never binds a new resource.
|
|
["GET /session-live"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local jid = params["jid"];
|
|
if not jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local session = prosody.full_sessions[jid];
|
|
if not session then
|
|
return { status_code = 404; body = '{"error":"session not found"}' };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode({
|
|
auth_token = session.auth_token,
|
|
jitsi_web_query_room = session.jitsi_web_query_room,
|
|
jitsi_web_query_prefix = session.jitsi_web_query_prefix,
|
|
jitsi_meet_room = session.jitsi_meet_room,
|
|
jitsi_meet_domain = session.jitsi_meet_domain,
|
|
jitsi_meet_str_tenant = session.jitsi_meet_str_tenant,
|
|
jitsi_meet_context_user = session.jitsi_meet_context_user,
|
|
jitsi_meet_context_group = session.jitsi_meet_context_group,
|
|
jitsi_meet_context_features = session.jitsi_meet_context_features,
|
|
});
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/rooms/lobby
|
|
-- Body: { "jid": "room@conference.localhost", "enabled": true|false }
|
|
-- Enables or disables the lobby for the given room by firing the
|
|
-- create-lobby-room / destroy-lobby-room global Prosody events.
|
|
-- Enabling also sets members-only; disabling unsets it.
|
|
-- Response: { ok, lobbyroom } (lobbyroom is nil when disabling).
|
|
["POST /rooms/lobby"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local room_jid = data.jid;
|
|
local enabled = data.enabled;
|
|
if not room_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
if enabled then
|
|
prosody.events.fire_event('create-lobby-room', { room = room });
|
|
else
|
|
room:set_members_only(false);
|
|
prosody.events.fire_event('destroy-lobby-room', { room = room });
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode({ ok = true; lobbyroom = room._data.lobbyroom });
|
|
};
|
|
end;
|
|
|
|
-- POST /test-observer/rooms/affiliation
|
|
-- Body: { "jid": "room@conference.localhost", "occupant_jid": "user@localhost", "affiliation": "member" }
|
|
-- Sets the affiliation of occupant_jid in the given room.
|
|
["POST /rooms/affiliation"] = function(event)
|
|
local data = json.decode(event.request.body or "{}") or {};
|
|
local room_jid = data.jid;
|
|
local occupant_jid = data.occupant_jid;
|
|
local affiliation = data.affiliation;
|
|
if not room_jid or not occupant_jid or not affiliation then
|
|
return { status_code = 400; body = '{"error":"missing required fields"}' };
|
|
end
|
|
local room = (shared.rooms or {})[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
room:set_affiliation(true, occupant_jid, affiliation);
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = '{"ok":true}';
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/rooms/metadata?jid=room@conference.localhost
|
|
-- Returns: { jid, metadata } where metadata is room.jitsiMetadata.
|
|
["GET /rooms/metadata"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local room_jid = params["jid"];
|
|
if not room_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local rooms = shared.rooms or {};
|
|
local room = rooms[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
local encoded, err = json.encode({
|
|
jid = room.jid;
|
|
metadata = room.jitsiMetadata or {};
|
|
});
|
|
if not encoded then
|
|
return { status_code = 500; body = json.encode({ error = 'encode failed: ' .. tostring(err) }) };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = encoded;
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/rooms/audio-translation-requests?jid=room@conference.localhost
|
|
-- Returns: { jid, audioTranslationRequests } where the value is the
|
|
-- aggregated map stored on room._data by mod_audio_translation_component
|
|
-- (omitted when there are no subscriptions).
|
|
["GET /rooms/audio-translation-requests"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local room_jid = params["jid"];
|
|
if not room_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local rooms = shared.rooms or {};
|
|
local room = rooms[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
local encoded, err = json.encode({
|
|
jid = room.jid;
|
|
audioTranslationRequests = room._data.audioTranslationRequests;
|
|
});
|
|
if not encoded then
|
|
return { status_code = 500; body = json.encode({ error = 'encode failed: ' .. tostring(err) }) };
|
|
end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = encoded;
|
|
};
|
|
end;
|
|
|
|
-- GET /test-observer/rooms?jid=room@conference.localhost
|
|
-- Returns: { jid, hidden, occupant_count }
|
|
["GET /rooms"] = function(event)
|
|
local params = parse_query(event.request.url.query);
|
|
local room_jid = params["jid"];
|
|
if not room_jid then
|
|
return { status_code = 400; body = '{"error":"missing jid param"}' };
|
|
end
|
|
local rooms = shared.rooms or {};
|
|
local room = rooms[room_jid];
|
|
if not room then
|
|
return { status_code = 404; body = '{"error":"room not found"}' };
|
|
end
|
|
local count = 0;
|
|
for _ in room:each_occupant() do count = count + 1; end
|
|
return {
|
|
status_code = 200;
|
|
headers = { ["Content-Type"] = "application/json" };
|
|
body = json.encode({
|
|
jid = room.jid;
|
|
hidden = room:get_hidden();
|
|
occupant_count = count;
|
|
});
|
|
};
|
|
end;
|
|
};
|
|
});
|
|
|
|
module:log("info", "test_observer_http loaded");
|