Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 03d735f

Browse files
committed
Support changing your integration manager in the UI
Part of element-hq/element-web#10161
1 parent e21c12c commit 03d735f

File tree

8 files changed

+289
-4
lines changed

8 files changed

+289
-4
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
@import "./views/settings/_PhoneNumbers.scss";
170170
@import "./views/settings/_ProfileSettings.scss";
171171
@import "./views/settings/_SetIdServer.scss";
172+
@import "./views/settings/_SetIntegrationManager.scss";
172173
@import "./views/settings/tabs/_SettingsTab.scss";
173174
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
174175
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
Copyright 2019 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_SetIntegrationManager .mx_Field_input {
18+
margin-right: 100px; // Align with the other fields on the page
19+
}
20+
21+
.mx_SetIntegrationManager {
22+
margin-top: 10px;
23+
margin-bottom: 10px;
24+
}
25+
26+
.mx_SetIntegrationManager > .mx_SettingsTab_heading {
27+
margin-bottom: 10px;
28+
}
29+
30+
.mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading {
31+
display: inline-block;
32+
padding-left: 5px;
33+
font-size: 14px;
34+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
Copyright 2019 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
import {_t} from "../../../languageHandler";
19+
import sdk from '../../../index';
20+
import Field from "../elements/Field";
21+
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
22+
23+
export default class SetIntegrationManager extends React.Component {
24+
constructor() {
25+
super();
26+
27+
const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager();
28+
29+
this.state = {
30+
currentManager,
31+
url: "", // user-entered text
32+
error: null,
33+
busy: false,
34+
};
35+
}
36+
37+
_onUrlChanged = (ev) => {
38+
const u = ev.target.value;
39+
this.setState({url: u});
40+
};
41+
42+
_getTooltip = () => {
43+
if (this.state.busy) {
44+
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
45+
return <div>
46+
<InlineSpinner />
47+
{ _t("Checking server") }
48+
</div>;
49+
} else if (this.state.error) {
50+
return this.state.error;
51+
} else {
52+
return null;
53+
}
54+
};
55+
56+
_canChange = () => {
57+
return !!this.state.url && !this.state.busy;
58+
};
59+
60+
_setManager = async (ev) => {
61+
// Don't reload the page when the user hits enter in the form.
62+
ev.preventDefault();
63+
ev.stopPropagation();
64+
65+
this.setState({busy: true});
66+
67+
const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
68+
if (!manager) {
69+
this.setState({
70+
busy: false,
71+
error: _t("Integration manager offline or not accessible."),
72+
});
73+
return;
74+
}
75+
76+
try {
77+
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
78+
this.setState({
79+
busy: false,
80+
error: null,
81+
currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
82+
url: "", // clear input
83+
});
84+
} catch (e) {
85+
console.error(e);
86+
this.setState({
87+
busy: false,
88+
error: _t("Failed to update integration manager"),
89+
});
90+
}
91+
};
92+
93+
render() {
94+
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
95+
96+
const currentManager = this.state.currentManager;
97+
let managerName;
98+
let bodyText;
99+
if (currentManager) {
100+
managerName = `(${currentManager.name})`;
101+
bodyText = _t(
102+
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, " +
103+
"and sticker packs.",
104+
{serverName: currentManager.name},
105+
{ b: sub => <b>{sub}</b> },
106+
);
107+
} else {
108+
bodyText = _t(
109+
"Add which integration manager you want to manage your bots, widgets, " +
110+
"and sticker packs.",
111+
);
112+
}
113+
114+
return (
115+
<form className="mx_SettingsTab_section mx_SetIntegrationManager" onSubmit={this._setManager}>
116+
<div className="mx_SettingsTab_heading">
117+
<span>{_t("Integration Manager")}</span>
118+
<span className="mx_SettingsTab_subheading">{managerName}</span>
119+
</div>
120+
<span className="mx_SettingsTab_subsectionText">
121+
{bodyText}
122+
</span>
123+
<Field label={_t("Enter a new integration manager")}
124+
id="mx_SetIntegrationManager_newUrl"
125+
type="text" value={this.state.url} autoComplete="off"
126+
onChange={this._onUrlChanged}
127+
tooltip={this._getTooltip()}
128+
/>
129+
<AccessibleButton
130+
kind="primary_sm"
131+
type="submit"
132+
disabled={!this._canChange()}
133+
onClick={this._setManager}
134+
>{_t("Change")}</AccessibleButton>
135+
</form>
136+
);
137+
}
138+
}

src/components/views/settings/tabs/user/GeneralUserSettingsTab.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,17 @@ export default class GeneralUserSettingsTab extends React.Component {
204204
);
205205
}
206206

207+
_renderIntegrationManagerSection() {
208+
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
209+
210+
return (
211+
<div className="mx_SettingsTab_section">
212+
{ /* has its own heading as it includes the current integration manager */ }
213+
<SetIntegrationManager />
214+
</div>
215+
);
216+
}
217+
207218
render() {
208219
return (
209220
<div className="mx_SettingsTab">
@@ -214,6 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component {
214225
{this._renderThemeSection()}
215226
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
216227
{this._renderDiscoverySection()}
228+
{this._renderIntegrationManagerSection() /* Has its own title */}
217229
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
218230
{this._renderManagementSection()}
219231
</div>

src/i18n/strings/en_EN.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,6 +548,13 @@
548548
"Identity Server": "Identity Server",
549549
"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",
550550
"Change": "Change",
551+
"Checking server": "Checking server",
552+
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
553+
"Failed to update integration manager": "Failed to update integration manager",
554+
"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.",
555+
"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.",
556+
"Integration Manager": "Integration Manager",
557+
"Enter a new integration manager": "Enter a new integration manager",
551558
"Flair": "Flair",
552559
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
553560
"Success": "Success",

src/integrations/IntegrationManagerInstance.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,30 @@ import sdk from "../index";
1919
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
2020
import type {Room} from "matrix-js-sdk";
2121
import Modal from '../Modal';
22+
import url from 'url';
23+
24+
export const KIND_ACCOUNT = "account";
25+
export const KIND_CONFIG = "config";
2226

2327
export class IntegrationManagerInstance {
2428
apiUrl: string;
2529
uiUrl: string;
30+
kind: string;
2631

27-
constructor(apiUrl: string, uiUrl: string) {
32+
constructor(kind: string, apiUrl: string, uiUrl: string) {
33+
this.kind = kind;
2834
this.apiUrl = apiUrl;
2935
this.uiUrl = uiUrl;
3036

3137
// Per the spec: UI URL is optional.
3238
if (!this.uiUrl) this.uiUrl = this.apiUrl;
3339
}
3440

41+
get name(): string {
42+
const parsed = url.parse(this.uiUrl);
43+
return parsed.hostname;
44+
}
45+
3546
getScalarClient(): ScalarAuthClient {
3647
return new ScalarAuthClient(this.apiUrl, this.uiUrl);
3748
}

src/integrations/IntegrationManagers.js

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
import SdkConfig from '../SdkConfig';
1818
import sdk from "../index";
1919
import Modal from '../Modal';
20-
import {IntegrationManagerInstance} from "./IntegrationManagerInstance";
20+
import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG} from "./IntegrationManagerInstance";
2121
import type {MatrixClient, MatrixEvent} from "matrix-js-sdk";
2222
import WidgetUtils from "../utils/WidgetUtils";
2323
import MatrixClientPeg from "../MatrixClientPeg";
@@ -62,7 +62,7 @@ export class IntegrationManagers {
6262
const uiUrl = SdkConfig.get()['integrations_ui_url'];
6363

6464
if (apiUrl && uiUrl) {
65-
this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl));
65+
this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl));
6666
}
6767
}
6868

@@ -77,7 +77,7 @@ export class IntegrationManagers {
7777
const apiUrl = data['api_url'];
7878
if (!apiUrl || !uiUrl) return;
7979

80-
this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl));
80+
this._managers.push(new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl));
8181
});
8282
}
8383

@@ -107,6 +107,74 @@ export class IntegrationManagers {
107107
{configured: false}, 'mx_IntegrationsManager',
108108
);
109109
}
110+
111+
async overwriteManagerOnAccount(manager: IntegrationManagerInstance) {
112+
// TODO: TravisR - We should be logging out of scalar clients.
113+
await WidgetUtils.removeIntegrationManagerWidgets();
114+
115+
// TODO: TravisR - We should actually be carrying over the discovery response verbatim.
116+
await WidgetUtils.setUserWidget(
117+
"integration_manager_" + (new Date().getTime()),
118+
"m.integration_manager",
119+
manager.uiUrl,
120+
"Integration Manager",
121+
{"api_url": manager.apiUrl},
122+
);
123+
}
124+
125+
/**
126+
* Attempts to discover an integration manager using only its name.
127+
* @param {string} domainName The domain name to look up.
128+
* @returns {Promise<IntegrationManagerInstance>} Resolves to an integration manager instance,
129+
* or null if none was found.
130+
*/
131+
async tryDiscoverManager(domainName: string): IntegrationManagerInstance {
132+
console.log("Looking up integration manager via .well-known");
133+
if (domainName.startsWith("http:") || domainName.startsWith("https:")) {
134+
// trim off the scheme and just use the domain
135+
const url = url.parse(domainName);
136+
domainName = url.host;
137+
}
138+
139+
let wkConfig;
140+
try {
141+
const result = await fetch(`https://${domainName}/.well-known/matrix/integrations`);
142+
wkConfig = await result.json();
143+
} catch (e) {
144+
console.error(e);
145+
console.warn("Failed to locate integration manager");
146+
return null;
147+
}
148+
149+
if (!wkConfig || !wkConfig["m.integrations_widget"]) {
150+
console.warn("Missing integrations widget on .well-known response");
151+
return null;
152+
}
153+
154+
const widget = wkConfig["m.integrations_widget"];
155+
if (!widget["url"] || !widget["data"] || !widget["data"]["api_url"]) {
156+
console.warn("Malformed .well-known response for integrations widget");
157+
return null;
158+
}
159+
160+
// All discovered managers are per-user managers
161+
const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]);
162+
console.log("Got integration manager response, checking for responsiveness");
163+
164+
// Test the manager
165+
const client = manager.getScalarClient();
166+
try {
167+
// not throwing an error is a success here
168+
await client.connect();
169+
} catch (e) {
170+
console.error(e);
171+
console.warn("Integration manager failed liveliness check");
172+
return null;
173+
}
174+
175+
console.log("Integration manager is alive and functioning");
176+
return manager;
177+
}
110178
}
111179

112180
// For debugging

src/utils/WidgetUtils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,20 @@ export default class WidgetUtils {
351351
return widgets.filter(w => w.content && imTypes.includes(w.content.type));
352352
}
353353

354+
static removeIntegrationManagerWidgets() {
355+
const client = MatrixClientPeg.get();
356+
if (!client) {
357+
throw new Error('User not logged in');
358+
}
359+
const userWidgets = client.getAccountData('m.widgets').getContent() || {};
360+
Object.entries(userWidgets).forEach(([key, widget]) => {
361+
if (widget.content && widget.content.type === 'm.integration_manager') {
362+
delete userWidgets[key];
363+
}
364+
});
365+
return client.setAccountData('m.widgets', userWidgets);
366+
}
367+
354368
/**
355369
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
356370
* @return {Promise} Resolves on account data updated

0 commit comments

Comments
 (0)