Merge pull request #3308 from matrix-org/travis/integs/account_set

Support changing your integration manager in the UI
This commit is contained in:
Travis Ralston 2019-08-14 09:44:13 -06:00 committed by GitHub
commit f69235b884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 293 additions and 7 deletions

View file

@ -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";

View file

@ -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;
}

View file

@ -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>
);
}
}

View file

@ -217,6 +217,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() {
return (
<div className="mx_SettingsTab">
@ -227,6 +238,7 @@ export default class GeneralUserSettingsTab extends React.Component {
{this._renderThemeSection()}
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
{this._renderDiscoverySection()}
{this._renderIntegrationManagerSection() /* Has its own title */}
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
{this._renderManagementSection()}
</div>

View file

@ -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 <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",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",

View file

@ -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);
}

View file

@ -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<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

View file

@ -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},
);
}
/**