Transition all remaining messaging over (delete the old stuff)
This commit is contained in:
parent
9377306b81
commit
f27071ee64
13 changed files with 29 additions and 868 deletions
|
@ -75,7 +75,6 @@ import {base32} from "rfc4648";
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import WidgetStore from "./stores/WidgetStore";
|
import WidgetStore from "./stores/WidgetStore";
|
||||||
import ActiveWidgetStore from "./stores/ActiveWidgetStore";
|
|
||||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
|
|
||||||
|
|
|
@ -1,278 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 Travis Ralston
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import URL from 'url';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
|
||||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
|
||||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import {Capability} from "./widgets/WidgetApi";
|
|
||||||
import {objectClone} from "./utils/objects";
|
|
||||||
|
|
||||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
|
||||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
|
||||||
'0.0.1',
|
|
||||||
'0.0.2',
|
|
||||||
];
|
|
||||||
const INBOUND_API_NAME = 'fromWidget';
|
|
||||||
|
|
||||||
// Listen for and handle incoming requests using the 'fromWidget' postMessage
|
|
||||||
// API and initiate responses
|
|
||||||
export default class FromWidgetPostMessageApi {
|
|
||||||
constructor() {
|
|
||||||
this.widgetMessagingEndpoints = [];
|
|
||||||
this.widgetListeners = {}; // {action: func[]}
|
|
||||||
|
|
||||||
this.start = this.start.bind(this);
|
|
||||||
this.stop = this.stop.bind(this);
|
|
||||||
this.onPostMessage = this.onPostMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a listener for a given action
|
|
||||||
* @param {string} action The action to listen for.
|
|
||||||
* @param {Function} callbackFn A callback function to be called when the action is
|
|
||||||
* encountered. Called with two parameters: the interesting request information and
|
|
||||||
* the raw event received from the postMessage API. The raw event is meant to be used
|
|
||||||
* for sendResponse and similar functions.
|
|
||||||
*/
|
|
||||||
addListener(action, callbackFn) {
|
|
||||||
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
|
|
||||||
this.widgetListeners[action].push(callbackFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a listener for a given action.
|
|
||||||
* @param {string} action The action that was subscribed to.
|
|
||||||
* @param {Function} callbackFn The original callback function that was used to subscribe
|
|
||||||
* to updates.
|
|
||||||
*/
|
|
||||||
removeListener(action, callbackFn) {
|
|
||||||
if (!this.widgetListeners[action]) return;
|
|
||||||
|
|
||||||
const idx = this.widgetListeners[action].indexOf(callbackFn);
|
|
||||||
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a widget endpoint for trusted postMessage communication
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
*/
|
|
||||||
addEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
|
||||||
if (this.widgetMessagingEndpoints.some(function(ep) {
|
|
||||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
|
||||||
})) {
|
|
||||||
// Message endpoint already registered
|
|
||||||
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
|
||||||
this.widgetMessagingEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* De-register a widget endpoint from trusted communication sources
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
|
||||||
* @return {boolean} True if endpoint was successfully removed
|
|
||||||
*/
|
|
||||||
removeEndpoint(widgetId, endpointUrl) {
|
|
||||||
const u = URL.parse(endpointUrl);
|
|
||||||
if (!u || !u.protocol || !u.host) {
|
|
||||||
console.warn('Remove widget messaging endpoint - Invalid origin');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const origin = u.protocol + '//' + u.host;
|
|
||||||
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
|
||||||
const length = this.widgetMessagingEndpoints.length;
|
|
||||||
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
|
|
||||||
.filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
|
|
||||||
return (length > this.widgetMessagingEndpoints.length);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle widget postMessage events
|
|
||||||
* Messages are only handled where a valid, registered messaging endpoints
|
|
||||||
* @param {Event} event Event to handle
|
|
||||||
* @return {undefined}
|
|
||||||
*/
|
|
||||||
onPostMessage(event) {
|
|
||||||
if (!event.origin) { // Handle chrome
|
|
||||||
event.origin = event.originalEvent.origin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event origin is empty string if undefined
|
|
||||||
if (
|
|
||||||
event.origin.length === 0 ||
|
|
||||||
!this.trustedEndpoint(event.origin) ||
|
|
||||||
event.data.api !== INBOUND_API_NAME ||
|
|
||||||
!event.data.widgetId
|
|
||||||
) {
|
|
||||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call any listeners we have registered
|
|
||||||
if (this.widgetListeners[event.data.action]) {
|
|
||||||
for (const fn of this.widgetListeners[event.data.action]) {
|
|
||||||
fn(event.data, event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Although the requestId is required, we don't use it. We'll be nice and process the message
|
|
||||||
// if the property is missing, but with a warning for widget developers.
|
|
||||||
if (!event.data.requestId) {
|
|
||||||
console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
|
|
||||||
}
|
|
||||||
|
|
||||||
const action = event.data.action;
|
|
||||||
const widgetId = event.data.widgetId;
|
|
||||||
if (action === 'content_loaded') {
|
|
||||||
console.log('Widget reported content loaded for', widgetId);
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'widget_content_loaded',
|
|
||||||
widgetId: widgetId,
|
|
||||||
});
|
|
||||||
this.sendResponse(event, {success: true});
|
|
||||||
} else if (action === 'supported_api_versions') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
|
||||||
});
|
|
||||||
} else if (action === 'api_version') {
|
|
||||||
this.sendResponse(event, {
|
|
||||||
api: INBOUND_API_NAME,
|
|
||||||
version: WIDGET_API_VERSION,
|
|
||||||
});
|
|
||||||
} else if (action === 'm.sticker') {
|
|
||||||
// console.warn('Got sticker message from widget', widgetId);
|
|
||||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
|
||||||
const data = event.data.data || event.data.widgetData;
|
|
||||||
dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
|
|
||||||
} else if (action === 'integration_manager_open') {
|
|
||||||
// Close the stickerpicker
|
|
||||||
dis.dispatch({action: 'stickerpicker_close'});
|
|
||||||
// Open the integration manager
|
|
||||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
|
||||||
const data = event.data.data || event.data.widgetData;
|
|
||||||
const integType = (data && data.integType) ? data.integType : null;
|
|
||||||
const integId = (data && data.integId) ? data.integId : null;
|
|
||||||
|
|
||||||
// TODO: Open the right integration manager for the widget
|
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
|
||||||
IntegrationManagers.sharedInstance().openAll(
|
|
||||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
|
||||||
`type_${integType}`,
|
|
||||||
integId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
|
||||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
|
||||||
`type_${integType}`,
|
|
||||||
integId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (action === 'set_always_on_screen') {
|
|
||||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
|
||||||
const data = event.data.data;
|
|
||||||
const val = data.value;
|
|
||||||
|
|
||||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
|
||||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
// acknowledge
|
|
||||||
this.sendResponse(event, {});
|
|
||||||
} else if (action === 'get_openid') {
|
|
||||||
// Handled by caller
|
|
||||||
} else {
|
|
||||||
console.warn('Widget postMessage event unhandled');
|
|
||||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if message origin is registered as trusted
|
|
||||||
* @param {string} origin PostMessage origin to check
|
|
||||||
* @return {boolean} True if trusted
|
|
||||||
*/
|
|
||||||
trustedEndpoint(origin) {
|
|
||||||
if (!origin) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.widgetMessagingEndpoints.some((endpoint) => {
|
|
||||||
// TODO / FIXME -- Should this also check the widgetId?
|
|
||||||
return endpoint.endpointUrl === origin;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a postmessage response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {Object} res Response data
|
|
||||||
*/
|
|
||||||
sendResponse(event, res) {
|
|
||||||
const data = objectClone(event.data);
|
|
||||||
data.response = res;
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an error response to a postMessage request
|
|
||||||
* @param {Event} event The original postMessage request event
|
|
||||||
* @param {string} msg Error message
|
|
||||||
* @param {Error} nestedError Nested error event (optional)
|
|
||||||
*/
|
|
||||||
sendError(event, msg, nestedError) {
|
|
||||||
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
|
||||||
const data = objectClone(event.data);
|
|
||||||
data.response = {
|
|
||||||
error: {
|
|
||||||
message: msg,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
if (nestedError) {
|
|
||||||
data.response.error._error = nestedError;
|
|
||||||
}
|
|
||||||
event.source.postMessage(data, event.origin);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,84 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// const OUTBOUND_API_NAME = 'toWidget';
|
|
||||||
|
|
||||||
// Initiate requests using the "toWidget" postMessage API and handle responses
|
|
||||||
// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
|
|
||||||
// response field
|
|
||||||
export default class ToWidgetPostMessageApi {
|
|
||||||
constructor(timeoutMs) {
|
|
||||||
this._timeoutMs = timeoutMs || 5000; // default to 5s timer
|
|
||||||
this._counter = 0;
|
|
||||||
this._requestMap = {
|
|
||||||
// $ID: {resolve, reject}
|
|
||||||
};
|
|
||||||
this.start = this.start.bind(this);
|
|
||||||
this.stop = this.stop.bind(this);
|
|
||||||
this.onPostMessage = this.onPostMessage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
window.addEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
window.removeEventListener('message', this.onPostMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPostMessage(ev) {
|
|
||||||
// THIS IS ALL UNSAFE EXECUTION.
|
|
||||||
// We do not verify who the sender of `ev` is!
|
|
||||||
const payload = ev.data;
|
|
||||||
// NOTE: Workaround for running in a mobile WebView where a
|
|
||||||
// postMessage immediately triggers this callback even though it is
|
|
||||||
// not the response.
|
|
||||||
if (payload.response === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const promise = this._requestMap[payload.requestId];
|
|
||||||
if (!promise) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete this._requestMap[payload.requestId];
|
|
||||||
promise.resolve(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initiate outbound requests (toWidget)
|
|
||||||
exec(action, targetWindow, targetOrigin) {
|
|
||||||
targetWindow = targetWindow || window.parent; // default to parent window
|
|
||||||
targetOrigin = targetOrigin || "*";
|
|
||||||
this._counter += 1;
|
|
||||||
action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this._requestMap[action.requestId] = {resolve, reject};
|
|
||||||
targetWindow.postMessage(action, targetOrigin);
|
|
||||||
|
|
||||||
if (this._timeoutMs > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!this._requestMap[action.requestId]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
|
|
||||||
this._requestMap);
|
|
||||||
this._requestMap[action.requestId].reject(new Error("Timed out"));
|
|
||||||
delete this._requestMap[action.requestId];
|
|
||||||
}, this._timeoutMs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,223 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
Copyright 2019 Travis Ralston
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
|
|
||||||
* spec. details / documentation.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
|
|
||||||
import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
|
|
||||||
import Modal from "./Modal";
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
|
|
||||||
import WidgetUtils from "./utils/WidgetUtils";
|
|
||||||
import {KnownWidgetActions} from "./widgets/WidgetApi";
|
|
||||||
|
|
||||||
if (!global.mxFromWidgetMessaging) {
|
|
||||||
global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
|
|
||||||
global.mxFromWidgetMessaging.start();
|
|
||||||
}
|
|
||||||
if (!global.mxToWidgetMessaging) {
|
|
||||||
global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
|
|
||||||
global.mxToWidgetMessaging.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
const OUTBOUND_API_NAME = 'toWidget';
|
|
||||||
|
|
||||||
export default class WidgetMessaging {
|
|
||||||
/**
|
|
||||||
* @param {string} widgetId The widget's ID
|
|
||||||
* @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
|
|
||||||
* @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
|
|
||||||
* or a different URL of the clients choosing if it is using its own impl).
|
|
||||||
* @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
|
|
||||||
* @param {object} target Where widget messages should be sent (eg. the iframe object)
|
|
||||||
*/
|
|
||||||
constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
|
|
||||||
this.widgetId = widgetId;
|
|
||||||
this.wurl = wurl;
|
|
||||||
this.renderedUrl = renderedUrl;
|
|
||||||
this.isUserWidget = isUserWidget;
|
|
||||||
this.target = target;
|
|
||||||
this.fromWidget = global.mxFromWidgetMessaging;
|
|
||||||
this.toWidget = global.mxToWidgetMessaging;
|
|
||||||
this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
|
|
||||||
this.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
messageToWidget(action) {
|
|
||||||
action.widgetId = this.widgetId; // Required to be sent for all outbound requests
|
|
||||||
|
|
||||||
return this.toWidget.exec(action, this.target).then((data) => {
|
|
||||||
// Check for errors and reject if found
|
|
||||||
if (data.response === undefined) { // null is valid
|
|
||||||
throw new Error("Missing 'response' field");
|
|
||||||
}
|
|
||||||
if (data.response && data.response.error) {
|
|
||||||
const err = data.response.error;
|
|
||||||
const msg = String(err.message ? err.message : "An error was returned");
|
|
||||||
if (err._error) {
|
|
||||||
console.error(err._error);
|
|
||||||
}
|
|
||||||
// Potential XSS attack if 'msg' is not appropriately sanitized,
|
|
||||||
// as it is untrusted input by our parent window (which we assume is Element).
|
|
||||||
// We can't aggressively sanitize [A-z0-9] since it might be a translation.
|
|
||||||
throw new Error(msg);
|
|
||||||
}
|
|
||||||
// Return the response field for the request
|
|
||||||
return data.response;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget that the client is ready to handle further widget requests.
|
|
||||||
* @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
|
|
||||||
*/
|
|
||||||
flagReadyToContinue() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.ClientReady,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget that it should terminate now.
|
|
||||||
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
|
|
||||||
*/
|
|
||||||
terminate() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.Terminate,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tells the widget to hang up on its call.
|
|
||||||
* @returns {Promise<*>} Resolves when the widget has acknowledged the message.
|
|
||||||
*/
|
|
||||||
hangup() {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: KnownWidgetActions.Hangup,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request a screenshot from a widget
|
|
||||||
* @return {Promise} To be resolved with screenshot data when it has been generated
|
|
||||||
*/
|
|
||||||
getScreenshot() {
|
|
||||||
console.log('Requesting screenshot for', this.widgetId);
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "screenshot",
|
|
||||||
})
|
|
||||||
.catch((error) => new Error("Failed to get screenshot: " + error.message))
|
|
||||||
.then((response) => response.screenshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Request capabilities required by the widget
|
|
||||||
* @return {Promise} To be resolved with an array of requested widget capabilities
|
|
||||||
*/
|
|
||||||
getCapabilities() {
|
|
||||||
console.log('Requesting capabilities for', this.widgetId);
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "capabilities",
|
|
||||||
}).then((response) => {
|
|
||||||
console.log('Got capabilities for', this.widgetId, response.capabilities);
|
|
||||||
return response.capabilities;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sendVisibility(visible) {
|
|
||||||
return this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "visibility",
|
|
||||||
visible,
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Failed to send visibility: ", error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
|
|
||||||
this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
stop() {
|
|
||||||
this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
|
|
||||||
this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async _onOpenIdRequest(ev, rawEv) {
|
|
||||||
if (ev.widgetId !== this.widgetId) return; // not interesting
|
|
||||||
|
|
||||||
const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
|
|
||||||
|
|
||||||
const settings = SettingsStore.getValue("widgetOpenIDPermissions");
|
|
||||||
if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
|
|
||||||
this.fromWidget.sendResponse(rawEv, {state: "blocked"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
|
|
||||||
const responseBody = {state: "allowed"};
|
|
||||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
|
||||||
Object.assign(responseBody, credentials);
|
|
||||||
this.fromWidget.sendResponse(rawEv, responseBody);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm that we received the request
|
|
||||||
this.fromWidget.sendResponse(rawEv, {state: "request"});
|
|
||||||
|
|
||||||
// Actually ask for permission to send the user's data
|
|
||||||
Modal.createTrackedDialog("OpenID widget permissions", '',
|
|
||||||
WidgetOpenIDPermissionsDialog, {
|
|
||||||
widgetUrl: this.wurl,
|
|
||||||
widgetId: this.widgetId,
|
|
||||||
isUserWidget: this.isUserWidget,
|
|
||||||
|
|
||||||
onFinished: async (confirm) => {
|
|
||||||
const responseBody = {
|
|
||||||
// Legacy (early draft) fields
|
|
||||||
success: confirm,
|
|
||||||
|
|
||||||
// New style MSC1960 fields
|
|
||||||
state: confirm ? "allowed" : "blocked",
|
|
||||||
original_request_id: ev.requestId, // eslint-disable-line camelcase
|
|
||||||
};
|
|
||||||
if (confirm) {
|
|
||||||
const credentials = await MatrixClientPeg.get().getOpenIdToken();
|
|
||||||
Object.assign(responseBody, credentials);
|
|
||||||
}
|
|
||||||
this.messageToWidget({
|
|
||||||
api: OUTBOUND_API_NAME,
|
|
||||||
action: "openid_credentials",
|
|
||||||
data: responseBody,
|
|
||||||
}).catch((error) => {
|
|
||||||
console.error("Failed to send OpenID credentials: ", error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents mapping of widget instance to URLs for trusted postMessage communication.
|
|
||||||
*/
|
|
||||||
export default class WidgetMessageEndpoint {
|
|
||||||
/**
|
|
||||||
* Mapping of widget instance to URL for trusted postMessage communication.
|
|
||||||
* @param {string} widgetId Unique widget identifier
|
|
||||||
* @param {string} endpointUrl Widget wurl origin.
|
|
||||||
*/
|
|
||||||
constructor(widgetId, endpointUrl) {
|
|
||||||
if (!widgetId) {
|
|
||||||
throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
if (!endpointUrl) {
|
|
||||||
throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
|
|
||||||
}
|
|
||||||
this.widgetId = widgetId;
|
|
||||||
this.endpointUrl = endpointUrl;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,12 +36,12 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
import PersistedElement from "./PersistedElement";
|
import PersistedElement from "./PersistedElement";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
import {Capability} from "../../../widgets/WidgetApi";
|
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
import WidgetStore from "../../../stores/WidgetStore";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||||
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -305,7 +305,7 @@ export default class AppTile extends React.Component {
|
||||||
if (payload.widgetId === this.props.app.id) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
if (this._sgWidget.widgetApi.hasCapability(Capability.Sticker)) {
|
if (this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.StickerSending)) {
|
||||||
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
dis.dispatch({action: 'post_sticker_message', data: payload.data});
|
||||||
} else {
|
} else {
|
||||||
console.warn('Ignoring sticker message. Invalid capability');
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
|
@ -562,7 +562,7 @@ export default class AppTile extends React.Component {
|
||||||
const canUserModify = this._canUserModify();
|
const canUserModify = this._canUserModify();
|
||||||
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
|
||||||
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
|
||||||
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(Capability.Screenshot)
|
const showPictureSnapshotButton = this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots)
|
||||||
&& this.props.show;
|
&& this.props.show;
|
||||||
|
|
||||||
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
|
||||||
|
|
|
@ -36,11 +36,12 @@ import IconizedContextMenu, {
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../context_menus/IconizedContextMenu";
|
} from "../context_menus/IconizedContextMenu";
|
||||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
||||||
import {Capability} from "../../../widgets/WidgetApi";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
|
import { Capability } from "matrix-widget-api/lib/interfaces/Capabilities";
|
||||||
|
import { MatrixCapabilities } from "matrix-widget-api";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
|
@ -80,7 +81,7 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
let snapshotButton;
|
let snapshotButton;
|
||||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
||||||
if (widgetMessaging?.hasCapability(Capability.Screenshot)) {
|
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||||
const onSnapshotClick = () => {
|
const onSnapshotClick = () => {
|
||||||
widgetMessaging.takeScreenshot().then(data => {
|
widgetMessaging.takeScreenshot().then(data => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
|
@ -66,14 +66,7 @@ class ActiveWidgetStore extends EventEmitter {
|
||||||
if (id !== this._persistentWidgetId) return;
|
if (id !== this._persistentWidgetId) return;
|
||||||
const toDeleteId = this._persistentWidgetId;
|
const toDeleteId = this._persistentWidgetId;
|
||||||
|
|
||||||
const result = WidgetMessagingStore.instance.findWidgetById(id);
|
WidgetMessagingStore.instance.stopMessagingById(id);
|
||||||
if (result) {
|
|
||||||
if (result.room) {
|
|
||||||
WidgetMessagingStore.instance.stopMessagingForRoomWidget(result.room, result.widget);
|
|
||||||
} else {
|
|
||||||
WidgetMessagingStore.instance.stopMessagingForAccountWidget(result.widget);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setWidgetPersistence(toDeleteId, false);
|
this.setWidgetPersistence(toDeleteId, false);
|
||||||
this.delRoomId(toDeleteId);
|
this.delRoomId(toDeleteId);
|
||||||
|
|
|
@ -16,12 +16,16 @@
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {
|
import {
|
||||||
ClientWidgetApi, IStickerActionRequest,
|
ClientWidgetApi,
|
||||||
|
IStickerActionRequest,
|
||||||
IStickyActionRequest,
|
IStickyActionRequest,
|
||||||
IWidget, IWidgetApiRequest,
|
IWidget,
|
||||||
|
IWidgetApiRequest,
|
||||||
IWidgetApiRequestEmptyData,
|
IWidgetApiRequestEmptyData,
|
||||||
IWidgetData,
|
IWidgetData,
|
||||||
Widget, WidgetApiFromWidgetAction
|
MatrixCapabilities,
|
||||||
|
Widget,
|
||||||
|
WidgetApiFromWidgetAction
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
|
import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
|
@ -33,11 +37,9 @@ import WidgetUtils from '../../utils/WidgetUtils';
|
||||||
import { IntegrationManagers } from "../../integrations/IntegrationManagers";
|
import { IntegrationManagers } from "../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import { WidgetType } from "../../widgets/WidgetType";
|
import { WidgetType } from "../../widgets/WidgetType";
|
||||||
import { Capability } from "../../widgets/WidgetApi";
|
|
||||||
import ActiveWidgetStore from "../ActiveWidgetStore";
|
import ActiveWidgetStore from "../ActiveWidgetStore";
|
||||||
import { objectShallowClone } from "../../utils/objects";
|
import { objectShallowClone } from "../../utils/objects";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
|
||||||
import { ElementWidgetActions } from "./ElementWidgetActions";
|
import { ElementWidgetActions } from "./ElementWidgetActions";
|
||||||
|
|
||||||
// TODO: Destroy all of this code
|
// TODO: Destroy all of this code
|
||||||
|
@ -171,7 +173,7 @@ export class StopGapWidget extends EventEmitter {
|
||||||
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
|
||||||
this.messaging.addEventListener("action:set_always_on_screen",
|
this.messaging.addEventListener("action:set_always_on_screen",
|
||||||
(ev: CustomEvent<IStickyActionRequest>) => {
|
(ev: CustomEvent<IStickyActionRequest>) => {
|
||||||
if (this.messaging.hasCapability(Capability.AlwaysOnScreen)) {
|
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
|
||||||
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
|
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
|
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
|
||||||
|
|
|
@ -61,6 +61,15 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<unknown> {
|
||||||
return this.widgetMap.get(widget.id);
|
return this.widgetMap.get(widget.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the widget messaging instance for a given widget ID.
|
||||||
|
* @param {string} widgetId The widget ID.
|
||||||
|
* @deprecated Widget IDs are not globally unique.
|
||||||
|
*/
|
||||||
|
public stopMessagingById(widgetId: string) {
|
||||||
|
this.widgetMap.remove(widgetId)?.stop();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the widget messaging class for a given widget ID.
|
* Gets the widget messaging class for a given widget ID.
|
||||||
* @param {string} widgetId The widget ID.
|
* @param {string} widgetId The widget ID.
|
||||||
|
|
|
@ -28,11 +28,11 @@ const WIDGET_WAIT_TIME = 20000;
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
||||||
import {Capability} from "../widgets/WidgetApi";
|
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {WidgetType} from "../widgets/WidgetType";
|
import {WidgetType} from "../widgets/WidgetType";
|
||||||
import {objectClone} from "./objects";
|
import {objectClone} from "./objects";
|
||||||
import {_t} from "../languageHandler";
|
import {_t} from "../languageHandler";
|
||||||
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
|
||||||
export default class WidgetUtils {
|
export default class WidgetUtils {
|
||||||
/* Returns true if user is able to send state events to modify widgets in this room
|
/* Returns true if user is able to send state events to modify widgets in this room
|
||||||
|
@ -416,13 +416,13 @@ export default class WidgetUtils {
|
||||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
||||||
|
|
||||||
const capWhitelist = enableScreenshots ? [Capability.Screenshot] : [];
|
const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : [];
|
||||||
|
|
||||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||||
// so this doesn't really offer much over the set of domains we load
|
// so this doesn't really offer much over the set of domains we load
|
||||||
// widgets from at all, but it probably makes sense for sanity.
|
// widgets from at all, but it probably makes sense for sanity.
|
||||||
if (WidgetType.JITSI.matches(appType)) {
|
if (WidgetType.JITSI.matches(appType)) {
|
||||||
capWhitelist.push(Capability.AlwaysOnScreen);
|
capWhitelist.push(MatrixCapabilities.AlwaysOnScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
return capWhitelist;
|
return capWhitelist;
|
||||||
|
|
|
@ -1,222 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Dev note: This is largely inspired by Dimension. Used with permission.
|
|
||||||
// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
|
|
||||||
|
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
|
||||||
import { EventEmitter } from "events";
|
|
||||||
import { objectClone } from "../utils/objects";
|
|
||||||
|
|
||||||
export enum Capability {
|
|
||||||
Screenshot = "m.capability.screenshot",
|
|
||||||
Sticker = "m.sticker",
|
|
||||||
AlwaysOnScreen = "m.always_on_screen",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum KnownWidgetActions {
|
|
||||||
GetSupportedApiVersions = "supported_api_versions",
|
|
||||||
TakeScreenshot = "screenshot",
|
|
||||||
GetCapabilities = "capabilities",
|
|
||||||
SendEvent = "send_event",
|
|
||||||
UpdateVisibility = "visibility",
|
|
||||||
GetOpenIDCredentials = "get_openid",
|
|
||||||
ReceiveOpenIDCredentials = "openid_credentials",
|
|
||||||
SetAlwaysOnScreen = "set_always_on_screen",
|
|
||||||
ClientReady = "im.vector.ready",
|
|
||||||
Terminate = "im.vector.terminate",
|
|
||||||
Hangup = "im.vector.hangup",
|
|
||||||
}
|
|
||||||
|
|
||||||
export type WidgetAction = KnownWidgetActions | string;
|
|
||||||
|
|
||||||
export enum WidgetApiType {
|
|
||||||
ToWidget = "toWidget",
|
|
||||||
FromWidget = "fromWidget",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WidgetRequest {
|
|
||||||
api: WidgetApiType;
|
|
||||||
widgetId: string;
|
|
||||||
requestId: string;
|
|
||||||
data: any;
|
|
||||||
action: WidgetAction;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ToWidgetRequest extends WidgetRequest {
|
|
||||||
api: WidgetApiType.ToWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FromWidgetRequest extends WidgetRequest {
|
|
||||||
api: WidgetApiType.FromWidget;
|
|
||||||
response: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OpenIDCredentials {
|
|
||||||
accessToken: string;
|
|
||||||
tokenType: string;
|
|
||||||
matrixServerName: string;
|
|
||||||
expiresIn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles Element <--> Widget interactions for embedded/standalone widgets.
|
|
||||||
*
|
|
||||||
* Emitted events:
|
|
||||||
* - terminate(wait): client requested the widget to terminate.
|
|
||||||
* Call the argument 'wait(promise)' to postpone the finalization until
|
|
||||||
* the given promise resolves.
|
|
||||||
*/
|
|
||||||
export class WidgetApi extends EventEmitter {
|
|
||||||
private readonly origin: string;
|
|
||||||
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
|
|
||||||
private readonly readyPromise: Promise<any>;
|
|
||||||
private readyPromiseResolve: () => void;
|
|
||||||
private openIDCredentialsCallback: () => void;
|
|
||||||
public openIDCredentials: OpenIDCredentials;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
|
|
||||||
*/
|
|
||||||
public expectingExplicitReady = false;
|
|
||||||
|
|
||||||
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.origin = new URL(currentUrl).origin;
|
|
||||||
|
|
||||||
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
|
||||||
|
|
||||||
window.addEventListener("message", event => {
|
|
||||||
if (event.origin !== this.origin) return; // ignore: invalid origin
|
|
||||||
if (!event.data) return; // invalid schema
|
|
||||||
if (event.data.widgetId !== this.widgetId) return; // not for us
|
|
||||||
|
|
||||||
const payload = <WidgetRequest>event.data;
|
|
||||||
if (payload.api === WidgetApiType.ToWidget && payload.action) {
|
|
||||||
console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
|
|
||||||
|
|
||||||
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
|
||||||
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
|
||||||
if (!this.expectingExplicitReady) {
|
|
||||||
this.readyPromiseResolve();
|
|
||||||
}
|
|
||||||
} else if (payload.action === KnownWidgetActions.ClientReady) {
|
|
||||||
this.readyPromiseResolve();
|
|
||||||
|
|
||||||
// Automatically acknowledge so we can move on
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
} else if (payload.action === KnownWidgetActions.Terminate
|
|
||||||
|| payload.action === KnownWidgetActions.Hangup) {
|
|
||||||
// Finalization needs to be async, so postpone with a promise
|
|
||||||
let finalizePromise = Promise.resolve();
|
|
||||||
const wait = (promise) => {
|
|
||||||
finalizePromise = finalizePromise.then(() => promise);
|
|
||||||
};
|
|
||||||
const emitName = payload.action === KnownWidgetActions.Terminate ? 'terminate' : 'hangup';
|
|
||||||
this.emit(emitName, wait);
|
|
||||||
Promise.resolve(finalizePromise).then(() => {
|
|
||||||
// Acknowledge that we're shut down now
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
});
|
|
||||||
} else if (payload.action === KnownWidgetActions.ReceiveOpenIDCredentials) {
|
|
||||||
// Save OpenID credentials
|
|
||||||
this.setOpenIDCredentials(<ToWidgetRequest>payload);
|
|
||||||
this.replyToRequest(<ToWidgetRequest>payload, {});
|
|
||||||
} else {
|
|
||||||
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
|
||||||
}
|
|
||||||
} else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
|
|
||||||
console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
|
|
||||||
const handler = this.inFlightRequests[payload.requestId];
|
|
||||||
delete this.inFlightRequests[payload.requestId];
|
|
||||||
handler(<FromWidgetRequest>payload);
|
|
||||||
} else {
|
|
||||||
console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setOpenIDCredentials(value: WidgetRequest) {
|
|
||||||
const data = value.data;
|
|
||||||
if (data.state === 'allowed') {
|
|
||||||
this.openIDCredentials = {
|
|
||||||
accessToken: data.access_token,
|
|
||||||
tokenType: data.token_type,
|
|
||||||
matrixServerName: data.matrix_server_name,
|
|
||||||
expiresIn: data.expires_in,
|
|
||||||
}
|
|
||||||
} else if (data.state === 'blocked') {
|
|
||||||
this.openIDCredentials = null;
|
|
||||||
}
|
|
||||||
if (['allowed', 'blocked'].includes(data.state) && this.openIDCredentialsCallback) {
|
|
||||||
this.openIDCredentialsCallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public requestOpenIDCredentials(credentialsResponseCallback: () => void) {
|
|
||||||
this.openIDCredentialsCallback = credentialsResponseCallback;
|
|
||||||
this.callAction(
|
|
||||||
KnownWidgetActions.GetOpenIDCredentials,
|
|
||||||
{},
|
|
||||||
this.setOpenIDCredentials,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public waitReady(): Promise<any> {
|
|
||||||
return this.readyPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
private replyToRequest(payload: ToWidgetRequest, reply: any) {
|
|
||||||
if (!window.parent) return;
|
|
||||||
|
|
||||||
const request: ToWidgetRequest & {response?: any} = objectClone(payload);
|
|
||||||
request.response = reply;
|
|
||||||
|
|
||||||
window.parent.postMessage(request, this.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onCapabilitiesRequest(payload: ToWidgetRequest) {
|
|
||||||
return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
|
|
||||||
}
|
|
||||||
|
|
||||||
public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
|
|
||||||
if (!window.parent) return;
|
|
||||||
|
|
||||||
const request: FromWidgetRequest = {
|
|
||||||
api: WidgetApiType.FromWidget,
|
|
||||||
widgetId: this.widgetId,
|
|
||||||
action: action,
|
|
||||||
requestId: randomString(160),
|
|
||||||
data: payload,
|
|
||||||
response: {}, // Not used at this layer - it's used when the client responds
|
|
||||||
};
|
|
||||||
|
|
||||||
if (callback) {
|
|
||||||
this.inFlightRequests[request.requestId] = callback;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[WidgetAPI] Sending request: `, request);
|
|
||||||
window.parent.postMessage(request, "*");
|
|
||||||
}
|
|
||||||
|
|
||||||
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
|
||||||
return new Promise<any>(resolve => {
|
|
||||||
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, null);
|
|
||||||
resolve(); // SetAlwaysOnScreen is currently fire-and-forget, but that could change.
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO: Move to matrix-widget-api
|
||||||
export class WidgetType {
|
export class WidgetType {
|
||||||
public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
|
public static readonly JITSI = new WidgetType("m.jitsi", "jitsi");
|
||||||
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
public static readonly STICKERPICKER = new WidgetType("m.stickerpicker", "m.stickerpicker");
|
||||||
|
|
Loading…
Reference in a new issue