Update PoliticTalk Jitsi room policy
This commit is contained in:
52
README.md
52
README.md
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
BIN
assets/public/politictalk/favicon.ico
Normal file
BIN
assets/public/politictalk/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/public/politictalk/pg_globe.png
Normal file
BIN
assets/public/politictalk/pg_globe.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -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: {
|
||||
|
||||
21
interface_config/politictalk-overrides.js
Normal file
21
interface_config/politictalk-overrides.js
Normal 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
|
||||
@@ -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`.
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
329
prosody-plugins/mod_politictalk_roles.lua
Normal file
329
prosody-plugins/mod_politictalk_roles.lua
Normal 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);
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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.
|
||||
|
||||
10
templates/pgapi-politictalk-jwt.env.example
Normal file
10
templates/pgapi-politictalk-jwt.env.example
Normal 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
|
||||
35
templates/prosody-token-auth.cfg.lua.example
Normal file
35
templates/prosody-token-auth.cfg.lua.example
Normal 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
114
web/plugin.head.html
Normal 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
9
web/title.html
Normal 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" />
|
||||
Reference in New Issue
Block a user