From 03d735f4ed03fd3692bf02fdb17eeead490d0239 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 12 Aug 2019 15:35:39 -0600 Subject: [PATCH 1/5] Support changing your integration manager in the UI Part of https://github.com/vector-im/riot-web/issues/10161 --- res/css/_components.scss | 1 + .../settings/_SetIntegrationManager.scss | 34 +++++ .../views/settings/SetIntegrationManager.js | 138 ++++++++++++++++++ .../tabs/user/GeneralUserSettingsTab.js | 12 ++ src/i18n/strings/en_EN.json | 7 + .../IntegrationManagerInstance.js | 13 +- src/integrations/IntegrationManagers.js | 74 +++++++++- src/utils/WidgetUtils.js | 14 ++ 8 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 res/css/views/settings/_SetIntegrationManager.scss create mode 100644 src/components/views/settings/SetIntegrationManager.js diff --git a/res/css/_components.scss b/res/css/_components.scss index abfce47916..579369a509 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -169,6 +169,7 @@ @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; @import "./views/settings/_SetIdServer.scss"; +@import "./views/settings/_SetIntegrationManager.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; @import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; @import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss new file mode 100644 index 0000000000..8a1380cd1f --- /dev/null +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -0,0 +1,34 @@ +/* +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. +*/ + +.mx_SetIntegrationManager .mx_Field_input { + margin-right: 100px; // Align with the other fields on the page +} + +.mx_SetIntegrationManager { + margin-top: 10px; + margin-bottom: 10px; +} + +.mx_SetIntegrationManager > .mx_SettingsTab_heading { + margin-bottom: 10px; +} + +.mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading { + display: inline-block; + padding-left: 5px; + font-size: 14px; +} diff --git a/src/components/views/settings/SetIntegrationManager.js b/src/components/views/settings/SetIntegrationManager.js new file mode 100644 index 0000000000..20300f548e --- /dev/null +++ b/src/components/views/settings/SetIntegrationManager.js @@ -0,0 +1,138 @@ +/* +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 React from 'react'; +import {_t} from "../../../languageHandler"; +import sdk from '../../../index'; +import Field from "../elements/Field"; +import {IntegrationManagers} from "../../../integrations/IntegrationManagers"; + +export default class SetIntegrationManager extends React.Component { + constructor() { + super(); + + const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager(); + + this.state = { + currentManager, + url: "", // user-entered text + error: null, + busy: false, + }; + } + + _onUrlChanged = (ev) => { + const u = ev.target.value; + this.setState({url: u}); + }; + + _getTooltip = () => { + if (this.state.busy) { + const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner'); + return
+ + { _t("Checking server") } +
; + } else if (this.state.error) { + return this.state.error; + } else { + return null; + } + }; + + _canChange = () => { + return !!this.state.url && !this.state.busy; + }; + + _setManager = async (ev) => { + // Don't reload the page when the user hits enter in the form. + ev.preventDefault(); + ev.stopPropagation(); + + this.setState({busy: true}); + + const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url); + if (!manager) { + this.setState({ + busy: false, + error: _t("Integration manager offline or not accessible."), + }); + return; + } + + try { + await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager); + this.setState({ + busy: false, + error: null, + currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(), + url: "", // clear input + }); + } catch (e) { + console.error(e); + this.setState({ + busy: false, + error: _t("Failed to update integration manager"), + }); + } + }; + + render() { + const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton'); + + const currentManager = this.state.currentManager; + let managerName; + let bodyText; + if (currentManager) { + managerName = `(${currentManager.name})`; + bodyText = _t( + "You are currently using %(serverName)s to manage your bots, widgets, " + + "and sticker packs.", + {serverName: currentManager.name}, + { b: sub => {sub} }, + ); + } else { + bodyText = _t( + "Add which integration manager you want to manage your bots, widgets, " + + "and sticker packs.", + ); + } + + return ( +
+
+ {_t("Integration Manager")} + {managerName} +
+ + {bodyText} + + + {_t("Change")} + + ); + } +} diff --git a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index 7e0d9f686f..0bf396c740 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -204,6 +204,17 @@ export default class GeneralUserSettingsTab extends React.Component { ); } + _renderIntegrationManagerSection() { + const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager"); + + return ( +
+ { /* has its own heading as it includes the current integration manager */ } + +
+ ); + } + render() { return (
@@ -214,6 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component { {this._renderThemeSection()}
{_t("Discovery")}
{this._renderDiscoverySection()} + {this._renderIntegrationManagerSection() /* Has its own title */}
{_t("Deactivate account")}
{this._renderManagementSection()}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 154871a977..a925010f6e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -548,6 +548,13 @@ "Identity Server": "Identity Server", "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below", "Change": "Change", + "Checking server": "Checking server", + "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", + "Failed to update integration manager": "Failed to update integration manager", + "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", + "Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.", + "Integration Manager": "Integration Manager", + "Enter a new integration manager": "Enter a new integration manager", "Flair": "Flair", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", diff --git a/src/integrations/IntegrationManagerInstance.js b/src/integrations/IntegrationManagerInstance.js index 4d0181f017..c21fff0fd3 100644 --- a/src/integrations/IntegrationManagerInstance.js +++ b/src/integrations/IntegrationManagerInstance.js @@ -19,12 +19,18 @@ import sdk from "../index"; import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms"; import type {Room} from "matrix-js-sdk"; import Modal from '../Modal'; +import url from 'url'; + +export const KIND_ACCOUNT = "account"; +export const KIND_CONFIG = "config"; export class IntegrationManagerInstance { apiUrl: string; uiUrl: string; + kind: string; - constructor(apiUrl: string, uiUrl: string) { + constructor(kind: string, apiUrl: string, uiUrl: string) { + this.kind = kind; this.apiUrl = apiUrl; this.uiUrl = uiUrl; @@ -32,6 +38,11 @@ export class IntegrationManagerInstance { if (!this.uiUrl) this.uiUrl = this.apiUrl; } + get name(): string { + const parsed = url.parse(this.uiUrl); + return parsed.hostname; + } + getScalarClient(): ScalarAuthClient { return new ScalarAuthClient(this.apiUrl, this.uiUrl); } diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 9c9a1fa228..9b852fe61a 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -17,7 +17,7 @@ limitations under the License. import SdkConfig from '../SdkConfig'; import sdk from "../index"; import Modal from '../Modal'; -import {IntegrationManagerInstance} from "./IntegrationManagerInstance"; +import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG} from "./IntegrationManagerInstance"; import type {MatrixClient, MatrixEvent} from "matrix-js-sdk"; import WidgetUtils from "../utils/WidgetUtils"; import MatrixClientPeg from "../MatrixClientPeg"; @@ -62,7 +62,7 @@ export class IntegrationManagers { const uiUrl = SdkConfig.get()['integrations_ui_url']; if (apiUrl && uiUrl) { - this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl)); } } @@ -77,7 +77,7 @@ export class IntegrationManagers { const apiUrl = data['api_url']; if (!apiUrl || !uiUrl) return; - this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl)); + this._managers.push(new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl)); }); } @@ -107,6 +107,74 @@ export class IntegrationManagers { {configured: false}, 'mx_IntegrationsManager', ); } + + async overwriteManagerOnAccount(manager: IntegrationManagerInstance) { + // TODO: TravisR - We should be logging out of scalar clients. + await WidgetUtils.removeIntegrationManagerWidgets(); + + // TODO: TravisR - We should actually be carrying over the discovery response verbatim. + await WidgetUtils.setUserWidget( + "integration_manager_" + (new Date().getTime()), + "m.integration_manager", + manager.uiUrl, + "Integration Manager", + {"api_url": manager.apiUrl}, + ); + } + + /** + * Attempts to discover an integration manager using only its name. + * @param {string} domainName The domain name to look up. + * @returns {Promise} Resolves to an integration manager instance, + * or null if none was found. + */ + async tryDiscoverManager(domainName: string): IntegrationManagerInstance { + console.log("Looking up integration manager via .well-known"); + if (domainName.startsWith("http:") || domainName.startsWith("https:")) { + // trim off the scheme and just use the domain + const url = url.parse(domainName); + domainName = url.host; + } + + let wkConfig; + try { + const result = await fetch(`https://${domainName}/.well-known/matrix/integrations`); + wkConfig = await result.json(); + } catch (e) { + console.error(e); + console.warn("Failed to locate integration manager"); + return null; + } + + if (!wkConfig || !wkConfig["m.integrations_widget"]) { + console.warn("Missing integrations widget on .well-known response"); + return null; + } + + const widget = wkConfig["m.integrations_widget"]; + if (!widget["url"] || !widget["data"] || !widget["data"]["api_url"]) { + console.warn("Malformed .well-known response for integrations widget"); + return null; + } + + // All discovered managers are per-user managers + const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]); + console.log("Got integration manager response, checking for responsiveness"); + + // Test the manager + const client = manager.getScalarClient(); + try { + // not throwing an error is a success here + await client.connect(); + } catch (e) { + console.error(e); + console.warn("Integration manager failed liveliness check"); + return null; + } + + console.log("Integration manager is alive and functioning"); + return manager; + } } // For debugging diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index 1e47554914..edac449ccf 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -351,6 +351,20 @@ export default class WidgetUtils { return widgets.filter(w => w.content && imTypes.includes(w.content.type)); } + static removeIntegrationManagerWidgets() { + const client = MatrixClientPeg.get(); + if (!client) { + throw new Error('User not logged in'); + } + const userWidgets = client.getAccountData('m.widgets').getContent() || {}; + Object.entries(userWidgets).forEach(([key, widget]) => { + if (widget.content && widget.content.type === 'm.integration_manager') { + delete userWidgets[key]; + } + }); + return client.setAccountData('m.widgets', userWidgets); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated From 7a1a2458992a2bc728c4d6d0fbe8b23d614b2728 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:52:05 -0600 Subject: [PATCH 2/5] Fix i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e31a604fe3..22c2786922 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -552,7 +552,6 @@ "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.": "You are not currently using an identity server. To discover and be discoverable by existing contacts you know, add one below.", "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.": "Disconnecting from your identity server will mean you won't be discoverable by other users and you won't be able to invite others by email or phone.", "Change": "Change", - "Checking server": "Checking server", "Integration manager offline or not accessible.": "Integration manager offline or not accessible.", "Failed to update integration manager": "Failed to update integration manager", "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.": "You are currently using %(serverName)s to manage your bots, widgets, and sticker packs.", From 02a41214016d79933e521dcef88f3c8a8663b00e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:55:49 -0600 Subject: [PATCH 3/5] Mixin for field width --- res/css/views/settings/_SetIntegrationManager.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/css/views/settings/_SetIntegrationManager.scss b/res/css/views/settings/_SetIntegrationManager.scss index 8a1380cd1f..7fda042864 100644 --- a/res/css/views/settings/_SetIntegrationManager.scss +++ b/res/css/views/settings/_SetIntegrationManager.scss @@ -15,7 +15,7 @@ limitations under the License. */ .mx_SetIntegrationManager .mx_Field_input { - margin-right: 100px; // Align with the other fields on the page + @mixin mx_Settings_fullWidthField; } .mx_SetIntegrationManager { From 8b1c90a01e567f9f807122ce761267d31a1e7b2f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 08:57:38 -0600 Subject: [PATCH 4/5] Convert to using im.vector.integration_manager for IM widget This avoids us having to throw the entirety of MSC1957 into the queue, particularly when we're only using a third of the MSC. --- src/integrations/IntegrationManagers.js | 8 +------- src/utils/WidgetUtils.js | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/integrations/IntegrationManagers.js b/src/integrations/IntegrationManagers.js index 9b852fe61a..49356676e6 100644 --- a/src/integrations/IntegrationManagers.js +++ b/src/integrations/IntegrationManagers.js @@ -113,13 +113,7 @@ export class IntegrationManagers { await WidgetUtils.removeIntegrationManagerWidgets(); // TODO: TravisR - We should actually be carrying over the discovery response verbatim. - await WidgetUtils.setUserWidget( - "integration_manager_" + (new Date().getTime()), - "m.integration_manager", - manager.uiUrl, - "Integration Manager", - {"api_url": manager.apiUrl}, - ); + await WidgetUtils.addIntegrationManagerWidget(manager.name, manager.uiUrl, manager.apiUrl); } /** diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index edac449ccf..fb79ec0ae7 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -29,6 +29,9 @@ import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; +// We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. +const IM_WIDGET_TYPES = ["m.integration_manager", "im.vector.integration_manager"]; + /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. @@ -346,9 +349,7 @@ export default class WidgetUtils { */ static getIntegrationManagerWidgets() { const widgets = WidgetUtils.getUserWidgetsArray(); - // We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. - const imTypes = ["m.integration_manager", "im.vector.integration_manager"]; - return widgets.filter(w => w.content && imTypes.includes(w.content.type)); + return widgets.filter(w => w.content && IM_WIDGET_TYPES.includes(w.content.type)); } static removeIntegrationManagerWidgets() { @@ -358,13 +359,23 @@ export default class WidgetUtils { } const userWidgets = client.getAccountData('m.widgets').getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.content && widget.content.type === 'm.integration_manager') { + if (widget.content && IM_WIDGET_TYPES.includes(widget.content.type)) { delete userWidgets[key]; } }); return client.setAccountData('m.widgets', userWidgets); } + static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { + return WidgetUtils.setUserWidget( + "integration_manager_" + (new Date().getTime()), + "im.vector.integration_manager", // TODO: Use m.integration_manager post-MSC1957 + uiUrl, + "Integration Manager: " + name, + {"api_url": apiUrl}, + ); + } + /** * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated From b77be2d3808f6784cb6dd12f5c988b9985c731fc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 14 Aug 2019 09:33:02 -0600 Subject: [PATCH 5/5] Just use MSC1957 --- src/utils/WidgetUtils.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js index fb79ec0ae7..12c1578474 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -29,9 +29,6 @@ import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; -// We'll be using im.vector.integration_manager until MSC1957 or similar is accepted. -const IM_WIDGET_TYPES = ["m.integration_manager", "im.vector.integration_manager"]; - /** * Encodes a URI according to a set of template variables. Variables will be * passed through encodeURIComponent. @@ -349,7 +346,7 @@ export default class WidgetUtils { */ static getIntegrationManagerWidgets() { const widgets = WidgetUtils.getUserWidgetsArray(); - return widgets.filter(w => w.content && IM_WIDGET_TYPES.includes(w.content.type)); + return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } static removeIntegrationManagerWidgets() { @@ -359,7 +356,7 @@ export default class WidgetUtils { } const userWidgets = client.getAccountData('m.widgets').getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { - if (widget.content && IM_WIDGET_TYPES.includes(widget.content.type)) { + if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; } }); @@ -369,7 +366,7 @@ export default class WidgetUtils { static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { return WidgetUtils.setUserWidget( "integration_manager_" + (new Date().getTime()), - "im.vector.integration_manager", // TODO: Use m.integration_manager post-MSC1957 + "m.integration_manager", uiUrl, "Integration Manager: " + name, {"api_url": apiUrl},