Merge pull request #413 from anoadragon453/anoa/bbb_call_button
BigBlueButton: Allow creating meetings from the client
This commit is contained in:
commit
b97a0f4bc2
|
@ -91,6 +91,26 @@ dimension:
|
||||||
# to your own Dimension instance.
|
# to your own Dimension instance.
|
||||||
publicUrl: "https://dimension.example.org"
|
publicUrl: "https://dimension.example.org"
|
||||||
|
|
||||||
|
bigbluebutton:
|
||||||
|
# The full base URL of the API of your BigBlueButton instance. The API is
|
||||||
|
# used to create and join meetings.
|
||||||
|
apiBaseUrl: "https://bbb.example.org/bigbluebutton/api"
|
||||||
|
|
||||||
|
# The "shared secret" of your BigBlueButton instance. This is used to
|
||||||
|
# authenticate to the API above.
|
||||||
|
sharedSecret: "YourSharedSecretHere"
|
||||||
|
|
||||||
|
# The title for BigBlueButton widgets that are generated by Dimension.
|
||||||
|
widgetName: "BigBlueButton Conference"
|
||||||
|
|
||||||
|
# The subtitle for BigBlueButton widgets that are generated by Dimension.
|
||||||
|
widgetTitle: "Join the conference"
|
||||||
|
|
||||||
|
# The avatar for BigBlueButton widgets that are generated by Dimension.
|
||||||
|
# Usually this doen't need to be changed, however if your homeserver
|
||||||
|
# is not able to reach t2bot.io then you should specify your own here.
|
||||||
|
widgetAvatarUrl: "mxc://t2bot.io/be1650140620d8bb61a8cf5baeb05f24a734434c"
|
||||||
|
|
||||||
# Settings for controlling how logging works
|
# Settings for controlling how logging works
|
||||||
logging:
|
logging:
|
||||||
file: logs/dimension.log
|
file: logs/dimension.log
|
||||||
|
|
218
docs/bigbluebutton.md
Normal file
218
docs/bigbluebutton.md
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
# BigBlueButton
|
||||||
|
|
||||||
|
[BigBlueButton](https://bigbluebutton.org/) is open-source video calling software aimed primarily at educators and has
|
||||||
|
useful features such as screen and PDF sharing, collaborative document editing, polls and more.
|
||||||
|
|
||||||
|
Dimension supports embedding BigBlueButton meetings into a Matrix room via widgets, and can do so in two ways:
|
||||||
|
|
||||||
|
* [Greenlight](https://docs.bigbluebutton.org/greenlight/gl-overview.html) is a frontend for BigBlueButton meeting
|
||||||
|
administration that allows for creating/ending meetings, inviting people via link or email etc. Dimension supports
|
||||||
|
taking a Greenlight URL and turning it into a widget in a Matrix room that room occupants can use to join the meeting.
|
||||||
|
In this instance, Greenlight is acting as a client of BigBlueButton's server-side API, and Dimension is relying on a
|
||||||
|
Greenlight URL to allow Matrix users to join a meeting.
|
||||||
|
* Alternatively, Dimension can be configured to connect directly to BigBlueButton and create meetings itself. This does
|
||||||
|
require running your own BigBlueButton server which Dimension will need authorization to control. This configuration is
|
||||||
|
useful as meetings can be created without needing to leave your Matrix client, as well as sidesteps the need to set up
|
||||||
|
Greenlight on your deployment. This method can also be used to allow the 'Video Call' button in Element to start a
|
||||||
|
meeting.
|
||||||
|
|
||||||
|
Note that with the first method, Dimension can be given a Greenlight URL pointing to any public instance of
|
||||||
|
Greenlight/BigBlueButton. With the second, Dimension will only talk to, and create meetings on, the BigBlueButton server
|
||||||
|
it has been configured to talk to.
|
||||||
|
|
||||||
|
## Usage Guide
|
||||||
|
|
||||||
|
### Using a Greenlight URL
|
||||||
|
|
||||||
|
Dimension does not require any extra configuration to allow creating meetings using a Greenlight URL.
|
||||||
|
|
||||||
|
* Open the integration manager window and add a BigBlueButton widget.
|
||||||
|
* Enter the URL you received from Greenlight. It should be in the form: `https://bbb.example.com/abc-def-ghi`.
|
||||||
|
* Click the 'Add Widget' button.
|
||||||
|
|
||||||
|
Members in the room will see a BigBlueButton widget appear. They can click "Join Conference" inside the widget to join.
|
||||||
|
|
||||||
|
### Have Dimension Create Meetings (with Element's Video Call button)
|
||||||
|
|
||||||
|
Fill out the `bigbluebutton` section of Dimension's config file. Both the `apiBaseUrl` and `sharedSecret` fields must be
|
||||||
|
set to the values corresponding to your own BigBlueButton server.
|
||||||
|
|
||||||
|
Element can be configured to ask Dimension to start a meeting without any client-side changes. Simply add the following
|
||||||
|
information to the [Client well-known](https://matrix.org/docs/spec/client_server/r0.6.1#get-well-known-matrix-client)
|
||||||
|
file of your homeserver:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"io.element.call_behaviour": {
|
||||||
|
"widget_build_url": "https://dimension.example.com/api/v1/dimension/bigbluebutton/widget_state",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
then close and reopen Element so that it picks up the new behaviour.
|
||||||
|
|
||||||
|
Now pressing the Video Call button in a room with more than two users in it should spawn a BigBlueButton meeting! Note
|
||||||
|
that BigBlueButton will automatically end meetings that all users have left (after a minute or two). Thus if all users
|
||||||
|
have left a meeting, you will need to recreate the widget in order to spawn a new meeting.
|
||||||
|
|
||||||
|
Created meetings are protected by a generated password that is included in the widget room state event. Thus one will
|
||||||
|
only be able to gain access to the meeting if they are in the room, or if someone in the room shares that information
|
||||||
|
with them. All users that join will have moderator permissions in the meeting, though this may change in the future to
|
||||||
|
allow meeting permissions based of room power level.
|
||||||
|
|
||||||
|
### Troubleshooting
|
||||||
|
|
||||||
|
This code was last tested with the [BigBlueButton v2.3 docker image](https://github.com/bigbluebutton/docker/).
|
||||||
|
|
||||||
|
#### When using a Greenlight URL
|
||||||
|
|
||||||
|
Let us know if you run into any problems!
|
||||||
|
|
||||||
|
#### When having Dimension create meetings
|
||||||
|
|
||||||
|
*My users are seeing 401 Unknown Session errors when trying to join a meeting!*
|
||||||
|
|
||||||
|
BigBlueButton (or at least the [docker image](https://github.com/bigbluebutton/docker)) has a strange bug where
|
||||||
|
calling the [`/join`](https://docs.bigbluebutton.org/dev/api.html#join) API ends up failing to verify the session ID
|
||||||
|
*which it just passed to you*. There's discussion on the
|
||||||
|
issue [here](https://github.com/bigbluebutton/bigbluebutton/issues/6343)
|
||||||
|
and [here](https://groups.google.com/forum/#!topic/bigbluebutton-dev/TkjyUZP_gO8), but **TL;DR if you're getting
|
||||||
|
invalid session ID errors**, a workaround is to set `allowRequestsWithoutSession=true`
|
||||||
|
in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` (located in the `bbb-docker_bbb-web` container for
|
||||||
|
docker deployments).
|
||||||
|
|
||||||
|
*I'm using the BigBlueButton docker image and it's taking about 20-30 seconds for a user to join a meeting. Why is it so slow?*
|
||||||
|
|
||||||
|
This appears to be due to the BigBlueButton nginx container attempting to contact multiple html5-frontend instances, of
|
||||||
|
which you may only have configured one or two to start. nginx will wait for every request to timeout before returning
|
||||||
|
the result to yours though, hence the long wait. It's possible to edit the nginx configuration inside the containers to
|
||||||
|
remove the extra upstream directives and eliminate the timeout. But this is something that really needs to be fixed
|
||||||
|
upstream.
|
||||||
|
|
||||||
|
*It sometimes says that my meeting does not exist! Where'd it go?*
|
||||||
|
|
||||||
|
As mentioned above, BigBlueButton server will automatically remove meetings that everyone has left. Unfortunately we don't
|
||||||
|
currently have a way to remove the widget when this happens. Someone will permissions in the room will need to recreate
|
||||||
|
the widget and thus create a new meeting.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### When using a Greenlight URL
|
||||||
|
|
||||||
|
See the explanation [here](https://github.com/turt2live/matrix-dimension/blob/70608c2a96a11953e0bf3bf197bb81d852df801d/src/api/dimension/DimensionBigBlueButtonService.ts#L28-L111).
|
||||||
|
|
||||||
|
### When having Dimension create meetings
|
||||||
|
|
||||||
|
Matrix clients can create widgets by sending a widget state event in the room with the appropriate fields. To retrieve
|
||||||
|
the necessary content of a state event that embeds a BigBlueButton meeting, clients can
|
||||||
|
call `GET /api/v1/dimension/bigbluebutton/widget_state?roomId=!room:domain`
|
||||||
|
on Dimension to retrieve the necessary json contents for the state event. An example response may look like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"widget_id": "24faa4cfd11d3b915664b7b393866974517014d43e5e682f8c930ec3fbaac337",
|
||||||
|
"widget": {
|
||||||
|
"creatorUserId": "@dimension:localhost",
|
||||||
|
"id": "24faa4cfd11d3b915664b7b393866974517014d43e5e682f8c930ec3fbaac337",
|
||||||
|
"type": "m.custom",
|
||||||
|
"waitForIframeLoad": true,
|
||||||
|
"name": "BigBlueButton Conference",
|
||||||
|
"avatar_url": "mxc://t2bot.io/be1650140620d8bb61a8cf5baeb05f24a734434c",
|
||||||
|
"url": "https://dimension.example.com/widgets/bigbluebutton?widgetId=$matrix_widget_id&roomId=$matrix_room_id&createMeeting=true&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&meetingId=GsmiDReG&meetingPassword=dvKIv7EX&auth=$openidtoken-jwt",
|
||||||
|
"data": {
|
||||||
|
"title": "Join the conference"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"container": "top",
|
||||||
|
"index": 0,
|
||||||
|
"width": 65,
|
||||||
|
"height": 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The contents of the `widget` dict from the response is what should be placed in the `content` of the widget state
|
||||||
|
event. An example widget state event generated from the above looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "im.vector.modular.widgets",
|
||||||
|
"sender": "@admin:localhost",
|
||||||
|
"content": {
|
||||||
|
"creatorUserId": "@admin:localhost",
|
||||||
|
"id": "24faa4cfd11d3b915664b7b393866974517014d43e5e682f8c930ec3fbaac337",
|
||||||
|
"type": "m.custom",
|
||||||
|
"waitForIframeLoad": true,
|
||||||
|
"name": "BigBlueButton Conference",
|
||||||
|
"avatar_url": "mxc://t2bot.io/be1650140620d8bb61a8cf5baeb05f24a734434c",
|
||||||
|
"url": "https://dimension.example.com/widgets/bigbluebutton?widgetId=$matrix_widget_id&roomId=$matrix_room_id&createMeeting=true&displayName=$matrix_display_name&avatarUrl=$matrix_avatar_url&userId=$matrix_user_id&meetingId=GsmiDReG&meetingPassword=dvKIv7EX&auth=$openidtoken-jwt",
|
||||||
|
"data": {
|
||||||
|
"title": "Join the conference"
|
||||||
|
},
|
||||||
|
"roomId": "!ZsCMQAoIIHgOlXMzwX:localhost",
|
||||||
|
"eventId": "$2RbnJDUPFIMTDVda_-Z01lyZt30W-bZnw3z7CFIRWKQ"
|
||||||
|
},
|
||||||
|
"state_key": "24faa4cfd11d3b915664b7b393866974517014d43e5e682f8c930ec3fbaac337",
|
||||||
|
"origin_server_ts": 1620386456620,
|
||||||
|
"unsigned": {
|
||||||
|
"age": 96
|
||||||
|
},
|
||||||
|
"event_id": "$2RbnJDUPFIMTDVda_-Z01lyZt30W-bZnw3z7CFIRWKQ",
|
||||||
|
"room_id": "!ZsCMQAoIIHgOlXMzwX:localhost"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
While servicing the `/widget_state` call, Dimension will create the BigBlueButton meeting by calling the
|
||||||
|
[BigBlueButton `/create` API](https://docs.bigbluebutton.org/dev/api.html#create). We get back a meeting ID and two
|
||||||
|
passwords from BigBlueButton: one "attendee" and one "moderator" password. We can pass either of these back to the user,
|
||||||
|
and it'll be placed in the widget state event in the room. (For now, we just pass back the moderator password). You'll
|
||||||
|
notice that it's included in the `url` field as a query parameter. Those query parameters are passed to the widget when
|
||||||
|
it loads, which the widget then uses to populate fields when making subsequent calls to Dimension.
|
||||||
|
|
||||||
|
The widget will then use the meetingID, password and some additional user metadata (displayname, userID, avatarURL) to call
|
||||||
|
`POST /api/v1/dimension/bigbluebutton/getJoinUrl`. Dimension will craft a URL that the widget can use to call the
|
||||||
|
[BigBlueButton `/join` API](https://docs.bigbluebutton.org/dev/api.html#join), and respond with the following:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"url": "https://bbb.example.com/bigbluebutton/api/join?meetingID=2QdrCVAl&password=yX2w587M&fullName=bob%20(%40bob%3Aexample.com)&userID=%40bob%3Aexample.com&checksum=fa35ecdf711478423bf5173cb81c5b2e0e9e59bb8779811b614a44645ee94d89"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That URL is embedded and loaded by the widget - joining the meeting.
|
||||||
|
|
||||||
|
Note that if everyone in the meeting leaves, the meeting will be garbage-collected automatically server-side by BigBlueButton.
|
||||||
|
However the widget will still remain in the room. In this case, if users attempt to join the meeting again, they will be
|
||||||
|
informed that a new meeting needs to be created. This works by having Dimension check if a meeting is still running in
|
||||||
|
`/getJoinUrl`. It does so by using the
|
||||||
|
[BigBlueButton `getMeetingInfo` API](https://docs.bigbluebutton.org/dev/api.html#getmeetinginfo) to check that:
|
||||||
|
|
||||||
|
* A current or past meeting actually exists with the provided meeting ID - if not, the following will be returned to the
|
||||||
|
widget:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonResponse": {
|
||||||
|
"error": "This meeting does not exist.",
|
||||||
|
"dim_errcode": "UNKNOWN_MEETING_ID",
|
||||||
|
"errcode": "UNKNOWN_MEETING_ID"
|
||||||
|
},
|
||||||
|
"statusCode": 400,
|
||||||
|
"errorCode": "UNKNOWN_MEETING_ID"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* A meeting exists, but has both `running` as `false` and has an `endTime` other than 0. If both of those are true,
|
||||||
|
then the meeting existed but is no longer running. In that case, the following will be returned to the widget:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"jsonResponse": {
|
||||||
|
"error": "This meeting does not exist.",
|
||||||
|
"dim_errcode": "MEETING_HAS_ENDED",
|
||||||
|
"errcode": "MEETING_HAS_ENDED"
|
||||||
|
},
|
||||||
|
"statusCode": 400,
|
||||||
|
"errorCode": "MEETING_HAS_ENDED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Upon receiving either of these, the widget will inform the user with an error message that a new meeting must be created.
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -13962,6 +13962,20 @@
|
||||||
"async-limiter": "~1.0.0"
|
"async-limiter": "~1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"xml2js": {
|
||||||
|
"version": "0.4.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||||
|
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||||
|
"requires": {
|
||||||
|
"sax": ">=0.6.0",
|
||||||
|
"xmlbuilder": "~11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"xmlbuilder": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||||
|
},
|
||||||
"xtend": {
|
"xtend": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
|
||||||
|
|
|
@ -61,7 +61,8 @@
|
||||||
"typescript-ioc": "^1.2.5",
|
"typescript-ioc": "^1.2.5",
|
||||||
"typescript-rest": "^2.2.0",
|
"typescript-rest": "^2.2.0",
|
||||||
"umzug": "^2.2.0",
|
"umzug": "^2.2.0",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0",
|
||||||
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/animations": "^8.0.3",
|
"@angular/animations": "^8.0.3",
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { GET, Path, QueryParam } from "typescript-rest";
|
import { GET, POST, Path, QueryParam } from "typescript-rest";
|
||||||
import * as request from "request";
|
import * as request from "request";
|
||||||
import { LogService } from "matrix-js-snippets";
|
import { LogService } from "matrix-js-snippets";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import { BigBlueButtonJoinRequest } from "../../models/Widget";
|
import { BigBlueButtonGetJoinUrlRequest } from "../../models/Widget";
|
||||||
import { BigBlueButtonJoinResponse } from "../../models/WidgetResponses";
|
import { BigBlueButtonJoinResponse, BigBlueButtonCreateAndJoinMeetingResponse, BigBlueButtonWidgetResponse } from "../../models/WidgetResponses";
|
||||||
import { AutoWired } from "typescript-ioc/es6";
|
import { AutoWired } from "typescript-ioc/es6";
|
||||||
import { ApiError } from "../ApiError";
|
import { ApiError } from "../ApiError";
|
||||||
|
import { sha256 } from "../../utils/hashing";
|
||||||
|
import config from "../../config";
|
||||||
|
import { parseStringPromise } from "xml2js";
|
||||||
|
import * as randomString from "random-string";
|
||||||
|
import { MatrixStickerBot } from "../../matrix/MatrixStickerBot";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API for the BigBlueButton widget.
|
* API for the BigBlueButton widget.
|
||||||
|
@ -21,6 +26,9 @@ export class DimensionBigBlueButtonService {
|
||||||
private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`);
|
private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`);
|
||||||
|
|
||||||
// join handles the request from a client to join a BigBlueButton meeting
|
// join handles the request from a client to join a BigBlueButton meeting
|
||||||
|
// via a Greenlight URL. Note that this is no longer the only way to join a
|
||||||
|
// BigBlueButton meeting. See xxx below for the API that bypasses Greenlight
|
||||||
|
// and instead calls the BigBlueButton API directly.
|
||||||
//
|
//
|
||||||
// The client is expected to send a link created by greenlight, the nice UI
|
// The client is expected to send a link created by greenlight, the nice UI
|
||||||
// that's recommended to be installed on top of BBB, which is itself a BBB
|
// that's recommended to be installed on top of BBB, which is itself a BBB
|
||||||
|
@ -104,7 +112,6 @@ export class DimensionBigBlueButtonService {
|
||||||
@GET
|
@GET
|
||||||
@Path("join")
|
@Path("join")
|
||||||
public async join(
|
public async join(
|
||||||
joinRequest: BigBlueButtonJoinRequest,
|
|
||||||
@QueryParam("greenlightUrl") greenlightURL: string,
|
@QueryParam("greenlightUrl") greenlightURL: string,
|
||||||
@QueryParam("fullName") fullName: string,
|
@QueryParam("fullName") fullName: string,
|
||||||
): Promise<BigBlueButtonJoinResponse|ApiError> {
|
): Promise<BigBlueButtonJoinResponse|ApiError> {
|
||||||
|
@ -114,7 +121,6 @@ export class DimensionBigBlueButtonService {
|
||||||
LogService.info("BigBlueButton", "URL from client: " + greenlightURL);
|
LogService.info("BigBlueButton", "URL from client: " + greenlightURL);
|
||||||
LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID);
|
LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID);
|
||||||
LogService.info("BigBlueButton", "Name given from client: " + fullName);
|
LogService.info("BigBlueButton", "Name given from client: " + fullName);
|
||||||
LogService.info("BigBlueButton", joinRequest);
|
|
||||||
|
|
||||||
// Query the URL the user has given us
|
// Query the URL the user has given us
|
||||||
let response = await this.doRequest("GET", greenlightURL);
|
let response = await this.doRequest("GET", greenlightURL);
|
||||||
|
@ -163,6 +169,238 @@ export class DimensionBigBlueButtonService {
|
||||||
return {url: joinUrl};
|
return {url: joinUrl};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clients can call this endpoint in order to retrieve the contents of the widget room state.
|
||||||
|
* This endpoint will create a BigBlueButton meeting and place the returned ID and password in the room state.
|
||||||
|
* @param {string} roomId The ID of the room that the widget will live in.
|
||||||
|
*/
|
||||||
|
@GET
|
||||||
|
@Path("widget_state")
|
||||||
|
public async widget(
|
||||||
|
@QueryParam("roomId") roomId: string,
|
||||||
|
): Promise<BigBlueButtonWidgetResponse|ApiError> {
|
||||||
|
// Hash the room ID in order to generate a unique widget ID
|
||||||
|
const widgetId = sha256(roomId + "bigbluebutton");
|
||||||
|
|
||||||
|
const widgetName = config.bigbluebutton.widgetName;
|
||||||
|
const widgetTitle = config.bigbluebutton.widgetTitle;
|
||||||
|
const widgetAvatarUrl = config.bigbluebutton.widgetAvatarUrl;
|
||||||
|
|
||||||
|
LogService.info("BigBlueButton", "Got a meeting create request for room: " + roomId);
|
||||||
|
|
||||||
|
// NOTE: BBB meetings will by default end a minute or two after the last person leaves.
|
||||||
|
const createQueryParameters = {
|
||||||
|
meetingID: randomString(20),
|
||||||
|
// To help admins link meeting IDs to rooms
|
||||||
|
meta_MatrixRoomID: roomId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a new meeting.
|
||||||
|
const createResponse = await this.makeBBBApiCall("GET", "create", createQueryParameters, null);
|
||||||
|
LogService.info("BigBlueButton", createResponse);
|
||||||
|
|
||||||
|
// The password users will join with.
|
||||||
|
// TODO: We currently give users access to moderate the meeting by returning createResponse.moderatorPW instead
|
||||||
|
// of createResponse.attendeePW. The latter lets people join as viewers without any moderator permissions.
|
||||||
|
// Allowing this would likely require saving the moderator password for a meeting in Dimension and authenticating
|
||||||
|
// users by room power level when they call getJoinUrl. Unfortunately, doing would either require us to have a
|
||||||
|
// user in the room or have the user be a Synapse server admin (so that they can see room state).
|
||||||
|
const meetingPassword = createResponse.moderatorPW[0];
|
||||||
|
|
||||||
|
// Retrieve the user ID that dimension is running as
|
||||||
|
const widgetCreatorUserId = await MatrixStickerBot.getUserId();
|
||||||
|
|
||||||
|
// Add all necessary client variables to the url when loading the widget
|
||||||
|
const widgetUrl = config.dimension.publicUrl +
|
||||||
|
"/widgets/bigbluebutton" +
|
||||||
|
"?widgetId=$matrix_widget_id" +
|
||||||
|
"&roomId=$matrix_room_id" +
|
||||||
|
// Indicate that we would like to join a meeting created by Dimension, rather than doing so via
|
||||||
|
// a greenlight URL
|
||||||
|
"&createMeeting=true" +
|
||||||
|
"&displayName=$matrix_display_name" +
|
||||||
|
"&avatarUrl=$matrix_avatar_url" +
|
||||||
|
"&userId=$matrix_user_id" +
|
||||||
|
// Provide the meeting details in the state event
|
||||||
|
`&meetingId=${createResponse.meetingID[0]}` +
|
||||||
|
`&meetingPassword=${meetingPassword}` +
|
||||||
|
"&auth=$openidtoken-jwt";
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widget_id": widgetId,
|
||||||
|
"widget": {
|
||||||
|
"creatorUserId": widgetCreatorUserId,
|
||||||
|
"id": widgetId,
|
||||||
|
"type": "m.custom",
|
||||||
|
"waitForIframeLoad": true,
|
||||||
|
"name": widgetName,
|
||||||
|
"avatar_url": widgetAvatarUrl,
|
||||||
|
"url": widgetUrl,
|
||||||
|
"data": {
|
||||||
|
"title": widgetTitle,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"container": "top",
|
||||||
|
"index": 0,
|
||||||
|
"width": 65,
|
||||||
|
"height": 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clients can call this endpoint in order to retrieve a URL that leads to the BigBlueButton API that they can
|
||||||
|
* use to join the meeting with. They will need to provide the meeting ID and password which are only available
|
||||||
|
* from the widget room state event.
|
||||||
|
* @param {BigBlueButtonGetJoinUrlRequest} getJoinUrlRequest The body of the request.
|
||||||
|
*/
|
||||||
|
@POST
|
||||||
|
@Path("getJoinUrl")
|
||||||
|
public async getJoinUrl(
|
||||||
|
getJoinUrlRequest: BigBlueButtonGetJoinUrlRequest,
|
||||||
|
): Promise<BigBlueButtonCreateAndJoinMeetingResponse|ApiError> {
|
||||||
|
// Check if the meeting exists and is running. If not, return an error for each case
|
||||||
|
let getMeetingInfoParameters = {
|
||||||
|
meetingID: getJoinUrlRequest.meetingId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMeetingInfoResponse = await this.makeBBBApiCall("GET", "getMeetingInfo", getMeetingInfoParameters, null);
|
||||||
|
LogService.info("BigBlueButton", getMeetingInfoResponse)
|
||||||
|
if (getMeetingInfoResponse.returncode[0] === "FAILED") {
|
||||||
|
// This meeting does not exist, inform the user
|
||||||
|
return new ApiError(
|
||||||
|
400,
|
||||||
|
{error: "This meeting does not exist."},
|
||||||
|
"UNKNOWN_MEETING_ID",
|
||||||
|
);
|
||||||
|
} else if (getMeetingInfoResponse.running[0] === "false" && getMeetingInfoResponse.endTime[0] !== "0") {
|
||||||
|
// This meeting did exist, but has ended. Inform the user
|
||||||
|
return new ApiError(
|
||||||
|
400,
|
||||||
|
{error: "This meeting has ended."},
|
||||||
|
"MEETING_HAS_ENDED",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a fullName parameter from the provided user ID and display name (if provided).
|
||||||
|
// It's important to display the user ID so that users cannot impersonate each other.
|
||||||
|
let fullName: string;
|
||||||
|
if (getJoinUrlRequest.displayName) {
|
||||||
|
fullName = `${getJoinUrlRequest.displayName} (${getJoinUrlRequest.userId})`;
|
||||||
|
} else {
|
||||||
|
fullName = getJoinUrlRequest.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let joinQueryParameters = {
|
||||||
|
meetingID: getJoinUrlRequest.meetingId,
|
||||||
|
password: getJoinUrlRequest.meetingPassword,
|
||||||
|
fullName: fullName,
|
||||||
|
userID: getJoinUrlRequest.userId,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add an avatar to the join request if the user provided one
|
||||||
|
if (getJoinUrlRequest.avatarUrl.startsWith("mxc")) {
|
||||||
|
joinQueryParameters["avatarURL"] = this.getHTTPAvatarUrlFromMXCUrl(getJoinUrlRequest.avatarUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the checksum for the join URL. We need to do so as a browser would as we're passing this back to a browser
|
||||||
|
const checksum = this.bbbChecksumFromCallNameAndQueryParamaters("join", joinQueryParameters, true);
|
||||||
|
|
||||||
|
// Construct the join URL, which we'll give back to the client, who can then add additional parameters to (or we just do it)
|
||||||
|
const url = `${config.bigbluebutton.apiBaseUrl}/join?${this.queryStringFromObject(joinQueryParameters, true)}&checksum=${checksum}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make an API call to the configured BBB server instance.
|
||||||
|
* @param {string} method The HTTP method to use for the request.
|
||||||
|
* @param {string} apiCallName The name of the API (the last bit of the endpoint) to call. e.g 'create', 'join'.
|
||||||
|
* @param {any} queryParameters The query parameters to use in the request.
|
||||||
|
* @param {any} body The body of the request.
|
||||||
|
* @returns {any} The response to the call.
|
||||||
|
*/
|
||||||
|
private async makeBBBApiCall(
|
||||||
|
method: string,
|
||||||
|
apiCallName: string,
|
||||||
|
queryParameters: any,
|
||||||
|
body: any,
|
||||||
|
): Promise<any> {
|
||||||
|
// Compute the checksum needed to authenticate the request (as derived from the configured shared secret)
|
||||||
|
queryParameters.checksum = this.bbbChecksumFromCallNameAndQueryParamaters(apiCallName, queryParameters, false);
|
||||||
|
|
||||||
|
// Get the URL host and path using the configured api base and the API call name
|
||||||
|
const url = `${config.bigbluebutton.apiBaseUrl}/${apiCallName}`;
|
||||||
|
|
||||||
|
// Now make the request!
|
||||||
|
const response = await this.doRequest(method, url, queryParameters, body);
|
||||||
|
|
||||||
|
// Parse and return the XML from the response
|
||||||
|
// TODO: XML parsing error handling
|
||||||
|
const parsedResponse = await parseStringPromise(response.body);
|
||||||
|
|
||||||
|
// Extract the "response" object
|
||||||
|
return parsedResponse.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object representing a query string into a checksum suitable for appending to a BBB API call.
|
||||||
|
* Docs: https://docs.bigbluebutton.org/dev/api.html#usage
|
||||||
|
* @param {string} apiCallName The name of the API to call, e.g "create", "join".
|
||||||
|
* @param {any} queryParameters An object representing a set of query parameters represented by keys and values.
|
||||||
|
* @param {boolean} encodeAsBrowser Whether to encode the query string as a browser would.
|
||||||
|
* @returns {string} The checksum for the request.
|
||||||
|
*/
|
||||||
|
private bbbChecksumFromCallNameAndQueryParamaters(apiCallName: string, queryParameters: any, encodeAsBrowser: boolean): string {
|
||||||
|
// Convert the query parameters object into a string
|
||||||
|
// We URL encode each value as a browser would. If we don't, our resulting checksum will not match.
|
||||||
|
const widgetQueryString = this.queryStringFromObject(queryParameters, encodeAsBrowser);
|
||||||
|
|
||||||
|
LogService.info("BigBlueButton", "Built widget string:" + widgetQueryString);
|
||||||
|
LogService.info("BigBlueButton", "Hashing:" + apiCallName + widgetQueryString + config.bigbluebutton.sharedSecret);
|
||||||
|
|
||||||
|
// Hash the api name and query parameters to get the checksum, and add it to the set of query parameters
|
||||||
|
return sha256(apiCallName + widgetQueryString + config.bigbluebutton.sharedSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an object containing keys and values as strings into a string representing URL query parameters.
|
||||||
|
* @param queryParameters
|
||||||
|
* @param encodeAsBrowser
|
||||||
|
* @returns {string} The query parameter object as a string.
|
||||||
|
*/
|
||||||
|
private queryStringFromObject(queryParameters: any, encodeAsBrowser: boolean): string {
|
||||||
|
return Object.keys(queryParameters).map(k => k + "=" + this.encodeForUrl(queryParameters[k], encodeAsBrowser)).join("&");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a string in the same fashion browsers do (encoding ! and other characters).
|
||||||
|
* @param {string} text The text to encode.
|
||||||
|
* @param {boolean} encodeAsBrowser Whether to encode the query string as a browser would.
|
||||||
|
* @returns {string} The encoded text.
|
||||||
|
*/
|
||||||
|
private encodeForUrl(text: string, encodeAsBrowser: boolean): string {
|
||||||
|
let encodedText = encodeURIComponent(text);
|
||||||
|
if (!encodeAsBrowser) {
|
||||||
|
// use + instead of %20 for space to match what the 'request' JavaScript library does do.
|
||||||
|
// encodeURIComponent doesn't escape !'()*, so manually escape them.
|
||||||
|
encodedText = encodedText.replace(/%20/g, '+').replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
|
||||||
|
}
|
||||||
|
|
||||||
|
return encodedText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an HTTP request.
|
||||||
|
* @param {string} method The HTTP method to use.
|
||||||
|
* @param {string} url The URL (without query parameters) to request.
|
||||||
|
* @param {string} qs The query parameters to use with the request.
|
||||||
|
* @param {string} body The JSON body of the request
|
||||||
|
* @param {boolean} followRedirect Whether to follow redirect responses automatically.
|
||||||
|
*/
|
||||||
private async doRequest(
|
private async doRequest(
|
||||||
method: string,
|
method: string,
|
||||||
url: string,
|
url: string,
|
||||||
|
@ -180,6 +418,7 @@ export class DimensionBigBlueButtonService {
|
||||||
followRedirect: followRedirect,
|
followRedirect: followRedirect,
|
||||||
jar: true, // remember cookies between requests
|
jar: true, // remember cookies between requests
|
||||||
json: false, // expect html
|
json: false, // expect html
|
||||||
|
|
||||||
}, (err, res, _body) => {
|
}, (err, res, _body) => {
|
||||||
try {
|
try {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -204,4 +443,13 @@ export class DimensionBigBlueButtonService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getHTTPAvatarUrlFromMXCUrl(mxc: string): string {
|
||||||
|
const width = 64;
|
||||||
|
const height = 64;
|
||||||
|
const method = "scale";
|
||||||
|
|
||||||
|
mxc = mxc.substring("mxc://".length).split('?')[0];
|
||||||
|
return `${config.dimension.publicUrl}/api/v1/dimension/media/thumbnail/${mxc}?width=${width}&height=${height}&method=${method}&animated=false`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,13 @@ export interface DimensionConfig {
|
||||||
telegram: {
|
telegram: {
|
||||||
botToken: string;
|
botToken: string;
|
||||||
};
|
};
|
||||||
|
bigbluebutton: {
|
||||||
|
apiBaseUrl: string;
|
||||||
|
sharedSecret: string;
|
||||||
|
widgetName: string;
|
||||||
|
widgetTitle: string;
|
||||||
|
widgetAvatarUrl: string;
|
||||||
|
};
|
||||||
stickers: {
|
stickers: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
stickerBot: string;
|
stickerBot: string;
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
export interface BigBlueButtonJoinRequest {
|
export interface BigBlueButtonGetJoinUrlRequest {
|
||||||
// A URL supplied by greenlight, BigBlueButton's nice UI project that is itself
|
// The display name of the user attempting to join the meeting.
|
||||||
// a BigBlueButton client
|
// Will be combined with userId and passed to BigBlueButton.
|
||||||
greenlightUrl: string;
|
displayName: string;
|
||||||
// The name the user wishes to join the meeting with
|
// The user ID of the user attempting to join the meeting.
|
||||||
fullName: string;
|
// Will be combined with displayName and passed to BigBlueButton.
|
||||||
|
userId: string;
|
||||||
|
// Optional. The avatar of the user attempting to join the meeting.
|
||||||
|
// Will be passed to BigBlueButton.
|
||||||
|
avatarUrl: string;
|
||||||
|
// The ID of the meeting to join.
|
||||||
|
meetingId: string;
|
||||||
|
// The password to join the meeting with.
|
||||||
|
meetingPassword: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,3 +2,30 @@ export interface BigBlueButtonJoinResponse {
|
||||||
// The meeting URL the client should load to join the meeting
|
// The meeting URL the client should load to join the meeting
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BigBlueButtonCreateAndJoinMeetingResponse {
|
||||||
|
// The meeting URL the client should load to join the meeting
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BigBlueButtonWidgetResponse {
|
||||||
|
widget_id: string;
|
||||||
|
widget: {
|
||||||
|
creatorUserId: string;
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
waitForIframeLoad: boolean;
|
||||||
|
name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
url: string;
|
||||||
|
data: {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
layout: {
|
||||||
|
container: string;
|
||||||
|
index: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,3 +3,7 @@ import * as crypto from "crypto";
|
||||||
export function md5(text: string): string {
|
export function md5(text: string): string {
|
||||||
return crypto.createHash("md5").update(text).digest('hex').toLowerCase();
|
return crypto.createHash("md5").update(text).digest('hex').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sha256(text: string): string {
|
||||||
|
return crypto.createHash("sha256").update(text).digest('hex').toLowerCase();
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||||
|
|
||||||
protected OnNewWidgetPrepared(widget: EditableWidget): void {
|
protected OnNewWidgetPrepared(widget: EditableWidget): void {
|
||||||
widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl;
|
widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl;
|
||||||
|
widget.dimension.newData["createMeeting"] = this.bigBlueButtonWidget.options.createMeeting;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected OnWidgetBeforeAdd(widget: EditableWidget) {
|
protected OnWidgetBeforeAdd(widget: EditableWidget) {
|
||||||
|
@ -43,6 +44,7 @@ export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||||
let widgetQueryString = url.format({
|
let widgetQueryString = url.format({
|
||||||
query: {
|
query: {
|
||||||
"conferenceUrl": "$conferenceUrl",
|
"conferenceUrl": "$conferenceUrl",
|
||||||
|
"createMeeting": "$createMeeting",
|
||||||
"displayName": "$matrix_display_name",
|
"displayName": "$matrix_display_name",
|
||||||
"avatarUrl": "$matrix_avatar_url",
|
"avatarUrl": "$matrix_avatar_url",
|
||||||
"userId": "$matrix_user_id",
|
"userId": "$matrix_user_id",
|
||||||
|
@ -50,5 +52,6 @@ export class BigBlueButtonConfigComponent extends WidgetComponent {
|
||||||
});
|
});
|
||||||
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
|
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
|
||||||
widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString;
|
widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString;
|
||||||
|
console.log("URL ended up as:", widget.dimension.newUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,11 @@ export interface FE_BigBlueButtonJoin {
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FE_BigBlueButtonCreateAndJoinMeeting {
|
||||||
|
// The meeting URL the client should load to join the meeting
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FE_StickerConfig {
|
export interface FE_StickerConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
stickerBot: string;
|
stickerBot: string;
|
||||||
|
@ -96,6 +101,7 @@ export interface FE_JitsiWidget extends FE_Widget {
|
||||||
export interface FE_BigBlueButtonWidget extends FE_Widget {
|
export interface FE_BigBlueButtonWidget extends FE_Widget {
|
||||||
options: {
|
options: {
|
||||||
conferenceUrl: string;
|
conferenceUrl: string;
|
||||||
|
createMeeting: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from "@angular/core";
|
import { Injectable } from "@angular/core";
|
||||||
import { AuthedApi } from "../authed-api";
|
import { AuthedApi } from "../authed-api";
|
||||||
import { FE_BigBlueButtonJoin } from "../../models/integration"
|
import { FE_BigBlueButtonJoin, FE_BigBlueButtonCreateAndJoinMeeting } from "../../models/integration"
|
||||||
import { HttpClient } from "@angular/common/http";
|
import { HttpClient } from "@angular/common/http";
|
||||||
import { ApiError } from "../../../../../src/api/ApiError";
|
import { ApiError } from "../../../../../src/api/ApiError";
|
||||||
|
|
||||||
|
@ -10,7 +10,15 @@ export class BigBlueButtonApiService extends AuthedApi {
|
||||||
super(http);
|
super(http);
|
||||||
}
|
}
|
||||||
|
|
||||||
public joinMeeting(url: string, name: string): Promise<FE_BigBlueButtonJoin|ApiError> {
|
public joinMeetingWithGreenlightUrl(url: string, name: string): Promise<FE_BigBlueButtonJoin|ApiError> {
|
||||||
return this.authedGet<FE_BigBlueButtonJoin|ApiError>("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise();
|
return this.authedGet<FE_BigBlueButtonJoin|ApiError>("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getJoinUrl(displayName: string, userId: string, avatarUrl: string, meetingId: string, meetingPassword: string): Promise<FE_BigBlueButtonCreateAndJoinMeeting|ApiError> {
|
||||||
|
return this.authedPost<FE_BigBlueButtonCreateAndJoinMeeting|ApiError>(
|
||||||
|
"/api/v1/dimension/bigbluebutton/getJoinUrl",
|
||||||
|
{displayName: displayName, userId: userId, avatarUrl: avatarUrl, meetingId: meetingId, meetingPassword: meetingPassword},
|
||||||
|
).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { WidgetApiService } from "../../shared/services/integrations/widget-api.service";
|
|
||||||
import { Subscription } from "rxjs/Subscription";
|
import { Subscription } from "rxjs/Subscription";
|
||||||
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
|
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
|
||||||
import { CapableWidget } from "../capable-widget";
|
import { CapableWidget } from "../capable-widget";
|
||||||
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
|
||||||
import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service";
|
import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service";
|
||||||
import { FE_BigBlueButtonJoin } from "../../shared/models/integration";
|
import { FE_BigBlueButtonCreateAndJoinMeeting, FE_BigBlueButtonJoin } from "../../shared/models/integration";
|
||||||
import { TranslateService } from "@ngx-translate/core";
|
import { TranslateService } from "@ngx-translate/core";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -24,6 +23,28 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
private conferenceUrl: string;
|
private conferenceUrl: string;
|
||||||
private displayName: string;
|
private displayName: string;
|
||||||
private userId: string;
|
private userId: string;
|
||||||
|
private avatarUrl: string;
|
||||||
|
private meetingId: string;
|
||||||
|
private meetingPassword: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* The name to join the BigBlueButton meeting with. Made up of metadata the client passes to us.
|
||||||
|
*/
|
||||||
|
private joinName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether we expect the meeting to be created on command.
|
||||||
|
*
|
||||||
|
* True if we'd like the meeting to be created, false if we have a greenlight URL leading to an existing meeting
|
||||||
|
* and would like Dimension to translate that to a BigBlueButton meeting URL.
|
||||||
|
*/
|
||||||
|
private createMeeting: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the room, required if createMeeting is true.
|
||||||
|
*/
|
||||||
|
private roomId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The poll period in ms while waiting for a meeting to start
|
* The poll period in ms while waiting for a meeting to start
|
||||||
|
@ -52,7 +73,6 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
|
|
||||||
constructor(activatedRoute: ActivatedRoute,
|
constructor(activatedRoute: ActivatedRoute,
|
||||||
private bigBlueButtonApi: BigBlueButtonApiService,
|
private bigBlueButtonApi: BigBlueButtonApiService,
|
||||||
private widgetApi: WidgetApiService,
|
|
||||||
private sanitizer: DomSanitizer,
|
private sanitizer: DomSanitizer,
|
||||||
public translate: TranslateService) {
|
public translate: TranslateService) {
|
||||||
super();
|
super();
|
||||||
|
@ -60,12 +80,22 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
|
|
||||||
let params: any = activatedRoute.snapshot.queryParams;
|
let params: any = activatedRoute.snapshot.queryParams;
|
||||||
|
|
||||||
console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl);
|
this.roomId = params.roomId;
|
||||||
|
this.createMeeting = params.createMeeting;
|
||||||
this.conferenceUrl = params.conferenceUrl;
|
this.conferenceUrl = params.conferenceUrl;
|
||||||
this.displayName = params.displayName;
|
this.displayName = params.displayName;
|
||||||
|
this.avatarUrl = params.avatarUrl;
|
||||||
|
this.meetingId = params.meetingId;
|
||||||
|
this.meetingPassword = params.meetingPassword;
|
||||||
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
|
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
|
||||||
|
|
||||||
|
// Create a nick to display in the meeting
|
||||||
|
this.joinName = `${this.displayName} (${this.userId})`;
|
||||||
|
|
||||||
|
console.log("BigBlueButton: should create meeting: " + this.createMeeting);
|
||||||
|
console.log("BigBlueButton: will join as: " + this.joinName);
|
||||||
|
console.log("BigBlueButton: got room ID: " + this.roomId);
|
||||||
|
|
||||||
// Set the widget ID if we have it
|
// Set the widget ID if we have it
|
||||||
ScalarWidgetApi.widgetId = params.widgetId;
|
ScalarWidgetApi.widgetId = params.widgetId;
|
||||||
}
|
}
|
||||||
|
@ -101,12 +131,53 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
this.statusMessage = "Joining conference...";
|
this.statusMessage = "Joining conference...";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a nick to display in the meeting
|
|
||||||
const joinName = `${this.displayName} (${this.userId})`;
|
|
||||||
|
|
||||||
// Make a request to Dimension requesting the join URL
|
// Make a request to Dimension requesting the join URL
|
||||||
|
if (this.createMeeting) {
|
||||||
|
// Ask Dimension to return a URL for joining a meeting that it created
|
||||||
|
this.joinThroughDimension();
|
||||||
|
} else {
|
||||||
|
// Provide Dimension with a Greenlight URL, which it will transform into
|
||||||
|
// a BBB meeting URL
|
||||||
|
this.joinThroughGreenlightUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask Dimension to create a meeting (or use an existing one) for this room and return the embeddable meeting URL
|
||||||
|
private async joinThroughDimension() {
|
||||||
|
console.log("BigBlueButton: Joining meeting created by Dimension with meeting ID: " + this.meetingId);
|
||||||
|
|
||||||
|
this.bigBlueButtonApi.getJoinUrl(this.displayName, this.userId, this.avatarUrl, this.meetingId, this.meetingPassword).then((response) => {
|
||||||
|
console.log("The response");
|
||||||
|
console.log(response);
|
||||||
|
if ("errorCode" in response) {
|
||||||
|
// This is an instance of ApiError
|
||||||
|
if (response.errorCode === "UNKNOWN_MEETING_ID") {
|
||||||
|
// This meeting ID is invalid.
|
||||||
|
// Inform the user that they should try and start a new meeting
|
||||||
|
this.statusMessage = "This meeting has ended or otherwise does not exist.<br>Please start a new meeting.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.errorCode === "MEETING_HAS_ENDED") {
|
||||||
|
// It's likely that everyone has left the meeting, and it's been garbage collected.
|
||||||
|
// Inform the user that they should try and start a new meeting
|
||||||
|
this.statusMessage = "This meeting has ended.<br>Please start a new meeting.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Otherwise this is a generic error
|
||||||
|
this.statusMessage = "An error occurred while loading the meeting";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve and embed the meeting URL
|
||||||
|
const joinUrl = (response as FE_BigBlueButtonCreateAndJoinMeeting).url;
|
||||||
|
this.embedMeetingWithUrl(joinUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand Dimension a Greenlight URL and receive a translated, embeddable meeting URL in response
|
||||||
|
private joinThroughGreenlightUrl() {
|
||||||
console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl);
|
console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl);
|
||||||
this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).then((response) => {
|
this.bigBlueButtonApi.joinMeetingWithGreenlightUrl(this.conferenceUrl, this.joinName).then((response) => {
|
||||||
if ("errorCode" in response) {
|
if ("errorCode" in response) {
|
||||||
// This is an instance of ApiError
|
// This is an instance of ApiError
|
||||||
if (response.errorCode === "WAITING_FOR_MEETING_START") {
|
if (response.errorCode === "WAITING_FOR_MEETING_START") {
|
||||||
|
@ -122,24 +193,22 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
this.statusMessage = "An error occurred while loading the meeting";
|
this.statusMessage = "An error occurred while loading the meeting";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Retrieve and embed the meeting URL
|
||||||
const joinUrl = (response as FE_BigBlueButtonJoin).url;
|
const joinUrl = (response as FE_BigBlueButtonJoin).url;
|
||||||
|
this.embedMeetingWithUrl(joinUrl);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the given URL is embeddable
|
private embedMeetingWithUrl(url: string) {
|
||||||
this.widgetApi.isEmbeddable(joinUrl).then(result => {
|
// Hide widget-related UI
|
||||||
this.canEmbed = result.canEmbed;
|
|
||||||
this.statusMessage = null;
|
this.statusMessage = null;
|
||||||
|
|
||||||
// Embed the return meeting URL, joining the meeting
|
// Embed the return meeting URL, joining the meeting
|
||||||
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl);
|
this.canEmbed = true;
|
||||||
|
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(url);
|
||||||
|
|
||||||
// Inform the client that we would like the meeting to remain visible for its duration
|
// Inform the client that we would like the meeting to remain visible for its duration
|
||||||
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
|
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
|
||||||
}).catch(err => {
|
|
||||||
console.error(err);
|
|
||||||
this.canEmbed = false;
|
|
||||||
this.statusMessage = "Unable to embed meeting";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -148,6 +217,8 @@ export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implement
|
||||||
|
|
||||||
protected onCapabilitiesSent(): void {
|
protected onCapabilitiesSent(): void {
|
||||||
super.onCapabilitiesSent();
|
super.onCapabilitiesSent();
|
||||||
|
|
||||||
|
// Don't set alwaysOnScreen until we start a meeting
|
||||||
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
|
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -293,7 +293,7 @@
|
||||||
"There are currently no integrations which support encrypted rooms. Sorry about that!": "Derzeit gibt es keine Integrationen, die verschlüsselte Räume unterstützen. Das tut mir leid!",
|
"There are currently no integrations which support encrypted rooms. Sorry about that!": "Derzeit gibt es keine Integrationen, die verschlüsselte Räume unterstützen. Das tut mir leid!",
|
||||||
"No integrations available": "Keine Integrationen verfügbar",
|
"No integrations available": "Keine Integrationen verfügbar",
|
||||||
"This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "Dieser Raum hat keine kompatiblen Integrationen. Bitte wenden Sie sich an den Server-Administrator, wenn diese Meldung angezeigt wird.",
|
"This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "Dieser Raum hat keine kompatiblen Integrationen. Bitte wenden Sie sich an den Server-Administrator, wenn diese Meldung angezeigt wird.",
|
||||||
"BigBlueButton Conference": "",
|
"BigBlueButton Conference": "BigBlueButton Konferenz",
|
||||||
"Join Conference": "An der Konferenz teilnehmen",
|
"Join Conference": "An der Konferenz teilnehmen",
|
||||||
"Sorry, this content cannot be embedded": "Dieser Inhalt kann leider nicht eingebettet werden",
|
"Sorry, this content cannot be embedded": "Dieser Inhalt kann leider nicht eingebettet werden",
|
||||||
"Start camera:": "",
|
"Start camera:": "",
|
||||||
|
|
|
@ -293,7 +293,7 @@
|
||||||
"There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!",
|
"There are currently no integrations which support encrypted rooms. Sorry about that!": "There are currently no integrations which support encrypted rooms. Sorry about that!",
|
||||||
"No integrations available": "No integrations available",
|
"No integrations available": "No integrations available",
|
||||||
"This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.",
|
"This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.": "This room does not have any compatible integrations. Please contact the server owner if you're seeing this message.",
|
||||||
"BigBlueButton Conference": "",
|
"BigBlueButton Conference": "BigBlueButton Conference",
|
||||||
"Join Conference": "Join Conference",
|
"Join Conference": "Join Conference",
|
||||||
"Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded",
|
"Sorry, this content cannot be embedded": "Sorry, this content cannot be embedded",
|
||||||
"Start camera:": "",
|
"Start camera:": "",
|
||||||
|
|
Loading…
Reference in a new issue