Update PoliticTalk Jitsi room policy

This commit is contained in:
2026-05-16 21:51:14 +05:30
parent 8997f4804f
commit def2d46096
20 changed files with 748 additions and 26 deletions

View File

@@ -17,8 +17,11 @@ jitsi/
assets/ assets/
public/politictalk/ public/politictalk/
branding.json branding.json
favicon.ico
pgLogo.svg pgLogo.svg
pg_bg.png pg_bg.png
interface_config/
politictalk-overrides.js
local/ local/
README.md README.md
custom-config.js custom-config.js
@@ -26,6 +29,8 @@ jitsi/
docker-compose.override.yml docker-compose.override.yml
nginx/ nginx/
politictalk.parallelglobe.io.conf politictalk.parallelglobe.io.conf
prosody-plugins/
mod_politictalk_roles.lua
scripts/ scripts/
deploy-vps.sh deploy-vps.sh
local-jitsi-setup.sh local-jitsi-setup.sh
@@ -33,20 +38,28 @@ jitsi/
local-jitsi-stop.sh local-jitsi-stop.sh
local-jitsi-sync.sh local-jitsi-sync.sh
templates/ templates/
web/
plugin.head.html
title.html
``` ```
## Current Meeting Policy ## Current Meeting Policy
- Meetings start in audio-only mode. - Meetings start in audio-only mode.
- Participants join with microphone muted. - Participants join with microphone muted.
- Participants cannot unmute themselves until a host allows them through Jitsi AV moderation.
- Participants join with camera off. - Participants join with camera off.
- Camera/prejoin camera controls are hidden. - Jitsi prejoin is disabled because PgPlatform owns the PoliticTalk prejoin/auth step.
- Camera/premeeting camera controls are hidden.
- Toolbar is limited to microphone, chat, raise hand, fullscreen, noise suppression, participants pane, and hangup. - Toolbar is limited to microphone, chat, raise hand, fullscreen, noise suppression, participants pane, and hangup.
- Chat and polls are enabled. - Chat and polls are enabled.
- Invite/share controls are disabled. - Invite/share controls are disabled.
- Room names are not stored in recent rooms. - Room names are not stored in recent rooms.
- E2EE support is enabled in the Jitsi config. - E2EE support is enabled in the Jitsi config.
- The logo and dynamic branding point to PoliticTalk public assets. - The logo and dynamic branding point to PoliticTalk public assets.
- Browser title, favicon, Open Graph metadata, and in-meeting watermark/logo use PoliticTalk branding.
- When JWT auth is enabled, Jitsi auto-owner is disabled so the first entrant cannot become moderator automatically.
- A custom Prosody module maps PoliticTalk JWT roles to Jitsi moderator/member roles, enables host-controlled AV moderation, and closes the room when the host leaves.
## Local Docker Testing ## Local Docker Testing
@@ -91,6 +104,19 @@ jitsi/config/politictalk.parallelglobe.io-config.js
jitsi/assets/public/politictalk/* jitsi/assets/public/politictalk/*
-> /etc/jitsi/meet/public/politictalk/ -> /etc/jitsi/meet/public/politictalk/
-> /usr/share/jitsi-meet/images/politictalk/
jitsi/web/title.html
-> /usr/share/jitsi-meet/title.html
jitsi/web/plugin.head.html
-> /usr/share/jitsi-meet/plugin.head.html
jitsi/interface_config/politictalk-overrides.js
-> appended to /usr/share/jitsi-meet/interface_config.js
jitsi/prosody-plugins/mod_politictalk_roles.lua
-> /usr/share/jitsi-meet/prosody-plugins/mod_politictalk_roles.lua
jitsi/nginx/politictalk.parallelglobe.io.conf jitsi/nginx/politictalk.parallelglobe.io.conf
-> /etc/nginx/sites-available/politictalk.parallelglobe.io.conf -> /etc/nginx/sites-available/politictalk.parallelglobe.io.conf
@@ -114,15 +140,33 @@ cd /path/to/pg/jitsi
sudo DEPLOY_NGINX=1 ./scripts/deploy-vps.sh sudo DEPLOY_NGINX=1 ./scripts/deploy-vps.sh
``` ```
For current `config.js`, branding, and asset changes, an nginx reload is enough. Future JWT/auth changes may require restarting Prosody and Jicofo. For current `config.js`, branding, and asset changes, an nginx reload is enough. JWT/auth and Prosody plugin changes require restarting Prosody and Jicofo, and usually Jitsi Videobridge.
## Future Auth Work The `politictalk_roles` Prosody module should be configured with the PgApi
inactive-room callback from `templates/prosody-token-auth.cfg.lua.example`.
That callback clears the event `meetingCode` when the last host leaves, so
participants remain on the platform waiting screen instead of reaching a
blocked Jitsi room.
The future authenticated flow should be: ## Token Auth Rollout
The authenticated flow is:
```text ```text
PgPlatform -> PgApi verifies event/user -> PgApi creates short-lived Jitsi JWT PgPlatform -> PgApi verifies event/user -> PgApi creates short-lived Jitsi JWT
-> PgPlatform prejoin/iframe -> Jitsi validates JWT on VPS -> PgPlatform prejoin/iframe -> Jitsi validates JWT on VPS
``` ```
PgApi now supports JWT-backed `join-link` responses when these environment values are set:
```text
jitsi/templates/pgapi-politictalk-jwt.env.example
```
The VPS-side Prosody token-auth shape is documented here:
```text
jitsi/templates/prosody-token-auth.cfg.lua.example
```
Do not commit real secrets here. JWT secrets, Prosody passwords, and private keys should stay in VPS-only environment/config files. Do not commit real secrets here. JWT secrets, Prosody passwords, and private keys should stay in VPS-only environment/config files.

View File

@@ -1,9 +1,9 @@
{ {
"inviteDomain": "politictalk.parallelglobe.io", "inviteDomain": "politictalk.parallelglobe.io",
"backgroundColor": "#101820", "backgroundColor": "#101820",
"logoClickUrl": "https://parallelglobe.io/", "logoClickUrl": "https://parallelglobe.io/politictalk",
"logoImageUrl": "/_api/public/politictalk/pgLogo.svg", "logoImageUrl": "/images/politictalk/pgLogo.svg",
"premeetingBackground": "url(/_api/public/politictalk/pg_bg.png)", "premeetingBackground": "url(/images/politictalk/pg_bg.png)",
"customTheme": { "customTheme": {
"palette": { "palette": {
"ui01": "#101820", "ui01": "#101820",

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -711,7 +711,7 @@ var config = {
// will be joined when no room is specified. // will be joined when no room is specified.
disabled: false, disabled: false,
// If set, landing page will redirect to this URL. // If set, landing page will redirect to this URL.
customUrl: 'https://parallelglobe.io/calendar' customUrl: 'https://parallelglobe.io/politictalk'
}, },
// Configs for the lobby screen. // Configs for the lobby screen.
@@ -790,9 +790,9 @@ var config = {
// Configs for prejoin page. // Configs for prejoin page.
prejoinConfig: { prejoinConfig: {
// When true, users see the PoliticTalk prejoin screen before entering the room. // Platform owns PoliticTalk prejoin/auth, so Jitsi enters the room directly.
enabled: true, enabled: false,
hideDisplayName: false, hideDisplayName: true,
hideExtraJoinButtons: ['no-audio', 'by-phone'], hideExtraJoinButtons: ['no-audio', 'by-phone'],
preCallTestEnabled: false, preCallTestEnabled: false,
preCallTestICEUrl: '', preCallTestICEUrl: '',
@@ -1465,7 +1465,7 @@ var config = {
} }
} }
*/ */
dynamicBrandingUrl: '/_api/public/politictalk/branding.json', dynamicBrandingUrl: '/images/politictalk/branding.json',
// A list of allowed URL domains for shared video. // A list of allowed URL domains for shared video.
// //
@@ -1862,7 +1862,7 @@ var config = {
// }, // },
// Application logo url // Application logo url
defaultLogoUrl: '/_api/public/politictalk/pgLogo.svg', defaultLogoUrl: '/images/politictalk/pgLogo.svg',
// Settings for the Excalidraw whiteboard integration. // Settings for the Excalidraw whiteboard integration.
// whiteboard: { // whiteboard: {

View File

@@ -0,0 +1,21 @@
// BEGIN POLITICTALK INTERFACE OVERRIDES
interfaceConfig.APP_NAME = 'PoliticTalk';
interfaceConfig.NATIVE_APP_NAME = 'PoliticTalk';
interfaceConfig.PROVIDER_NAME = 'ParallelGlobe';
interfaceConfig.DEFAULT_LOGO_URL = '/images/politictalk/pgLogo.svg';
interfaceConfig.DEFAULT_WELCOME_PAGE_LOGO_URL = '/images/politictalk/pgLogo.svg';
interfaceConfig.SHOW_JITSI_WATERMARK = false;
interfaceConfig.SHOW_BRAND_WATERMARK = true;
interfaceConfig.BRAND_WATERMARK_LINK = 'https://parallelglobe.io/politictalk';
interfaceConfig.SHOW_POWERED_BY = false;
interfaceConfig.MOBILE_APP_PROMO = false;
interfaceConfig.DISPLAY_WELCOME_FOOTER = false;
interfaceConfig.DISPLAY_WELCOME_PAGE_CONTENT = false;
interfaceConfig.DISPLAY_WELCOME_PAGE_ADDITIONAL_CARD = false;
interfaceConfig.DISPLAY_WELCOME_PAGE_TOOLBAR_ADDITIONAL_CONTENT = false;
interfaceConfig.JITSI_WATERMARK_LINK = '';
interfaceConfig.SUPPORT_URL = '';
// END POLITICTALK INTERFACE OVERRIDES

View File

@@ -31,7 +31,7 @@ The browser may warn about a self-signed certificate. That is expected for local
## After Editing Overrides ## After Editing Overrides
When you edit `local/custom-config.js`, `local/custom-interface_config.js`, or local assets, run: When you edit `local/custom-config.js`, `local/custom-interface_config.js`, `web/title.html`, `web/plugin.head.html`, or local assets, run:
```bash ```bash
./scripts/local-jitsi-sync.sh ./scripts/local-jitsi-sync.sh
@@ -47,11 +47,30 @@ When you edit `local/custom-config.js`, `local/custom-interface_config.js`, or l
## What This Tests ## What This Tests
- PoliticTalk branding assets - PoliticTalk branding assets
- prejoin behavior - PoliticTalk browser title, favicon, and in-meeting logo
- direct room entry after the PgPlatform prejoin/auth step
- host-only room creation and participant-only Jitsi roles when JWT auth is enabled
- audio-only policy - audio-only policy
- muted microphone/camera startup - muted microphone/camera startup
- host-controlled participant unmute through Jitsi AV moderation
- toolbar restrictions - toolbar restrictions
- chat, polls, raise hand, fullscreen, noise suppression - chat, polls, raise hand, fullscreen, noise suppression
- E2EE UI availability - E2EE UI availability
Final production verification still happens on the VPS because real WebRTC networking, domain, HTTPS, and future JWT auth depend on the server environment. Final production verification still happens on the VPS because real WebRTC networking, domain, HTTPS, and future JWT auth depend on the server environment.
## Optional JWT Auth Check
Local Docker Jitsi can be switched into JWT mode when you want to test token-only joins:
```bash
ENABLE_POLITICTALK_JWT_AUTH=1 \
POLITICTALK_JITSI_JWT_APP_ID=politictalk-local \
POLITICTALK_JITSI_JWT_APP_SECRET=replace-with-local-secret \
POLITICTALK_ROOM_INACTIVE_CALLBACK_SECRET=politictalk-local-lifecycle-secret \
./scripts/local-jitsi-setup.sh
./scripts/local-jitsi-start.sh
```
Use the same app id and secret in `pgapi/.env` so `/events/:id/join-link` returns a URL with `?jwt=...`.
Use the same callback secret in `pgapi/.env` as `POLITICTALK_JITSI_LIFECYCLE_SECRET`.

View File

@@ -4,6 +4,9 @@
config.defaultLogoUrl = '/images/politictalk/pgLogo.svg'; config.defaultLogoUrl = '/images/politictalk/pgLogo.svg';
config.dynamicBrandingUrl = '/images/politictalk/branding.json'; config.dynamicBrandingUrl = '/images/politictalk/branding.json';
config.welcomePage = config.welcomePage || {};
config.welcomePage.disabled = false;
config.welcomePage.customUrl = 'http://localhost:3000/politictalk';
config.startAudioOnly = true; config.startAudioOnly = true;
config.startAudioMuted = 0; config.startAudioMuted = 0;
@@ -25,8 +28,8 @@ config.enableNoisyMicDetection = true;
config.disableRemoteMute = false; config.disableRemoteMute = false;
config.prejoinConfig = { config.prejoinConfig = {
enabled: true, enabled: false,
hideDisplayName: false, hideDisplayName: true,
hideExtraJoinButtons: ['no-audio', 'by-phone'], hideExtraJoinButtons: ['no-audio', 'by-phone'],
preCallTestEnabled: false, preCallTestEnabled: false,
preCallTestICEUrl: '', preCallTestICEUrl: '',

View File

@@ -6,10 +6,11 @@ interfaceConfig.APP_NAME = 'PoliticTalk';
interfaceConfig.NATIVE_APP_NAME = 'PoliticTalk'; interfaceConfig.NATIVE_APP_NAME = 'PoliticTalk';
interfaceConfig.PROVIDER_NAME = 'ParallelGlobe'; interfaceConfig.PROVIDER_NAME = 'ParallelGlobe';
interfaceConfig.DEFAULT_LOGO_URL = '/images/politictalk/pgLogo.svg';
interfaceConfig.DEFAULT_WELCOME_PAGE_LOGO_URL = '/images/politictalk/pgLogo.svg'; interfaceConfig.DEFAULT_WELCOME_PAGE_LOGO_URL = '/images/politictalk/pgLogo.svg';
interfaceConfig.SHOW_JITSI_WATERMARK = false; interfaceConfig.SHOW_JITSI_WATERMARK = false;
interfaceConfig.SHOW_BRAND_WATERMARK = true; interfaceConfig.SHOW_BRAND_WATERMARK = true;
interfaceConfig.BRAND_WATERMARK_LINK = 'https://parallelglobe.io/'; interfaceConfig.BRAND_WATERMARK_LINK = 'https://parallelglobe.io/politictalk';
interfaceConfig.SHOW_POWERED_BY = false; interfaceConfig.SHOW_POWERED_BY = false;
interfaceConfig.MOBILE_APP_PROMO = false; interfaceConfig.MOBILE_APP_PROMO = false;

View File

@@ -2,3 +2,9 @@ services:
web: web:
volumes: volumes:
- ../../assets/public/politictalk:/usr/share/jitsi-meet/images/politictalk:ro - ../../assets/public/politictalk:/usr/share/jitsi-meet/images/politictalk:ro
- ../../web/title.html:/usr/share/jitsi-meet/title.html:ro
- ../../web/plugin.head.html:/usr/share/jitsi-meet/plugin.head.html:ro
prosody:
environment:
- POLITICTALK_ROOM_INACTIVE_CALLBACK_URL=${POLITICTALK_ROOM_INACTIVE_CALLBACK_URL:-http://host.docker.internal:9000/events/politictalk/jitsi/room-inactive}
- POLITICTALK_ROOM_INACTIVE_CALLBACK_SECRET=${POLITICTALK_ROOM_INACTIVE_CALLBACK_SECRET:-politictalk-local-lifecycle-secret}

View File

@@ -0,0 +1,329 @@
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);

View File

@@ -10,12 +10,26 @@ BACKUP_DIR="$BACKUP_ROOT/$STAMP"
CONFIG_SRC="$JITSI_DIR/config/$DOMAIN-config.js" CONFIG_SRC="$JITSI_DIR/config/$DOMAIN-config.js"
ASSETS_SRC="$JITSI_DIR/assets/public/politictalk" ASSETS_SRC="$JITSI_DIR/assets/public/politictalk"
PROSODY_PLUGINS_SRC="$JITSI_DIR/prosody-plugins"
TITLE_SRC="$JITSI_DIR/web/title.html"
PLUGIN_HEAD_SRC="$JITSI_DIR/web/plugin.head.html"
INTERFACE_SRC="$JITSI_DIR/interface_config/politictalk-overrides.js"
NGINX_SRC="$JITSI_DIR/nginx/$DOMAIN.conf" NGINX_SRC="$JITSI_DIR/nginx/$DOMAIN.conf"
CONFIG_DEST="/etc/jitsi/meet/$DOMAIN-config.js" CONFIG_DEST="/etc/jitsi/meet/$DOMAIN-config.js"
ASSETS_DEST="/etc/jitsi/meet/public/politictalk" ASSETS_DEST="/etc/jitsi/meet/public/politictalk"
WEB_IMAGES_DEST="/usr/share/jitsi-meet/images/politictalk"
PROSODY_PLUGINS_DEST="/usr/share/jitsi-meet/prosody-plugins"
TITLE_DEST="/usr/share/jitsi-meet/title.html"
PLUGIN_HEAD_DEST="/usr/share/jitsi-meet/plugin.head.html"
INTERFACE_DEST="/usr/share/jitsi-meet/interface_config.js"
NGINX_DEST="/etc/nginx/sites-available/$DOMAIN.conf" NGINX_DEST="/etc/nginx/sites-available/$DOMAIN.conf"
INTERFACE_MARKER_START="// BEGIN POLITICTALK INTERFACE OVERRIDES"
INTERFACE_MARKER_END="// END POLITICTALK INTERFACE OVERRIDES"
PLUGIN_MARKER_START="<!-- BEGIN POLITICTALK PLUGIN HEAD -->"
PLUGIN_MARKER_END="<!-- END POLITICTALK PLUGIN HEAD -->"
if [[ "$(id -u)" -ne 0 ]]; then if [[ "$(id -u)" -ne 0 ]]; then
echo "Run this script with sudo on the VPS." echo "Run this script with sudo on the VPS."
exit 1 exit 1
@@ -31,6 +45,31 @@ if [[ ! -d "$ASSETS_SRC" ]]; then
exit 1 exit 1
fi fi
if [[ ! -d "$PROSODY_PLUGINS_SRC" ]]; then
echo "Missing Prosody plugins source: $PROSODY_PLUGINS_SRC"
exit 1
fi
if [[ ! -f "$TITLE_SRC" ]]; then
echo "Missing title source: $TITLE_SRC"
exit 1
fi
if [[ ! -f "$PLUGIN_HEAD_SRC" ]]; then
echo "Missing plugin head source: $PLUGIN_HEAD_SRC"
exit 1
fi
if [[ ! -f "$INTERFACE_SRC" ]]; then
echo "Missing interface source: $INTERFACE_SRC"
exit 1
fi
if [[ ! -f "$INTERFACE_DEST" ]]; then
echo "Missing Jitsi interface config destination: $INTERFACE_DEST"
exit 1
fi
install -d -m 0755 "$BACKUP_DIR" install -d -m 0755 "$BACKUP_DIR"
if [[ -f "$CONFIG_DEST" ]]; then if [[ -f "$CONFIG_DEST" ]]; then
@@ -42,16 +81,72 @@ if [[ -d "$ASSETS_DEST" ]]; then
cp -a "$ASSETS_DEST" "$BACKUP_DIR/public/" cp -a "$ASSETS_DEST" "$BACKUP_DIR/public/"
fi fi
if [[ -d "$WEB_IMAGES_DEST" ]]; then
install -d -m 0755 "$BACKUP_DIR/usr-share-jitsi-meet/images"
cp -a "$WEB_IMAGES_DEST" "$BACKUP_DIR/usr-share-jitsi-meet/images/"
fi
if [[ -f "$TITLE_DEST" ]]; then
install -d -m 0755 "$BACKUP_DIR/usr-share-jitsi-meet"
cp -a "$TITLE_DEST" "$BACKUP_DIR/usr-share-jitsi-meet/"
fi
if [[ -f "$PLUGIN_HEAD_DEST" ]]; then
install -d -m 0755 "$BACKUP_DIR/usr-share-jitsi-meet"
cp -a "$PLUGIN_HEAD_DEST" "$BACKUP_DIR/usr-share-jitsi-meet/"
fi
if [[ -f "$INTERFACE_DEST" ]]; then
install -d -m 0755 "$BACKUP_DIR/usr-share-jitsi-meet"
cp -a "$INTERFACE_DEST" "$BACKUP_DIR/usr-share-jitsi-meet/"
fi
if [[ -d "$PROSODY_PLUGINS_DEST" ]]; then
install -d -m 0755 "$BACKUP_DIR/usr-share-jitsi-meet/prosody-plugins"
find "$PROSODY_PLUGINS_DEST" -maxdepth 1 -name 'mod_politictalk_*.lua' -type f -exec cp -a {} "$BACKUP_DIR/usr-share-jitsi-meet/prosody-plugins/" \;
fi
if [[ "${DEPLOY_NGINX:-0}" == "1" && -f "$NGINX_DEST" ]]; then if [[ "${DEPLOY_NGINX:-0}" == "1" && -f "$NGINX_DEST" ]]; then
cp -a "$NGINX_DEST" "$BACKUP_DIR/" cp -a "$NGINX_DEST" "$BACKUP_DIR/"
fi fi
install -d -m 0755 "$ASSETS_DEST" install -d -m 0755 "$ASSETS_DEST"
install -d -m 0755 "$WEB_IMAGES_DEST"
install -m 0644 "$CONFIG_SRC" "$CONFIG_DEST" install -m 0644 "$CONFIG_SRC" "$CONFIG_DEST"
install -m 0644 "$TITLE_SRC" "$TITLE_DEST"
find "$ASSETS_SRC" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do find "$ASSETS_SRC" -maxdepth 1 -type f -print0 | while IFS= read -r -d '' file; do
install -m 0644 "$file" "$ASSETS_DEST/" install -m 0644 "$file" "$ASSETS_DEST/"
install -m 0644 "$file" "$WEB_IMAGES_DEST/"
done done
install -d -m 0755 "$PROSODY_PLUGINS_DEST"
find "$PROSODY_PLUGINS_SRC" -maxdepth 1 -name 'mod_politictalk_*.lua' -type f -print0 | while IFS= read -r -d '' file; do
install -m 0644 "$file" "$PROSODY_PLUGINS_DEST/"
done
tmp_plugin_head="$(mktemp)"
awk -v start="$PLUGIN_MARKER_START" -v end="$PLUGIN_MARKER_END" '
$0 == start { skip = 1; next }
$0 == end { skip = 0; next }
!skip { print }
' "$PLUGIN_HEAD_DEST" > "$tmp_plugin_head"
printf '\n%s\n' "$PLUGIN_MARKER_START" >> "$tmp_plugin_head"
cat "$PLUGIN_HEAD_SRC" >> "$tmp_plugin_head"
printf '\n%s\n' "$PLUGIN_MARKER_END" >> "$tmp_plugin_head"
install -m 0644 "$tmp_plugin_head" "$PLUGIN_HEAD_DEST"
rm -f "$tmp_plugin_head"
tmp_interface="$(mktemp)"
awk -v start="$INTERFACE_MARKER_START" -v end="$INTERFACE_MARKER_END" '
$0 == start { skip = 1; next }
$0 == end { skip = 0; next }
!skip { print }
' "$INTERFACE_DEST" > "$tmp_interface"
printf '\n' >> "$tmp_interface"
cat "$INTERFACE_SRC" >> "$tmp_interface"
install -m 0644 "$tmp_interface" "$INTERFACE_DEST"
rm -f "$tmp_interface"
if [[ "${DEPLOY_NGINX:-0}" == "1" ]]; then if [[ "${DEPLOY_NGINX:-0}" == "1" ]]; then
if [[ ! -f "$NGINX_SRC" ]]; then if [[ ! -f "$NGINX_SRC" ]]; then
echo "Missing nginx source: $NGINX_SRC" echo "Missing nginx source: $NGINX_SRC"

View File

@@ -13,4 +13,4 @@ fi
"$SCRIPT_DIR/local-jitsi-sync.sh" "$SCRIPT_DIR/local-jitsi-sync.sh"
cd "$STACK_DIR" cd "$STACK_DIR"
docker compose restart web docker compose up -d --force-recreate web

View File

@@ -67,7 +67,7 @@ set_env HTTP_PORT "$HTTP_PORT"
set_env HTTPS_PORT "$HTTPS_PORT" set_env HTTPS_PORT "$HTTPS_PORT"
set_env PUBLIC_URL "$PUBLIC_URL" set_env PUBLIC_URL "$PUBLIC_URL"
set_env ENABLE_PREJOIN_PAGE 1 set_env ENABLE_PREJOIN_PAGE 0
set_env ENABLE_WELCOME_PAGE 1 set_env ENABLE_WELCOME_PAGE 1
set_env ENABLE_NOISY_MIC_DETECTION 1 set_env ENABLE_NOISY_MIC_DETECTION 1
set_env ENABLE_NO_AUDIO_DETECTION 1 set_env ENABLE_NO_AUDIO_DETECTION 1
@@ -86,6 +86,37 @@ set_env DYNAMIC_BRANDING_URL /images/politictalk/branding.json
set_env TOOLBAR_BUTTONS microphone,chat,raisehand,fullscreen,noisesuppression,participants-pane,hangup set_env TOOLBAR_BUTTONS microphone,chat,raisehand,fullscreen,noisesuppression,participants-pane,hangup
set_env HIDE_PREMEETING_BUTTONS microphone,camera,select-background,invite,settings set_env HIDE_PREMEETING_BUTTONS microphone,camera,select-background,invite,settings
set_env HIDE_PREJOIN_EXTRA_BUTTONS no-audio,by-phone set_env HIDE_PREJOIN_EXTRA_BUTTONS no-audio,by-phone
set_env XMPP_MUC_MODULES politictalk_roles
set_env ENABLE_AUTH 0
set_env AUTH_TYPE internal
set_env ENABLE_GUESTS 0
set_env JICOFO_ENABLE_AUTH 0
set_env ENABLE_AUTO_OWNER 1
set_env ENABLE_MODERATOR_CHECKS 0
set_env WAIT_FOR_HOST_DISABLE_AUTO_OWNERS false
if [[ "${ENABLE_POLITICTALK_JWT_AUTH:-0}" == "1" ]]; then
if [[ -z "${POLITICTALK_JITSI_JWT_APP_ID:-}" || -z "${POLITICTALK_JITSI_JWT_APP_SECRET:-}" ]]; then
echo "Set POLITICTALK_JITSI_JWT_APP_ID and POLITICTALK_JITSI_JWT_APP_SECRET before enabling local JWT auth."
exit 1
fi
set_env ENABLE_AUTH 1
set_env AUTH_TYPE jwt
set_env ENABLE_GUESTS 0
set_env JWT_APP_ID "$POLITICTALK_JITSI_JWT_APP_ID"
set_env JWT_APP_SECRET "$POLITICTALK_JITSI_JWT_APP_SECRET"
set_env JWT_ACCEPTED_ISSUERS "$POLITICTALK_JITSI_JWT_APP_ID"
set_env JWT_ACCEPTED_AUDIENCES "${POLITICTALK_JITSI_JWT_AUDIENCE:-jitsi}"
set_env JWT_ALLOW_EMPTY 0
set_env JWT_ENABLE_DOMAIN_VERIFICATION 0
set_env JICOFO_AUTH_TYPE jwt
set_env JICOFO_ENABLE_AUTH 1
set_env ENABLE_AUTO_OWNER 0
set_env ENABLE_MODERATOR_CHECKS 1
set_env WAIT_FOR_HOST_DISABLE_AUTO_OWNERS true
fi
mkdir -p "$CONFIG_DIR"/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri} mkdir -p "$CONFIG_DIR"/{web,transcripts,prosody/config,prosody/prosody-plugins-custom,jicofo,jvb,jigasi,jibri}

View File

@@ -17,5 +17,9 @@ mkdir -p "$CONFIG_DIR"/{web,transcripts,prosody/config,prosody/prosody-plugins-c
cp "$LOCAL_DIR/custom-config.js" "$CONFIG_DIR/web/custom-config.js" cp "$LOCAL_DIR/custom-config.js" "$CONFIG_DIR/web/custom-config.js"
cp "$LOCAL_DIR/custom-interface_config.js" "$CONFIG_DIR/web/custom-interface_config.js" cp "$LOCAL_DIR/custom-interface_config.js" "$CONFIG_DIR/web/custom-interface_config.js"
cp "$LOCAL_DIR/docker-compose.override.yml" "$STACK_DIR/docker-compose.override.yml" cp "$LOCAL_DIR/docker-compose.override.yml" "$STACK_DIR/docker-compose.override.yml"
find "$CONFIG_DIR/prosody/prosody-plugins-custom" -maxdepth 1 -name 'mod_politictalk_*.lua' -type f -delete
find "$ROOT_DIR/prosody-plugins" -maxdepth 1 -name 'mod_politictalk_*.lua' -type f -print0 | while IFS= read -r -d '' file; do
cp "$file" "$CONFIG_DIR/prosody/prosody-plugins-custom/"
done
echo "Synced PoliticTalk local overrides into Docker Jitsi runtime." echo "Synced PoliticTalk local overrides into Docker Jitsi runtime."

View File

@@ -5,7 +5,7 @@ These notes are intentionally not deployed yet. They describe the local files an
## Target Flow ## Target Flow
```text ```text
PgPlatform calendar event PgPlatform PoliticTalk event
-> PgPlatform PoliticTalk prejoin page -> PgPlatform PoliticTalk prejoin page
-> PgApi verifies logged-in user, event ownership/invite/access, and event time window -> PgApi verifies logged-in user, event ownership/invite/access, and event time window
-> PgApi returns a short-lived Jitsi JWT -> PgApi returns a short-lived Jitsi JWT
@@ -13,21 +13,22 @@ PgPlatform calendar event
-> Jitsi VPS validates JWT through Prosody token auth -> Jitsi VPS validates JWT through Prosody token auth
``` ```
## Local Files To Add Later ## Local Files
```text ```text
jitsi/templates/prosody-token-auth.cfg.lua.example jitsi/templates/prosody-token-auth.cfg.lua.example
jitsi/templates/jicofo-token-auth.conf.example jitsi/templates/pgapi-politictalk-jwt.env.example
jitsi/templates/jitsi-token-auth.env.example
``` ```
Only templates should be committed. Real JWT secrets must stay in VPS-only files. Only templates should be committed. Real JWT secrets must stay in VPS-only files.
## Platform Changes To Add Later ## Platform Changes To Add Later
- PgApi endpoint to issue Jitsi JWTs for valid PoliticTalk event access. - PgApi `join-link` issues Jitsi JWTs when token-auth env values are present.
- JWT claims for room, user display name, email/id, moderator flag, expiry, and feature permissions. - JWT claims for room, user display name, email/id, moderator flag, expiry, and feature permissions.
- PgPlatform prejoin route that shows event title and immutable platform user name. - PgPlatform prejoin route shows event title and immutable platform user name.
- PgPlatform embedded Jitsi room using the IFrame API. - PgPlatform embedded Jitsi room using the IFrame API.
- Participants must wait on the PgPlatform prejoin page until the host starts the room.
- Jitsi auto-owner must be disabled once JWT auth is enabled so moderator status comes only from PgApi JWT claims.
- Moderator-only controls for audio moderation and poll permissions. - Moderator-only controls for audio moderation and poll permissions.
- Event end-time enforcement through JWT expiry and/or iframe hangup. - Event end-time enforcement through JWT expiry and/or iframe hangup.

View File

@@ -0,0 +1,10 @@
# Add these to pgapi/.env only when the Jitsi VPS has token auth enabled.
# The app id and secret must match the Prosody token-auth config on the VPS.
POLITICTALK_MEETING_BASE_URL=https://politictalk.parallelglobe.io
POLITICTALK_JITSI_JWT_APP_ID=politictalk
POLITICTALK_JITSI_JWT_APP_SECRET=replace-with-a-long-random-secret
POLITICTALK_JITSI_JWT_AUDIENCE=jitsi
POLITICTALK_JITSI_JWT_SUBJECT=politictalk.parallelglobe.io
POLITICTALK_JITSI_JWT_TTL_SECONDS=21600
POLITICTALK_JITSI_LIFECYCLE_SECRET=replace-with-a-second-long-random-secret

View File

@@ -0,0 +1,35 @@
-- Reference snippet for the PoliticTalk VPS.
-- Apply this manually inside:
-- /etc/prosody/conf.avail/politictalk.parallelglobe.io.cfg.lua
--
-- Do not commit real app secrets. The app_id/app_secret values must match
-- POLITICTALK_JITSI_JWT_APP_ID and POLITICTALK_JITSI_JWT_APP_SECRET in PgApi.
VirtualHost "politictalk.parallelglobe.io"
authentication = "token"
app_id = "POLITICTALK_JITSI_JWT_APP_ID"
app_secret = "POLITICTALK_JITSI_JWT_APP_SECRET"
allow_empty_token = false
enable_domain_verification = false
Component "conference.politictalk.parallelglobe.io" "muc"
politictalk_room_inactive_callback_url = "https://api.parallelglobe.is/events/politictalk/jitsi/room-inactive"
politictalk_room_inactive_callback_secret = "POLITICTALK_JITSI_LIFECYCLE_SECRET"
modules_enabled = {
-- keep the existing modules already present in the VPS file
"token_verification";
"politictalk_roles";
}
-- Also set the equivalent Jicofo options so only PgApi JWT moderator
-- claims can create room moderators:
--
-- /etc/jitsi/jicofo/jicofo.conf
--
-- jicofo {
-- conference {
-- enable-auto-owner = false
-- enable-moderator-checks = true
-- }
-- }

114
web/plugin.head.html Normal file
View File

@@ -0,0 +1,114 @@
<style>
.politictalk-room-logo {
align-items: center;
display: flex;
height: 64px;
justify-content: center;
left: max(20px, env(safe-area-inset-left));
opacity: 0.96;
position: fixed;
top: max(20px, env(safe-area-inset-top));
width: 64px;
z-index: 2147483000;
}
.politictalk-room-logo img {
display: block;
height: 100%;
object-fit: contain;
width: 100%;
}
@media (max-width: 640px) {
.politictalk-room-logo {
height: 52px;
left: max(12px, env(safe-area-inset-left));
top: max(12px, env(safe-area-inset-top));
width: 52px;
}
}
</style>
<script>
(function() {
function decodeBase64Url(value) {
var base64 = value.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}
var binary = window.atob(base64);
var bytes = new Uint8Array(binary.length);
for (var index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return new TextDecoder().decode(bytes);
}
function getPoliticTalkMeetingTitle() {
try {
var token = new URLSearchParams(window.location.search).get('jwt');
if (!token) {
return '';
}
var payload = JSON.parse(decodeBase64Url(token.split('.')[1] || ''));
var context = payload.context || {};
var room = context.room || {};
var title = (context.politictalk && context.politictalk.title)
|| room.subject
|| room.name
|| '';
title = String(title);
return title.trim() ? title : '';
} catch (error) {
return '';
}
}
function applyPoliticTalkMeetingTitle() {
var meetingTitle = getPoliticTalkMeetingTitle();
if (!meetingTitle) {
return;
}
window.config = window.config || {};
window.config.subject = meetingTitle;
window.config.localSubject = meetingTitle;
}
function mountPoliticTalkLogo() {
if (!document.body || document.getElementById('politictalk-room-logo')) {
return;
}
var link = document.createElement('a');
link.className = 'politictalk-room-logo';
link.href = 'https://parallelglobe.io/politictalk';
link.id = 'politictalk-room-logo';
link.rel = 'noopener noreferrer';
link.target = '_blank';
var image = document.createElement('img');
image.alt = 'ParallelGlobe';
image.src = '/images/politictalk/pg_globe.png';
link.appendChild(image);
document.body.appendChild(link);
}
applyPoliticTalkMeetingTitle();
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mountPoliticTalkLogo);
} else {
mountPoliticTalkLogo();
}
}());
</script>

9
web/title.html Normal file
View File

@@ -0,0 +1,9 @@
<title>PoliticTalk</title>
<meta property="og:title" content="PoliticTalk" />
<meta property="og:image" content="/images/politictalk/pgLogo.svg" />
<meta property="og:description" content="Join a PoliticTalk session by ParallelGlobe" />
<meta name="description" content="Join a PoliticTalk session by ParallelGlobe" />
<meta itemprop="name" content="PoliticTalk" />
<meta itemprop="description" content="Join a PoliticTalk session by ParallelGlobe" />
<meta itemprop="image" content="/images/politictalk/pgLogo.svg" />
<link rel="icon" href="/images/politictalk/favicon.ico" />