Drop provisioning API.

This commit is contained in:
Half-Shot 2024-04-17 11:42:17 +01:00
parent ab918bd293
commit fc9233ad0a
16 changed files with 17 additions and 572 deletions

View File

@ -26,7 +26,6 @@ import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher"
import { UserTokenStore } from "./tokens/UserTokenStore";
import * as GitHubWebhookTypes from "@octokit/webhooks-types";
import { Logger } from "matrix-appservice-bridge";
import { Provisioner } from "./provisioning/provisioner";
import { JiraProvisionerRouter } from "./jira/Router";
import { GitHubProvisionerRouter } from "./github/Router";
import { OAuthRequest } from "./WebhookTypes";
@ -56,7 +55,6 @@ export class Bridge {
private adminRooms: Map<string, AdminRoom> = new Map();
private feedReader?: FeedReader;
private houndReader?: HoundReader;
private provisioningApi?: Provisioner;
private replyProcessor = new RichRepliesPreprocessor(true);
private ready = false;
@ -135,34 +133,6 @@ export class Bridge {
this.github,
);
if (this.config.provisioning) {
const routers = [];
if (this.config.jira) {
routers.push({
route: "/v1/jira",
router: new JiraProvisionerRouter(this.config.jira, this.tokenStore).getRouter(),
});
this.connectionManager.registerProvisioningConnection(JiraProjectConnection);
}
if (this.config.github && this.github) {
routers.push({
route: "/v1/github",
router: new GitHubProvisionerRouter(this.config.github, this.tokenStore, this.github).getRouter(),
});
this.connectionManager.registerProvisioningConnection(GitHubRepoConnection);
}
if (this.config.generic) {
this.connectionManager.registerProvisioningConnection(GenericHookConnection);
}
this.provisioningApi = new Provisioner(
this.config.provisioning,
this.connectionManager,
this.botUsersManager,
this.as,
routers,
);
}
this.as.on("query.room", async (roomAlias, cb) => {
try {
cb(await this.onQueryRoom(roomAlias));
@ -775,9 +745,6 @@ export class Bridge {
);
}
if (this.provisioningApi) {
this.listener.bindResource('provisioning', this.provisioningApi.expressRouter);
}
if (this.config.metrics?.enabled) {
this.listener.bindResource('metrics', Metrics.expressRouter);
}

View File

@ -11,7 +11,7 @@ import { CommentProcessor } from "./CommentProcessor";
import { ConnectionDeclaration, ConnectionDeclarations, GenericHookConnection, GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection,
GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection, IConnectionState, JiraProjectConnection } from "./Connections";
import { FigmaFileConnection, FeedConnection } from "./Connections";
import { GetConnectionTypeResponseItem } from "./provisioning/api";
import { GetConnectionTypeResponseItem } from "./Widgets/api";
import { GitLabClient } from "./Gitlab/Client";
import { GithubInstance } from "./github/GithubInstance";
import { IBridgeStorageProvider } from "./Stores/StorageProvider";

View File

@ -6,7 +6,7 @@ import { Logger } from "matrix-appservice-bridge";
import { BaseConnection } from "./BaseConnection";
import markdown from "markdown-it";
import { Connection, ProvisionConnectionOpts } from "./IConnection";
import { GetConnectionsResponseItem } from "../provisioning/api";
import { GetConnectionsResponseItem } from "../Widgets/api";
import { readFeed, sanitizeHtml } from "../libRs";
import UserAgent from "../UserAgent";
import { retry, retryMatrixErrorFilter } from "../PromiseUtil";

View File

@ -7,7 +7,7 @@ import { MatrixEvent } from "../MatrixEvent";
import { Appservice, Intent, StateEvent } from "matrix-bot-sdk";
import { ApiError, ErrCode } from "../api";
import { BaseConnection } from "./BaseConnection";
import { GetConnectionsResponseItem } from "../provisioning/api";
import { GetConnectionsResponseItem } from "../Widgets/api";
import { BridgeConfigGenericWebhooks } from "../config/Config";
import { ensureUserIsInRoom } from "../IntentUtils";
import { randomUUID } from 'node:crypto';

View File

@ -4,7 +4,7 @@ import { CommentProcessor } from "../CommentProcessor";
import { FormatUtil, LooseMinimalGitHubRepo } from "../FormatUtil";
import { Octokit } from "@octokit/rest";
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
import { GetConnectionsResponseItem } from "../provisioning/api";
import { GetConnectionsResponseItem } from "../Widgets/api";
import { IssuesOpenedEvent, IssuesReopenedEvent, IssuesEditedEvent, PullRequestOpenedEvent, IssuesClosedEvent, PullRequestClosedEvent,
PullRequestReadyForReviewEvent, PullRequestReviewSubmittedEvent, ReleasePublishedEvent, ReleaseCreatedEvent,
IssuesLabeledEvent, IssuesUnlabeledEvent, WorkflowRunCompletedEvent, IssueCommentCreatedEvent, PushEvent

View File

@ -8,7 +8,7 @@ import { BridgeConfigGitLab, GitLabInstance } from "../config/Config";
import { IGitlabMergeRequest, IGitlabProject, IGitlabUser, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes";
import { CommandConnection } from "./CommandConnection";
import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection";
import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api";
import { ConnectionWarning, GetConnectionsResponseItem } from "../Widgets/api";
import { ErrCode, ApiError, ValidatorApiError } from "../api"
import { AccessLevel, SerializedGitlabDiscussionThreads } from "../Gitlab/Types";
import Ajv, { JSONSchemaType } from "ajv";

View File

@ -1,6 +1,6 @@
import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent";
import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types";
import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api";
import { ConnectionWarning, GetConnectionsResponseItem } from "../Widgets/api";
import { Appservice, Intent, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk";
import { BridgeConfig, BridgePermissionLevel } from "../config/Config";
import { UserTokenStore } from "../tokens/UserTokenStore";

View File

@ -13,7 +13,7 @@ import { UserTokenStore } from "../tokens/UserTokenStore";
import { CommandError, NotLoggedInError } from "../errors";
import { ApiError, ErrCode } from "../api";
import JiraApi from "jira-client";
import { GetConnectionsResponseItem } from "../provisioning/api";
import { GetConnectionsResponseItem } from "../Widgets/api";
import { BridgeConfigJira } from "../config/Config";
import { HookshotJiraApi } from "../jira/Client";
import { GrantChecker } from "../grants/GrantCheck";

View File

@ -5,8 +5,8 @@ import { errorMiddleware } from "./api";
// Appserices can't be handled yet because the bot-sdk maintains control of it.
// See https://github.com/turt2live/matrix-bot-sdk/issues/191
export type ResourceName = "webhooks"|"widgets"|"metrics"|"provisioning";
export const ResourceTypeArray: ResourceName[] = ["webhooks","widgets","metrics","provisioning"];
export type ResourceName = "webhooks"|"widgets"|"metrics";
export const ResourceTypeArray: ResourceName[] = ["webhooks","widgets","metrics"];
import { Handlers } from "@sentry/node";
export interface BridgeConfigListener {
bindAddress?: string;

View File

@ -8,7 +8,7 @@ import { ProvisioningApi, ProvisioningRequest } from "matrix-appservice-bridge";
import { IBridgeStorageProvider } from "../Stores/StorageProvider";
import { ConnectionManager } from "../ConnectionManager";
import BotUsersManager, {BotUser} from "../Managers/BotUsersManager";
import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "../provisioning/api";
import { assertUserPermissionsInRoom, GetConnectionsResponseItem } from "./api";
import { Appservice, PowerLevelsEvent } from "matrix-bot-sdk";
import { GithubInstance } from '../github/GithubInstance';
import { AllowedTokenTypes, TokenType, UserTokenStore } from '../tokens/UserTokenStore';

View File

@ -1,4 +1,4 @@
import { GetConnectionsResponseItem } from "../provisioning/api";
import { GetConnectionsResponseItem } from "./api";
export interface BridgeRoomStateGitHub {
enabled: boolean;

View File

@ -434,12 +434,6 @@ export interface BridgeConfigServiceBot {
service: string;
}
export interface BridgeConfigProvisioning {
bindAddress?: string;
port?: number;
secret: string;
}
export interface BridgeConfigMetrics {
enabled: boolean;
bindAddress?: string;
@ -472,7 +466,6 @@ export interface BridgeConfigRoot {
metrics?: BridgeConfigMetrics;
passFile: string;
permissions?: BridgeConfigActorPermission[];
provisioning?: BridgeConfigProvisioning;
queue?: BridgeConfigQueue;
sentry?: BridgeConfigSentry;
serviceBots?: BridgeConfigServiceBot[];
@ -524,8 +517,6 @@ export class BridgeConfig {
public readonly serviceBots?: BridgeConfigServiceBot[];
@configKey("EXPERIMENTAL support for complimentary widgets", true)
public readonly widgets?: BridgeWidgetConfig;
@configKey("Provisioning API for integration managers", true)
public readonly provisioning?: BridgeConfigProvisioning;
@configKey("Prometheus metrics support", true)
public readonly metrics?: BridgeConfigMetrics;
@ -559,7 +550,6 @@ export class BridgeConfig {
this.jira = configData.jira && new BridgeConfigJira(configData.jira);
this.generic = configData.generic && new BridgeConfigGenericWebhooks(configData.generic);
this.feeds = configData.feeds && new BridgeConfigFeeds(configData.feeds);
this.provisioning = configData.provisioning;
this.passFile = configData.passFile ?? "./passkey.pem";
this.bot = configData.bot;
this.serviceBots = configData.serviceBots;
@ -627,7 +617,11 @@ export class BridgeConfig {
}
if ('goNebMigrator' in configData) {
log.warn(`The GoNEB migrator has been removed from this release. You should remove the 'goNebMigrator' from your config.`);
log.warn(`The GoNEB migrator has been removed from Hookshot. You should remove the 'goNebMigrator' from your config.`);
}
if ('provisioning' in configData) {
log.warn(`The provisioning API has been removed from Hookshot. You should remove the 'provisioning' from your config.`);
}
// Listeners is a bit special
@ -650,15 +644,6 @@ export class BridgeConfig {
})
}
if (this.provisioning?.port) {
this.listeners.push({
resources: ['provisioning'],
port: this.provisioning.port,
bindAddress: this.provisioning.bindAddress,
})
log.warn("The `provisioning` configuration still specifies a port/bindAddress. This should be moved to the `listeners` config.");
}
if (this.metrics?.port) {
this.listeners.push({
resources: ['metrics'],

View File

@ -126,9 +126,6 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
pollTimeoutSeconds: 30,
pollConcurrency: 4,
},
provisioning: {
secret: "!secretToken"
},
metrics: {
enabled: true,
},
@ -141,7 +138,7 @@ export const DefaultConfigRoot: BridgeConfigRoot = {
{
port: 9001,
bindAddress: '127.0.0.1',
resources: ['metrics', 'provisioning'],
resources: ['metrics'],
},
{
port: 9002,

View File

@ -1,295 +0,0 @@
Provisioning API for matrix-hookshot
-----------------------------
# Overview
This document describes how to integrate with `matrix-hookshot`'s provisioning API.
Requests made to the bridge must be against the API listener defined in the config under `provisioning`, not
the appservice or webhook listeners.
Requests should always be authenticated with the secret given in the config, inside the `Authorization` header.
Requests being made on behalf of users (most provisioning APIs) should include the userId as a query parameter.
```
GET /v1/health?userId=%40Half-Shot%3Ahalf-shot.uk
Authorization: Bearer secret
```
APIs are versioned independently so two endpoints on the latest version may not always have the same version.
# APIs
## GET /v1/health
Request the status of the provisioning API.
### Response
```
HTTP 200
{}
```
Any other response should be considered a failed request (e.g. 404, 502 etc).
## GET /v1/connectiontypes
Request the connection types enabled for this bridge.
### Response
```json5
{
"JiraProject": {
"type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type for the connection
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
}
}
```
## GET /v1/{roomId}/connections
Request the connections for a given room. The `{roomId}` parameter is the target Matrix room.
### Response
```json5
[{
"type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": {
// ... connection specific details, can be configured.
}
}]
```
## GET /v1/{roomId}/connections/{id}
Request details of a single connection. The `{roomId}` parameter is the target Matrix room.
### Response
```json5
{
"type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": {
// ... connection specific details, can be configured.
}
}
```
## PUT /v1/{roomId}/connections/{type}
Create a new connection of a given type. The type refers to the `eventType` (`IConnection.CanonicalEventType`). The `{roomId}` parameter is the target Matrix room.
The body of the request is the configuration for the connection, which will be the "ConnectionState" interface for each connection.
### Request body
```json5
{
// ... connection specific details, can be configured.
}
```
### Response
```json5
{
"type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": {
// ... connection specific details, can be configured.
}
}
```
## PATCH /v1/{roomId}/connections/{id}
Update a connection's configuration. The `id` refers to the `id` returned in the GET response.
The body of the request is the configuration for the connection, which will be the "ConnectionState" interface for each connection.
### Request body
```json5
{
// ... connection specific details, can be configured.
}
```
### Response
```json5
{
"type": "JiraProject", // The name of the connection
"eventType": "uk.half-shot.matrix-hookshot.jira.project", // Corresponds to the state type in the connection
"id": "opaque-unique-id", // An opaque ID used to refer to this connection. Should **NOT** be assumed to be stable.
"service": "jira", // or github, webhook. A human-readable service name to make things look pretty
"botUserId": "@hookshot:yourdomain.com", // The bot mxid for the service. Currently, this is the sender_localpart, but may change in the future.
"config": {
// ... connection specific details, can be configured.
}
}
```
## DELETE /v1/{roomId}/connections/{id}
Delete a connection. The `id` refers to the `id` returned in the GET response.
### Response
```json5
{
"ok": true
}
```
# Service specific APIs
Some services have specific APIs for additional functionality, like OAuth.
## GitHub
### GET /v1/github/oauth?userId={userId}
Request an OAuth url for the given user. Once the user has completed the steps in the OAuth process,
the bridge will be granted access.
### Response
```json5
[{
"user_url": "https://github.com/login/oauth/authorize?...",
"org_url": "https://github.com/apps/matrix-bridge/installations/new",
}]
```
### GET /v1/github/account?userId={userId}
Request the status of the users account. This will return a `loggedIn` value to determine if the
bridge has a GitHub identity stored for the user, and any organisations they have access to.
### Response
```json5
{
"loggedIn": true,
"organisations":[{
"name": "half-shot",
"avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4"
}]
}
```
### GET /v1/github/orgs/{orgName}/repositories?userId={userId}&page={page}&perPage={perPage}
Request a list of all repositories a user is a member of in the given org. The `owner` and `name` value of a repository can be given to create a new GitHub connection.
This request is paginated, and `page` sets the page (defaults to `1`) while `perPage` (defaults to `10`) sets the number of entries per page.
This request can be retried until the number of entries is less than the value of `perPage`.
### Response
```json5
{
"loggedIn": true,
"repositories":[{
"name": "matrix-hookshot",
"owner": "matrix-org",
"fullName": "matrix-org/matrix-hookshot",
"avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4",
"description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA. "
}]
}
```
### GET /v1/github/repositories?userId={userId}&page={page}&perPage={perPage}
Request a list of all repositories a user is a member of (including those not belonging to an org). The `owner` and `name` value of a repository can be given to create a new GitHub connection.
If the user has only allowed a subset of repositories to be bridged, `changeSelectionUrl` will be defined and can be used to expand the search query.
This request is paginated, and `page` sets the page (defaults to `1`) while `perPage` (defaults to `10`) sets the number of entries per page.
This request can be retried until the number of entries is less than the value of `perPage`.
### Response
```json5
{
"loggedIn": true,
"changeSelectionUrl": "https://github.com/settings/installations/12345",
"repositories":[{
"name": "matrix-hookshot",
"owner": "matrix-org",
"fullName": "matrix-org/matrix-hookshot",
"avatarUrl": "https://avatars.githubusercontent.com/u/8418310?v=4",
"description": "A bridge between Matrix and multiple project management services, such as GitHub, GitLab and JIRA. "
}]
}
```
## JIRA
### GET /v1/jira/oauth?userId={userId}
Request an OAuth url for the given user. Once the user has completed the steps in the OAuth process,
the bridge will be granted access.
### Response
```json5
{
"url": "https://auth.atlassian.com/authorize?..."
}
```
### GET /v1/jira/account?userId={userId}
Request the status of the users account. This will return a `loggedIn` value to determine if the
bridge has a JIRA identity stored for the user, and any instances they have access to. Note that if a
user does not have access to an instance, they can authenticate again to gain access to it (if they are able
to consent).
### Response
```json5
{
"loggedIn": true,
"instances":[{
"name": "acme",
"url": "https://acme.atlassian.net"
}]
}
```
### GET /v1/jira/instances/{instanceName}/projects?userId={userId}
Request a list of all projects a user can see in a given instance. The `url` value of a project can be given to create
a new JIRA connection.
### Response
```json5
[{
"key": "PLAY",
"name": "Jira Playground",
"url": "https://acme.atlassian.net/projects/PLAY"
}]
```

View File

@ -1,209 +0,0 @@
import { BridgeConfigProvisioning } from "../config/Config";
import { Router, default as express, NextFunction, Request, Response } from "express";
import { ConnectionManager } from "../ConnectionManager";
import { Logger } from "matrix-appservice-bridge";
import { assertUserPermissionsInRoom, GetConnectionsResponseItem, GetConnectionTypeResponseItem } from "./api";
import { ApiError, ErrCode } from "../api";
import { Appservice } from "matrix-bot-sdk";
import Metrics from "../Metrics";
import BotUsersManager from "../Managers/BotUsersManager";
const log = new Logger("Provisioner");
// Simple validator
const ROOM_ID_VALIDATOR = /!.+:.+/;
const USER_ID_VALIDATOR = /@.+:.+/;
export class Provisioner {
public readonly expressRouter: Router = Router();
constructor(
private readonly config: BridgeConfigProvisioning,
private readonly connMan: ConnectionManager,
private readonly botUsersManager: BotUsersManager,
private readonly as: Appservice,
additionalRoutes: {route: string, router: Router}[]) {
if (!this.config.secret) {
throw Error('Missing secret in provisioning config');
}
this.expressRouter.use("/v1", (req, _res, next) => {
Metrics.provisioningHttpRequest.inc({path: req.path, method: req.method});
next();
});
this.expressRouter.get("/v1/health", this.getHealth);
this.expressRouter.use("/v1", this.checkAuth.bind(this));
this.expressRouter.use(express.json());
// Room Routes
this.expressRouter.get(
"/v1/connectiontypes",
this.getConnectionTypes.bind(this),
);
this.expressRouter.use("/v1", this.checkUserId.bind(this));
additionalRoutes.forEach(route => {
this.expressRouter.use(route.route, route.router);
});
this.expressRouter.get<{roomId: string}, unknown, unknown, {userId: string}>(
"/v1/:roomId/connections",
this.checkRoomId.bind(this),
(...args) => this.checkUserPermission("read", ...args),
this.getConnections.bind(this),
);
this.expressRouter.get<{roomId: string, connectionId: string}, unknown, unknown, {userId: string}>(
"/v1/:roomId/connections/:connectionId",
this.checkRoomId.bind(this),
(...args) => this.checkUserPermission("read", ...args),
this.getConnection.bind(this),
);
this.expressRouter.put<{roomId: string, type: string}, unknown, Record<string, unknown>, {userId: string}>(
"/v1/:roomId/connections/:type",
this.checkRoomId.bind(this),
(...args) => this.checkUserPermission("write", ...args),
this.putConnection.bind(this),
);
this.expressRouter.patch<{roomId: string, connectionId: string}, unknown, Record<string, unknown>, {userId: string}>(
"/v1/:roomId/connections/:connectionId",
this.checkRoomId.bind(this),
(...args) => this.checkUserPermission("write", ...args),
this.patchConnection.bind(this),
);
this.expressRouter.delete<{roomId: string, connectionId: string}, unknown, unknown, {userId: string}>(
"/v1/:roomId/connections/:connectionId",
this.checkRoomId.bind(this),
(...args) => this.checkUserPermission("write", ...args),
this.deleteConnection.bind(this),
);
}
private checkAuth(req: Request, _res: Response, next: NextFunction) {
if (req.headers.authorization === `Bearer ${this.config.secret}`) {
return next();
}
throw new ApiError("Unauthorized", ErrCode.BadToken);
}
private checkRoomId(req: Request<{roomId: string}>, _res: Response, next: NextFunction) {
if (!req.params.roomId || !ROOM_ID_VALIDATOR.exec(req.params.roomId)) {
throw new ApiError("Invalid roomId", ErrCode.BadValue);
}
next();
}
private checkUserId(req: Request, _res: Response, next: NextFunction) {
if (typeof req.query.userId !== "string" || !USER_ID_VALIDATOR.exec(req.query.userId)) {
throw new ApiError("Invalid userId", ErrCode.BadValue);
}
next();
}
private async checkUserPermission(requiredPermission: "read"|"write", req: Request<{roomId: string}, unknown, unknown, {userId: string}>, res: Response, next: NextFunction) {
const userId = req.query.userId;
const roomId = req.params.roomId;
const botUser = this.botUsersManager.getBotUserInRoom(roomId);
if (!botUser) {
throw new ApiError("Bot is not joined to the room.", ErrCode.NotInRoom);
}
try {
await assertUserPermissionsInRoom(userId, roomId, requiredPermission, botUser.intent);
next();
} catch (ex) {
next(ex);
}
}
private getHealth(_req: Request, res: Response) {
return res.send({})
}
private getConnectionTypes(_req: Request, res: Response<Record<string, GetConnectionTypeResponseItem>>) {
return res.send(this.connMan.enabledForProvisioning);
}
private getConnections(req: Request<{roomId: string}>, res: Response<GetConnectionsResponseItem[]>) {
const connections = this.connMan.getAllConnectionsForRoom(req.params.roomId);
const details = connections.map(c => c.getProvisionerDetails?.()).filter(c => !!c) as GetConnectionsResponseItem[];
return res.send(details);
}
private getConnection(req: Request<{roomId: string, connectionId: string}>, res: Response<GetConnectionsResponseItem>) {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) {
throw new ApiError("Connection does not exist.", ErrCode.NotFound);
}
if (!connection.getProvisionerDetails) {
throw new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation);
}
return res.send(connection.getProvisionerDetails());
}
private async putConnection(req: Request<{roomId: string, type: string}, unknown, Record<string, unknown>, {userId: string}>, res: Response<GetConnectionsResponseItem>, next: NextFunction) {
const roomId = req.params.roomId;
const userId = req.query.userId;
const eventType = req.params.type;
const connectionType = this.connMan.getConnectionTypeForEventType(eventType);
if (!connectionType) {
throw new ApiError("Unknown event type", ErrCode.NotFound);
}
const serviceType = connectionType.ServiceCategory;
// Need to figure out which connections are available
try {
if (!req.body || typeof req.body !== "object") {
throw new ApiError("A JSON body must be provided", ErrCode.BadValue);
}
this.connMan.validateCommandPrefix(roomId, req.body);
const botUser = this.botUsersManager.getBotUserInRoom(roomId, serviceType);
if (!botUser) {
throw new ApiError("Bot is not joined to the room.", ErrCode.NotInRoom);
}
const result = await this.connMan.provisionConnection(roomId, botUser.intent, userId, connectionType, req.body);
if (!result.connection.getProvisionerDetails) {
throw new Error('Connection supported provisioning but not getProvisionerDetails');
}
res.send({
...result.connection.getProvisionerDetails(true),
warning: result.warning,
});
} catch (ex) {
log.error(`Failed to create connection for ${roomId}`, ex);
return next(ex);
}
}
private async patchConnection(req: Request<{roomId: string, connectionId: string}, unknown, Record<string, unknown>, {userId: string}>, res: Response<GetConnectionsResponseItem>, next: NextFunction) {
try {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) {
return next(new ApiError("Connection does not exist.", ErrCode.NotFound));
}
if (!connection.provisionerUpdateConfig || !connection.getProvisionerDetails) {
return next(new ApiError("Connection type does not support updates.", ErrCode.UnsupportedOperation));
}
this.connMan.validateCommandPrefix(req.params.roomId, req.body, connection);
await connection.provisionerUpdateConfig(req.query.userId, req.body);
res.send(connection.getProvisionerDetails(true));
} catch (ex) {
next(ex);
}
}
private async deleteConnection(req: Request<{roomId: string, connectionId: string}>, res: Response<{ok: true}>, next: NextFunction) {
try {
const connection = this.connMan.getConnectionById(req.params.roomId, req.params.connectionId);
if (!connection) {
return next(new ApiError("Connection does not exist.", ErrCode.NotFound));
}
if (!connection.onRemove) {
return next(new ApiError("Connection does not support removal.", ErrCode.UnsupportedOperation));
}
await this.connMan.purgeConnection(req.params.roomId, req.params.connectionId);
res.send({ok: true});
} catch (ex) {
return next(ex);
}
}
}