local st = require "util.stanza"; local http = require "net.http"; local jid = require "util.jid"; local json = require "util.json"; local util = module:require "util"; local is_admin = util.is_admin; local ROLE_JOINING = module:shared("politictalk/roles/joining"); local ROOM_INACTIVE_CALLBACK_URL = module:get_option_string( "politictalk_room_inactive_callback_url", os.getenv("POLITICTALK_ROOM_INACTIVE_CALLBACK_URL") ); local ROOM_INACTIVE_CALLBACK_SECRET = module:get_option_string( "politictalk_room_inactive_callback_secret", os.getenv("POLITICTALK_ROOM_INACTIVE_CALLBACK_SECRET") ); module:log("info", "Loaded PoliticTalk JWT role enforcement"); local function get_user(session) return session and session.jitsi_meet_context_user or nil; end local function get_user_id(session) local user = get_user(session); return user and user.id or nil; end local function is_moderator_session(session) local user = get_user(session); local moderator = user and user.moderator; return moderator == true or moderator == "true"; end local function table_contains(values, value) if not values or not value then return false; end for _, current in ipairs(values) do if current == value then return true; end end return false; end local function add_unique(values, value) if not value then return values or {}; end values = values or {}; if not table_contains(values, value) then table.insert(values, value); end return values; end local function remove_value(values, value) if not values or not value then return values or {}; end local filtered = {}; for _, current in ipairs(values) do if current ~= value then table.insert(filtered, current); end end return filtered; end local function ensure_room_data(room) if not room then return false; end room._data = room._data or {}; room._data.moderators = room._data.moderators or {}; room._data.participants = room._data.participants or {}; room._data.politictalk_host_jids = room._data.politictalk_host_jids or {}; room._data.politictalk_jid_user_ids = room._data.politictalk_jid_user_ids or {}; -- Enables Jitsi AV moderation automatically: hosts can speak, participants -- must be explicitly allowed by a host before they can unmute. room._data.av_can_unmute = false; return true; end local function update_room_role_lists(room, user_id, is_moderator) if not room or not user_id then return; end ensure_room_data(room); if is_moderator then room._data.moderators = add_unique(room._data.moderators, user_id); room._data.participants = remove_value(room._data.participants, user_id); else room._data.participants = add_unique(room._data.participants, user_id); room._data.moderators = remove_value(room._data.moderators, user_id); end module:fire_event("room-metadata-changed", { room = room; }); end local function mark_host(room, occupant, user_id) if not room or not occupant or not occupant.bare_jid or not user_id then return; end ensure_room_data(room); room._data.politictalk_host_jids = add_unique(room._data.politictalk_host_jids, occupant.bare_jid); room._data.politictalk_jid_user_ids[occupant.bare_jid] = user_id; end local function mark_participant(room, occupant, user_id) if not room or not occupant or not occupant.bare_jid or not user_id then return; end ensure_room_data(room); room._data.politictalk_jid_user_ids[occupant.bare_jid] = user_id; end local function unmark_occupant(room, occupant) if not room or not occupant or not occupant.bare_jid then return nil; end ensure_room_data(room); local user_id = room._data.politictalk_jid_user_ids[occupant.bare_jid]; room._data.politictalk_jid_user_ids[occupant.bare_jid] = nil; room._data.politictalk_host_jids = remove_value(room._data.politictalk_host_jids, occupant.bare_jid); return user_id; end local function has_active_host(room) if not room then return false; end ensure_room_data(room); for _, occupant in room:each_occupant() do if table_contains(room._data.politictalk_host_jids, occupant.bare_jid) then return true; end end return false; end local function deny_join(event, reason) if event.origin and event.stanza then event.origin.send(st.error_reply(event.stanza, "cancel", "not-allowed", reason)); end return true; end local function get_room_meeting_code(room) if not room or not room.jid then return nil; end return jid.split(room.jid); end local function notify_room_inactive(room, reason) if not ROOM_INACTIVE_CALLBACK_URL or ROOM_INACTIVE_CALLBACK_URL == "" or not ROOM_INACTIVE_CALLBACK_SECRET or ROOM_INACTIVE_CALLBACK_SECRET == "" then module:log("warn", "Skipping PoliticTalk inactive-room callback because callback URL or secret is missing"); return; end local meeting_code = get_room_meeting_code(room); if not meeting_code then module:log("warn", "Skipping PoliticTalk inactive-room callback because room meeting code is missing"); return; end ensure_room_data(room); if room._data.politictalk_inactive_notified then return; end room._data.politictalk_inactive_notified = true; http.request(ROOM_INACTIVE_CALLBACK_URL, { method = "POST"; headers = { ["Content-Type"] = "application/json"; ["X-PoliticTalk-Jitsi-Secret"] = ROOM_INACTIVE_CALLBACK_SECRET; }; body = json.encode({ meetingCode = meeting_code; reason = reason or "host_left"; roomJid = room.jid; }); }, function(_, code) if code and code >= 200 and code < 300 then module:log("info", "Marked PoliticTalk room inactive in PgApi: %s", tostring(meeting_code)); else module:log( "warn", "Failed to mark PoliticTalk room inactive in PgApi: room=%s status=%s", tostring(meeting_code), tostring(code) ); end end); end module:hook("muc-room-pre-create", function(event) if event.stanza and event.stanza.attr and is_admin(event.stanza.attr.from) then return; end if is_moderator_session(event.origin) then return; end module:log("warn", "Blocking non-host from creating PoliticTalk room: %s", tostring(event.stanza and event.stanza.attr.to)); return deny_join(event, "PoliticTalk room is waiting for the host"); end, 90); module:hook("muc-occupant-pre-join", function(event) local session = event.origin; local room = event.room; local occupant = event.occupant; local user_id = get_user_id(session); local is_moderator = is_moderator_session(session); local occupant_jid = occupant and occupant.bare_jid; if occupant_jid and is_admin(occupant_jid) then return; end if not occupant_jid or not user_id then module:log("warn", "Blocking participant without PoliticTalk JWT user id from room: %s", tostring(room and room.jid)); return deny_join(event, "PoliticTalk identity is required"); end if not is_moderator and not has_active_host(room) then module:log("warn", "Blocking participant %s from room without host: %s", tostring(user_id), tostring(room and room.jid)); return deny_join(event, "PoliticTalk room is waiting for the host"); end ROLE_JOINING[occupant_jid] = { is_moderator = is_moderator; user_id = user_id; }; update_room_role_lists(room, user_id, is_moderator); end, 80); module:hook("muc-occupant-joined", function(event) local room = event.room; local occupant = event.occupant; local occupant_jid = occupant and occupant.bare_jid; local role = occupant_jid and ROLE_JOINING[occupant_jid] or nil; if occupant_jid then ROLE_JOINING[occupant_jid] = nil; end if not role then return; end if role.is_moderator then mark_host(room, occupant, role.user_id); update_room_role_lists(room, role.user_id, true); room:set_affiliation(true, occupant.bare_jid, "owner"); module:log( "info", "PoliticTalk host joined as moderator with AV moderation: %s room=%s", tostring(role.user_id), tostring(room.jid) ); else mark_participant(room, occupant, role.user_id); update_room_role_lists(room, role.user_id, false); room:set_affiliation(true, occupant.bare_jid, "member"); module:log("info", "PoliticTalk participant joined as member: %s room=%s", tostring(role.user_id), tostring(room.jid)); end end, 1); module:hook("muc-occupant-left", function(event) local room = event.room; local occupant = event.occupant; local was_host = room and occupant and room._data and table_contains(room._data.politictalk_host_jids, occupant.bare_jid); local user_id = unmark_occupant(room, occupant) or get_user_id(event.origin); if room and user_id then ensure_room_data(room); room._data.moderators = remove_value(room._data.moderators, user_id); room._data.participants = remove_value(room._data.participants, user_id); end if was_host and not has_active_host(room) then module:log("info", "Destroying PoliticTalk room after host left: %s", tostring(room.jid)); notify_room_inactive(room, "host_left"); room:destroy(nil, "The host has left the PoliticTalk room"); end end, -20); module:hook("muc-room-destroyed", function(event) local room = event.room; if room and room._data and room._data.politictalk_host_jids then notify_room_inactive(room, "room_destroyed"); end end, -30);