From a3c7a2926771e9d9bb5fbb3980dfd4aa6aa66d3b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 3 Dec 2021 17:27:22 +0000 Subject: [PATCH] Support updating some configs --- src/ConnectionManager.ts | 4 +- src/Connections/GenericHook.ts | 69 +++++++++++++++++++++------------ src/Connections/JiraProject.ts | 20 +++++++--- src/provisioning/provisioner.ts | 14 +++++++ 4 files changed, 75 insertions(+), 32 deletions(-) diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 7cf3cc1f..b9623843 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -83,7 +83,7 @@ export class ConnectionManager { throw Error('GitHub is not configured'); } const res = await GitHubRepoConnection.provisionConnection(roomId, userId, data, this.as, this.tokenStore, this.github, this.config.github); - await this.as.botIntent.underlyingClient.sendStateEvent(roomId, JiraProjectConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent); + await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GitHubRepoConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent); this.push(res.connection); return res.connection; } @@ -92,7 +92,7 @@ export class ConnectionManager { throw Error('Generic hook support not supported'); } const res = await GenericHookConnection.provisionConnection(roomId, this.as, data, this.config.generic, this.messageClient); - await this.as.botIntent.underlyingClient.sendStateEvent(roomId, JiraProjectConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent); + await this.as.botIntent.underlyingClient.sendStateEvent(roomId, GenericHookConnection.CanonicalEventType, res.connection.stateKey, res.stateEventContent); this.push(res.connection); return res.connection; } diff --git a/src/Connections/GenericHook.ts b/src/Connections/GenericHook.ts index fb4a6d20..ad43dd12 100644 --- a/src/Connections/GenericHook.ts +++ b/src/Connections/GenericHook.ts @@ -13,11 +13,11 @@ export interface GenericHookConnectionState { /** * This is ONLY used for display purposes, but the account data value is used to prevent misuse. */ - hookId: string; + hookId?: string; /** * The name given in the provisioning UI and displaynames. */ - name?: string; + name: string; transformationFunction?: string; } @@ -39,29 +39,38 @@ const TRANSFORMATION_TIMEOUT_MS = 2000; */ export class GenericHookConnection extends BaseConnection implements IConnection { + static validateState(state: Record, allowJsTransformationFunctions: boolean): GenericHookConnectionState { + const {name, transformationFunction} = state; + let transformationFunctionResult: string|undefined; + if (transformationFunction) { + if (!allowJsTransformationFunctions) { + throw new ApiError('Transformation functions are not allowed', ErrCode.DisabledFeature); + } + if (typeof transformationFunction !== "string") { + throw new ApiError('Transformation functions must be a string', ErrCode.BadValue); + } + transformationFunctionResult = transformationFunction; + } + if (!name) { + throw new ApiError('Missing name', ErrCode.BadValue); + } + if (typeof name !== "string" || name.length < 3 || name.length > 64) { + throw new ApiError("'name' must be a string between 3-64 characters long", ErrCode.BadValue); + } + return { + name, + ...(transformationFunctionResult && {transformationFunction: transformationFunctionResult}), + }; + } + static async provisionConnection(roomId: string, as: Appservice, data: Record = {}, config: BridgeGenericWebhooksConfig, messageClient: MessageSenderClient) { const hookId = uuid(); const validState: GenericHookConnectionState = { + ...GenericHookConnection.validateState(data, config.allowJsTransformationFunctions || false), hookId, }; - if (data.transformationFunction) { - if (!config.allowJsTransformationFunctions) { - throw new ApiError('Transformation functions are not allowed', ErrCode.DisabledFeature); - } - if (typeof data.transformationFunction !== "string") { - throw new ApiError('Transformation functions must be a string', ErrCode.BadValue); - } - validState.transformationFunction = data.transformationFunction; - } - if (!data.name) { - throw new ApiError('Missing name', ErrCode.BadValue); - } - if (typeof data.name !== "string" || data.name.length < 3 || data.name.length > 64) { - throw new ApiError("'name' must be a string between 3-64 characters long", ErrCode.BadValue); - } - validState.name = data.name; - const connection = new GenericHookConnection(roomId, validState, hookId, data.name, messageClient, config, as); - await GenericHookConnection.ensureRoomAccountData(roomId, as, hookId, data.name); + const connection = new GenericHookConnection(roomId, validState, hookId, validState.name, messageClient, config, as); + await GenericHookConnection.ensureRoomAccountData(roomId, as, hookId, validState.name); return { connection, stateEventContent: validState, @@ -98,7 +107,7 @@ export class GenericHookConnection extends BaseConnection implements IConnection private cachedDisplayname?: string; constructor(roomId: string, - private readonly state: GenericHookConnectionState, + private state: GenericHookConnectionState, public readonly hookId: string, stateKey: string, private readonly messageClient: MessageSenderClient, @@ -149,15 +158,15 @@ export class GenericHookConnection extends BaseConnection implements IConnection } public async onStateUpdate(stateEv: MatrixEvent) { - const state = stateEv.content as GenericHookConnectionState; - if (state.transformationFunction && this.config.allowJsTransformationFunctions) { + const validatedConfig = GenericHookConnection.validateState(stateEv.content as Record, this.config.allowJsTransformationFunctions || false); + if (validatedConfig.transformationFunction) { try { - this.transformationFunction = new Script(state.transformationFunction); + this.transformationFunction = new Script(validatedConfig.transformationFunction); } catch (ex) { await this.messageClient.sendMatrixText(this.roomId, 'Could not compile transformation function:' + ex); } } - this.state.name = state.name; + this.state = validatedConfig; } public transformHookData(data: Record): string { @@ -250,6 +259,16 @@ export class GenericHookConnection extends BaseConnection implements IConnection await GenericHookConnection.ensureRoomAccountData(this.roomId, this.as, this.hookId, this.stateKey, true); } + public async provisionerUpdateConfig(userId: string, config: Record) { + const validatedConfig = GenericHookConnection.validateState(config, this.config.allowJsTransformationFunctions || false); + await this.as.botClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, this.stateKey, + { + ...validatedConfig, + hookId: this.hookId + } + ); + } + public toString() { return `GenericHookConnection ${this.hookId}`; } diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts index 41a28c06..144443f7 100644 --- a/src/Connections/JiraProject.ts +++ b/src/Connections/JiraProject.ts @@ -9,7 +9,7 @@ import markdownit from "markdown-it"; import { generateJiraWebLinkFromIssue } from "../Jira"; import { JiraProject } from "../Jira/Types"; import { botCommand, BotCommands, compileBotCommands } from "../BotCommands"; -import { MatrixMessageContent } from "../MatrixEvent"; +import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; import { CommandConnection } from "./CommandConnection"; import { UserTokenStore } from "../UserTokenStore"; import { CommandError, NotLoggedInError } from "../errors"; @@ -102,6 +102,10 @@ export class JiraProjectConnection extends CommandConnection implements IConnect return parts ? parts[parts.length - 1] : undefined; } + public toString() { + return `JiraProjectConnection ${this.projectId || this.projectUrl}`; + } + public isInterestedInHookEvent(eventName: string) { return !this.state.events || this.state.events?.includes(eventName as JiraAllowedEventsNames); } @@ -153,6 +157,11 @@ export class JiraProjectConnection extends CommandConnection implements IConnect return JiraProjectConnection.EventTypes.includes(eventType) && this.stateKey === stateKey; } + public async onStateUpdate(event: MatrixEvent) { + const validatedConfig = validateJiraConnectionState(event.content as JiraProjectConnectionState); + this.state = validatedConfig; + } + public async onJiraIssueCreated(data: JiraIssueEvent) { log.info(`onIssueCreated ${this.roomId} ${this.projectId} ${data.issue.id}`); @@ -337,10 +346,6 @@ export class JiraProjectConnection extends CommandConnection implements IConnect await api.updateAssigneeWithId(issueKey, searchForUser[0].accountId); } - public toString() { - return `JiraProjectConnection ${this.projectId || this.projectUrl}`; - } - public async onRemove() { log.info(`Removing ${this.toString()} for ${this.roomId}`); // Do a sanity check that the event exists. @@ -352,6 +357,11 @@ export class JiraProjectConnection extends CommandConnection implements IConnect await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.LegacyCanonicalEventType, this.stateKey, { disabled: true }); } } + + public async provisionerUpdateConfig(userId: string, config: Record) { + const validatedConfig = validateJiraConnectionState(config); + await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, this.stateKey, validatedConfig); + } } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/provisioning/provisioner.ts b/src/provisioning/provisioner.ts index 772a8f14..3875f011 100644 --- a/src/provisioning/provisioner.ts +++ b/src/provisioning/provisioner.ts @@ -113,6 +113,20 @@ export class Provisioner { 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; + try { + const membership = await this.intent.underlyingClient.getRoomStateEvent(roomId, "m.room.member", this.intent.userId) as MembershipEventContent; + if (membership.membership === "invite") { + await this.intent.underlyingClient.joinRoom(roomId); + } else if (membership.membership !== "join") { + return next(new ApiError("Bot is not joined to the room.", ErrCode.NotInRoom)); + } + } catch (ex) { + if (ex.body.errcode === "M_NOT_FOUND") { + return next(new ApiError("User is not joined to the room.", ErrCode.NotInRoom)); + } + log.warn(`Failed to find member event for ${req.query.userId} in room ${roomId}`, ex); + return next(new ApiError(`Could not determine if the user is in the room.`, ErrCode.NotInRoom)); + } // If the user just wants to read, just ensure they are in the room. try { const membership = await this.intent.underlyingClient.getRoomStateEvent(roomId, "m.room.member", userId) as MembershipEventContent;