Refactor integration manager handling into a common place

It was already in a common place, but this is the boilerplate for supporting multiple integration managers, and multiple integration manager sources. 

For https://github.com/vector-im/riot-web/issues/4913 / https://github.com/vector-im/riot-web/issues/10161
This commit is contained in:
Travis Ralston 2019-08-09 16:05:05 -06:00
parent 75bcd61934
commit ffa49df889
11 changed files with 267 additions and 147 deletions

View file

@ -63,7 +63,7 @@ import SdkConfig from './SdkConfig';
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore';
import ScalarAuthClient from './ScalarAuthClient';
import {IntegrationManagers} from "./integrations/IntegrationManagers";
global.mxCalls = {
//room_id: MatrixCall
@ -348,14 +348,20 @@ async function _startCallApp(roomId, type) {
// the state event in anyway, but the resulting widget would then not
// work for us. Better that the user knows before everyone else in the
// room sees it.
const scalarClient = new ScalarAuthClient();
let haveScalar = false;
try {
await scalarClient.connect();
haveScalar = scalarClient.hasCredentials();
} catch (e) {
// fall through
const managers = IntegrationManagers.sharedInstance();
let haveScalar = true;
if (managers.hasManager()) {
try {
const scalarClient = managers.getPrimaryManager().getScalarClient();
await scalarClient.connect();
haveScalar = scalarClient.hasCredentials();
} catch (e) {
// ignore
}
} else {
haveScalar = false;
}
if (!haveScalar) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

View file

@ -22,7 +22,7 @@ import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
import ActiveWidgetStore from './stores/ActiveWidgetStore';
import MatrixClientPeg from "./MatrixClientPeg";
import RoomViewStore from "./stores/RoomViewStore";
import { showIntegrationsManager } from './integrations/integrations';
import {IntegrationManagers} from "./integrations/IntegrationManagers";
const WIDGET_API_VERSION = '0.0.2'; // Current API version
const SUPPORTED_WIDGET_API_VERSIONS = [
@ -193,11 +193,12 @@ export default class FromWidgetPostMessageApi {
const integType = (data && data.integType) ? data.integType : null;
const integId = (data && data.integId) ? data.integId : null;
showIntegrationsManager({
room: MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
screen: 'type_' + integType,
integrationId: integId,
});
// TODO: Open the right integration manager for the widget
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;

View file

@ -29,20 +29,43 @@ import * as Matrix from 'matrix-js-sdk';
// The version of the integration manager API we're intending to work with
const imApiVersion = "1.1";
class ScalarAuthClient {
constructor() {
export default class ScalarAuthClient {
constructor(apiUrl, uiUrl) {
this.apiUrl = apiUrl;
this.uiUrl = uiUrl;
this.scalarToken = null;
// `undefined` to allow `startTermsFlow` to fallback to a default
// callback if this is unset.
this.termsInteractionCallback = undefined;
// We try and store the token on a per-manager basis, but need a fallback
// for the default manager.
const configApiUrl = SdkConfig.get()['integrations_rest_url'];
const configUiUrl = SdkConfig.get()['integrations_ui_url'];
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
}
/**
* Determines if setting up a ScalarAuthClient is even possible
* @returns {boolean} true if possible, false otherwise.
*/
static isPossible() {
return SdkConfig.get()['integrations_rest_url'] && SdkConfig.get()['integrations_ui_url'];
_writeTokenToStore() {
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
if (this.isDefaultManager) {
// We remove the old token from storage to migrate upwards. This is safe
// to do because even if the user switches to /app when this is on /develop
// they'll at worst register for a new token.
window.localStorage.removeItem("mx_scalar_token"); // no-op when not present
}
}
_readTokenFromStore() {
let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl);
if (!token && this.isDefaultManager) {
token = window.localStorage.getItem("mx_scalar_token");
}
return token;
}
_readToken() {
if (this.scalarToken) return this.scalarToken;
return this._readTokenFromStore();
}
setTermsInteractionCallback(callback) {
@ -61,8 +84,7 @@ class ScalarAuthClient {
// Returns a promise that resolves to a scalar_token string
getScalarToken() {
let token = this.scalarToken;
if (!token) token = window.localStorage.getItem("mx_scalar_token");
const token = this._readToken();
if (!token) {
return this.registerForToken();
@ -78,7 +100,7 @@ class ScalarAuthClient {
}
_getAccountName(token) {
const url = SdkConfig.get().integrations_rest_url + "/account";
const url = this.apiUrl + "/account";
return new Promise(function(resolve, reject) {
request({
@ -111,7 +133,7 @@ class ScalarAuthClient {
return token;
}).catch((e) => {
if (e instanceof TermsNotSignedError) {
console.log("Integrations manager requires new terms to be agreed to");
console.log("Integration manager requires new terms to be agreed to");
// The terms endpoints are new and so live on standard _matrix prefixes,
// but IM rest urls are currently configured with paths, so remove the
// path from the base URL before passing it to the js-sdk
@ -126,7 +148,7 @@ class ScalarAuthClient {
// Once we've fully transitioned to _matrix URLs, we can give people
// a grace period to update their configs, then use the rest url as
// a regular base url.
const parsedImRestUrl = url.parse(SdkConfig.get().integrations_rest_url);
const parsedImRestUrl = url.parse(this.apiUrl);
parsedImRestUrl.path = '';
parsedImRestUrl.pathname = '';
return startTermsFlow([new Service(
@ -147,17 +169,18 @@ class ScalarAuthClient {
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject);
}).then((tokenObject) => {
}).then((token) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
return this._checkToken(tokenObject);
}).then((tokenObject) => {
window.localStorage.setItem("mx_scalar_token", tokenObject);
return tokenObject;
return this._checkToken(token);
}).then((token) => {
this.scalarToken = token;
this._writeTokenToStore();
return token;
});
}
exchangeForScalarToken(openidTokenObject) {
const scalarRestUrl = SdkConfig.get().integrations_rest_url;
const scalarRestUrl = this.apiUrl;
return new Promise(function(resolve, reject) {
request({
@ -181,7 +204,7 @@ class ScalarAuthClient {
}
getScalarPageTitle(url) {
let scalarPageLookupUrl = SdkConfig.get().integrations_rest_url + '/widgets/title_lookup';
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
@ -217,7 +240,7 @@ class ScalarAuthClient {
* @return {Promise} Resolves on completion
*/
disableWidgetAssets(widgetType, widgetId) {
let url = SdkConfig.get().integrations_rest_url + '/widgets/set_assets_state';
let url = this.apiUrl + '/widgets/set_assets_state';
url = this.getStarterLink(url);
return new Promise((resolve, reject) => {
request({
@ -246,7 +269,7 @@ class ScalarAuthClient {
getScalarInterfaceUrlForRoom(room, screen, id) {
const roomId = room.roomId;
const roomName = room.name;
let url = SdkConfig.get().integrations_ui_url;
let url = this.uiUrl;
url += "?scalar_token=" + encodeURIComponent(this.scalarToken);
url += "&room_id=" + encodeURIComponent(roomId);
url += "&room_name=" + encodeURIComponent(roomName);
@ -264,5 +287,3 @@ class ScalarAuthClient {
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
}
}
module.exports = ScalarAuthClient;

View file

@ -22,7 +22,6 @@ import qs from 'querystring';
import React from 'react';
import PropTypes from 'prop-types';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import WidgetMessaging from '../../../WidgetMessaging';
import AccessibleButton from './AccessibleButton';
import Modal from '../../../Modal';
@ -35,7 +34,7 @@ import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -178,9 +177,22 @@ export default class AppTile extends React.Component {
return;
}
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
console.warn("No integration manager - not setting scalar token", url);
this.setState({
error: null,
widgetUrl: this._addWurlParams(this.props.url),
initialising: false,
});
return;
}
// TODO: Pick the right manager for the widget
// Fetch the token before loading the iframe as we need it to mangle the URL
if (!this._scalarClient) {
this._scalarClient = new ScalarAuthClient();
this._scalarClient = managers.getPrimaryManager().getScalarClient();
}
this._scalarClient.getScalarToken().done((token) => {
// Append scalar_token as a query param if not already present
@ -189,7 +201,7 @@ export default class AppTile extends React.Component {
const params = qs.parse(u.query);
if (!params.scalar_token) {
params.scalar_token = encodeURIComponent(token);
// u.search must be set to undefined, so that u.format() uses query paramerters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
// u.search must be set to undefined, so that u.format() uses query parameters - https://nodejs.org/docs/latest/api/url.html#url_url_format_url_options
u.search = undefined;
u.query = params;
}
@ -251,11 +263,12 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
showIntegrationsManager({
room: this.props.room,
screen: 'type_' + this.props.type,
integrationId: this.props.id,
});
// TODO: Open the right manager for the widget
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
this.props.type,
this.props.id,
);
}
}

View file

@ -18,9 +18,8 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import { _t } from '../../../languageHandler';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
@ -30,12 +29,17 @@ export default class ManageIntegsButton extends React.Component {
onManageIntegrations = (ev) => {
ev.preventDefault();
showIntegrationsManager({ room: this.props.room });
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
managers.getPrimaryManager().open(this.props.room);
}
};
render() {
let integrationsButton = <div />;
if (ScalarAuthClient.isPossible()) {
if (IntegrationManagers.sharedInstance().hasManager()) {
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
integrationsButton = (
<AccessibleButton

View file

@ -25,7 +25,6 @@ import highlight from 'highlight.js';
import * as HtmlUtils from '../../../HtmlUtils';
import {formatDate} from '../../../DateUtils';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher';
@ -35,6 +34,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import ReplyThread from "../elements/ReplyThread";
import {host as matrixtoHost} from '../../../matrix-to';
import {pillifyLinks} from '../../../utils/pillify';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
module.exports = React.createClass({
displayName: 'TextualBody',
@ -318,12 +318,19 @@ module.exports = React.createClass({
// which requires the user to click through and THEN we can open the link in a new tab because
// the window.open command occurs in the same stack frame as the onClick callback.
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
return;
}
// Go fetch a scalar token
const scalarClient = new ScalarAuthClient();
const integrationManager = managers.getPrimaryManager();
const scalarClient = integrationManager.getScalarClient();
scalarClient.connect().then(() => {
const completeUrl = scalarClient.getStarterLink(starterLink);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const integrationsUrl = SdkConfig.get().integrations_ui_url;
const integrationsUrl = integrationManager.uiUrl;
Modal.createTrackedDialog('Add an integration', '', QuestionDialog, {
title: _t("Add an Integration"),
description:

View file

@ -29,7 +29,7 @@ import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -128,10 +128,7 @@ module.exports = React.createClass({
},
_launchManageIntegrations: function() {
showIntegrationsManager({
room: this.props.room,
screen: 'add_integ',
});
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');
},
onClickAddWidget: function(e) {

View file

@ -18,13 +18,12 @@ import {_t, _td} from '../../../languageHandler';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import ScalarAuthClient from '../../../ScalarAuthClient';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import PersistedElement from "../elements/PersistedElement";
import { showIntegrationsManager } from '../../../integrations/integrations';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
const widgetType = 'm.stickerpicker';
@ -67,8 +66,9 @@ export default class Stickerpicker extends React.Component {
_acquireScalarClient() {
if (this.scalarClient) return Promise.resolve(this.scalarClient);
if (ScalarAuthClient.isPossible()) {
this.scalarClient = new ScalarAuthClient();
// TODO: Pick the right manager for the widget
if (IntegrationManagers.sharedInstance().hasManager()) {
this.scalarClient = IntegrationManagers.sharedInstance().getPrimaryManager().getScalarClient();
return this.scalarClient.connect().then(() => {
this.forceUpdate();
return this.scalarClient;
@ -348,11 +348,12 @@ export default class Stickerpicker extends React.Component {
* Launch the integrations manager on the stickers integration page
*/
_launchManageIntegrations() {
showIntegrationsManager({
room: this.props.room,
screen: `type_${widgetType}`,
integrationId: this.state.widgetId,
});
// TODO: Open the right integration manager for the widget
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
`type_${widgetType}`,
this.state.widgetId,
);
}
render() {

View file

@ -0,0 +1,81 @@
/*
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 ScalarAuthClient from "../ScalarAuthClient";
import sdk from "../index";
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
import type {Room} from "matrix-js-sdk";
import Modal from '../Modal';
export class IntegrationManagerInstance {
apiUrl: string;
uiUrl: string;
constructor(apiUrl: string, uiUrl: string) {
this.apiUrl = apiUrl;
this.uiUrl = uiUrl;
// Per the spec: UI URL is optional.
if (!this.uiUrl) this.uiUrl = this.apiUrl;
}
getScalarClient(): ScalarAuthClient {
return new ScalarAuthClient(this.apiUrl, this.uiUrl);
}
async open(room: Room = null, screen: string = null, integrationId: string = null): void {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
const dialog = Modal.createTrackedDialog(
'Integration Manager', '', IntegrationsManager,
{loading: true}, 'mx_IntegrationsManager',
);
const client = this.getScalarClient();
client.setTermsInteractionCallback((policyInfo, agreedUrls) => {
// To avoid visual glitching of two modals stacking briefly, we customise the
// terms dialog sizing when it will appear for the integrations manager so that
// it gets the same basic size as the IM's own modal.
return dialogTermsInteractionCallback(
policyInfo, agreedUrls, 'mx_TermsDialog_forIntegrationsManager',
);
});
let newProps = {};
try {
await client.connect();
if (!client.hasCredentials()) {
newProps["connected"] = false;
} else {
newProps["url"] = client.getScalarInterfaceUrlForRoom(room, screen, integrationId);
}
} catch (e) {
if (e instanceof TermsNotSignedError) {
dialog.close();
return;
}
console.error(e);
props["connected"] = false;
}
// Close the old dialog and open a new one
dialog.close();
Modal.createTrackedDialog(
'Integration Manager', '', IntegrationsManager,
newProps, 'mx_IntegrationsManager',
);
}
}

View file

@ -0,0 +1,68 @@
/*
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 SdkConfig from '../SdkConfig';
import sdk from "../index";
import Modal from '../Modal';
import {IntegrationManagerInstance} from "./IntegrationManagerInstance";
export class IntegrationManagers {
static _instance;
static sharedInstance(): IntegrationManagers {
if (!IntegrationManagers._instance) {
IntegrationManagers._instance = new IntegrationManagers();
}
return IntegrationManagers._instance;
}
_managers: IntegrationManagerInstance[] = [];
constructor() {
this._setupConfiguredManager();
}
_setupConfiguredManager() {
const apiUrl = SdkConfig.get()['integrations_rest_url'];
const uiUrl = SdkConfig.get()['integrations_ui_url'];
if (apiUrl && uiUrl) {
this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl));
}
}
hasManager(): boolean {
return this._managers.length > 0;
}
getPrimaryManager(): IntegrationManagerInstance {
if (this.hasManager()) {
// TODO: TravisR - Handle custom integration managers (widgets)
return this._managers[0];
} else {
return null;
}
}
openNoManagerDialog(): void {
// TODO: Is it Integrations (plural) or Integration (singular). Singular is easier spoken.
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
Modal.createTrackedDialog(
"Integration Manager", "None", IntegrationsManager,
{configured: false}, 'mx_IntegrationsManager',
);
}
}

View file

@ -1,79 +0,0 @@
/*
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 sdk from "../index";
import ScalarAuthClient from '../ScalarAuthClient';
import Modal from '../Modal';
import { TermsNotSignedError, dialogTermsInteractionCallback } from '../Terms';
export async function showIntegrationsManager(opts) {
const IntegrationsManager = sdk.getComponent("views.settings.IntegrationsManager");
let props = {};
if (ScalarAuthClient.isPossible()) {
props.loading = true;
} else {
props.configured = false;
}
const close = Modal.createTrackedDialog(
'Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager",
).close;
if (!ScalarAuthClient.isPossible()) {
return;
}
const scalarClient = new ScalarAuthClient();
scalarClient.setTermsInteractionCallback(integrationsTermsInteractionCallback);
try {
await scalarClient.connect();
if (!scalarClient.hasCredentials()) {
props = { connected: false };
} else {
props = {
url: scalarClient.getScalarInterfaceUrlForRoom(
opts.room,
opts.screen,
opts.integrationId,
),
};
}
} catch (err) {
if (err instanceof TermsNotSignedError) {
// user canceled terms dialog, so just cancel the action
close();
return;
}
console.error(err);
props = { connected: false };
}
close();
Modal.createTrackedDialog('Integrations Manager', '', IntegrationsManager, props, "mx_IntegrationsManager");
}
/*
* To avoid visual glitching of two modals stacking briefly, we customise the
* terms dialog sizing when it will appear for the integrations manager so that
* it gets the same basic size as the IM's own modal.
*/
function integrationsTermsInteractionCallback(policiesAndServicePairs, agreedUrls) {
return dialogTermsInteractionCallback(
policiesAndServicePairs,
agreedUrls,
"mx_TermsDialog_forIntegrationsManager",
);
}