2016-08-23 11:00:11 +00:00
|
|
|
/*
|
|
|
|
Copyright 2016 OpenMarket Ltd
|
2017-06-09 10:21:37 +00:00
|
|
|
Copyright 2017 Vector Creations Ltd
|
2016-08-23 11:00:11 +00:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
|
|
|
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
|
|
|
|
{
|
2017-07-11 14:20:33 +00:00
|
|
|
action: "invite" | "membership_state" | "bot_options" | "set_bot_options" | etc... ,
|
2016-08-23 12:31:55 +00:00
|
|
|
room_id: $ROOM_ID,
|
|
|
|
user_id: $USER_ID
|
2016-08-24 12:23:06 +00:00
|
|
|
// additional request fields
|
2016-08-23 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
The complete request object is returned to the caller with an additional "response" key like so:
|
|
|
|
{
|
2016-08-24 12:23:06 +00:00
|
|
|
action: "invite" | "membership_state" | "bot_options" | "set_bot_options",
|
2016-08-23 12:31:55 +00:00
|
|
|
room_id: $ROOM_ID,
|
|
|
|
user_id: $USER_ID,
|
2016-08-24 12:23:06 +00:00
|
|
|
// additional request fields
|
2016-08-23 12:31:55 +00:00
|
|
|
response: { ... }
|
2016-08-23 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
The "action" determines the format of the request and response. All actions can return an error response.
|
|
|
|
An error response is a "response" object which consists of a sole "error" key to indicate an error.
|
|
|
|
They look like:
|
2016-08-23 11:00:11 +00:00
|
|
|
{
|
2016-08-23 12:31:55 +00:00
|
|
|
error: {
|
|
|
|
message: "Unable to invite user into room.",
|
|
|
|
_error: <Original Error Object>
|
|
|
|
}
|
2016-08-23 11:00:11 +00:00
|
|
|
}
|
|
|
|
The "message" key should be a human-friendly string.
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
ACTIONS
|
|
|
|
=======
|
|
|
|
All actions can return an error response instead of the response outlined below.
|
|
|
|
|
|
|
|
invite
|
|
|
|
------
|
|
|
|
Invites a user into a room.
|
|
|
|
|
|
|
|
Request:
|
|
|
|
- room_id is the room to invite the user into.
|
|
|
|
- user_id is the user ID to invite.
|
|
|
|
- No additional fields.
|
|
|
|
Response:
|
|
|
|
{
|
|
|
|
success: true
|
|
|
|
}
|
|
|
|
Example:
|
2016-08-23 11:00:11 +00:00
|
|
|
{
|
2016-08-24 12:23:06 +00:00
|
|
|
action: "invite",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
user_id: "@invitee:bar",
|
|
|
|
response: {
|
|
|
|
success: true
|
|
|
|
}
|
2016-08-23 11:00:11 +00:00
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
set_bot_options
|
|
|
|
---------------
|
|
|
|
Set the m.room.bot.options state event for a bot user.
|
|
|
|
|
|
|
|
Request:
|
|
|
|
- room_id is the room to send the state event into.
|
|
|
|
- user_id is the user ID of the bot who you're setting options for.
|
|
|
|
- "content" is an object consisting of the content you wish to set.
|
|
|
|
Response:
|
2016-08-23 11:00:11 +00:00
|
|
|
{
|
2016-08-24 12:23:06 +00:00
|
|
|
success: true
|
2016-08-23 11:00:11 +00:00
|
|
|
}
|
2016-08-24 12:23:06 +00:00
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "set_bot_options",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
user_id: "@bot:bar",
|
|
|
|
content: {
|
|
|
|
default_option: "alpha"
|
|
|
|
},
|
|
|
|
response: {
|
|
|
|
success: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-06 15:41:08 +00:00
|
|
|
get_membership_count
|
|
|
|
--------------------
|
|
|
|
Get the number of joined users in the room.
|
|
|
|
|
|
|
|
Request:
|
|
|
|
- room_id is the room to get the count in.
|
|
|
|
Response:
|
|
|
|
78
|
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "get_membership_count",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
response: 78
|
|
|
|
}
|
|
|
|
|
2017-07-11 14:20:33 +00:00
|
|
|
can_send_event
|
|
|
|
--------------
|
|
|
|
Check if the client can send the given event into the given room. If the client
|
|
|
|
is unable to do this, an error response is returned instead of 'response: false'.
|
|
|
|
|
|
|
|
Request:
|
|
|
|
- room_id is the room to do the check in.
|
|
|
|
- event_type is the event type which will be sent.
|
|
|
|
- is_state is true if the event to be sent is a state event.
|
|
|
|
Response:
|
|
|
|
true
|
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "can_send_event",
|
|
|
|
is_state: false,
|
|
|
|
event_type: "m.room.message",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
response: true
|
|
|
|
}
|
|
|
|
|
2017-06-09 10:21:37 +00:00
|
|
|
set_widget
|
|
|
|
----------
|
|
|
|
Set a new widget in the room. Clobbers based on the ID.
|
|
|
|
|
|
|
|
Request:
|
|
|
|
- `room_id` (String) is the room to set the widget in.
|
|
|
|
- `widget_id` (String) is the ID of the widget to add (or replace if it already exists).
|
|
|
|
It can be an arbitrary UTF8 string and is purely for distinguishing between widgets.
|
|
|
|
- `url` (String) is the URL that clients should load in an iframe to run the widget.
|
|
|
|
All widgets must have a valid URL. If the URL is `null` (not `undefined`), the
|
|
|
|
widget will be removed from the room.
|
|
|
|
- `type` (String) is the type of widget, which is provided as a hint for matrix clients so they
|
|
|
|
can configure/lay out the widget in different ways. All widgets must have a type.
|
|
|
|
- `name` (String) is an optional human-readable string about the widget.
|
|
|
|
- `data` (Object) is some optional data about the widget, and can contain arbitrary key/value pairs.
|
|
|
|
Response:
|
|
|
|
{
|
|
|
|
success: true
|
|
|
|
}
|
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "set_widget",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
widget_id: "abc123",
|
|
|
|
url: "http://widget.url",
|
|
|
|
type: "example",
|
|
|
|
response: {
|
|
|
|
success: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
get_widgets
|
|
|
|
-----------
|
2017-07-11 11:15:27 +00:00
|
|
|
Get a list of all widgets in the room. The response is an array
|
|
|
|
of state events.
|
2017-06-09 10:21:37 +00:00
|
|
|
|
|
|
|
Request:
|
|
|
|
- `room_id` (String) is the room to get the widgets in.
|
|
|
|
Response:
|
2017-07-11 11:15:27 +00:00
|
|
|
[
|
|
|
|
{
|
|
|
|
type: "im.vector.modular.widgets",
|
|
|
|
state_key: "wid1",
|
|
|
|
content: {
|
|
|
|
type: "grafana",
|
|
|
|
url: "https://grafanaurl",
|
|
|
|
name: "dashboard",
|
|
|
|
data: {key: "val"}
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
2017-07-11 11:15:27 +00:00
|
|
|
room_id: “!foo:bar”,
|
|
|
|
sender: "@alice:localhost"
|
|
|
|
}
|
|
|
|
]
|
2017-06-09 10:21:37 +00:00
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "get_widgets",
|
|
|
|
room_id: "!foo:bar",
|
2017-07-11 11:15:27 +00:00
|
|
|
response: [
|
|
|
|
{
|
|
|
|
type: "im.vector.modular.widgets",
|
|
|
|
state_key: "wid1",
|
|
|
|
content: {
|
|
|
|
type: "grafana",
|
|
|
|
url: "https://grafanaurl",
|
|
|
|
name: "dashboard",
|
|
|
|
data: {key: "val"}
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
2017-07-11 11:15:27 +00:00
|
|
|
room_id: “!foo:bar”,
|
|
|
|
sender: "@alice:localhost"
|
|
|
|
}
|
|
|
|
]
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
|
|
|
|
2017-06-06 15:41:08 +00:00
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
membership_state AND bot_options
|
|
|
|
--------------------------------
|
|
|
|
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
|
|
|
|
|
|
|
NB: Whilst this API is basically equivalent to getStateEvent, we specifically do not
|
|
|
|
want external entities to be able to query any state event for any room, hence the
|
|
|
|
restrictive API outlined here.
|
2016-08-23 11:00:11 +00:00
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
Request:
|
|
|
|
- room_id is the room which has the state event.
|
|
|
|
- user_id is the state_key parameter which in both cases is a user ID (the member or the bot).
|
|
|
|
- No additional fields.
|
|
|
|
Response:
|
|
|
|
- The event content. If there is no state event, the "response" key should be null.
|
|
|
|
Example:
|
|
|
|
{
|
|
|
|
action: "membership_state",
|
|
|
|
room_id: "!foo:bar",
|
|
|
|
user_id: "@somemember:bar",
|
|
|
|
response: {
|
|
|
|
membership: "join",
|
|
|
|
displayname: "Bob",
|
|
|
|
avatar_url: null
|
|
|
|
}
|
|
|
|
}
|
2016-08-23 11:00:11 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
const SdkConfig = require('./SdkConfig');
|
|
|
|
const MatrixClientPeg = require("./MatrixClientPeg");
|
2016-09-07 16:06:57 +00:00
|
|
|
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
|
|
|
const dis = require("./dispatcher");
|
2018-01-11 10:32:37 +00:00
|
|
|
const Widgets = require('./utils/widgets');
|
2017-05-25 10:39:08 +00:00
|
|
|
import { _t } from './languageHandler';
|
2016-08-23 11:00:11 +00:00
|
|
|
|
2016-08-23 13:41:47 +00:00
|
|
|
function sendResponse(event, res) {
|
|
|
|
const data = JSON.parse(JSON.stringify(event.data));
|
|
|
|
data.response = res;
|
|
|
|
event.source.postMessage(data, event.origin);
|
|
|
|
}
|
|
|
|
|
|
|
|
function sendError(event, msg, nestedError) {
|
|
|
|
console.error("Action:" + event.data.action + " failed with message: " + msg);
|
2016-08-23 13:50:52 +00:00
|
|
|
const data = JSON.parse(JSON.stringify(event.data));
|
2016-08-23 13:41:47 +00:00
|
|
|
data.response = {
|
|
|
|
error: {
|
|
|
|
message: msg,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
if (nestedError) {
|
|
|
|
data.response.error._error = nestedError;
|
|
|
|
}
|
|
|
|
event.source.postMessage(data, event.origin);
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
function inviteUser(event, roomId, userId) {
|
2016-08-23 13:41:47 +00:00
|
|
|
console.log(`Received request to invite ${userId} into room ${roomId}`);
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be logged in.'));
|
2016-08-23 13:41:47 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (room) {
|
|
|
|
// if they are already invited we can resolve immediately.
|
|
|
|
const member = room.getMember(userId);
|
|
|
|
if (member && member.membership === "invite") {
|
|
|
|
sendResponse(event, {
|
2016-08-24 12:23:06 +00:00
|
|
|
success: true,
|
2016-08-23 13:41:47 +00:00
|
|
|
});
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:47:35 +00:00
|
|
|
client.invite(roomId, userId).done(function() {
|
2016-08-23 13:41:47 +00:00
|
|
|
sendResponse(event, {
|
2016-08-24 12:23:06 +00:00
|
|
|
success: true,
|
2016-08-23 13:41:47 +00:00
|
|
|
});
|
|
|
|
}, function(err) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be able to invite users to do that.'), err);
|
2016-08-23 13:41:47 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-05-15 10:41:16 +00:00
|
|
|
/**
|
|
|
|
* Returns a promise that resolves when a widget with the given
|
|
|
|
* ID has been added as a user widget (ie. the accountData event
|
|
|
|
* arrives) or rejects after a timeout
|
2018-05-15 13:18:02 +00:00
|
|
|
*
|
|
|
|
* @param {string} widgetId The ID of the widget to wait for
|
2018-05-15 16:12:59 +00:00
|
|
|
* @param {boolean} add True to wait for the widget to be added,
|
|
|
|
* false to wait for it to be deleted.
|
2018-05-15 13:18:02 +00:00
|
|
|
* @returns {Promise} that resolves when the widget is available
|
2018-05-15 10:41:16 +00:00
|
|
|
*/
|
2018-05-15 16:12:59 +00:00
|
|
|
function waitForUserWidget(widgetId, add) {
|
2018-05-15 10:41:16 +00:00
|
|
|
return new Promise((resolve, reject) => {
|
2018-05-15 13:13:56 +00:00
|
|
|
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
|
2018-05-15 16:12:59 +00:00
|
|
|
|
2018-05-15 16:28:55 +00:00
|
|
|
// Tests an account data event, returning true if it's in the state
|
|
|
|
// we're waiting for it to be in
|
|
|
|
function eventInIntendedState(ev) {
|
2018-05-15 16:12:59 +00:00
|
|
|
if (!ev || !currentAccountDataEvent.getContent()) return false;
|
|
|
|
if (add) {
|
|
|
|
return ev.getContent()[widgetId] !== undefined;
|
|
|
|
} else {
|
|
|
|
return ev.getContent()[widgetId] === undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-15 16:34:02 +00:00
|
|
|
if (eventInIntendedState(currentAccountDataEvent)) {
|
2018-05-15 11:06:23 +00:00
|
|
|
resolve();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-05-15 10:41:16 +00:00
|
|
|
function onAccountData(ev) {
|
2018-05-15 16:34:02 +00:00
|
|
|
if (eventInIntendedState(currentAccountDataEvent)) {
|
2018-05-15 10:41:16 +00:00
|
|
|
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
|
|
|
clearTimeout(timerId);
|
|
|
|
resolve();
|
|
|
|
}
|
|
|
|
}
|
2018-05-15 13:18:02 +00:00
|
|
|
const timerId = setTimeout(() => {
|
2018-05-15 10:41:16 +00:00
|
|
|
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
2018-05-15 11:06:23 +00:00
|
|
|
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
|
2018-05-15 10:41:16 +00:00
|
|
|
}, 10000);
|
|
|
|
MatrixClientPeg.get().on('accountData', onAccountData);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-06-09 10:21:37 +00:00
|
|
|
function setWidget(event, roomId) {
|
|
|
|
const widgetId = event.data.widget_id;
|
|
|
|
const widgetType = event.data.type;
|
|
|
|
const widgetUrl = event.data.url;
|
2017-06-09 11:34:19 +00:00
|
|
|
const widgetName = event.data.name; // optional
|
|
|
|
const widgetData = event.data.data; // optional
|
2018-01-08 15:38:01 +00:00
|
|
|
const userWidget = event.data.userWidget;
|
2017-06-09 11:34:19 +00:00
|
|
|
|
2017-06-09 10:21:37 +00:00
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
|
|
|
sendError(event, _t('You need to be logged in.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-09 11:34:19 +00:00
|
|
|
// both adding/removing widgets need these checks
|
|
|
|
if (!widgetId || widgetUrl === undefined) {
|
|
|
|
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
2017-06-09 10:21:37 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-06-09 11:34:19 +00:00
|
|
|
|
|
|
|
if (widgetUrl !== null) { // if url is null it is being deleted, don't need to check name/type/etc
|
|
|
|
// check types of fields
|
|
|
|
if (widgetName !== undefined && typeof widgetName !== 'string') {
|
|
|
|
sendError(event, _t("Unable to create widget."), new Error("Optional field 'name' must be a string."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (widgetData !== undefined && !(widgetData instanceof Object)) {
|
|
|
|
sendError(event, _t("Unable to create widget."), new Error("Optional field 'data' must be an Object."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (typeof widgetType !== 'string') {
|
|
|
|
sendError(event, _t("Unable to create widget."), new Error("Field 'type' must be a string."));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (typeof widgetUrl !== 'string') {
|
|
|
|
sendError(event, _t("Unable to create widget."), new Error("Field 'url' must be a string or null."));
|
|
|
|
return;
|
|
|
|
}
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
|
|
|
|
2017-07-11 11:15:27 +00:00
|
|
|
let content = {
|
|
|
|
type: widgetType,
|
|
|
|
url: widgetUrl,
|
|
|
|
name: widgetName,
|
|
|
|
data: widgetData,
|
|
|
|
};
|
|
|
|
|
2018-01-08 15:38:01 +00:00
|
|
|
if (userWidget) {
|
|
|
|
const client = MatrixClientPeg.get();
|
2018-03-09 09:15:16 +00:00
|
|
|
const userWidgets = Widgets.getUserWidgets();
|
2018-01-08 15:38:01 +00:00
|
|
|
|
|
|
|
// Delete existing widget with ID
|
2018-01-09 14:37:45 +00:00
|
|
|
try {
|
|
|
|
delete userWidgets[widgetId];
|
|
|
|
} catch (e) {
|
|
|
|
console.error(`$widgetId is non-configurable`);
|
|
|
|
}
|
2018-01-08 15:38:01 +00:00
|
|
|
|
|
|
|
// Add new widget / update
|
|
|
|
if (widgetUrl !== null) {
|
2018-01-08 16:47:49 +00:00
|
|
|
userWidgets[widgetId] = {
|
|
|
|
content: content,
|
|
|
|
sender: client.getUserId(),
|
2018-05-09 15:55:49 +00:00
|
|
|
state_key: widgetId,
|
2018-04-02 09:02:41 +00:00
|
|
|
type: 'm.widget',
|
2018-01-09 14:37:45 +00:00
|
|
|
id: widgetId,
|
2018-01-08 16:47:49 +00:00
|
|
|
};
|
2018-01-08 15:38:01 +00:00
|
|
|
}
|
|
|
|
|
2018-05-15 10:41:16 +00:00
|
|
|
// This starts listening for when the echo comes back from the server
|
|
|
|
// since the widget won't appear added until this happens. If we don't
|
|
|
|
// wait for this, the action will complete but if the user is fast enough,
|
|
|
|
// the widget still won't actually be there.
|
2018-03-29 19:07:26 +00:00
|
|
|
client.setAccountData('m.widgets', userWidgets).then(() => {
|
2018-05-15 16:12:59 +00:00
|
|
|
return waitForUserWidget(widgetId, widgetUrl !== null);
|
2018-05-15 10:41:16 +00:00
|
|
|
}).then(() => {
|
2018-03-29 19:07:26 +00:00
|
|
|
sendResponse(event, {
|
|
|
|
success: true,
|
|
|
|
});
|
2018-02-07 09:23:00 +00:00
|
|
|
|
2018-03-29 19:07:26 +00:00
|
|
|
dis.dispatch({ action: "user_widget_updated" });
|
2018-05-15 13:53:49 +00:00
|
|
|
}).catch((e) => {
|
2018-05-15 14:19:28 +00:00
|
|
|
sendError(event, _t('Unable to create widget.'), e);
|
2018-03-29 19:07:26 +00:00
|
|
|
});
|
2018-01-08 15:38:01 +00:00
|
|
|
} else { // Room widget
|
2018-01-09 14:37:45 +00:00
|
|
|
if (!roomId) {
|
|
|
|
sendError(event, _t('Missing roomId.'), null);
|
|
|
|
}
|
|
|
|
|
2018-01-08 15:38:01 +00:00
|
|
|
if (widgetUrl === null) { // widget is being deleted
|
|
|
|
content = {};
|
|
|
|
}
|
2018-04-02 09:02:41 +00:00
|
|
|
// TODO - Room widgets need to be moved to 'm.widget' state events
|
|
|
|
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
2018-01-08 15:38:01 +00:00
|
|
|
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
2018-05-15 10:41:16 +00:00
|
|
|
// XXX: We should probably wait for the echo of the state event to come back from the server,
|
2018-05-15 10:50:24 +00:00
|
|
|
// as we do with user widgets.
|
2018-01-08 15:38:01 +00:00
|
|
|
sendResponse(event, {
|
|
|
|
success: true,
|
|
|
|
});
|
|
|
|
}, (err) => {
|
|
|
|
sendError(event, _t('Failed to send request.'), err);
|
2017-06-09 10:21:37 +00:00
|
|
|
});
|
2018-01-08 15:38:01 +00:00
|
|
|
}
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getWidgets(event, roomId) {
|
2017-07-11 11:15:27 +00:00
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
|
|
|
sendError(event, _t('You need to be logged in.'));
|
|
|
|
return;
|
|
|
|
}
|
2018-01-09 14:37:45 +00:00
|
|
|
let widgetStateEvents = [];
|
|
|
|
|
|
|
|
if (roomId) {
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (!room) {
|
|
|
|
sendError(event, _t('This room is not recognised.'));
|
|
|
|
return;
|
2017-07-11 11:15:27 +00:00
|
|
|
}
|
2018-04-02 09:02:41 +00:00
|
|
|
// TODO - Room widgets need to be moved to 'm.widget' state events
|
|
|
|
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
2018-01-09 14:37:45 +00:00
|
|
|
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
|
|
|
// Only return widgets which have required fields
|
|
|
|
if (room) {
|
|
|
|
stateEvents.forEach((ev) => {
|
|
|
|
if (ev.getContent().type && ev.getContent().url) {
|
|
|
|
widgetStateEvents.push(ev.event); // return the raw event
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add user widgets (not linked to a specific room)
|
2018-03-09 09:15:16 +00:00
|
|
|
const userWidgets = Widgets.getUserWidgetsArray();
|
2018-01-11 10:32:37 +00:00
|
|
|
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
2017-07-11 11:15:27 +00:00
|
|
|
|
|
|
|
sendResponse(event, widgetStateEvents);
|
2017-06-09 10:21:37 +00:00
|
|
|
}
|
|
|
|
|
2017-12-05 11:59:02 +00:00
|
|
|
function getRoomEncState(event, roomId) {
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
|
|
|
sendError(event, _t('You need to be logged in.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (!room) {
|
|
|
|
sendError(event, _t('This room is not recognised.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(roomId);
|
|
|
|
|
|
|
|
sendResponse(event, roomIsEncrypted);
|
|
|
|
}
|
|
|
|
|
2016-09-15 14:24:08 +00:00
|
|
|
function setPlumbingState(event, roomId, status) {
|
|
|
|
if (typeof status !== 'string') {
|
|
|
|
throw new Error('Plumbing state status should be a string');
|
|
|
|
}
|
|
|
|
console.log(`Received request to set plumbing state to status "${status}" in room ${roomId}`);
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be logged in.'));
|
2016-09-15 14:24:08 +00:00
|
|
|
return;
|
|
|
|
}
|
2017-10-11 16:56:17 +00:00
|
|
|
client.sendStateEvent(roomId, "m.room.plumbing", { status: status }).done(() => {
|
2016-09-15 14:24:08 +00:00
|
|
|
sendResponse(event, {
|
|
|
|
success: true,
|
|
|
|
});
|
|
|
|
}, (err) => {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
2016-09-15 14:24:08 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
function setBotOptions(event, roomId, userId) {
|
|
|
|
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be logged in.'));
|
2016-08-23 13:41:47 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-08-24 13:54:44 +00:00
|
|
|
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
|
2016-08-24 12:23:06 +00:00
|
|
|
sendResponse(event, {
|
|
|
|
success: true,
|
|
|
|
});
|
|
|
|
}, (err) => {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
2016-08-24 12:23:06 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-09-07 08:57:07 +00:00
|
|
|
function setBotPower(event, roomId, userId, level) {
|
|
|
|
if (!(Number.isInteger(level) && level >= 0)) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('Power level must be positive integer.'));
|
2016-09-07 08:57:07 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-09-08 16:38:51 +00:00
|
|
|
console.log(`Received request to set power level to ${level} for bot ${userId} in room ${roomId}.`);
|
2016-09-07 08:57:07 +00:00
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be logged in.'));
|
2016-09-07 08:57:07 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-09-07 16:06:57 +00:00
|
|
|
|
2016-09-07 16:08:02 +00:00
|
|
|
client.getStateEvent(roomId, "m.room.power_levels", "").then((powerLevels) => {
|
2017-10-11 16:56:17 +00:00
|
|
|
const powerEvent = new MatrixEvent(
|
2016-09-07 16:06:57 +00:00
|
|
|
{
|
|
|
|
type: "m.room.power_levels",
|
2016-09-07 16:08:02 +00:00
|
|
|
content: powerLevels,
|
2017-10-11 16:56:17 +00:00
|
|
|
},
|
2016-09-07 16:06:57 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
|
|
|
|
sendResponse(event, {
|
|
|
|
success: true,
|
|
|
|
});
|
|
|
|
}, (err) => {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, err.message ? err.message : _t('Failed to send request.'), err);
|
2016-09-07 08:57:07 +00:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
function getMembershipState(event, roomId, userId) {
|
2016-08-23 13:41:47 +00:00
|
|
|
console.log(`membership_state of ${userId} in room ${roomId} requested.`);
|
2016-08-24 12:23:06 +00:00
|
|
|
returnStateEvent(event, roomId, "m.room.member", userId);
|
|
|
|
}
|
|
|
|
|
2016-09-05 13:58:16 +00:00
|
|
|
function getJoinRules(event, roomId) {
|
|
|
|
console.log(`join_rules of ${roomId} requested.`);
|
2016-09-06 09:29:38 +00:00
|
|
|
returnStateEvent(event, roomId, "m.room.join_rules", "");
|
2016-09-05 13:58:16 +00:00
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
function botOptions(event, roomId, userId) {
|
|
|
|
console.log(`bot_options of ${userId} in room ${roomId} requested.`);
|
2016-08-24 13:54:44 +00:00
|
|
|
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
|
2016-08-24 12:23:06 +00:00
|
|
|
}
|
|
|
|
|
2017-06-06 15:41:08 +00:00
|
|
|
function getMembershipCount(event, roomId) {
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
|
|
|
sendError(event, _t('You need to be logged in.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (!room) {
|
|
|
|
sendError(event, _t('This room is not recognised.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const count = room.getJoinedMembers().length;
|
|
|
|
sendResponse(event, count);
|
|
|
|
}
|
|
|
|
|
2017-07-11 14:20:33 +00:00
|
|
|
function canSendEvent(event, roomId) {
|
|
|
|
const evType = "" + event.data.event_type; // force stringify
|
|
|
|
const isState = Boolean(event.data.is_state);
|
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
|
|
|
sendError(event, _t('You need to be logged in.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (!room) {
|
|
|
|
sendError(event, _t('This room is not recognised.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const me = client.credentials.userId;
|
|
|
|
const member = room.getMember(me);
|
|
|
|
if (!member || member.membership !== "join") {
|
|
|
|
sendError(event, _t('You are not in this room.'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let canSend = false;
|
|
|
|
if (isState) {
|
|
|
|
canSend = room.currentState.maySendStateEvent(evType, me);
|
2017-10-11 16:56:17 +00:00
|
|
|
} else {
|
2017-07-11 14:20:33 +00:00
|
|
|
canSend = room.currentState.maySendEvent(evType, me);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!canSend) {
|
2017-07-14 10:17:59 +00:00
|
|
|
sendError(event, _t('You do not have permission to do that in this room.'));
|
2017-07-11 14:20:33 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
sendResponse(event, true);
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
function returnStateEvent(event, roomId, eventType, stateKey) {
|
2016-08-23 13:41:47 +00:00
|
|
|
const client = MatrixClientPeg.get();
|
|
|
|
if (!client) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('You need to be logged in.'));
|
2016-08-23 13:41:47 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
const room = client.getRoom(roomId);
|
|
|
|
if (!room) {
|
2017-05-25 18:21:18 +00:00
|
|
|
sendError(event, _t('This room is not recognised.'));
|
2016-08-23 13:41:47 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-08-24 12:23:06 +00:00
|
|
|
const stateEvent = room.currentState.getStateEvents(eventType, stateKey);
|
|
|
|
if (!stateEvent) {
|
|
|
|
sendResponse(event, null);
|
2016-08-24 13:10:21 +00:00
|
|
|
return;
|
2016-08-23 13:41:47 +00:00
|
|
|
}
|
2016-08-24 12:23:06 +00:00
|
|
|
sendResponse(event, stateEvent.getContent());
|
2016-08-23 13:41:47 +00:00
|
|
|
}
|
|
|
|
|
2017-10-11 16:56:17 +00:00
|
|
|
let currentRoomId = null;
|
|
|
|
let currentRoomAlias = null;
|
2016-09-05 14:13:48 +00:00
|
|
|
|
|
|
|
// Listen for when a room is viewed
|
|
|
|
dis.register(onAction);
|
|
|
|
function onAction(payload) {
|
2016-09-06 09:36:44 +00:00
|
|
|
if (payload.action !== "view_room") {
|
2016-09-06 09:29:38 +00:00
|
|
|
return;
|
2016-09-05 14:13:48 +00:00
|
|
|
}
|
2016-09-06 09:29:38 +00:00
|
|
|
currentRoomId = payload.room_id;
|
2016-09-09 15:06:19 +00:00
|
|
|
currentRoomAlias = payload.room_alias;
|
2016-09-05 14:13:48 +00:00
|
|
|
}
|
|
|
|
|
2016-08-23 11:00:11 +00:00
|
|
|
const onMessage = function(event) {
|
2016-08-23 12:31:55 +00:00
|
|
|
if (!event.origin) { // stupid chrome
|
|
|
|
event.origin = event.originalEvent.origin;
|
|
|
|
}
|
|
|
|
|
2016-12-06 14:30:21 +00:00
|
|
|
// Check that the integrations UI URL starts with the origin of the event
|
|
|
|
// This means the URL could contain a path (like /develop) and still be used
|
|
|
|
// to validate event origins, which do not specify paths.
|
|
|
|
// (See https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage)
|
|
|
|
//
|
|
|
|
// All strings start with the empty string, so for sanity return if the length
|
|
|
|
// of the event origin is 0.
|
2017-12-19 15:39:13 +00:00
|
|
|
//
|
|
|
|
// TODO -- Scalar postMessage API should be namespaced with event.data.api field
|
|
|
|
// Fix following "if" statement to respond only to specific API messages.
|
2017-10-11 16:56:17 +00:00
|
|
|
const url = SdkConfig.get().integrations_ui_url;
|
2017-12-19 15:39:13 +00:00
|
|
|
if (
|
|
|
|
event.origin.length === 0 ||
|
2018-02-16 23:59:48 +00:00
|
|
|
!url.startsWith(event.origin + '/') ||
|
2017-12-19 15:39:13 +00:00
|
|
|
!event.data.action ||
|
|
|
|
event.data.api // Ignore messages with specific API set
|
|
|
|
) {
|
2016-09-19 09:38:42 +00:00
|
|
|
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.data.action === "close_scalar") {
|
|
|
|
dis.dispatch({ action: "close_scalar" });
|
|
|
|
sendResponse(event, null);
|
2016-08-23 12:31:55 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2016-08-24 12:23:06 +00:00
|
|
|
const roomId = event.data.room_id;
|
|
|
|
const userId = event.data.user_id;
|
2018-01-09 14:37:45 +00:00
|
|
|
|
2018-03-13 11:58:18 +00:00
|
|
|
if (!roomId) {
|
|
|
|
// These APIs don't require roomId
|
|
|
|
// Get and set user widgets (not associated with a specific room)
|
|
|
|
// If roomId is specified, it must be validated, so room-based widgets agreed
|
|
|
|
// handled further down.
|
2018-03-13 10:07:27 +00:00
|
|
|
if (event.data.action === "get_widgets") {
|
|
|
|
getWidgets(event, null);
|
|
|
|
return;
|
|
|
|
} else if (event.data.action === "set_widget") {
|
|
|
|
setWidget(event, null);
|
|
|
|
return;
|
2018-03-13 11:58:18 +00:00
|
|
|
} else {
|
|
|
|
sendError(event, _t('Missing room_id in request'));
|
|
|
|
return;
|
2018-03-13 10:07:27 +00:00
|
|
|
}
|
2018-01-09 14:37:45 +00:00
|
|
|
}
|
2016-09-09 15:06:19 +00:00
|
|
|
let promise = Promise.resolve(currentRoomId);
|
2016-09-05 14:13:48 +00:00
|
|
|
if (!currentRoomId) {
|
2016-09-09 15:06:19 +00:00
|
|
|
if (!currentRoomAlias) {
|
2017-05-23 14:16:31 +00:00
|
|
|
sendError(event, _t('Must be viewing a room'));
|
2016-09-09 15:14:41 +00:00
|
|
|
return;
|
2016-09-09 15:06:19 +00:00
|
|
|
}
|
|
|
|
// no room ID but there is an alias, look it up.
|
|
|
|
console.log("Looking up alias " + currentRoomAlias);
|
|
|
|
promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
|
|
|
|
return res.room_id;
|
|
|
|
});
|
2016-09-05 14:13:48 +00:00
|
|
|
}
|
|
|
|
|
2016-09-09 15:06:19 +00:00
|
|
|
promise.then((viewingRoomId) => {
|
|
|
|
if (roomId !== viewingRoomId) {
|
2017-05-23 14:16:31 +00:00
|
|
|
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
2016-09-09 15:06:19 +00:00
|
|
|
return;
|
|
|
|
}
|
2016-09-07 08:58:48 +00:00
|
|
|
|
2018-02-23 14:53:52 +00:00
|
|
|
// Get and set room-based widgets
|
|
|
|
if (event.data.action === "get_widgets") {
|
2018-03-13 10:07:27 +00:00
|
|
|
getWidgets(event, roomId);
|
2018-02-23 14:53:52 +00:00
|
|
|
return;
|
|
|
|
} else if (event.data.action === "set_widget") {
|
2018-03-13 10:07:27 +00:00
|
|
|
setWidget(event, roomId);
|
2018-02-23 14:53:52 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-06-09 10:21:37 +00:00
|
|
|
// These APIs don't require userId
|
2016-09-09 15:06:19 +00:00
|
|
|
if (event.data.action === "join_rules_state") {
|
|
|
|
getJoinRules(event, roomId);
|
|
|
|
return;
|
2016-09-15 14:24:08 +00:00
|
|
|
} else if (event.data.action === "set_plumbing_state") {
|
|
|
|
setPlumbingState(event, roomId, event.data.status);
|
|
|
|
return;
|
2017-06-06 15:41:08 +00:00
|
|
|
} else if (event.data.action === "get_membership_count") {
|
|
|
|
getMembershipCount(event, roomId);
|
|
|
|
return;
|
2017-12-05 11:59:02 +00:00
|
|
|
} else if (event.data.action === "get_room_enc_state") {
|
|
|
|
getRoomEncState(event, roomId);
|
|
|
|
return;
|
2017-07-11 14:20:33 +00:00
|
|
|
} else if (event.data.action === "can_send_event") {
|
|
|
|
canSendEvent(event, roomId);
|
|
|
|
return;
|
2016-09-09 15:06:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!userId) {
|
2017-05-23 14:16:31 +00:00
|
|
|
sendError(event, _t('Missing user_id in request'));
|
2016-09-09 15:06:19 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
switch (event.data.action) {
|
|
|
|
case "membership_state":
|
|
|
|
getMembershipState(event, roomId, userId);
|
|
|
|
break;
|
|
|
|
case "invite":
|
|
|
|
inviteUser(event, roomId, userId);
|
|
|
|
break;
|
|
|
|
case "bot_options":
|
|
|
|
botOptions(event, roomId, userId);
|
|
|
|
break;
|
|
|
|
case "set_bot_options":
|
|
|
|
setBotOptions(event, roomId, userId);
|
|
|
|
break;
|
|
|
|
case "set_bot_power":
|
|
|
|
setBotPower(event, roomId, userId, event.data.level);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}, (err) => {
|
|
|
|
console.error(err);
|
2017-05-23 14:16:31 +00:00
|
|
|
sendError(event, _t('Failed to lookup current room') + '.');
|
2017-01-20 14:22:27 +00:00
|
|
|
});
|
2016-08-23 11:00:11 +00:00
|
|
|
};
|
|
|
|
|
2017-06-30 14:42:51 +00:00
|
|
|
let listenerCount = 0;
|
2016-08-23 11:00:11 +00:00
|
|
|
module.exports = {
|
2016-08-23 12:31:55 +00:00
|
|
|
startListening: function() {
|
2017-06-30 14:42:51 +00:00
|
|
|
if (listenerCount === 0) {
|
|
|
|
window.addEventListener("message", onMessage, false);
|
|
|
|
}
|
|
|
|
listenerCount += 1;
|
2016-08-23 12:31:55 +00:00
|
|
|
},
|
2016-08-23 11:00:11 +00:00
|
|
|
|
2016-08-23 12:31:55 +00:00
|
|
|
stopListening: function() {
|
2017-06-30 14:42:51 +00:00
|
|
|
listenerCount -= 1;
|
|
|
|
if (listenerCount === 0) {
|
|
|
|
window.removeEventListener("message", onMessage);
|
|
|
|
}
|
|
|
|
if (listenerCount < 0) {
|
|
|
|
// Make an error so we get a stack trace
|
|
|
|
const e = new Error(
|
|
|
|
"ScalarMessaging: mismatched startListening / stopListening detected." +
|
2017-10-11 16:56:17 +00:00
|
|
|
" Negative count",
|
2017-06-30 14:42:51 +00:00
|
|
|
);
|
|
|
|
console.error(e);
|
|
|
|
}
|
2016-08-23 13:41:47 +00:00
|
|
|
},
|
2016-08-23 11:00:11 +00:00
|
|
|
};
|