mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00

* Start work on improved widget interface for admin room * Add changes needed for InviteView * More widget bits * Lots more work to support setup widgets * Redesign work * Fix build order for web * Remove unused invite code * Update configs * Cleanup * Support widget branding * Tidy up web app * Support generic service config lookup * Stronger typings for configuration * Improve error text * fix patch * changelog * First pass on documentation * Fix tests * fixes * Don't require room state / admin rooms for room config requests * Fix no render when secrets isn't provided * rework if logic * Fixes after feedback * more review fixes * Cleanup unused code * fix indent * Update matrix-appservice-bridge to 4.0.1 * Update widget API
129 lines
5.4 KiB
TypeScript
129 lines
5.4 KiB
TypeScript
import { BridgeRoomState, GetConnectionsForServiceResponse, WidgetConfigurationSection } from '../src/Widgets/BridgeWidgetInterface';
|
|
import { GetConnectionsResponseItem } from "../src/provisioning/api";
|
|
import { ExchangeOpenAPIRequestBody, ExchangeOpenAPIResponseBody } from "matrix-appservice-bridge";
|
|
import { WidgetApi } from 'matrix-widget-api';
|
|
import { ApiError } from '../src/api';
|
|
export class BridgeAPIError extends Error {
|
|
constructor(msg: string, public readonly body: ApiError) {
|
|
super(msg);
|
|
}
|
|
|
|
public get errcode() {
|
|
return this.body.errcode as string;
|
|
}
|
|
public get error() {
|
|
return this.body.error as string;
|
|
}
|
|
}
|
|
|
|
export default class BridgeAPI {
|
|
static async getBridgeAPI(baseUrl: string, widgetApi: WidgetApi): Promise<BridgeAPI> {
|
|
const sessionToken = localStorage.getItem('hookshot-sessionToken');
|
|
baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
if (sessionToken) {
|
|
const client = new BridgeAPI(baseUrl, sessionToken);
|
|
try {
|
|
await client.verify();
|
|
return client;
|
|
} catch (ex) {
|
|
// TODO: Check that the token is actually invalid, rather than just assuming we need to refresh.
|
|
console.warn(`Failed to verify token, fetching new token`, ex);
|
|
localStorage.removeItem(sessionToken);
|
|
}
|
|
}
|
|
const creds = await widgetApi.requestOpenIDConnectToken();
|
|
const { matrix_server_name, access_token } = creds;
|
|
// eslint-disable-next-line camelcase
|
|
if (!matrix_server_name || !access_token) {
|
|
throw Error('Server OpenID response missing values');
|
|
}
|
|
|
|
const res = await fetch(`${baseUrl}/widgetapi/v1/exchange_openid`, {
|
|
cache: 'no-cache',
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
// eslint-disable-next-line camelcase
|
|
matrixServer: matrix_server_name,
|
|
// eslint-disable-next-line camelcase
|
|
openIdToken: access_token,
|
|
} as ExchangeOpenAPIRequestBody),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
if (res.status !== 200) {
|
|
if (res.headers.get('Content-Type').includes("application/json")) {
|
|
const resultBody = await res.json();
|
|
throw new BridgeAPIError(resultBody?.error || 'Request failed', resultBody);
|
|
} else {
|
|
throw new Error(`API request failed: ${await res.text()}`, );
|
|
}
|
|
}
|
|
const response = await res.json() as ExchangeOpenAPIResponseBody;
|
|
localStorage.setItem('hookshot-sessionToken', response.token);
|
|
return new BridgeAPI(baseUrl, response.token);
|
|
}
|
|
|
|
private constructor(private readonly baseUrl: string, private readonly accessToken?: string) {
|
|
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
|
|
}
|
|
|
|
async request(method: string, endpoint: string, body?: unknown) {
|
|
const res = await fetch(`${this.baseUrl}${endpoint}`, {
|
|
cache: 'no-cache',
|
|
method,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
// Only set Content-Type if we send a body
|
|
...(!!body && {
|
|
'Content-Type': 'application/json',
|
|
}),
|
|
},
|
|
});
|
|
if (res.status === 204) {
|
|
return;
|
|
}
|
|
if (res.status === 200) {
|
|
return res.json();
|
|
}
|
|
const resultBody = await res.json();
|
|
throw new BridgeAPIError(resultBody?.error || 'Request failed', resultBody);
|
|
}
|
|
|
|
async verify(): Promise<{ userId: string, type: "widget" }> {
|
|
return this.request('GET', `/widgetapi/v1/session`);
|
|
}
|
|
|
|
async state(): Promise<BridgeRoomState> {
|
|
return this.request('GET', `/widgetapi/v1/state`);
|
|
}
|
|
|
|
async getEnabledConfigSections(): Promise<{[sectionName: string]: boolean}> {
|
|
return this.request('GET', '/widgetapi/v1/config/sections');
|
|
}
|
|
|
|
async getServiceConfig<T>(service: string): Promise<T> {
|
|
return this.request('GET', `/widgetapi/v1/service/${service}/config`);
|
|
}
|
|
|
|
async getConnectionsForRoom(roomId: string): Promise<GetConnectionsResponseItem[]> {
|
|
return this.request('GET', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections`);
|
|
}
|
|
|
|
async getConnectionsForService<T extends GetConnectionsResponseItem >(roomId: string, service: string): Promise<GetConnectionsForServiceResponse<T>> {
|
|
return this.request('GET', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(service)}`);
|
|
}
|
|
|
|
async createConnection(roomId: string, type: string, config: unknown) {
|
|
return this.request('POST', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(type)}`, config);
|
|
}
|
|
|
|
async updateConnection(roomId: string, connectionId: string, config: unknown) {
|
|
return this.request('PUT', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(connectionId)}`, config);
|
|
}
|
|
|
|
removeConnection(roomId: string, connectionId: string) {
|
|
return this.request('DELETE', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(connectionId)}`);
|
|
}
|
|
} |