Support updating some configs

This commit is contained in:
Will Hunt 2021-12-03 17:27:22 +00:00
parent 431a96f693
commit a3c7a29267
4 changed files with 75 additions and 32 deletions

View File

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

View File

@ -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<string, unknown>, 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<string, unknown> = {}, 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<unknown>) {
const state = stateEv.content as GenericHookConnectionState;
if (state.transformationFunction && this.config.allowJsTransformationFunctions) {
const validatedConfig = GenericHookConnection.validateState(stateEv.content as Record<string, unknown>, 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, unknown>): 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<string, unknown>) {
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}`;
}

View File

@ -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<unknown>) {
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<string, unknown>) {
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

View File

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