element-web/src/ScalarMessaging.js

355 lines
10 KiB
JavaScript
Raw Normal View History

/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/*
Listens for incoming postMessage requests from the integrations UI URL. The following API is exposed:
{
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
// additional request fields
}
The complete request object is returned to the caller with an additional "response" key like so:
{
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,
// additional request fields
2016-08-23 12:31:55 +00:00
response: { ... }
}
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 12:31:55 +00:00
error: {
message: "Unable to invite user into room.",
_error: <Original Error Object>
}
}
The "message" key should be a human-friendly string.
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:
{
action: "invite",
room_id: "!foo:bar",
user_id: "@invitee:bar",
response: {
success: true
}
}
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:
{
success: true
}
Example:
{
action: "set_bot_options",
room_id: "!foo:bar",
user_id: "@bot:bar",
content: {
default_option: "alpha"
},
response: {
success: true
}
}
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.
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
}
}
*/
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");
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);
}
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) {
sendError(event, "You need to be logged in.");
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, {
success: true,
2016-08-23 13:41:47 +00:00
});
return;
}
}
client.invite(roomId, userId).done(function() {
2016-08-23 13:41:47 +00:00
sendResponse(event, {
success: true,
2016-08-23 13:41:47 +00:00
});
}, function(err) {
sendError(event, "You need to be able to invite users to do that.", err);
});
}
function setBotOptions(event, roomId, userId) {
console.log(`Received request to set options for bot ${userId} in room ${roomId}`);
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
2016-08-23 13:41:47 +00:00
return;
}
client.sendStateEvent(roomId, "m.room.bot.options", event.data.content, "_" + userId).done(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err);
});
}
2016-09-07 08:57:07 +00:00
function setBotPower(event, roomId, userId, level) {
if (!(Number.isInteger(level) && level >= 0)) {
sendError(event, "Power level must be positive integer.");
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) {
sendError(event, "You need to be logged in.");
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) => {
2016-09-07 16:06:57 +00:00
let powerEvent = new MatrixEvent(
{
type: "m.room.power_levels",
2016-09-07 16:08:02 +00:00
content: powerLevels,
2016-09-07 16:06:57 +00:00
}
);
client.setPowerLevel(roomId, userId, level, powerEvent).done(() => {
sendResponse(event, {
success: true,
});
}, (err) => {
sendError(event, err.message ? err.message : "Failed to send request.", err);
2016-09-07 08:57:07 +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.`);
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.`);
returnStateEvent(event, roomId, "m.room.join_rules", "");
2016-09-05 13:58:16 +00:00
}
function botOptions(event, roomId, userId) {
console.log(`bot_options of ${userId} in room ${roomId} requested.`);
returnStateEvent(event, roomId, "m.room.bot.options", "_" + userId);
}
function returnStateEvent(event, roomId, eventType, stateKey) {
2016-08-23 13:41:47 +00:00
const client = MatrixClientPeg.get();
if (!client) {
sendError(event, "You need to be logged in.");
return;
}
const room = client.getRoom(roomId);
if (!room) {
sendError(event, "This room is not recognised.");
return;
}
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
}
sendResponse(event, stateEvent.getContent());
2016-08-23 13:41:47 +00:00
}
var currentRoomId = null;
var currentRoomAlias = null;
// 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") {
return;
}
currentRoomId = payload.room_id;
currentRoomAlias = payload.room_alias;
}
const onMessage = function(event) {
2016-08-23 12:31:55 +00:00
if (!event.origin) { // stupid chrome
event.origin = event.originalEvent.origin;
}
// check it is from the integrations UI URL (remove trailing spaces)
let url = SdkConfig.get().integrations_ui_url;
if (url.endsWith("/")) {
url = url.substr(0, url.length - 1);
}
if (url !== event.origin) {
console.warn("Unauthorised postMessage received. Source URL: " + event.origin);
return;
}
const roomId = event.data.room_id;
const userId = event.data.user_id;
if (!roomId) {
sendError(event, "Missing room_id in request");
return;
}
let promise = Promise.resolve(currentRoomId);
if (!currentRoomId) {
if (!currentRoomAlias) {
sendError(event, "Must be viewing a room");
}
// 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;
});
}
promise.then((viewingRoomId) => {
if (roomId !== viewingRoomId) {
sendError(event, "Room " + roomId + " not visible");
return;
}
// Getting join rules does not require userId
if (event.data.action === "join_rules_state") {
getJoinRules(event, roomId);
return;
}
if (!userId) {
sendError(event, "Missing user_id in request");
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);
sendError(event, "Failed to lookup current room.");
})
};
module.exports = {
2016-08-23 12:31:55 +00:00
startListening: function() {
window.addEventListener("message", onMessage, false);
},
2016-08-23 12:31:55 +00:00
stopListening: function() {
window.removeEventListener("message", onMessage);
2016-08-23 13:41:47 +00:00
},
};