Merge branch 'master' into jitsi-audio-only

This commit is contained in:
Travis Ralston 2020-12-28 20:34:39 -07:00 committed by GitHub
commit 020166e76c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 1280 additions and 497 deletions

View file

@ -1,6 +1,6 @@
# Dimension Development
Dimension is split into two layers: the frontend (web) and backend. The frontend is responsible for interacting with the client (Riot) directly and hands off any complex work to the backend for processing.
Dimension is split into two layers: the frontend (web) and backend. The frontend is responsible for interacting with the client (Element) directly and hands off any complex work to the backend for processing.
**For help and support related to Dimension development, please visit:**
[![#dimension:t2bot.io](https://img.shields.io/badge/matrix-%23dimension:t2bot.io-brightgreen.svg)](https://matrix.to/#/#dimension:t2bot.io)
@ -33,7 +33,7 @@ Integrations are defined into one of four categories:
* Bridges - Application services that bridge the room in some way to an external network (IRC, Webhooks, etc)
* Widgets - Added functionality through iframes for rooms/users
The backend further breaks these categories out to redirect traffic to the correct place. For instance, the admin backend
The backend further breaks these categories out to redirect traffic to the correct place. For instance, the admin backend
breaks out go-neb specifically as it's configuration is fairly involved.
The backend has 3 major layers:
@ -41,13 +41,13 @@ The backend has 3 major layers:
* The data stores (where requests normally get routed to)
* The proxy (where we flip between using upstream configurations and self-hosted)
Many of the API routes are generic, however many of the integrations require additional structure that the routes cannot
Many of the API routes are generic, however many of the integrations require additional structure that the routes cannot
provide. For example, the IRC bridge is complicated in that it needs a dedicated API in order to be configured, however
the bots can work well within their constraints.
## Frontend Architecture
The frontend app is split into two major parts: The Riot frontend and the admin section. The components are nested under
The frontend app is split into two major parts: The Element frontend and the admin section. The components are nested under
their respective categories and route. For example, the edit page for the Jitsi widget is under the Widgets directory.
The frontend is otherwise a fairly basic Angular 5 application: there's components, services, etc. The services should be

View file

@ -1,47 +1,44 @@
FROM node:10.16.0-alpine
FROM node:12.16.1-alpine AS builder
LABEL maintainer="Andreas Peters <support@aventer.biz>"
#Upstream URL: https://git.aventer.biz/AVENTER/docker-matrix-dimension
RUN apk add dos2unix --no-cache --repository http://dl-3.alpinelinux.org/alpine/edge/community/ --allow-untrusted
WORKDIR /home/node/matrix-dimension
RUN apk update && \
apk add --no-cache bash gcc python make g++ sqlite && \
mkdir /home/node/.npm-global && \
mkdir -p /home/node/app
RUN mkdir -p /home/node/matrix-dimension
COPY ./docker-entrypoint.sh /
COPY . /home/node/matrix-dimension
RUN chown -R node:node /home/node/app && \
chown -R node:node /home/node/.npm-global && \
chown -R node:node /home/node/matrix-dimension
RUN chown -R node /home/node/matrix-dimension
USER node
ENV PATH=/home/node/.npm-global/bin:$PATH
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
RUN npm clean-install && \
node /home/node/matrix-dimension/scripts/convert-newlines.js /home/node/matrix-dimension/docker-entrypoint.sh && \
NODE_ENV=production npm run-script build
RUN cd /home/node/matrix-dimension && \
npm install -D wd rimraf webpack webpack-command sqlite3 && \
NODE_ENV=production npm run-script build:web && npm run-script build:app
FROM node:12.16.1-alpine
USER root
WORKDIR /home/node/matrix-dimension
RUN apk del gcc make g++ && \
rm /home/node/matrix-dimension/Dockerfile && \
rm /home/node/matrix-dimension/docker-entrypoint.sh && \
dos2unix /docker-entrypoint.sh
COPY --from=builder /home/node/matrix-dimension/docker-entrypoint.sh /
COPY --from=builder /home/node/matrix-dimension/build /home/node/matrix-dimension/build
COPY --from=builder /home/node/matrix-dimension/package* /home/node/matrix-dimension/
COPY --from=builder /home/node/matrix-dimension/config /home/node/matrix-dimension/config
RUN chown -R node /home/node/matrix-dimension
USER node
RUN npm clean-install --production
VOLUME ["/data"]
# Ensure the database doesn't get lost to the container
ENV DIMENSION_DB_PATH=/data/dimension.db
EXPOSE 8184
#CMD ["/bin/sh"]
# CMD ["/bin/sh"]
ENTRYPOINT ["/docker-entrypoint.sh"]

View file

@ -3,7 +3,7 @@
[![TravisCI badge](https://travis-ci.org/turt2live/matrix-dimension.svg?branch=master)](https://travis-ci.org/turt2live/matrix-dimension)
An open source integration manager for matrix clients, like Riot. For help and support, please visit
An open source integration manager for matrix clients, like Element. For help and support, please visit
us in [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io) on Matrix.
# Installing Dimension / Running your own
@ -22,13 +22,13 @@ port. Dimension will use the first record it sees and will only communicate over
3. **Verify the homeserver information in your configuration.** The name, access token, and client/
server API URL all need to be set to point towards your homeserver. It may also be necessary to set the
federation URL if you're running a private server.
4. **Run the troubleshooter.** If you're on Riot 1.1.0 or higher, type `/addwidget https://dimension.t2bot.io/widgets/manager-test`
4. **Run the troubleshooter.** If you're on Element, type `/addwidget https://dimension.t2bot.io/widgets/manager-test`
in a private room then click the button.
# Do I need an integrations manager?
Integration managers aim to ease a user's interaction with the various services a homeserver may
provide. Often times the integrations manager provided by Riot.im, named Modular, is more than suitable.
provide. Often times the integrations manager provided by Element, is more than suitable.
However, there are a few cases where running your own makes more sense:
* Wanting to self-host all aspects of your services (client, homeserver, and integrations)

View file

@ -30,7 +30,7 @@ homeserver:
accessToken: "something"
# These users can modify the integrations this Dimension supports.
# To access the admin interface, open Dimension in Riot and click the settings icon.
# To access the admin interface, open Dimension in Element and click the settings icon.
admins:
- "@someone:domain.com"
@ -99,4 +99,4 @@ logging:
fileLevel: verbose
rotate:
size: 52428800 # bytes, default is 50mb
count: 5
count: 5

View file

@ -1,7 +1,4 @@
#!/bin/bash
set -e
cd /home/node/matrix-dimension/
#!/bin/sh
if [ -f "/data/config.yaml" ]; then
cp /data/config.yaml /home/node/matrix-dimension/config/production.yaml

View file

@ -1,22 +1,22 @@
## Installing Dimension
**Note**: Dimension is currently only capable of running with Riot Web or Desktop. The iOS and Android
apps are not directly supported without compiling your own versions. In future, this should be handled
by [an integration manager specification](https://github.com/turt2live/matrix-dimension/issues/262).
**Note**: Dimension is only supported in Element Web and Desktop at the moment. With some effort,
it can be used in other clients or Element iOS/Android, though is not guaranteed to work. In future,
this should be handled by [an integration manager specification](https://github.com/turt2live/matrix-dimension/issues/262).
There are several options for installing Dimension. The easiest is dependent on how you have Riot
There are several options for installing Dimension. The easiest is dependent on how you have Element
and your homeserver set up. If you're using [matrix-docker-ansible-deploy](https://github.com/spantaleev/matrix-docker-ansible-deploy),
there are already options for configuring Dimension.
### Step 0: Requirements
You will need a functioning homeserver (such as [Synapse](https://github.com/matrix-org/synapse)) and
a client to access Dimension with. Currently, that means using [Riot Web or Desktop](https://riot.im).
a client to access Dimension with. Currently, that means using [Element Web or Desktop](https://element.io).
Additionally, you will need to be able to host Dimension on a dedicated domain. If your homeserver
is set up for example.org, we recommend using dimension.example.org for Dimension.
Finally, this guide assumes you are running nginx as a webserver for Riot, your homeserver, and
Finally, this guide assumes you are running nginx as a webserver for Element, your homeserver, and
Dimension. A basic configuration before setting up Dimension would be:
```conf
@ -34,7 +34,7 @@ server {
}
server {
# Simple configuration for serving Riot
# Simple configuration for serving Element
server_name chat.example.org;
listen 443 ssl;
listen [::]:443 ssl;
@ -62,7 +62,7 @@ it yourself.
If you're using Docker, create a directory at `/etc/dimension` (or wherever you'd like - just remember
where it is!).
To build Dimension yourself, you'll need Node 10, npm 6, and 2-4gb of RAM. The following steps are enough
To build Dimension yourself, you'll need Node 10+, npm 6+, and 2-4gb of RAM. The following steps are enough
to get you started:
```bash
# Download dimension
@ -134,11 +134,11 @@ Reload or restart nginx after creating the configuration.
### Step 5: Final steps
If everything went according to plan, you should be able to visit `https://dimension.example.org`
and see instructions for configuring Riot. If you don't, your configuration isn't working as
and see instructions for configuring Element. If you don't, your configuration isn't working as
intended - double check that all the configuration is set up and visit [#dimension:t2bot.io](https://matrix.to/#/#dimension:t2bot.io)
for further help.
After configuring Riot, click the integrations button (4 squares in the top right of any room) and
After configuring Element, click the integrations button (4 squares in the top right of any room) and
then click the gear icon. If you don't see a gear icon, you're not an admin in the config. This is
where you'll configure different integrations as Dimension doesn't ship with anything enabled by
default - click around and start enabling things.

View file

@ -1,6 +1,6 @@
# Riot Widgets
# Element Widgets
Riot uses some special interaction with the integration manager to make for a clean user experience.
Element uses some special interaction with the integration manager to make for a clean user experience.
### Edit Widget button
@ -19,4 +19,4 @@ Ends up calling `$scalar_ui_url?integ_id=...&screen=...` alongside the standard
* Creators of widgets do not get prompted for permission to load
* Only people with permission to add widgets can remove widgets (otherwise it just revokes permission)
* Only creators of widgets can edit them
* Only creators of widgets can edit them

View file

@ -1,12 +1,12 @@
# Riot's Widget API
# Element's Widget API
Widgets and Riot communicate using cross-origin messages in a defined format (described in this document). Widgets have access to the entire Scalar Client API, but generally do not need any of the endpoints there. Riot provides additional APIs available to particular widgets for which the integrations manager can not access. The full source for the widget messaging layer in Riot can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/WidgetMessaging.js). The API is restricted to ensure rogue widgets cannot take over the Riot instance.
Widgets and Element communicate using cross-origin messages in a defined format (described in this document). Widgets have access to the entire Scalar Client API, but generally do not need any of the endpoints there. Element provides additional APIs available to particular widgets for which the integrations manager can not access. The full source for the widget messaging layer in Element can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/WidgetMessaging.js). The API is restricted to ensure rogue widgets cannot take over the Element instance.
**Note**: This is largely out of date and better documented in the Matrix spec nowadays. See https://github.com/matrix-org/matrix-doc/issues/1236 for more information.
## Setting up communications
Riot will automatically open a channel for receiving messages. The widget needs to do the same so it can speak to Riot. Here's some sample JavaScript that will do this for us:
Element will automatically open a channel for receiving messages. The widget needs to do the same so it can speak to Element. Here's some sample JavaScript that will do this for us:
```
window.addEventListener("message", function(event) {
@ -18,7 +18,7 @@ window.addEventListener("message", function(event) {
function sendMessage(action, widgetId, otherFields) {
if (!otherFields) otherFields = {};
var request = otherFields;
request["widgetId"] = widgetId;
request["action"] = action;
@ -64,7 +64,7 @@ An error response will always have the following structure under `response`:
### Versions / Changelog
All versions use a semantic versioning scheme. The actions recorded in this document include which version they were implemented in. The changelog here is for convience.
All versions use a semantic versioning scheme. The actions recorded in this document include which version they were implemented in. The changelog here is for convience.
**v0.0.1**
* Initial release

View file

@ -1,13 +1,13 @@
# Scalar Authentication / Registration
When the "Manage Integrations" button is first clicked by a user, Riot will try and register with the Integrations Manager
When the "Manage Integrations" button is first clicked by a user, Element will try and register with the Integrations Manager
to get a `scalar_token` that it then uses to authenticate all future requests with the manager.
## `$restUrl/register`
This ends up mapping to `/api/v1/scalar/register` when Dimension is correctly set up for a Riot instance.
This ends up mapping to `/api/v1/scalar/register` when Dimension is correctly set up for a Element instance.
Riot will POST to this endpoint an OpenID object that looks similar to the following:
Element will POST to this endpoint an OpenID object that looks similar to the following:
```
{
"access_token": "ABCDEFGH",
@ -17,7 +17,7 @@ Riot will POST to this endpoint an OpenID object that looks similar to the follo
}
```
`expires_in` is given in seconds.
`expires_in` is given in seconds.
With this information, we can hit the federation API on the `matrix_server_name` to get ourselves the Matrix User ID (MXID)
of the user. This is a GET request to `http://matrix.org/_matrix/federation/v1/openid/userinfo?access_token=ABCDEFGH`.
@ -39,4 +39,4 @@ following JSON is more than enough:
}
```
Riot will now use this token in future requests by hitting the `"integrations_ui_url"` with `?access_token=some_generated_string`.
Element will now use this token in future requests by hitting the `"integrations_ui_url"` with `?access_token=some_generated_string`.

View file

@ -1,10 +1,10 @@
# Scalar API (Riot)
# Scalar API (Element)
Scalar and Riot communicate using cross-origin messages in a defined format (described in this document). The full source for the messaging layer in Riot can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/ScalarMessaging.js). With this API, the integrations manager is able to invite users, get some basic state information, and interact with the room in a limited capacity. The API is intentionally restricted to ensure that misbehaving domains don't have full control over Riot.
Scalar and Element communicate using cross-origin messages in a defined format (described in this document). The full source for the messaging layer in Element can be seen [here](https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/ScalarMessaging.js). With this API, the integrations manager is able to invite users, get some basic state information, and interact with the room in a limited capacity. The API is intentionally restricted to ensure that misbehaving domains don't have full control over Element.
## Setting up communications
Riot will automatically open a channel for receiving messages. The integrations manager needs to do the same so it can speak to Riot. Here's some sample JavaScript that will do this for us:
Element will automatically open a channel for receiving messages. The integrations manager needs to do the same so it can speak to Element. Here's some sample JavaScript that will do this for us:
```
window.addEventListener("message", function(event) {
@ -16,7 +16,7 @@ window.addEventListener("message", function(event) {
function sendMessage(action, roomId, userId, otherFields) {
if (!otherFields) otherFields = {};
var request = otherFields;
request["user_id"] = userId;
request["room_id"] = roomId;
@ -393,7 +393,7 @@ sendMessage("set_widget", "!curbf:matrix.org", null, {
```
*Note*: Widgets are documented by the matrix.org team [on this Google Doc](https://docs.google.com/document/d/1TiWNDcEOULeRYQpkJHQDjgIW32ohIJSi5MKv9oRdzCo/edit). That document is the source of truth for the event structure and usage.
*Note*: `scalar_token` will be appended to the query string if the widget's url matches the API URL of the integration manager (in Riot)
*Note*: `scalar_token` will be appended to the query string if the widget's url matches the API URL of the integration manager (in Element)
### Getting the room's encryption status
@ -432,4 +432,4 @@ sendMessage("close_scalar");
"action": "close_scalar",
"response": null
}
```
```

View file

@ -709,25 +709,9 @@ None of these are officially documented, and are subject to change.
"authenticated": true,
"session": {
"Repos": [
{
"name": "riot-welcome-page",
"description": "A welcome page specific for tang.ents.ca (built for Riot)",
"private": false,
"html_url": "https:\/\/github.com\/ENTS-Source\/riot-welcome-page",
"created_at": "2017-06-10T16:54:37Z",
"updated_at": "2017-06-10T19:10:21Z",
"pushed_at": "2017-06-10T18:15:07Z",
"fork": false,
"full_name": "ENTS-Source\/riot-welcome-page",
"permissions": {
"admin": true,
"pull": true,
"push": true
}
},
{
"name": "matrix-dimension",
"description": "An alternative integrations manager for Riot",
"description": "An alternative integrations manager for Element",
"private": false,
"html_url": "https:\/\/github.com\/turt2live\/matrix-dimension",
"created_at": "2017-05-25T21:41:55Z",
@ -818,4 +802,4 @@ None of these are officially documented, and are subject to change.
},
"cached_response": false
}
```
```

752
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "matrix-dimension",
"version": "1.0.0",
"description": "An alternative integrations manager for Riot",
"description": "An alternative integrations manager for Element",
"main": "build/app/index.js",
"license": "GPL-3.0",
"scripts": {
@ -32,7 +32,7 @@
"git-rev-sync": "^1.12.0",
"isipaddress": "0.0.2",
"js-yaml": "^3.13.1",
"lodash": "^4.17.13",
"lodash": "^4.17.19",
"matrix-bot-sdk": "^0.3.8",
"matrix-js-snippets": "^0.2.8",
"memory-cache": "^0.2.0",
@ -44,9 +44,9 @@
"request-promise": "^4.2.4",
"require-dir-all": "^0.4.15",
"semver": "^6.0.0",
"sequelize": "^5.15.1",
"sequelize": "^5.18.4",
"sequelize-typescript": "^1.0.0",
"sharp": "^0.21.1",
"sharp": "^0.25.3",
"split-host": "^0.1.1",
"spotify-uri": "^1.0.0",
"sqlite3": "^4.0.9",
@ -95,12 +95,12 @@
"html-loader": "^0.5.5",
"html-webpack-plugin": "^3.2.0",
"iso-639-1": "^2.0.5",
"jquery": "^3.4.1",
"jquery": "^3.5.0",
"json-loader": "^0.5.7",
"mini-css-extract-plugin": "^0.7.0",
"ng2-breadcrumbs": "^0.1.281",
"ngx-modialog": "^5.0.1",
"node-sass": "^4.12.0",
"node-sass": "^4.14.1",
"postcss-cssnext": "^3.1.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",

View file

@ -0,0 +1,23 @@
const fs = require('fs');
const util = require('util');
(async function () {
if (process.argv.length !== 3) {
console.error('Wrong number of arguments');
process.exit(-1);
}
const filePath = process.argv.pop();
const fileExists = await util.promisify(fs.exists)(filePath);
if (fileExists) {
const file = await fs.promises.readFile(filePath, { encoding: 'utf-8' });
await fs.promises.writeFile(
filePath,
file
.replace(/\r\n/g, '\n')
.replace(/\r/, '\n'),
);
}
})();

View file

@ -40,7 +40,7 @@ export default class Webserver {
// We register the default route last to make sure we don't override anything by accident.
// We'll pass off all other requests to the web app
this.app.get(/(widgets\/|riot\/|\/)*/, (_req, res) => {
this.app.get(/(widgets\/|riot\/|element\/|\/)*/, (_req, res) => {
res.sendFile(path.join(__dirname, "..", "..", "web", "index.html"));
});
@ -93,4 +93,4 @@ export default class Webserver {
this.app.listen(config.web.port, config.web.address);
LogService.info("Webserver", "API and UI listening on " + config.web.address + ":" + config.web.port);
}
}
}

View file

@ -1,4 +1,4 @@
import { Context, GET, Path, PathParam, POST, Security, ServiceContext } from "typescript-rest";
import { Context, GET, Path, PathParam, POST, DELETE, Security, ServiceContext } from "typescript-rest";
import StickerPack from "../../db/models/StickerPack";
import { ApiError } from "../ApiError";
import { DimensionStickerService, MemoryStickerPack } from "../dimension/DimensionStickerService";
@ -49,6 +49,19 @@ export class AdminStickerService {
return {}; // 200 OK
}
@DELETE
@Path("packs/:id")
@Security([ROLE_ADMIN])
public async removePack(@PathParam("id") packId: number): Promise<any> {
const pack = await StickerPack.findByPk(packId);
if (!pack) throw new ApiError(404, "Sticker pack not found");
await pack.destroy();
Cache.for(CACHE_STICKERS).clear();
return {}; // 200 OK
}
@POST
@Path("packs/import/telegram")
@Security([ROLE_USER, ROLE_ADMIN])
@ -85,16 +98,18 @@ export class AdminStickerService {
for (const tgSticker of tgPack.stickers) {
LogService.info("AdminStickerService", "Importing sticker from " + tgSticker.url);
const buffer = await mx.downloadFromUrl(tgSticker.url);
const png = await sharp(buffer).resize({
width: 512,
height: 512,
const image = await sharp(buffer);
const metadata = await image.metadata();
const png = await image.resize({
width: metadata.width,
height: metadata.height,
fit: 'contain',
background: 'rgba(0,0,0,0)',
}).png().toBuffer();
const mxc = await mx.upload(png, "image/png");
const serverName = mxc.substring("mxc://".length).split("/")[0];
const contentId = mxc.substring("mxc://".length).split("/")[1];
const thumbMxc = await mx.uploadFromUrl(await mx.getThumbnailUrl(serverName, contentId, 512, 512, "scale", false), "image/png");
const thumbMxc = await mx.uploadFromUrl(await mx.getThumbnailUrl(serverName, contentId, metadata.width, metadata.height, "scale", false), "image/png");
stickers.push(await Sticker.create({
packId: pack.id,
@ -102,8 +117,8 @@ export class AdminStickerService {
description: tgSticker.emoji,
imageMxc: mxc,
thumbnailMxc: thumbMxc,
thumbnailWidth: 512,
thumbnailHeight: 512,
thumbnailWidth: metadata.width,
thumbnailHeight: metadata.height,
mimetype: "image/png",
}));

View file

@ -0,0 +1,207 @@
import { GET, Path, QueryParam } from "typescript-rest";
import * as request from "request";
import { LogService } from "matrix-js-snippets";
import { URL } from "url";
import { BigBlueButtonJoinRequest } from "../../models/Widget";
import { BigBlueButtonJoinResponse } from "../../models/WidgetResponses";
import { AutoWired } from "typescript-ioc/es6";
import { ApiError } from "../ApiError";
/**
* API for the BigBlueButton widget.
*/
@Path("/api/v1/dimension/bigbluebutton")
@AutoWired
export class DimensionBigBlueButtonService {
/**
* A regex used for extracting the authenticity token from the HTML of a
* greenlight server response
*/
private authenticityTokenRegexp = new RegExp(`name="authenticity_token" value="([^"]+)".*`);
// join handles the request from a client to join a BigBlueButton meeting
//
// 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
// client.
//
// This greenlight link is nice, but greenlight unfortunately doesn't have any
// API, and no simple way for us to translate a link from it into a BBB meeting
// URL. It's intended to be loaded by browsers. You enter your preferred name,
// click submit, you potentially wait for the meeting to start, and then you
// finally get the link to join the meeting, and you load that.
//
// As there's no other way to do it, we just reverse-engineer it and pretend
// to be a browser below. We can't do this from the client side as widgets
// run in iframes and browsers can't inspect the content of an iframe if
// it's running on a separate domain.
//
// So the client gets a greenlight URL pasted into it. The flow is then:
//
//
// +---------+ +-----------+ +-------------+ +-----+
// | Client | | Dimension | | Greenlight | | BBB |
// +---------+ +-----------+ +-------------+ +-----+
// | | | |
// | | | |
// | | | |
// | | | |
// | /bigbluebutton/join&greenlightUrl=https://.../abc-def-123&fullName=bob | | |
// |---------------------------------------------------------------------------->| | |
// | | | |
// | | GET https://.../abc-def-123 | |
// | |-------------------------------------------------------------------------------------->| |
// | | | |
// | | Have some HTML | |
// | |<--------------------------------------------------------------------------------------| |
// | | | |
// | | Extract authenticity_token from HTML | |
// | |------------------------------------- | |
// | | | | |
// | |<------------------------------------ | |
// | | | |
// | | Extract cookies from HTTP response | |
// | |----------------------------------- | |
// | | | | |
// | |<---------------------------------- | |
// | | | |
// | | POST https://.../abc-def-123&authenticity_token=...&abc-def-123[join_name]=bob | |
// | |-------------------------------------------------------------------------------------->| |
// |===============================================================================================If the meeting has not started yet================================================|
// | | | |
// | | HTML https://.../abc-def-123 Meeting not started | |
// | |<--------------------------------------------------------------------------------------| |
// | | | |
// | 400 MEETING_NOT_STARTED_YET | | |
// |<----------------------------------------------------------------------------| | |
// | | | |
// | | | |
// | Wait a bit and restart the process | | |
// |------------------------------------- | | |
// | | | | |
// |<------------------------------------ | | |
// | | | |
// |=================================================================================================================================================================================|
// | | | |
// | | 302 Location: https://bbb.example.com/join?... | |
// | |<--------------------------------------------------------------------------------------| |
// | | | |
// | | Extract value of Location header | |
// | |--------------------------------- | |
// | | | | |
// | |<-------------------------------- | |
// | | | |
// | https://bbb.example.com/join?... | | |
// |<----------------------------------------------------------------------------| | |
// | | | |
// | GET https://bbb.example.com/join?... | | |
// |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------->|
// | | | |
// | | Send back meeting page HTML | |
// |<--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
//
@GET
@Path("join")
public async join(
joinRequest: BigBlueButtonJoinRequest,
@QueryParam("greenlightUrl") greenlightURL: string,
@QueryParam("fullName") fullName: string,
): Promise<BigBlueButtonJoinResponse|ApiError> {
// Parse the greenlight url and retrieve the path
const greenlightMeetingID = new URL(greenlightURL).pathname;
LogService.info("BigBlueButton", "URL from client: " + greenlightURL);
LogService.info("BigBlueButton", "MeetingID: " + greenlightMeetingID);
LogService.info("BigBlueButton", "Name given from client: " + fullName);
LogService.info("BigBlueButton", joinRequest);
// Query the URL the user has given us
let response = await this.doRequest("GET", greenlightURL);
if (!response || !response.body) {
throw new Error("Invalid response from Greenlight server while joining meeting");
}
// Attempt to extract the authenticity token
const matches = response.body.match(this.authenticityTokenRegexp);
if (matches.length < 2) {
throw new Error("Unable to find authenticity token for given 'greenlightUrl' parameter");
}
const authenticityToken = matches[1];
// Give the authenticity token and desired name to greenlight, getting the
// join URL in return. Greenlight will send the URL back as a Location:
// header. We want to extract and return the contents of this header, rather
// than following it ourselves
// Add authenticity token and full name to the query parameters
let queryParams = {authenticity_token: authenticityToken};
queryParams[`${greenlightMeetingID}[join_name]`] = fullName;
// Request the updated URL
response = await this.doRequest("POST", greenlightURL, queryParams, "{}", false);
if (!response || !response.body) {
throw new Error("Invalid response from Greenlight server while joining meeting");
}
if (!("location" in response.response.headers)) {
// We didn't get a meeting URL back. This could either happen due to an issue with the parameters
// sent to the server... or the meeting simply hasn't started yet.
// Assume it hasn't started yet. Send a custom error code back to the client informing them to try
// again in a bit
return new ApiError(
400,
{error: "Unable to find meeting URL in greenlight response"},
"WAITING_FOR_MEETING_START",
);
}
// Return the join URL for the client to load
const joinUrl = response.response.headers["location"];
LogService.info("BigBlueButton", "Sending back join URL: " + joinUrl)
return {url: joinUrl};
}
private async doRequest(
method: string,
url: string,
qs?: any,
body?: any,
followRedirect: boolean = true,
): Promise<any> {
// Query a URL, expecting an HTML response in return
return new Promise((resolve, reject) => {
request({
method: method,
url: url,
qs: qs,
body: body,
followRedirect: followRedirect,
jar: true, // remember cookies between requests
json: false, // expect html
}, (err, res, _body) => {
try {
if (err) {
LogService.error("BigBlueButtonWidget", "Error calling " + url);
LogService.error("BigBlueButtonWidget", err);
reject(err);
} else if (!res) {
LogService.error("BigBlueButtonWidget", "There is no response for " + url);
reject(new Error("No response provided - is the service online?"));
} else if (res.statusCode !== 200 && res.statusCode !== 302) {
LogService.error("BigBlueButtonWidget", "Got status code " + res.statusCode + " when calling " + url);
LogService.error("BigBlueButtonWidget", res.body);
reject({body: res.body, status: res.statusCode});
} else {
resolve({body: res.body, response: res});
}
} catch (e) {
LogService.error("BigBlueButtonWidget", e);
reject(e);
}
});
});
}
}

View file

@ -24,7 +24,7 @@ export class MatrixWellknownService {
public async getIntegrations(): Promise<WellknownResponse> {
const parsed = new URL(config.dimension.publicUrl);
parsed.pathname = '/riot';
parsed.pathname = '/element';
const uiUrl = parsed.toString();
parsed.pathname = '/api/v1/scalar';
@ -39,4 +39,4 @@ export class MatrixWellknownService {
},
};
}
}
}

View file

@ -11,7 +11,7 @@ interface UrlPreviewResponse {
page_title_cache_item: {
expires: string; // "2017-12-18T04:20:04.001806738Z"
cached_response_err: string;
cached_title: string; // the actual thing riot uses
cached_title: string; // the actual thing Element uses
};
error: {
message: string;
@ -68,4 +68,4 @@ export class ScalarWidgetService {
};
}
}
}
}

View file

@ -17,6 +17,7 @@ export interface DimensionConfig {
database: {
file: string;
botData: string;
uri: string;
};
admins: string[];
goneb: {
@ -38,4 +39,4 @@ export interface DimensionConfig {
logging: LogConfig;
}
export default <DimensionConfig>config;
export default <DimensionConfig>config;

View file

@ -35,14 +35,20 @@ class _DimensionStore {
private sequelize: Sequelize;
constructor() {
this.sequelize = new Sequelize({
dialect: 'sqlite',
database: "dimension",
storage: process.env['DIMENSION_DB_PATH'] || config.database.file,
username: "",
password: "",
logging: i => LogService.verbose("DimensionStore [SQL]", i)
});
if (process.env.DATABASE_URI || config.database.uri ) {
this.sequelize = new Sequelize(process.env.DATABASE_URI || config.database.uri , {
logging: i => LogService.verbose("DimensionStore [SQL]", i)
});
} else {
this.sequelize = new Sequelize({
dialect: 'sqlite',
database: "dimension",
storage: process.env['DIMENSION_DB_PATH'] || config.database.file,
username: "",
password: "",
logging: i => LogService.verbose("DimensionStore [SQL]", i)
});
}
this.sequelize.addModels([
User,
UserScalarToken,

View file

@ -18,7 +18,9 @@ export default {
licensePath: "/licenses/cc_by-nc_4.0.txt",
}
]))
.then(packId => {
.then(() => queryInterface.rawSelect('dimension_sticker_packs', { where: { name: "Loading Artist" } }, ['id']))
.then(packIds => {
const packId = Array.isArray(packIds) ? packIds[0] : packIds;
return queryInterface.bulkInsert("dimension_stickers", [
{
packId: packId,
@ -446,4 +448,4 @@ export default {
down: (_queryInterface: QueryInterface) => {
throw new Error("there is no going back");
}
}
}

View file

@ -0,0 +1,23 @@
import { QueryInterface } from "sequelize";
export default {
up: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkInsert("dimension_widgets", [
{
type: "bigbluebutton",
name: "BigBlueButton",
avatarUrl: "/img/avatars/bigbluebutton.png",
isEnabled: true,
isPublic: true,
description: "Embed a BigBlueButton conference",
}
]));
},
down: (queryInterface: QueryInterface) => {
return Promise.resolve()
.then(() => queryInterface.bulkDelete("dimension_widgets", {
type: "bigbluebutton",
}));
}
}

7
src/models/Widget.ts Normal file
View file

@ -0,0 +1,7 @@
export interface BigBlueButtonJoinRequest {
// A URL supplied by greenlight, BigBlueButton's nice UI project that is itself
// a BigBlueButton client
greenlightUrl: string;
// The name the user wishes to join the meeting with
fullName: string;
}

View file

@ -0,0 +1,4 @@
export interface BigBlueButtonJoinResponse {
// The meeting URL the client should load to join the meeting
url: string;
}

View file

@ -52,10 +52,13 @@
</span>
<ui-switch [checked]="pack.isEnabled" size="small" [disabled]="isUpdating"
(change)="toggleEnabled(pack)"></ui-switch>
<span *ngIf="!pack.isEnabled && !isUpdating" class="removeButton" title="remove stickerpack" (click)="removePack(pack)">
<i class="fa fa-trash"></i>
</span>
</td>
</tr>
</tbody>
</table>
</div>
</my-ibox>
</div>
</div>

View file

@ -7,6 +7,11 @@ tr td:last-child {
vertical-align: text-bottom;
}
.removeButton {
cursor: pointer;
vertical-align: text-bottom;
}
.telegram-import {
margin-bottom: 15px;
}

View file

@ -68,4 +68,22 @@ export class AdminStickerPacksComponent implements OnInit {
this.toaster.pop("error", "Error importing sticker pack");
});
}
public removePack(pack: FE_StickerPack) {
this.isUpdating = true;
this.adminStickers.removePack(pack.id).then(() => {
for (let i = 0; i < this.packs.length; ++i) {
if (this.packs[i].id === pack.id) {
this.packs.splice(i, 1);
break;
}
}
this.isUpdating = false;
this.toaster.pop("success", "Sticker pack removed");
}).catch(err => {
console.error(err);
this.isUpdating = false;
this.toaster.pop("error", "Error removing sticker pack");
});
}
}

View file

@ -118,6 +118,9 @@ import { CKEditorModule } from "@ckeditor/ckeditor5-angular";
import { AdminNewEditTermsComponent } from "./admin/terms/new-edit/new-edit.component";
import { AdminTermsNewEditPublishDialogComponent } from "./admin/terms/new-edit/publish/publish.component";
import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.component";
import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component";
import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component";
import { BigBlueButtonApiService } from "./shared/services/integrations/bigbluebutton-api.service";
@NgModule({
imports: [
@ -147,7 +150,9 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
FullscreenButtonComponent,
VideoWidgetWrapperComponent,
JitsiWidgetWrapperComponent,
BigBlueButtonWidgetWrapperComponent,
GCalWidgetWrapperComponent,
BigBlueButtonConfigComponent,
RiotHomeComponent,
IboxComponent,
ConfigScreenWidgetComponent,
@ -234,6 +239,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
AdminStickersApiService,
MediaService,
StickerApiService,
BigBlueButtonApiService,
AdminTelegramApiService,
TelegramApiService,
AdminWebhooksApiService,

View file

@ -2,6 +2,8 @@ import { RouterModule, Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { RiotComponent } from "./riot/riot.component";
import { GenericWidgetWrapperComponent } from "./widget-wrappers/generic/generic.component";
import { BigBlueButtonWidgetWrapperComponent } from "./widget-wrappers/bigbluebutton/bigbluebutton.component";
import { BigBlueButtonConfigComponent } from "./configs/widget/bigbluebutton/bigbluebutton.widget.component";
import { VideoWidgetWrapperComponent } from "./widget-wrappers/video/video.component";
import { JitsiWidgetWrapperComponent } from "./widget-wrappers/jitsi/jitsi.component";
import { GCalWidgetWrapperComponent } from "./widget-wrappers/gcal/gcal.component";
@ -51,6 +53,7 @@ import { TermsWidgetWrapperComponent } from "./widget-wrappers/terms/terms.compo
const routes: Routes = [
{path: "", component: HomeComponent},
{path: "riot", pathMatch: "full", redirectTo: "riot-app"},
{path: "element", pathMatch: "full", redirectTo: "riot-app"},
{
path: "riot-app",
component: RiotComponent,
@ -179,6 +182,11 @@ const routes: Routes = [
component: CustomWidgetConfigComponent,
data: {breadcrumb: "Custom Widgets", name: "Custom Widgets"},
},
{
path: "bigbluebutton",
component: BigBlueButtonConfigComponent,
data: {breadcrumb: "BigBlueButton Widgets", name: "BigBlueButton Widgets"},
},
{
path: "etherpad",
component: EtherpadWidgetConfigComponent,
@ -285,6 +293,7 @@ const routes: Routes = [
{path: "generic", component: GenericWidgetWrapperComponent},
{path: "video", component: VideoWidgetWrapperComponent},
{path: "jitsi", component: JitsiWidgetWrapperComponent},
{path: "bigbluebutton", component: BigBlueButtonWidgetWrapperComponent},
{path: "gcal", component: GCalWidgetWrapperComponent},
{path: "stickerpicker", component: StickerPickerWidgetWrapperComponent},
{path: "generic-fullscreen", component: GenericFullscreenWidgetWrapperComponent},

View file

@ -0,0 +1,11 @@
<my-widget-config [widgetComponent]="this">
<ng-template #widgetParamsTemplate let-widget="widget">
<label class="label-block">
BigBlueButton Meeting URL
<input type="text" class="form-control"
placeholder="https://bbb.example.com/abc-def-ghi"
[(ngModel)]="widget.dimension.newData.conferenceUrl" name="widget-url-{{widget.id}}"
[disabled]="isUpdating"/>
</label>
</ng-template>
</my-widget-config>

View file

@ -0,0 +1,53 @@
import { WidgetComponent, DISABLE_AUTOMATIC_WRAPPING } from "../widget.component";
import { WIDGET_BIGBLUEBUTTON, EditableWidget } from "../../../shared/models/widget";
import { Component } from "@angular/core";
import { FE_BigBlueButtonWidget } from "../../../shared/models/integration";
import { SessionStorage } from "../../../shared/SessionStorage";
import * as url from "url";
@Component({
templateUrl: "bigbluebutton.widget.component.html",
styleUrls: ["bigbluebutton.widget.component.scss"],
})
// Configuration of BigBlueButton widgets
export class BigBlueButtonConfigComponent extends WidgetComponent {
private bigBlueButtonWidget: FE_BigBlueButtonWidget = <FE_BigBlueButtonWidget>SessionStorage.editIntegration;
constructor() {
super(WIDGET_BIGBLUEBUTTON, "BigBlueButton Conference", DISABLE_AUTOMATIC_WRAPPING);
}
protected OnWidgetsDiscovered(widgets: EditableWidget[]) {
for (const widget of widgets) {
widget.data.conferenceUrl = this.templateUrl(widget.url, widget.data);
}
}
protected OnNewWidgetPrepared(widget: EditableWidget): void {
widget.dimension.newData["conferenceUrl"] = this.bigBlueButtonWidget.options.conferenceUrl;
}
protected OnWidgetBeforeAdd(widget: EditableWidget) {
this.setWidgetOptions(widget);
}
protected OnWidgetBeforeEdit(widget: EditableWidget) {
this.setWidgetOptions(widget);
}
private setWidgetOptions(widget: EditableWidget) {
widget.dimension.newData.url = widget.dimension.newData.conferenceUrl;
let widgetQueryString = url.format({
query: {
"conferenceUrl": "$conferenceUrl",
"displayName": "$matrix_display_name",
"avatarUrl": "$matrix_avatar_url",
"userId": "$matrix_user_id",
},
});
widgetQueryString = this.decodeParams(widgetQueryString, Object.keys(widget.dimension.newData).map(k => "$" + k));
widget.dimension.newUrl = window.location.origin + "/widgets/bigbluebutton" + widgetQueryString;
}
}

View file

@ -16,8 +16,8 @@
<div class="info-box try-dimension shadowed">
<h3>Try it out or <a href="https://github.com/turt2live/matrix-dimension#running-your-own" target="_blank">run your own</a></h3>
<p>
Visit <a href="https://t2bot.io/riot" target="_blank">t2bot.io/riot</a> and log in with your Matrix account
or point your Riot <code>config.json</code> at our servers:
Visit <a href="https://element.t2host.io" target="_blank">element.t2host.io</a> and log in with your Matrix account
or point your Element <code>config.json</code> at our servers:
</p>
<pre>{{ integrationsConfig }}</pre>
</div>
@ -69,6 +69,10 @@
<img src="/img/avatars/googlecalendar.png">
<span>Google Calendar</span>
</div>
<div class="integration">
<img src="/img/avatars/bigbluebutton.png">
<span>BigBlueButton</span>
</div>
<div class="integration">
<img src="/img/avatars/customwidget.png">
<span>Custom Widget</span>
@ -220,19 +224,19 @@
for news and updates. Don't forget to star the repository on
<a href="https://github.com/turt2live/matrix-dimension" target="_blank">GitHub</a>.
</p>
<p>Here's the configuration options you'll need to update in your Riot <code>config.json</code>:</p>
<p>Here's the configuration options you'll need to update in your Element <code>config.json</code>:</p>
<pre>{{ integrationsConfig }}</pre>
<h4>Configuring integrations</h4>
<p>
If everything is set up correctly, you'll be able to access the admin area of Dimension by clicking
the 3x3 grid in the top right of any room in Riot. The gear icon (<i class="fa fa-cog"></i>) in the
the 3x3 grid in the top right of any room in Element. The gear icon (<i class="fa fa-cog"></i>) in the
top right is where you can configure your bots, bridges, and widgets.
</p>
<h4>"Could not connect to integrations server" error</h4>
<p>
When Riot cannot reach Dimension or Dimension is unable to reach your homeserver an error saying "Could not
When Element cannot reach Dimension or Dimension is unable to reach your homeserver an error saying "Could not
contact integrations
server" shows up in every room. Before visiting us in <a href="https://matrix.to/#/#dimension:t2bot.io" target="_blank">#dimension:t2bot.io</a>
on Matrix, here's a few things to check:
@ -261,4 +265,4 @@
</a>
<a href="https://matrix.to/#/#dimension:t2bot.io">#dimension:t2bot.io</a>
</div>
</div>
</div>

View file

@ -11,7 +11,7 @@ export class HomeComponent {
public showPromoPage = this.hostname === "https://dimension.t2bot.io";
public integrationsConfig = `` +
`"integrations_ui_url": "${this.hostname}/riot",\n` +
`"integrations_ui_url": "${this.hostname}/element",\n` +
`"integrations_rest_url": "${this.hostname}/api/v1/scalar",\n` +
`"integrations_widgets_urls": ["${this.hostname}/widgets"],\n` +
`"integrations_jitsi_widget_url": "${this.hostname}/widgets/jitsi",\n`;

View file

@ -18,7 +18,7 @@
<strong>Integrations are not encrypted!</strong>
This means that some information about yourself and the
room may be leaked to the bot, bridge, or widget. This information includes the room ID, your display
name, your username, your avatar, information about Riot, and other similar details. Add integrations
name, your username, your avatar, information about Element, and other similar details. Add integrations
with caution.
</div>
<div class="alert alert-warning" *ngIf="!hasIntegrations() && isRoomEncrypted">
@ -42,4 +42,4 @@
</div>
</my-ibox>
</div>
</div>
</div>

View file

@ -77,7 +77,7 @@ export class RiotHomeComponent {
console.error("No user returned for token. Is the token registered in Dimension?");
this.isError = true;
this.isLoading = false;
this.errorMessage = "Could not verify your token. Please try logging out of Riot and back in. Be sure to back up your encryption keys!";
this.errorMessage = "Could not verify your token. Please try logging out of Element and back in. Be sure to back up your encryption keys!";
} else {
this.userId = userId;
console.log("Scalar token belongs to " + userId);
@ -189,7 +189,7 @@ export class RiotHomeComponent {
console.error(err);
this.isError = true;
this.isLoading = false;
this.errorMessage = "Unable to set up Dimension. This version of Riot may not supported or there may be a problem with the server.";
this.errorMessage = "Unable to set up Dimension. This version of Element may not supported or there may be a problem with the server.";
});
this.stickerApi.getPacks().then(packs => {
@ -265,7 +265,7 @@ export class RiotHomeComponent {
case "publicRoom":
return this.scalar.getJoinRule(this.roomId).then(payload => {
if (!payload.response) {
return Promise.reject("Could not communicate with Riot");
return Promise.reject("Could not communicate with Element");
}
const isPublic = payload.response.join_rule === "public";
if (isPublic !== requirement.expectedValue) {
@ -278,7 +278,7 @@ export class RiotHomeComponent {
if (response === true) return Promise.resolve();
if (response.error || response.error.message)
return Promise.reject("You cannot modify widgets in this room");
return Promise.reject("Error communicating with Riot");
return Promise.reject("Error communicating with Element");
};
let promiseChain = Promise.resolve();

View file

@ -64,6 +64,11 @@ export interface FE_Sticker {
};
}
export interface FE_BigBlueButtonJoin {
// The meeting URL the client should load to join the meeting
url: string;
}
export interface FE_StickerConfig {
enabled: boolean;
stickerBot: string;
@ -88,8 +93,14 @@ export interface FE_JitsiWidget extends FE_Widget {
};
}
export interface FE_BigBlueButtonWidget extends FE_Widget {
options: {
conferenceUrl: string;
};
}
export interface FE_IntegrationRequirement {
condition: "publicRoom" | "canSendEventTypes" | "userInRoom";
argument: any;
expectedValue: any;
}
}

View file

@ -1,6 +1,7 @@
import { WidgetsResponse } from "./server-client-responses";
export const WIDGET_CUSTOM = ["m.custom", "customwidget", "dimension-customwidget"];
export const WIDGET_BIGBLUEBUTTON = ["bigbluebutton", "dimension-bigbluebutton"];
export const WIDGET_ETHERPAD = ["m.etherpad", "etherpad", "dimension-etherpad"];
export const WIDGET_GOOGLE_DOCS = ["m.googledoc", "googledocs", "dimension-googledocs"];
export const WIDGET_GOOGLE_CALENDAR = ["m.googlecalendar", "googlecalendar", "dimension-googlecalendar"];

View file

@ -1,6 +1,7 @@
import { Injectable } from "@angular/core";
import {
WIDGET_CUSTOM,
WIDGET_BIGBLUEBUTTON,
WIDGET_ETHERPAD,
WIDGET_GOOGLE_CALENDAR,
WIDGET_GOOGLE_DOCS,
@ -35,6 +36,9 @@ export class IntegrationsRegistry {
"custom": {
types: WIDGET_CUSTOM,
},
"bigbluebutton": {
types: WIDGET_BIGBLUEBUTTON,
},
"youtube": {
types: WIDGET_YOUTUBE
},

View file

@ -20,4 +20,8 @@ export class AdminStickersApiService extends AuthedApi {
public importFromTelegram(packUrl: string): Promise<FE_StickerPack> {
return this.authedPost<FE_StickerPack>("/api/v1/dimension/admin/stickers/packs/import/telegram", {packUrl: packUrl}).toPromise();
}
public removePack(packId: number): Promise<any> {
return this.authedDelete("/api/v1/dimension/admin/stickers/packs/" + packId).toPromise();
}
}

View file

@ -0,0 +1,16 @@
import { Injectable } from "@angular/core";
import { AuthedApi } from "../authed-api";
import { FE_BigBlueButtonJoin } from "../../models/integration"
import { HttpClient } from "@angular/common/http";
import { ApiError } from "../../../../../src/api/ApiError";
@Injectable()
export class BigBlueButtonApiService extends AuthedApi {
constructor(http: HttpClient) {
super(http);
}
public joinMeeting(url: string, name: string): Promise<FE_BigBlueButtonJoin|ApiError> {
return this.authedGet<FE_BigBlueButtonJoin|ApiError>("/api/v1/dimension/bigbluebutton/join", {greenlightUrl: url, fullName: name}).toPromise();
}
}

View file

@ -68,7 +68,7 @@ export class ScalarWidgetApi {
public static sendSetAlwaysOnScreen(alwaysVisible: boolean): void {
ScalarWidgetApi.callAction("set_always_on_screen", {
// Send the value here and in data due to a Riot bug.
// Send the value here and in data due to a Element bug.
data: {
value: alwaysVisible,
},

View file

@ -0,0 +1,26 @@
<iframe *ngIf="embedUrl"
id="bigBlueButtonContainer"
[src]="embedUrl"
(load)="onIframeLoad()"
frameborder="0"
allowfullscreen
width="100%"
height="100%"
allow="camera; microphone; encrypted-media; autoplay;"
></iframe>
<div *ngIf="!embedUrl" class="join-conference-wrapper">
<div class="join-conference-boat">
<div *ngIf="statusMessage; else joinMeetingPrompt" class="join-conference-prompt">
<h4 [innerHTML]="statusMessage"></h4>
</div>
<ng-template #joinMeetingPrompt>
<div class="join-conference-prompt">
<h3>BigBlueButton Conference</h3>
<button type="button" (click)="joinConference()" class="btn btn-primary btn-large">
Join Conference
</button>
</div>
</ng-template>
</div>
</div>

View file

@ -0,0 +1,32 @@
// component styles are encapsulated and only applied to their components
@import "../../../style/themes/themes";
@include themifyComponent() {
#bigBlueButtonContainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.join-conference-wrapper {
display: table;
position: absolute;
height: 100%;
width: 100%;
background-color: themed(widgetWelcomeBgColor);
}
.join-conference-boat {
display: table-cell;
vertical-align: middle;
}
.join-conference-prompt {
margin-left: auto;
margin-right: auto;
width: 90%;
text-align: center;
}
}

View file

@ -0,0 +1,152 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { WidgetApiService } from "../../shared/services/integrations/widget-api.service";
import { Subscription } from "rxjs/Subscription";
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
import { CapableWidget } from "../capable-widget";
import { DomSanitizer, SafeUrl } from "@angular/platform-browser";
import { BigBlueButtonApiService } from "../../shared/services/integrations/bigbluebutton-api.service";
import { FE_BigBlueButtonJoin } from "../../shared/models/integration";
@Component({
selector: "my-bigbluebutton-widget-wrapper",
templateUrl: "bigbluebutton.component.html",
styleUrls: ["bigbluebutton.component.scss"],
})
export class BigBlueButtonWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy {
public canEmbed = true;
/**
* User metadata passed to us by the client
*/
private conferenceUrl: string;
private displayName: string;
private userId: string;
/**
* The poll period in ms while waiting for a meeting to start
*/
private pollIntervalMillis = 5000;
/**
* Subscriber for messages from the client via the postMessage API
*/
private bigBlueButtonApiSubscription: Subscription;
/**
* A status message to display to the user in the widget, typically for loading messages
*/
public statusMessage: string;
/**
* Whether we are currently in a meeting
*/
private inMeeting: boolean = false;
/**
* The URL to embed into the iframe
*/
public embedUrl: SafeUrl = null;
constructor(activatedRoute: ActivatedRoute,
private bigBlueButtonApi: BigBlueButtonApiService,
private widgetApi: WidgetApiService,
private sanitizer: DomSanitizer) {
super();
this.supportsAlwaysOnScreen = true;
let params: any = activatedRoute.snapshot.queryParams;
console.log("BigBlueButton: Given greenlight url: " + params.conferenceUrl);
this.conferenceUrl = params.conferenceUrl;
this.displayName = params.displayName;
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
// Set the widget ID if we have it
ScalarWidgetApi.widgetId = params.widgetId;
}
public ngOnInit() {
super.ngOnInit();
}
public onIframeLoad() {
if (this.inMeeting) {
// The meeting has ended and we've come back full circle
this.inMeeting = false;
this.statusMessage = null;
this.embedUrl = null;
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
return;
}
// Have a toggle for whether we're in a meeting. We do this as we don't have a method
// of checking which URL was just loaded in the iframe (due to different origin domains
// and browser security), so we have to guess that it'll always be the second load (the
// first being joining the meeting)
this.inMeeting = true;
// We've successfully joined the meeting
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
}
public joinConference(updateStatusMessage: boolean = true) {
if (updateStatusMessage) {
// Inform the user that we're loading their meeting
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
console.log("BigBlueButton: joining via greenlight url:", this.conferenceUrl);
this.bigBlueButtonApi.joinMeeting(this.conferenceUrl, joinName).then((response) => {
if ("errorCode" in response) {
// This is an instance of ApiError
if (response.errorCode == "WAITING_FOR_MEETING_START") {
// The meeting hasn't started yet
this.statusMessage = "Waiting for conference to start...";
// Poll until it has
setTimeout(this.joinConference.bind(this), this.pollIntervalMillis, false);
return;
}
// Otherwise this is a generic error
this.statusMessage = "An error occurred while loading the meeting";
}
const joinUrl = (response as FE_BigBlueButtonJoin).url;
// Check if the given URL is embeddable
this.widgetApi.isEmbeddable(joinUrl).then(result => {
this.canEmbed = result.canEmbed;
this.statusMessage = null;
// Embed the return meeting URL, joining the meeting
this.embedUrl = this.sanitizer.bypassSecurityTrustResourceUrl(joinUrl);
// Inform the client that we would like the meeting to remain visible for its duration
ScalarWidgetApi.sendSetAlwaysOnScreen(true);
}).catch(err => {
console.error(err);
this.canEmbed = false;
this.statusMessage = "Unable to embed meeting";
});
});
}
public ngOnDestroy() {
if (this.bigBlueButtonApiSubscription) this.bigBlueButtonApiSubscription.unsubscribe();
}
protected onCapabilitiesSent(): void {
super.onCapabilitiesSent();
ScalarWidgetApi.sendSetAlwaysOnScreen(false);
}
}

View file

@ -16,7 +16,7 @@
position: absolute;
height: 100%;
width: 100%;
background-color: themed(jitsiWelcomeBgColor);
background-color: themed(widgetWelcomeBgColor);
}
.join-conference-boat {
@ -30,4 +30,4 @@
width: 90%;
text-align: center;
}
}
}

View file

@ -38,9 +38,8 @@ export class JitsiWidgetWrapperComponent extends CapableWidget implements OnInit
this.conferenceId = params.conferenceId || params.confId;
this.displayName = params.displayName;
this.avatarUrl = params.avatarUrl;
this.userId = params.userId || params.email; // Riot uses `email` when placing a conference call
this.userId = params.userId || params.email; // Element uses `email` when placing a conference call
this.isAudioOnly = params.isAudioOnly === 'true';
this.toggleVideo = !this.isAudioOnly;
// Set the widget ID if we have it
@ -54,7 +53,7 @@ export class JitsiWidgetWrapperComponent extends CapableWidget implements OnInit
$.getScript(widget.options.scriptUrl);
if (!this.domain) {
// Always fall back to jitsi.riot.im to maintain compatibility with widgets created by Riot.
// Always fall back to jitsi.riot.im to maintain compatibility with widgets created by Element.
this.domain = widget.options.useDomainAsDefault ? widget.options.jitsiDomain : "jitsi.riot.im";
}
});

View file

@ -14,7 +14,7 @@
<button class="btn btn-link btn-sm" (click)="openIntegrationManager()">Add some stickers</button>
</div>
<div class="sticker-picker" *ngIf="!isLoading && !authError">
<div class="sticker-pack" *ngFor="let pack of packs trackById">
<div class="sticker-pack" *ngFor="let pack of packs trackById" [attr.id]="'pack-' + pack.id">
<div class="header">
<span class="title">{{ pack.displayName }}</span>
<span class="license"><a [href]="pack.license.urlPath"
@ -32,5 +32,15 @@
</div>
</div>
</div>
<div class="sticker-pack-list" [@hideList]="isListVisible ? 'visible' : 'hidden'" (wheel)="scrollHorizontal($event)" >
<div class="sticker-pack-list-item" *ngFor="let pack of packs trackById" (click)="scrollToPack('pack-' + pack.id)">
<img [src]="getThumbnailUrl(pack.stickers[0].thumbnail.mxc, 48, 48)" width="40" height="40" class="image"
[alt]="pack.displayName" [ngbTooltip]="pack.displayName" placement="top" container="body"/>
</div>
<div class="sticker-pack-list-config" (click)="openIntegrationManager()"
ngbTooltip="Settings" placement="top" container="body">
<i class="fas fa-2x fa-cog"></i>
</div>
</div>
</div>
</div>

View file

@ -43,6 +43,7 @@
.sticker-picker {
margin: 15px 15px 30px;
padding-bottom: 40px;
.sticker-pack {
.header {
@ -92,5 +93,33 @@
}
}
}
.sticker-pack-list {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: themed(stickerPickerControlBgColor);
border-top: 1px solid themed(stickerPickerShadowColor);
overflow-x: auto;
white-space: nowrap;
padding: 1px 15px;
.sticker-pack-list-item {
display: inline-block;
cursor: pointer;
padding: 0 3px;
}
.sticker-pack-list-config {
display: inline-block;
cursor: pointer;
height: 40px;
width: 40px;
padding: 3px;
text-align: center;
vertical-align: middle;
}
}
}
}

View file

@ -1,6 +1,22 @@
import {
animate,
state,
style,
transition,
trigger
} from '@angular/animations';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { CapableWidget, WIDGET_API_VERSION_OPENID } from "../capable-widget";
import { fromEvent } from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
pairwise,
share,
throttleTime
} from 'rxjs/operators';
import { Subscription } from "rxjs/Subscription";
import { ScalarWidgetApi } from "../../shared/services/scalar/scalar-widget.api";
import { StickerApiService } from "../../shared/services/integrations/sticker-api.service";
@ -14,10 +30,25 @@ import { WIDGET_STICKER_PICKER } from "../../shared/models/widget";
selector: "my-generic-widget-wrapper",
templateUrl: "sticker-picker.component.html",
styleUrls: ["sticker-picker.component.scss"],
animations: [
trigger('hideList', [
state(
'hidden',
style({ opacity: 0, transform: 'translateY(100%)' })
),
state(
'visible',
style({ opacity: 1, transform: 'translateY(0)' })
),
transition('* => *', animate('200ms ease-in'))
])
]
})
export class StickerPickerWidgetWrapperComponent extends CapableWidget implements OnInit, OnDestroy {
public isLoading = true;
public isListVisible = true;
public authError = false;
public packs: FE_UserStickerPack[];
@ -68,6 +99,28 @@ export class StickerPickerWidgetWrapperComponent extends CapableWidget implement
if (this.stickerWidgetApiSubscription) this.stickerWidgetApiSubscription.unsubscribe();
}
public ngAfterViewInit() {
const scroll$ = fromEvent(window, 'scroll').pipe(
throttleTime(10),
map(() => window.pageYOffset),
pairwise(),
map(([y1, y2]): string => (y2 < y1 ? 'up' : 'down')),
distinctUntilChanged(),
share()
);
const scrollUp$ = scroll$.pipe(
filter(direction => direction === 'up')
);
const scrollDown = scroll$.pipe(
filter(direction => direction === 'down')
);
scrollUp$.subscribe(() => (this.isListVisible = true));
scrollDown.subscribe(() => (this.isListVisible = false));
}
protected onSupportedVersionsFound(): void {
super.onSupportedVersionsFound();
@ -133,6 +186,16 @@ export class StickerPickerWidgetWrapperComponent extends CapableWidget implement
}
}
public scrollHorizontal(event: WheelEvent): void {
document.getElementsByClassName('sticker-pack-list')[0].scrollLeft += event.deltaY;
event.preventDefault();
}
public scrollToPack(id: string) {
const el = document.getElementById(id);
el.scrollIntoView({behavior: 'smooth'});
}
public sendSticker(sticker: FE_Sticker, pack: FE_UserStickerPack) {
ScalarWidgetApi.sendSticker(sticker, pack);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,15 +1 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="513.000000pt" height="513.000000pt" viewBox="0 0 513.000000 513.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,513.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M0 2565 l0 -2565 2565 0 2565 0 0 2565 0 2565 -2565 0 -2565 0 0
-2565z"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="684" height="684" preserveAspectRatio="xMidYMid meet" version="1.0" viewBox="0 0 513 513"><metadata>Created by potrace 1.11, written by Peter Selinger 2001-2013</metadata><g fill="#000" stroke="none"><path d="M0 2565 l0 -2565 2565 0 2565 0 0 2565 0 2565 -2565 0 -2565 0 0 -2565z" transform="translate(0.000000,513.000000) scale(0.100000,-0.100000)"/></g></svg>

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 407 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -1,4 +1,4 @@
// The CSS for the Riot breadcrumb is specified here to ensure that it's style can be overridden.
// The CSS for the Element breadcrumb is specified here to ensure that it's style can be overridden.
// In it's current position (as a component), the component-level stylesheet cannot access the
// elements, so we specify it in a more generic location.
@import "themes/themes";
@ -59,4 +59,4 @@
color: themed(quickActionHoverColor);
}
}
}
}

View file

@ -48,7 +48,7 @@ $theme_dark: (
stickerPickerStickerBgColor: #fff,
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
jitsiWelcomeBgColor: #fff,
widgetWelcomeBgColor: #fff,
troubleshooterBgColor: #2d2d2d,
troubleshooterNeutralColor: rgb(205, 215, 222),

View file

@ -48,7 +48,7 @@ $theme_light: (
stickerPickerStickerBgColor: #fff,
stickerPickerShadowColor: hsla(0, 0%, 0%, 0.2),
jitsiWelcomeBgColor: #fff,
widgetWelcomeBgColor: #fff,
troubleshooterBgColor: #fff,
troubleshooterNeutralColor: rgb(205, 215, 222),
@ -86,4 +86,4 @@ $theme_light: (
appserviceConfigPreFgColor: rgb(41, 43, 44),
appserviceConfigPreBorderColor: #ccc,
appserviceConfigPreBgColor: #eee,
);
);