2020-03-18 22:47:56 +01:00
|
|
|
/*
|
|
|
|
Copyright 2020 New Vector Ltd.
|
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
// We have to trick webpack into loading our CSS for us.
|
|
|
|
require("./index.scss");
|
|
|
|
|
|
|
|
import * as qs from 'querystring';
|
2020-09-07 17:10:19 +02:00
|
|
|
import {Capability, KnownWidgetActions, WidgetApi} from 'matrix-react-sdk/src/widgets/WidgetApi';
|
|
|
|
import {KJUR} from 'jsrsasign';
|
|
|
|
import {objectClone} from 'matrix-react-sdk/lib/utils/objects';
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
// Dev note: we use raw JS without many dependencies to reduce bundle size.
|
|
|
|
// We do not need all of React to render a Jitsi conference.
|
|
|
|
|
2020-07-21 12:30:28 +02:00
|
|
|
declare let JitsiMeetExternalAPI: any;
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
let inConference = false;
|
|
|
|
|
|
|
|
// Jitsi params
|
|
|
|
let jitsiDomain: string;
|
|
|
|
let conferenceId: string;
|
|
|
|
let displayName: string;
|
|
|
|
let avatarUrl: string;
|
|
|
|
let userId: string;
|
2020-09-04 12:14:52 +02:00
|
|
|
let jitsiAuth: string;
|
|
|
|
let roomId: string;
|
2020-09-07 17:10:19 +02:00
|
|
|
let openIDToken: string;
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
let widgetApi: WidgetApi;
|
|
|
|
|
2020-09-07 17:10:19 +02:00
|
|
|
function processOpenIDMessage(msg) {
|
|
|
|
const data = (msg.action === 'get_openid') ? msg.response : msg.data;
|
|
|
|
// TODO: just use data.state once https://github.com/matrix-org/matrix-react-sdk/pull/5172 is out
|
|
|
|
const result = (data.state !== undefined) ? data.state :
|
|
|
|
(data.success === true) ? 'allowed' : 'blocked';
|
|
|
|
|
|
|
|
switch (result) {
|
|
|
|
case 'allowed':
|
|
|
|
console.info('Successfully got OpenID credentials.');
|
|
|
|
openIDToken = data.access_token;
|
|
|
|
// Send a response if this was not a response
|
|
|
|
if (msg.action === 'openid_credentials') {
|
|
|
|
const request = objectClone(msg);
|
|
|
|
request.response = {};
|
|
|
|
window.parent.postMessage(request, '*');
|
|
|
|
}
|
|
|
|
enableJoinButton();
|
|
|
|
break;
|
|
|
|
case 'blocked':
|
|
|
|
console.warn('OpenID credentials request was blocked by user.');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// nothing to do
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements processing OpenID token requests as per MSC1960
|
|
|
|
*/
|
|
|
|
function onWidgetMessage(msg) {
|
|
|
|
const data = msg.data;
|
|
|
|
if (!data) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
switch (data.action) {
|
|
|
|
case 'get_openid':
|
|
|
|
case 'openid_credentials':
|
|
|
|
processOpenIDMessage(data);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
// Nothing to do
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-21 12:30:28 +02:00
|
|
|
(async function() {
|
2020-03-18 22:47:56 +01:00
|
|
|
try {
|
|
|
|
// The widget's options are encoded into the fragment to avoid leaking info to the server. The widget
|
|
|
|
// spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
|
|
|
|
const widgetQuery = qs.parse(window.location.hash.substring(1));
|
|
|
|
const query = Object.assign({}, qs.parse(window.location.search.substring(1)), widgetQuery);
|
|
|
|
const qsParam = (name: string, optional = false): string => {
|
|
|
|
if (!optional && (!query[name] || typeof (query[name]) !== 'string')) {
|
|
|
|
throw new Error(`Expected singular ${name} in query string`);
|
|
|
|
}
|
|
|
|
return <string>query[name];
|
|
|
|
};
|
|
|
|
|
2020-04-01 12:08:53 +02:00
|
|
|
// If we have these params, expect a widget API to be available (ie. to be in an iframe
|
|
|
|
// inside a matrix client). Otherwise, assume we're on our own, eg. have been popped
|
|
|
|
// out into a browser.
|
|
|
|
const parentUrl = qsParam('parentUrl', true);
|
|
|
|
const widgetId = qsParam('widgetId', true);
|
|
|
|
|
2020-07-13 18:32:17 +02:00
|
|
|
// Set this up as early as possible because Element will be hitting it almost immediately.
|
2020-04-01 12:08:53 +02:00
|
|
|
if (parentUrl && widgetId) {
|
|
|
|
widgetApi = new WidgetApi(qsParam('parentUrl'), qsParam('widgetId'), [
|
|
|
|
Capability.AlwaysOnScreen,
|
|
|
|
]);
|
|
|
|
widgetApi.expectingExplicitReady = true;
|
|
|
|
}
|
2020-03-24 16:14:59 +01:00
|
|
|
|
2020-03-18 22:47:56 +01:00
|
|
|
// Populate the Jitsi params now
|
2020-03-24 16:54:15 +01:00
|
|
|
jitsiDomain = qsParam('conferenceDomain');
|
2020-03-18 22:47:56 +01:00
|
|
|
conferenceId = qsParam('conferenceId');
|
|
|
|
displayName = qsParam('displayName', true);
|
|
|
|
avatarUrl = qsParam('avatarUrl', true); // http not mxc
|
|
|
|
userId = qsParam('userId');
|
2020-09-04 12:14:52 +02:00
|
|
|
jitsiAuth = qsParam('auth', true);
|
|
|
|
roomId = qsParam('roomId', true);
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2020-04-01 12:08:53 +02:00
|
|
|
if (widgetApi) {
|
|
|
|
await widgetApi.waitReady();
|
|
|
|
await widgetApi.setAlwaysOnScreen(false); // start off as detachable from the screen
|
2020-03-24 16:54:15 +01:00
|
|
|
|
2020-09-07 17:10:19 +02:00
|
|
|
if (jitsiAuth === 'openidtoken-jwt') {
|
|
|
|
window.addEventListener('message', onWidgetMessage);
|
|
|
|
widgetApi.callAction(
|
|
|
|
KnownWidgetActions.GetOpenIDCredentials,
|
|
|
|
{},
|
|
|
|
(response) => {console.log(response);},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
enableJoinButton();
|
|
|
|
}
|
|
|
|
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795)
|
|
|
|
} else {
|
|
|
|
enableJoinButton();
|
|
|
|
}
|
2020-03-18 22:47:56 +01:00
|
|
|
} catch (e) {
|
|
|
|
console.error("Error setting up Jitsi widget", e);
|
|
|
|
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget";
|
|
|
|
switchVisibleContainers();
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
2020-09-07 17:10:19 +02:00
|
|
|
|
|
|
|
function enableJoinButton() {
|
|
|
|
document.getElementById("joinButton").onclick = () => joinConference();
|
|
|
|
}
|
|
|
|
|
2020-03-18 22:47:56 +01:00
|
|
|
function switchVisibleContainers() {
|
|
|
|
inConference = !inConference;
|
|
|
|
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
|
|
|
|
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset';
|
|
|
|
}
|
|
|
|
|
2020-09-04 12:14:52 +02:00
|
|
|
/**
|
|
|
|
* Create a JWT token fot jitsi openidtoken-jwt auth
|
|
|
|
*
|
|
|
|
* See TODO add link
|
|
|
|
*/
|
|
|
|
function createJWTToken() {
|
|
|
|
// Header
|
|
|
|
const header = {alg: 'HS256', typ: 'JWT'};
|
|
|
|
// Payload
|
|
|
|
const payload = {
|
|
|
|
// TODO change this to refer to spec?
|
2020-09-07 17:10:19 +02:00
|
|
|
iss: 'app_id',
|
2020-09-04 12:14:52 +02:00
|
|
|
sub: jitsiDomain,
|
|
|
|
aud: `https://${jitsiDomain}`,
|
|
|
|
room: "*",
|
|
|
|
context: {
|
|
|
|
matrix: {
|
2020-09-07 17:10:19 +02:00
|
|
|
token: openIDToken,
|
2020-09-04 12:14:52 +02:00
|
|
|
room_id: roomId,
|
|
|
|
},
|
|
|
|
user: {
|
|
|
|
avatar: avatarUrl,
|
|
|
|
name: displayName,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
// Sign JWT
|
|
|
|
// The secret string here is irrelevant, we're only using the JWT
|
|
|
|
// to transport data to Prosody in the Jitsi stack.
|
|
|
|
// See TODO add link
|
|
|
|
return KJUR.jws.JWS.sign(
|
2020-09-07 17:10:19 +02:00
|
|
|
'HS256',
|
2020-09-04 12:14:52 +02:00
|
|
|
JSON.stringify(header),
|
|
|
|
JSON.stringify(payload),
|
2020-09-07 17:10:19 +02:00
|
|
|
'notused',
|
2020-09-04 12:14:52 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-03-18 22:47:56 +01:00
|
|
|
function joinConference() { // event handler bound in HTML
|
|
|
|
switchVisibleContainers();
|
|
|
|
|
|
|
|
// noinspection JSIgnoredPromiseFromCall
|
2020-04-01 12:08:53 +02:00
|
|
|
if (widgetApi) widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works
|
2020-03-18 22:47:56 +01:00
|
|
|
|
2020-03-31 20:19:17 +02:00
|
|
|
console.warn(
|
|
|
|
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " +
|
|
|
|
"they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
|
|
|
|
"our fragment values and not recognizing the options.",
|
|
|
|
);
|
2020-09-04 12:14:52 +02:00
|
|
|
const options = {
|
2020-03-18 22:47:56 +01:00
|
|
|
width: "100%",
|
|
|
|
height: "100%",
|
|
|
|
parentNode: document.querySelector("#jitsiContainer"),
|
|
|
|
roomName: conferenceId,
|
|
|
|
interfaceConfigOverwrite: {
|
|
|
|
SHOW_JITSI_WATERMARK: false,
|
|
|
|
SHOW_WATERMARK_FOR_GUESTS: false,
|
|
|
|
MAIN_TOOLBAR_BUTTONS: [],
|
|
|
|
VIDEO_LAYOUT_FIT: "height",
|
|
|
|
},
|
2020-09-04 12:14:52 +02:00
|
|
|
jwt: undefined,
|
|
|
|
};
|
2020-09-07 17:10:19 +02:00
|
|
|
if (jitsiAuth === 'penidtoken-jwt') {
|
2020-09-04 12:14:52 +02:00
|
|
|
options.jwt = createJWTToken();
|
|
|
|
}
|
|
|
|
const meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
|
2020-03-18 22:47:56 +01:00
|
|
|
if (displayName) meetApi.executeCommand("displayName", displayName);
|
|
|
|
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
|
|
|
|
if (userId) meetApi.executeCommand("email", userId);
|
|
|
|
|
|
|
|
meetApi.on("readyToClose", () => {
|
2020-09-07 17:10:19 +02:00
|
|
|
window.removeEventListener('message', onWidgetMessage);
|
2020-03-18 22:47:56 +01:00
|
|
|
switchVisibleContainers();
|
|
|
|
|
|
|
|
// noinspection JSIgnoredPromiseFromCall
|
2020-04-01 12:08:53 +02:00
|
|
|
if (widgetApi) widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works
|
2020-03-18 22:47:56 +01:00
|
|
|
|
|
|
|
document.getElementById("jitsiContainer").innerHTML = "";
|
|
|
|
});
|
|
|
|
}
|