Skip to content
Snippets Groups Projects

fix(opendesk-jitsi): Element's guest user in Jitsi meeting

Merged Emrah Eryilmaz requested to merge emrah/fix/guest-access-level into main
Files
12
@@ -3,252 +3,258 @@ SPDX-FileCopyrightText: 2023 Bundesministerium des Innern und für Heimat, PG Ze
SPDX-License-Identifier: Apache-2.0
-->
<script>
// -----------------------------------------------------------------------------
// This script manages OIDC flow.
// -----------------------------------------------------------------------------
let failedAttempt = 0;
function oidcRedirect() {
const qs = new URLSearchParams(window.location.search.substring(1));
qs.delete('oidc');
const path = encodeURIComponent(window.location.pathname);
const search = encodeURIComponent(qs.toString());
const hash = encodeURIComponent(window.location.hash.substring(1));
window.location = `/oidc/auth?path=${path}&search=${search}&hash=${hash}`;
try {
// remove react from DOM to prevent UI distortion
document.all.react.remove();
} catch {
// do nothing
}
}
// ---------------------------------------------------------------------------
// This script manages OIDC flow.
// ---------------------------------------------------------------------------
let failedAttempt = 0;
function oidcRedirect() {
const qs = new URLSearchParams(window.location.search.substring(1));
qs.delete("oidc");
function interceptLoginRequest() {
try {
// if conference is already started, dont wait for an auth request
if (APP.conference.isJoined()) return;
const path = encodeURIComponent(window.location.pathname);
const search = encodeURIComponent(qs.toString());
let hash = "config.prejoinConfig.enabled=false";
if (window.location.hash) {
hash = `${hash}&${window.location.hash.substring(1)}`;
}
hash = encodeURIComponent(hash);
// check if login or IamHost button is created in DOM
const _button = document.getElementById("modal-dialog-ok-button");
if (!_button) throw("button is not created yet");
window.location = `/oidc/auth?path=${path}&search=${search}&hash=${hash}`;
let labelKey;
try {
labelKey = Object.values(_button)[0].return.memoizedProps.labelKey;
// remove react from DOM to prevent UI distortion
document.all.react.remove();
} catch {
// do nothing
}
}
if (labelKey === "dialog.login") {
// if this is a login screen, redirect to OIDC login page
oidcRedirect();
} else if (labelKey === "dialog.IamHost") {
// if this is an IamHost screen, redirect to OIDC login page when clicked
_button.onclick = oidcRedirect
}
} catch(e) {
failedAttempt += 1;
function interceptLoginRequest() {
try {
// if conference is already started, dont wait for an auth request
if (APP.conference.isJoined()) return;
// check if login or IamHost button is created in DOM
const _button = document.getElementById("modal-dialog-ok-button");
if (!_button) throw ("button is not created yet");
let labelKey;
try {
labelKey = Object.values(_button)[0].return.memoizedProps.labelKey;
} catch {
// do nothing
}
if (labelKey === "dialog.login") {
// if this is a login screen, redirect to OIDC login page
oidcRedirect();
} else if (labelKey === "dialog.IamHost") {
// if this is an IamHost screen, redirect to OIDC login when clicked
_button.onclick = oidcRedirect;
}
} catch (e) {
failedAttempt += 1;
if (failedAttempt < 180) {
setTimeout(function() {interceptLoginRequest();}, 1000);
if (failedAttempt < 180) {
setTimeout(function () {
interceptLoginRequest();
}, 1000);
}
}
}
}
interceptLoginRequest();
interceptLoginRequest();
</script>
<script>
// -----------------------------------------------------------------------------
// This script customizes Jitsi UI if the room is created by Element Jitsi
// Widget. It senses the room created by Element by checking its name. The room
// created by Element has a special name format:
//
// base32decode(room_name) should match RegExp("^(!.*:.*[.][a-z]*)$")
// or
// room_name should match RegExp("^(Jitsi[A-Z][a-z]{23})$")
// -----------------------------------------------------------------------------
function base32decode(s) {
const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const pad = "=";
const len = s.length;
const apad = a + pad;
let v, x, r=0, bits=0, c, o="";
s = s.toUpperCase();
for(i=0; i<len; i+=1) {
v = apad.indexOf(s.charAt(i));
if (v>=0 && v<32) {
x = (x << 5) | v;
bits += 5;
if (bits >= 8) {
c = (x >> (bits - 8)) & 0xff;
o = o + String.fromCharCode(c);
bits -= 8;
// ---------------------------------------------------------------------------
// This script customizes Jitsi UI if the room is created by Element Jitsi
// Widget. It senses the room created by Element by checking its name. The
// room created by Element has a special name format:
//
// base32decode(room_name) should match RegExp("^(!.*:.*[.][a-z]*)$")
// or
// room_name should match RegExp("^(Jitsi[A-Z][a-z]{23})$")
// ---------------------------------------------------------------------------
function base32decode(s) {
const a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const pad = "=";
const len = s.length;
const apad = a + pad;
let v, x, r = 0, bits = 0, c, o = "";
s = s.toUpperCase();
for (i = 0; i < len; i += 1) {
v = apad.indexOf(s.charAt(i));
if (v >= 0 && v < 32) {
x = (x << 5) | v;
bits += 5;
if (bits >= 8) {
c = (x >> (bits - 8)) & 0xff;
o = o + String.fromCharCode(c);
bits -= 8;
}
}
}
}
// remaining bits are < 8
if (bits>0) {
c = ((x << (8 - bits)) & 0xff) >> (8 - bits);
// Dont append a null terminator.
if (c!==0) {
o = o + String.fromCharCode(c);
// remaining bits are < 8
if (bits > 0) {
c = ((x << (8 - bits)) & 0xff) >> (8 - bits);
// Dont append a null terminator.
if (c !== 0) {
o = o + String.fromCharCode(c);
}
}
}
return o;
}
function updateUIforElement() {
try {
if (!APP.conference.isJoined()) throw new Error("not joined yet");
return o;
}
function updateUIforElement() {
try {
const roomName = APP.store.getState()["features/base/conference"].room;
const decoded = base32decode(roomName);
const reg1 = new RegExp("^(!.*:.*[.][a-z]*)$");
const reg2 = new RegExp("^(Jitsi[A-Z][a-z]{23})$");
if (!reg1.test(decoded) && !reg2.test(roomName)) {
throw new Error("not a Matrix room");
}
if (!APP.conference.isJoined()) throw new Error("not joined yet");
APP.store.getState()["features/base/config"].toolbarButtons = [
"camera",
"closedcaptions",
"desktop",
"download",
"feedback",
"filmstrip",
"fullscreen",
"hangup",
"help",
"livestreaming",
"microphone",
"mute-everyone",
"mute-video-everyone",
"participants-pane",
"profile",
"raisehand",
"security",
"select-background",
"settings",
"shareaudio",
"shortcuts",
"stats",
"tileview",
"toggle-camera",
"videoquality",
];
try {
const roomName = APP.store.getState()["features/base/conference"].room;
const decoded = base32decode(roomName);
const reg1 = new RegExp("^(!.*:.*[.][a-z]*)$");
const reg2 = new RegExp("^(Jitsi[A-Z][a-z]{23})$");
if (!reg1.test(decoded) && !reg2.test(roomName)) {
throw new Error("not a Matrix room");
}
APP.store.getState()["features/toolbox"].toolbarButtons = [
"camera",
"closedcaptions",
"desktop",
"download",
"feedback",
"filmstrip",
"fullscreen",
"hangup",
"help",
"livestreaming",
"microphone",
"mute-everyone",
"mute-video-everyone",
"participants-pane",
"profile",
"raisehand",
"security",
"select-background",
"settings",
"shareaudio",
"shortcuts",
"stats",
"tileview",
"toggle-camera",
"videoquality",
];
} catch {
// do nothing
}
} catch {
// do nothing
setTimeout(updateUIforElement, 1000);
}
} catch {
setTimeout(updateUIforElement, 1000);
}
}
updateUIforElement();
updateUIforElement();
</script>
<script>
// -----------------------------------------------------------------------------
// This script manages the OpenDesk menu in Jitsi. The menu will be activated if
// Jitsi is opened as a standalone application. It will not be activated if it
// is inside an iframe (such as inside Element chat).
//
// It uses the language of the browser while getting the navigation data.
//
// This script silently refreshes the portal session by using a hidden iframe
// and uses this session while fetching the navigation data.
// -----------------------------------------------------------------------------
let ICS = "";
let PORTAL = "";
// -----------------------------------------------------------------------------
// Get the ICS URL from Jitsi setup and set it as a global variable.
// -----------------------------------------------------------------------------
async function setIcsUrl() {
try {
const url = "/static/url-ics";
const res = await fetch(url);
ICS = await res.text();
} catch {
// Do nothing
// ---------------------------------------------------------------------------
// This script manages the OpenDesk menu in Jitsi. The menu will be activated
// if Jitsi is opened as a standalone application. It will not be activated if
// it is inside an iframe (such as inside Element chat).
//
// It uses the language of the browser while getting the navigation data.
//
// This script silently refreshes the portal session by using a hidden iframe
// and uses this session while fetching the navigation data.
// ---------------------------------------------------------------------------
let ICS = "";
let PORTAL = "";
// ---------------------------------------------------------------------------
// Get the ICS URL from Jitsi setup and set it as a global variable.
// ---------------------------------------------------------------------------
async function setIcsUrl() {
try {
const url = "/static/url-ics";
const res = await fetch(url);
ICS = await res.text();
} catch {
// Do nothing
}
}
}
// -----------------------------------------------------------------------------
// Get the portal URL from Jitsi setup and set it as a global variable.
// -----------------------------------------------------------------------------
async function setPortalUrl() {
try {
const url = "/static/url-portal";
const res = await fetch(url);
PORTAL = await res.text();
} catch {
// Do nothing
// ---------------------------------------------------------------------------
// Get the portal URL from Jitsi setup and set it as a global variable.
// ---------------------------------------------------------------------------
async function setPortalUrl() {
try {
const url = "/static/url-portal";
const res = await fetch(url);
PORTAL = await res.text();
} catch {
// Do nothing
}
}
}
// -----------------------------------------------------------------------------
// Get the navigation data
// -----------------------------------------------------------------------------
async function getNavigation() {
try {
// Get the browser language.
let lang = navigator.language || "de-DE";
if (lang === "de") lang = "de-DE";
if (lang === "en") lang = "en-US";
const url = `${ICS}/navigation.json?language=${lang}`;
const res = await fetch(url, {
credentials: "include",
headers: {
Accept: "application/json",
},
});
return await res.json();
} catch {
return {};
// ---------------------------------------------------------------------------
// Get the navigation data
// ---------------------------------------------------------------------------
async function getNavigation() {
try {
// Get the browser language.
let lang = navigator.language || "de-DE";
if (lang === "de") lang = "de-DE";
if (lang === "en") lang = "en-US";
const url = `${ICS}/navigation.json?language=${lang}`;
const res = await fetch(url, {
credentials: "include",
headers: {
Accept: "application/json",
},
});
return await res.json();
} catch {
return {};
}
}
}
// -----------------------------------------------------------------------------
// Toggle menu. It will be triggered after clicking the menu icon.
// -----------------------------------------------------------------------------
function _toggleMenu() {
try {
const menu = document.getElementById("opendeskMenu");
if (menu.style.display !== "block") {
menu.style.display = "block";
} else {
menu.style.display = "none";
// ---------------------------------------------------------------------------
// Toggle menu. It will be triggered after clicking the menu icon.
// ---------------------------------------------------------------------------
function _toggleMenu() {
try {
const menu = document.getElementById("opendeskMenu");
if (menu.style.display !== "block") {
menu.style.display = "block";
} else {
menu.style.display = "none";
}
} catch {
// Do nothing
}
} catch {
// Do nothing
}
}
// -----------------------------------------------------------------------------
// Return the html content of the category (menu items of category)
// -----------------------------------------------------------------------------
function getCategoryItems(entries) {
try {
let items = "";
for (const entry of entries) {
items += `
// ---------------------------------------------------------------------------
// Return the html content of the category (menu items of category)
// ---------------------------------------------------------------------------
function getCategoryItems(entries) {
try {
let items = "";
for (const entry of entries) {
items += `
<div style="display: flex; align-items:center; gap:16px; padding:8px;
font-size:14;"
>
@@ -259,51 +265,51 @@ function getCategoryItems(entries) {
</a>
</div>
`;
}
}
return items;
} catch {
return "";
}
}
// -----------------------------------------------------------------------------
// Return the hmtl content of the navigation menu
// -----------------------------------------------------------------------------
function getMenuItems(nav) {
try {
// Skip if no portal session (anonymous user).
const identifier = nav.categories?.[0]?.identifier;
if (!identifier || identifier === "swp.anonymous") {
throw new Error("no item");
return items;
} catch {
return "";
}
}
let items = "";
for (const cat of nav.categories) {
// Category title in menu.
items += `
// ---------------------------------------------------------------------------
// Return the hmtl content of the navigation menu
// ---------------------------------------------------------------------------
function getMenuItems(nav) {
try {
// Skip if no portal session (anonymous user).
const identifier = nav.categories?.[0]?.identifier;
if (!identifier || identifier === "swp.anonymous") {
throw new Error("no item");
}
let items = "";
for (const cat of nav.categories) {
// Category title in menu.
items += `
<div style="font-size:17; font-weight:700; letter-spacing: 0.05em;
padding:16px 60px 4px 12px;"
>${cat.display_name}</div>
`;
// Category content (links) in menu.
items += getCategoryItems(cat.entries);
}
// Category content (links) in menu.
items += getCategoryItems(cat.entries);
}
return items;
} catch {
return "";
return items;
} catch {
return "";
}
}
}
// -----------------------------------------------------------------------------
// Create the navigation menu
// -----------------------------------------------------------------------------
function createNavigationMenu(nav) {
try {
const menuItems = getMenuItems(nav);
const menu = `
// ---------------------------------------------------------------------------
// Create the navigation menu
// ---------------------------------------------------------------------------
function createNavigationMenu(nav) {
try {
const menuItems = getMenuItems(nav);
const menu = `
<button id="opendeskMenuButton" tabindex="0" onclick="_toggleMenu()"
aria-label="Toogle menu" style="background:none; border:none;"
>
@@ -325,59 +331,62 @@ function createNavigationMenu(nav) {
</button>
`;
// Close menu if clicked somewhere else.
document.addEventListener("click", (event) => {
try {
// Skip if clicked to the menu button or to the menu.
const menuButton = document.getElementById("opendeskMenuButton");
if (menuButton.contains(event.target)) return;
// Close menu if already visible.
const menu = document.getElementById("opendeskMenu");
if (menu.style.display !== "none") menu.style.display = "none";
} catch {
// Do nothing
}
});
return menu;
} catch {
return "";
// Close menu if clicked somewhere else.
document.addEventListener("click", (event) => {
try {
// Skip if clicked to the menu button or to the menu.
const menuButton = document.getElementById("opendeskMenuButton");
if (menuButton.contains(event.target)) return;
// Close menu if already visible.
const menu = document.getElementById("opendeskMenu");
if (menu.style.display !== "none") menu.style.display = "none";
} catch {
// Do nothing
}
});
return menu;
} catch {
return "";
}
}
}
// -----------------------------------------------------------------------------
// Remove the header watermark. We dont want to see Jitsi's logo in the welcome
// screen since this screen has already a topbar and a logo inside this topbar.
// -----------------------------------------------------------------------------
function removeHeaderWatermark() {
try {
const elist = document.getElementsByClassName("header-watermark-container");
const el = elist?.[0];
if (el) el.remove();
} catch {
// Do nothing
// ---------------------------------------------------------------------------
// Remove the header watermark. We dont want to see Jitsi's logo in the
// welcome screen since this screen has already a topbar and a logo inside
// this topbar.
// ---------------------------------------------------------------------------
function removeHeaderWatermark() {
try {
const elist = document.getElementsByClassName(
"header-watermark-container",
);
const el = elist?.[0];
if (el) el.remove();
} catch {
// Do nothing
}
}
}
// -----------------------------------------------------------------------------
// Create topbar
// -----------------------------------------------------------------------------
function createTopbar(nav) {
try {
// Remove topbar if already exists to prevent duplicated topbars.
const el = document.getElementById("opendeskTopbar");
if (el) el.remove();
// Create topbar
const topDiv = document.createElement("div");
topDiv.id = "opendeskTopbar";
topDiv.style.gap = "14px";
topDiv.style.height = "63px";
topDiv.style.display = "flex";
topDiv.style.alignItems = "center";
topDiv.style.backgroundColor = "white";
topDiv.innerHTML = `
// ---------------------------------------------------------------------------
// Create topbar
// ---------------------------------------------------------------------------
function createTopbar(nav) {
try {
// Remove topbar if already exists to prevent duplicated topbars.
const el = document.getElementById("opendeskTopbar");
if (el) el.remove();
// Create topbar
const topDiv = document.createElement("div");
topDiv.id = "opendeskTopbar";
topDiv.style.gap = "14px";
topDiv.style.height = "63px";
topDiv.style.display = "flex";
topDiv.style.alignItems = "center";
topDiv.style.backgroundColor = "white";
topDiv.innerHTML = `
<a href="${PORTAL}" tabindex="0" target="_blank" aria-label="Show portal"
style="margin-left:16px"
>
@@ -387,133 +396,132 @@ function createTopbar(nav) {
</a>
`;
// If the navigation data exists then add the navigation menu.
const identifier = nav.categories?.[0]?.identifier;
if (identifier && identifier !== "swp.anonymous") {
topDiv.innerHTML += createNavigationMenu(nav);
}
// If the navigation data exists then add the navigation menu.
const identifier = nav.categories?.[0]?.identifier;
if (identifier && identifier !== "swp.anonymous") {
topDiv.innerHTML += createNavigationMenu(nav);
}
// If this is a welcome page then add topbar in the welcome div,
// otherwise add as the first elemenent.
const welcomeDiv = document.getElementById("welcome_page");
if (welcomeDiv) {
welcomeDiv.insertBefore(topDiv, welcomeDiv.firstChild);
removeHeaderWatermark();
} else {
document.body.insertBefore(topDiv, document.body.firstChild);
// If this is a welcome page then add topbar in the welcome div,
// otherwise add as the first elemenent.
const welcomeDiv = document.getElementById("welcome_page");
if (welcomeDiv) {
welcomeDiv.insertBefore(topDiv, welcomeDiv.firstChild);
removeHeaderWatermark();
} else {
document.body.insertBefore(topDiv, document.body.firstChild);
}
} catch {
// Do nothing
}
} catch {
// Do nothing
}
}
// -----------------------------------------------------------------------------
// Remove the hidden iframes after refreshing the portal session
// -----------------------------------------------------------------------------
function removeIframe() {
try {
const el = document.getElementById("opendeskIframeSilentLogin");
if (el) el.remove();
} catch {
// Do nothing
}
try {
const el = document.getElementById("opendeskIframeNavigation");
if (el) el.remove();
} catch {
// Do nothing
// ---------------------------------------------------------------------------
// Remove the hidden iframes after refreshing the portal session
// ---------------------------------------------------------------------------
function removeIframe() {
try {
const el = document.getElementById("opendeskIframeSilentLogin");
if (el) el.remove();
} catch {
// Do nothing
}
try {
const el = document.getElementById("opendeskIframeNavigation");
if (el) el.remove();
} catch {
// Do nothing
}
}
}
// -----------------------------------------------------------------------------
// Try refreshing the session and recreate topbar
// -----------------------------------------------------------------------------
function recreateTopbarAfterSilentLogin() {
try {
// Silent login inside a hidden iframe.
const iframeSilentLogin = document.createElement("iframe");
iframeSilentLogin.id = "opendeskIframeSilentLogin";
iframeSilentLogin.src = `${ICS}/silent`;
iframeSilentLogin.style.display = "none";
document.body.appendChild(iframeSilentLogin);
// Trigger the navigation iframe after the silent login is completed.
iframeSilentLogin.onload = () => {
// Navigation.json inside this hidden iframe is only to refresh the
// session. The silent login is not enough in some cases.
const iframeNavigation = document.createElement("iframe");
iframeNavigation.id = "opendeskIframeNavigation";
iframeNavigation.src = `${ICS}/navigation.json`;
iframeNavigation.style.display = "none";
document.body.appendChild(iframeNavigation);
// Get the navigation data and recreate topbar after the session is
// refreshed.
iframeNavigation.onload = async () => {
const nav = await getNavigation();
const identifier = nav.categories?.[0]?.identifier;
if (identifier && identifier !== "swp.anonymous") createTopbar(nav);
// ---------------------------------------------------------------------------
// Try refreshing the session and recreate topbar
// ---------------------------------------------------------------------------
function recreateTopbarAfterSilentLogin() {
try {
// Silent login inside a hidden iframe.
const iframeSilentLogin = document.createElement("iframe");
iframeSilentLogin.id = "opendeskIframeSilentLogin";
iframeSilentLogin.src = `${ICS}/silent`;
iframeSilentLogin.style.display = "none";
document.body.appendChild(iframeSilentLogin);
// Trigger the navigation iframe after the silent login is completed.
iframeSilentLogin.onload = () => {
// Navigation.json inside this hidden iframe is only to refresh the
// session. The silent login is not enough in some cases.
const iframeNavigation = document.createElement("iframe");
iframeNavigation.id = "opendeskIframeNavigation";
iframeNavigation.src = `${ICS}/navigation.json`;
iframeNavigation.style.display = "none";
document.body.appendChild(iframeNavigation);
// Get the navigation data and recreate topbar after the session is
// refreshed.
iframeNavigation.onload = async () => {
const nav = await getNavigation();
const identifier = nav.categories?.[0]?.identifier;
if (identifier && identifier !== "swp.anonymous") createTopbar(nav);
};
};
};
} catch {
// Do nothing
} finally {
// Wait a bit before removing the iframes.
setTimeout(removeIframe, 10000);
}
}
// -----------------------------------------------------------------------------
// Remove topbar after joining the meeting. Topbar breaks Jitsi UI if it is
// visible during the session.
// -----------------------------------------------------------------------------
function removeTopbarAfterJoining() {
try {
// Dont continue if no Jitsi session yet.
if (!APP.conference.isJoined()) throw new Error("no session yet");
// Remove topbar since the session started.
const el = document.getElementById("opendeskTopbar");
if (el) el.remove();
} catch {
// Try again after a while.
setTimeout(removeTopbarAfterJoining, 1000);
} catch {
// Do nothing
} finally {
// Wait a bit before removing the iframes.
setTimeout(removeIframe, 10000);
}
}
}
// -----------------------------------------------------------------------------
// Add topbar
// -----------------------------------------------------------------------------
async function addTopbar() {
try {
// Set the ICS URL as a global variable (ICS)
await setIcsUrl();
if (!ICS) throw new Error("unknown ics url");
// Set the portal URL as a global variable (PORTAL)
await setPortalUrl();
if (!PORTAL) throw new Error("unknown portal url");
// Trigger the event listener which will remove topbar after joining the
// meeting. Dont show the toolbar during the meeting session.
removeTopbarAfterJoining();
// Create the topbar (initially without navigation data). We want the topbar
// immediately even the navigation data is not available yet.
createTopbar({});
// Recreate the topbar after silent login.
recreateTopbarAfterSilentLogin();
} catch {
// Do nothing
// ---------------------------------------------------------------------------
// Remove topbar after joining the meeting. Topbar breaks Jitsi UI if it is
// visible during the session.
// ---------------------------------------------------------------------------
function removeTopbarAfterJoining() {
try {
// Dont continue if no Jitsi session yet.
if (!APP.conference.isJoined()) throw new Error("no session yet");
// Remove topbar since the session started.
const el = document.getElementById("opendeskTopbar");
if (el) el.remove();
} catch {
// Try again after a while.
setTimeout(removeTopbarAfterJoining, 1000);
}
}
}
// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------
// Add topbar if this is a standalone window (not an iframe)
if (!self.name || self.name === "tab_realtime_videoconference") addTopbar();
// ---------------------------------------------------------------------------
// Add topbar
// ---------------------------------------------------------------------------
async function addTopbar() {
try {
// Set the ICS URL as a global variable (ICS)
await setIcsUrl();
if (!ICS) throw new Error("unknown ics url");
// Set the portal URL as a global variable (PORTAL)
await setPortalUrl();
if (!PORTAL) throw new Error("unknown portal url");
// Trigger the event listener which will remove topbar after joining the
// meeting. Dont show the toolbar during the meeting session.
removeTopbarAfterJoining();
// Create the topbar (initially without navigation data). We want the
// topbar immediately even the navigation data is not available yet.
createTopbar({});
// Recreate the topbar after silent login.
recreateTopbarAfterSilentLogin();
} catch {
// Do nothing
}
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
// Add topbar if this is a standalone window (not an iframe)
if (!self.name || self.name === "tab_realtime_videoconference") addTopbar();
</script>
Loading

Consent

On this website, we use the web analytics service Matomo to analyze and review the use of our website. Through the collected statistics, we can improve our offerings and make them more appealing for you. Here, you can decide whether to allow us to process your data and set corresponding cookies for these purposes, in addition to technically necessary cookies. Further information on data protection—especially regarding "cookies" and "Matomo"—can be found in our privacy policy. You can withdraw your consent at any time.