mirror of https://github.com/vector-im/riot-web
Support changing your integration manager in the UI
Part of https://github.com/vector-im/riot-web/issues/10161pull/21833/head
parent
e21c12c2c9
commit
03d735f4ed
|
@ -169,6 +169,7 @@
|
||||||
@import "./views/settings/_PhoneNumbers.scss";
|
@import "./views/settings/_PhoneNumbers.scss";
|
||||||
@import "./views/settings/_ProfileSettings.scss";
|
@import "./views/settings/_ProfileSettings.scss";
|
||||||
@import "./views/settings/_SetIdServer.scss";
|
@import "./views/settings/_SetIdServer.scss";
|
||||||
|
@import "./views/settings/_SetIntegrationManager.scss";
|
||||||
@import "./views/settings/tabs/_SettingsTab.scss";
|
@import "./views/settings/tabs/_SettingsTab.scss";
|
||||||
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.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;
|
||||||
|
}
|
|
@ -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 <div>
|
||||||
|
<InlineSpinner />
|
||||||
|
{ _t("Checking server") }
|
||||||
|
</div>;
|
||||||
|
} 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 <b>%(serverName)s</b> to manage your bots, widgets, " +
|
||||||
|
"and sticker packs.",
|
||||||
|
{serverName: currentManager.name},
|
||||||
|
{ b: sub => <b>{sub}</b> },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
bodyText = _t(
|
||||||
|
"Add which integration manager you want to manage your bots, widgets, " +
|
||||||
|
"and sticker packs.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="mx_SettingsTab_section mx_SetIntegrationManager" onSubmit={this._setManager}>
|
||||||
|
<div className="mx_SettingsTab_heading">
|
||||||
|
<span>{_t("Integration Manager")}</span>
|
||||||
|
<span className="mx_SettingsTab_subheading">{managerName}</span>
|
||||||
|
</div>
|
||||||
|
<span className="mx_SettingsTab_subsectionText">
|
||||||
|
{bodyText}
|
||||||
|
</span>
|
||||||
|
<Field label={_t("Enter a new integration manager")}
|
||||||
|
id="mx_SetIntegrationManager_newUrl"
|
||||||
|
type="text" value={this.state.url} autoComplete="off"
|
||||||
|
onChange={this._onUrlChanged}
|
||||||
|
tooltip={this._getTooltip()}
|
||||||
|
/>
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary_sm"
|
||||||
|
type="submit"
|
||||||
|
disabled={!this._canChange()}
|
||||||
|
onClick={this._setManager}
|
||||||
|
>{_t("Change")}</AccessibleButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -204,6 +204,17 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderIntegrationManagerSection() {
|
||||||
|
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_SettingsTab_section">
|
||||||
|
{ /* has its own heading as it includes the current integration manager */ }
|
||||||
|
<SetIntegrationManager />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_SettingsTab">
|
<div className="mx_SettingsTab">
|
||||||
|
@ -214,6 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
||||||
{this._renderThemeSection()}
|
{this._renderThemeSection()}
|
||||||
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
|
||||||
{this._renderDiscoverySection()}
|
{this._renderDiscoverySection()}
|
||||||
|
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||||
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
||||||
{this._renderManagementSection()}
|
{this._renderManagementSection()}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -548,6 +548,13 @@
|
||||||
"Identity Server": "Identity Server",
|
"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",
|
"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",
|
"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 <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.": "You are currently using <b>%(serverName)s</b> 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",
|
"Flair": "Flair",
|
||||||
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
||||||
"Success": "Success",
|
"Success": "Success",
|
||||||
|
|
|
@ -19,12 +19,18 @@ import sdk from "../index";
|
||||||
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
|
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
|
||||||
import type {Room} from "matrix-js-sdk";
|
import type {Room} from "matrix-js-sdk";
|
||||||
import Modal from '../Modal';
|
import Modal from '../Modal';
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
export const KIND_ACCOUNT = "account";
|
||||||
|
export const KIND_CONFIG = "config";
|
||||||
|
|
||||||
export class IntegrationManagerInstance {
|
export class IntegrationManagerInstance {
|
||||||
apiUrl: string;
|
apiUrl: string;
|
||||||
uiUrl: string;
|
uiUrl: string;
|
||||||
|
kind: string;
|
||||||
|
|
||||||
constructor(apiUrl: string, uiUrl: string) {
|
constructor(kind: string, apiUrl: string, uiUrl: string) {
|
||||||
|
this.kind = kind;
|
||||||
this.apiUrl = apiUrl;
|
this.apiUrl = apiUrl;
|
||||||
this.uiUrl = uiUrl;
|
this.uiUrl = uiUrl;
|
||||||
|
|
||||||
|
@ -32,6 +38,11 @@ export class IntegrationManagerInstance {
|
||||||
if (!this.uiUrl) this.uiUrl = this.apiUrl;
|
if (!this.uiUrl) this.uiUrl = this.apiUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get name(): string {
|
||||||
|
const parsed = url.parse(this.uiUrl);
|
||||||
|
return parsed.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
getScalarClient(): ScalarAuthClient {
|
getScalarClient(): ScalarAuthClient {
|
||||||
return new ScalarAuthClient(this.apiUrl, this.uiUrl);
|
return new ScalarAuthClient(this.apiUrl, this.uiUrl);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import SdkConfig from '../SdkConfig';
|
import SdkConfig from '../SdkConfig';
|
||||||
import sdk from "../index";
|
import sdk from "../index";
|
||||||
import Modal from '../Modal';
|
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 type {MatrixClient, MatrixEvent} from "matrix-js-sdk";
|
||||||
import WidgetUtils from "../utils/WidgetUtils";
|
import WidgetUtils from "../utils/WidgetUtils";
|
||||||
import MatrixClientPeg from "../MatrixClientPeg";
|
import MatrixClientPeg from "../MatrixClientPeg";
|
||||||
|
@ -62,7 +62,7 @@ export class IntegrationManagers {
|
||||||
const uiUrl = SdkConfig.get()['integrations_ui_url'];
|
const uiUrl = SdkConfig.get()['integrations_ui_url'];
|
||||||
|
|
||||||
if (apiUrl && uiUrl) {
|
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'];
|
const apiUrl = data['api_url'];
|
||||||
if (!apiUrl || !uiUrl) return;
|
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',
|
{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<IntegrationManagerInstance>} 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
|
// For debugging
|
||||||
|
|
|
@ -351,6 +351,20 @@ export default class WidgetUtils {
|
||||||
return widgets.filter(w => w.content && imTypes.includes(w.content.type));
|
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)
|
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||||
* @return {Promise} Resolves on account data updated
|
* @return {Promise} Resolves on account data updated
|
||||||
|
|
Loading…
Reference in New Issue