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..7fda042864 --- /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 { + @mixin mx_Settings_fullWidthField; +} + +.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 b3c7aadd7b..08550db1d1 100644 --- a/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -217,6 +217,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 (
@@ -227,6 +238,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 e5ecc2bf19..22c2786922 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -552,6 +552,12 @@ "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", + "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..49356676e6 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,68 @@ 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.addIntegrationManagerWidget(manager.name, manager.uiUrl, 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..12c1578474 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.js @@ -346,9 +346,31 @@ 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 && w.content.type === "m.integration_manager"); + } + + 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); + } + + static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { + return WidgetUtils.setUserWidget( + "integration_manager_" + (new Date().getTime()), + "m.integration_manager", + uiUrl, + "Integration Manager: " + name, + {"api_url": apiUrl}, + ); } /**