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/
public/politictalk/
branding.json
favicon.ico
pgLogo.svg
pg_bg.png
interface_config/
politictalk-overrides.js
local/
README.md
custom-config.js
@@ -26,6 +29,8 @@ jitsi/
docker-compose.override.yml
nginx/
politictalk.parallelglobe.io.conf
prosody-plugins/
mod_politictalk_roles.lua
scripts/
deploy-vps.sh
local-jitsi-setup.sh
@@ -33,20 +38,28 @@ jitsi/
local-jitsi-stop.sh
local-jitsi-sync.sh
templates/
web/
plugin.head.html
title.html
```
## Current Meeting Policy
- Meetings start in audio-only mode.
- Participants join with microphone muted.
- Participants cannot unmute themselves until a host allows them through Jitsi AV moderation.
- 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.
- Chat and polls are enabled.
- Invite/share controls are disabled.
- Room names are not stored in recent rooms.
- E2EE support is enabled in the Jitsi config.
- 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
@@ -91,6 +104,19 @@ jitsi/config/politictalk.parallelglobe.io-config.js
jitsi/assets/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
-> /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
```
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
PgPlatform -> PgApi verifies event/user -> PgApi creates short-lived Jitsi JWT
-> 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.

View File

@@ -1,9 +1,9 @@
{
"inviteDomain": "politictalk.parallelglobe.io",
"backgroundColor": "#101820",
"logoClickUrl": "https://parallelglobe.io/",
"logoImageUrl": "/_api/public/politictalk/pgLogo.svg",
"premeetingBackground": "url(/_api/public/politictalk/pg_bg.png)",
"logoClickUrl": "https://parallelglobe.io/politictalk",
"logoImageUrl": "/images/politictalk/pgLogo.svg",
"premeetingBackground": "url(/images/politictalk/pg_bg.png)",
"customTheme": {
"palette": {
"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.
disabled: false,
// If set, landing page will redirect to this URL.
customUrl: 'https://parallelglobe.io/calendar'
customUrl: 'https://parallelglobe.io/politictalk'
},
// Configs for the lobby screen.
@@ -790,9 +790,9 @@ var config = {
// Configs for prejoin page.
prejoinConfig: {
// When true, users see the PoliticTalk prejoin screen before entering the room.
enabled: true,
hideDisplayName: false,
// Platform owns PoliticTalk prejoin/auth, so Jitsi enters the room directly.
enabled: false,
hideDisplayName: true,
hideExtraJoinButtons: ['no-audio', 'by-phone'],
preCallTestEnabled: false,
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.
//
@@ -1862,7 +1862,7 @@ var config = {
// },
// Application logo url
defaultLogoUrl: '/_api/public/politictalk/pgLogo.svg',
defaultLogoUrl: '/images/politictalk/pgLogo.svg',
// Settings for the Excalidraw whiteboard integration.
// 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
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
./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
- 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
- muted microphone/camera startup
- host-controlled participant unmute through Jitsi AV moderation
- toolbar restrictions
- chat, polls, raise hand, fullscreen, noise suppression
- 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.
## 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.dynamicBrandingUrl = '/images/politictalk/branding.json';
config.welcomePage = config.welcomePage || {};
config.welcomePage.disabled = false;
config.welcomePage.customUrl = 'http://localhost:3000/politictalk';
config.startAudioOnly = true;
config.startAudioMuted = 0;
@@ -25,8 +28,8 @@ config.enableNoisyMicDetection = true;
config.disableRemoteMute = false;
config.prejoinConfig = {
enabled: true,
hideDisplayName: false,
enabled: false,
hideDisplayName: true,
hideExtraJoinButtons: ['no-audio', 'by-phone'],
preCallTestEnabled: false,
preCallTestICEUrl: '',

View File

@@ -6,10 +6,11 @@ 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/';
interfaceConfig.BRAND_WATERMARK_LINK = 'https://parallelglobe.io/politictalk';
interfaceConfig.SHOW_POWERED_BY = false;
interfaceConfig.MOBILE_APP_PROMO = false;

View File

@@ -2,3 +2,9 @@ services:
web:
volumes:
- ../../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"
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"
CONFIG_DEST="/etc/jitsi/meet/$DOMAIN-config.js"
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"
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
echo "Run this script with sudo on the VPS."
exit 1
@@ -31,6 +45,31 @@ if [[ ! -d "$ASSETS_SRC" ]]; then
exit 1
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"
if [[ -f "$CONFIG_DEST" ]]; then
@@ -42,16 +81,72 @@ if [[ -d "$ASSETS_DEST" ]]; then
cp -a "$ASSETS_DEST" "$BACKUP_DIR/public/"
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
cp -a "$NGINX_DEST" "$BACKUP_DIR/"
fi
install -d -m 0755 "$ASSETS_DEST"
install -d -m 0755 "$WEB_IMAGES_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
install -m 0644 "$file" "$ASSETS_DEST/"
install -m 0644 "$file" "$WEB_IMAGES_DEST/"
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 [[ ! -f "$NGINX_SRC" ]]; then
echo "Missing nginx source: $NGINX_SRC"

View File

@@ -13,4 +13,4 @@ fi
"$SCRIPT_DIR/local-jitsi-sync.sh"
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 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_NOISY_MIC_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 HIDE_PREMEETING_BUTTONS microphone,camera,select-background,invite,settings
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}

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-interface_config.js" "$CONFIG_DIR/web/custom-interface_config.js"
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."

View File

@@ -5,7 +5,7 @@ These notes are intentionally not deployed yet. They describe the local files an
## Target Flow
```text
PgPlatform calendar event
PgPlatform PoliticTalk event
-> PgPlatform PoliticTalk prejoin page
-> PgApi verifies logged-in user, event ownership/invite/access, and event time window
-> PgApi returns a short-lived Jitsi JWT
@@ -13,21 +13,22 @@ PgPlatform calendar event
-> Jitsi VPS validates JWT through Prosody token auth
```
## Local Files To Add Later
## Local Files
```text
jitsi/templates/prosody-token-auth.cfg.lua.example
jitsi/templates/jicofo-token-auth.conf.example
jitsi/templates/jitsi-token-auth.env.example
jitsi/templates/pgapi-politictalk-jwt.env.example
```
Only templates should be committed. Real JWT secrets must stay in VPS-only files.
## 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.
- 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.
- 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.
- 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" />