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 (
+
+ );
+ }
+}
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