From a13f4c0de83553036a3025a1c8572fa340c0479c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 11:12:43 +0000 Subject: [PATCH 01/11] Ensure webhooks are handled internally --- src/ConnectionManager.ts | 12 +++++++++++- src/Connections/GenericHook.ts | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index d6d9e5de..49960685 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -8,7 +8,7 @@ import { Appservice, StateEvent } from "matrix-bot-sdk"; import { CommentProcessor } from "./CommentProcessor"; import { BridgeConfig, GitLabInstance } from "./Config/Config"; import { GitHubDiscussionConnection, GitHubDiscussionSpace, GitHubIssueConnection, GitHubProjectConnection, GitHubRepoConnection, GitHubUserSpace, GitLabIssueConnection, GitLabRepoConnection, IConnection } from "./Connections"; -import { GenericHookConnection } from "./Connections/GenericHook"; +import { GenericHookAccountData, GenericHookConnection } from "./Connections/GenericHook"; import { JiraProjectConnection } from "./Connections/JiraProject"; import { GithubInstance } from "./Github/GithubInstance"; import { GitLabClient } from "./Gitlab/Client"; @@ -16,6 +16,7 @@ import { JiraProject } from "./Jira/Types"; import LogWrapper from "./LogWrapper"; import { MessageSenderClient } from "./MatrixSender"; import { UserTokenStore } from "./UserTokenStore"; +import {v4 as uuid} from "uuid"; const log = new LogWrapper("ConnectionManager"); @@ -137,9 +138,18 @@ export class ConnectionManager { } if (GenericHookConnection.EventTypes.includes(state.type) && this.config.generic?.enabled) { + // Generic hooks store the hookId in the account data + let acctData = await this.as.botClient.getSafeRoomAccountData(GenericHookConnection.CanonicalEventType, roomId); + if (!acctData) { + log.info(`hookId for ${roomId} not set, setting`); + acctData = { hookId: uuid() }; + await this.as.botClient.setRoomAccountData(GenericHookConnection.CanonicalEventType, roomId, acctData); + await this.as.botClient.sendStateEvent(roomId, GenericHookConnection.CanonicalEventType, state.stateKey, {...state.content, hookId: acctData.hookId }); + } return new GenericHookConnection( roomId, state.content, + acctData, state.stateKey, this.messageClient, this.config.generic.allowJsTransformationFunctions diff --git a/src/Connections/GenericHook.ts b/src/Connections/GenericHook.ts index f971fbc1..a8415764 100644 --- a/src/Connections/GenericHook.ts +++ b/src/Connections/GenericHook.ts @@ -6,10 +6,20 @@ import { Script, createContext } from "vm"; import { MatrixEvent } from "../MatrixEvent"; export interface GenericHookConnectionState { + /** + * This is ONLY used for display purposes. + */ hookId: string; transformationFunction?: string; } +export interface GenericHookAccountData { + /** + * This is where the true hook ID is kept. + */ + hookId: string; +} + const log = new LogWrapper("GenericHookConnection"); const md = new markdownit(); @@ -28,13 +38,14 @@ export class GenericHookConnection implements IConnection { ]; public get hookId() { - return this.state.hookId; + return this.accountData.hookId; } private transformationFunction?: Script; constructor(public readonly roomId: string, - private state: GenericHookConnectionState, + state: GenericHookConnectionState, + private readonly accountData: GenericHookAccountData, private readonly stateKey: string, private messageClient: MessageSenderClient, private readonly allowJSTransformation: boolean = false) { @@ -58,11 +69,28 @@ export class GenericHookConnection implements IConnection { } } + public transformHookData(data: Record): string { + // Supported parameters https://developers.mattermost.com/integrate/incoming-webhooks/#parameters + let msg = ""; + if (typeof data.username === "string") { + // Create a matrix user for this person + msg = `**${data.username}**: ` + } + if (typeof data.text === "string") { + msg = data.text; + } else { + msg = `Recieved webhook data:\n\n\`\`\`${JSON.stringify(data, undefined, 2)}\`\`\``; + } + + // TODO: Transform Slackdown into markdown. + return msg; + } + public async onGenericHook(data: Record) { log.info(`onGenericHook ${this.roomId} ${this.hookId}`); let content: string; if (!this.transformationFunction) { - content = `Recieved webhook data:\n\n\`\`\`${JSON.stringify(data)}\`\`\``; + content = this.transformHookData(data); } else { try { const context = createContext({data}); From a3b01fbb51426ff619b170ea7588edd4bece4280 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 19:33:01 +0000 Subject: [PATCH 02/11] Add support for setup commands --- src/Bridge.ts | 48 ++++------ src/Connections/SetupConnection.ts | 144 +++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+), 32 deletions(-) create mode 100644 src/Connections/SetupConnection.ts diff --git a/src/Bridge.ts b/src/Bridge.ts index b8671903..432e5679 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -581,44 +581,28 @@ export class Bridge { log.debug("Content:", JSON.stringify(event)); const adminRoom = this.adminRooms.get(roomId); - if (adminRoom) { - if (adminRoom.userId !== event.sender) { - return; - } - - const replyProcessor = new RichRepliesPreprocessor(true); - const processedReply = await replyProcessor.processEvent(event, this.as.botClient); - - if (processedReply) { - const metadata: IRichReplyMetadata = processedReply.mx_richreply; - log.info(`Handling reply to ${metadata.parentEventId} for ${adminRoom.userId}`); - // This might be a reply to a notification + if (!adminRoom) { + let handled = false; + for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) { try { - const ev = metadata.realEvent; - const splitParts: string[] = ev.content["uk.half-shot.matrix-hookshot.github.repo"]?.name.split("/"); - const issueNumber = ev.content["uk.half-shot.matrix-hookshot.github.issue"]?.number; - if (splitParts && issueNumber) { - log.info(`Handling reply for ${splitParts}${issueNumber}`); - const connections = this.connectionManager.getConnectionsForGithubIssue(splitParts[0], splitParts[1], issueNumber); - await Promise.all(connections.map(async c => { - if (c instanceof GitHubIssueConnection) { - return c.onMatrixIssueComment(processedReply); - } - })); - } else { - log.info("Missing parts!:", splitParts, issueNumber); + if (connection.onMessageEvent) { + handled = await connection.onMessageEvent(event); } } catch (ex) { - await adminRoom.sendNotice("Failed to handle repy. You may not be authenticated to do that."); - log.error("Reply event could not be handled:", ex); + log.warn(`Connection ${connection.toString()} failed to handle message:`, ex); } - return; } - - const command = event.content.body; - if (command) { - await adminRoom.handleCommand(event.event_id, command); + if (!handled) { + // Divert to the setup room code if we didn't match any of these + try { + await ( + new SetupConnection(roomId, this.as, this.tokenStore, this.github, !!this.config.jira) + ).onMessageEvent(event); + } catch (ex) { + log.warn(`Setup connection failed to handle:`, ex); + } } + return; } for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) { diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts new file mode 100644 index 00000000..7bb8488d --- /dev/null +++ b/src/Connections/SetupConnection.ts @@ -0,0 +1,144 @@ +// We need to instantiate some functions which are not directly called, which confuses typescript. +/* eslint-disable @typescript-eslint/ban-ts-comment */ +import { Appservice } from "matrix-bot-sdk"; +import { BotCommands, botCommand, compileBotCommands } from "../BotCommands"; +import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; +import LogWrapper from "../LogWrapper"; +import { CommandConnection } from "./CommandConnection"; +import { GenericHookConnection, GitHubRepoConnection, GitHubRepoConnectionState, JiraProjectConnection, JiraProjectConnectionState } from "."; +import { CommandError } from "../errors"; +import { UserTokenStore } from "../UserTokenStore"; +import { GithubInstance } from "../Github/GithubInstance"; +import { JiraProject } from "../Jira/Types"; +import { v4 as uuid } from "uuid"; +import { BridgeGenericWebhooksConfig } from "../Config/Config"; +import markdown from "markdown-it"; +const md = new markdown(); + +const log = new LogWrapper("SetupConnection"); + +/** + * Handles setting up a room + */ +export class SetupConnection extends CommandConnection { + + static botCommands: BotCommands; + static helpMessage: (cmdPrefix?: string | undefined) => MatrixMessageContent; + + constructor(public readonly roomId: string, + private readonly as: Appservice, + private readonly tokenStore: UserTokenStore, + private readonly githubInstance?: GithubInstance, + private readonly jiraEnabled?: boolean, + private readonly webhooksConfig?: BridgeGenericWebhooksConfig) { + super( + roomId, + as.botClient, + SetupConnection.botCommands, + SetupConnection.helpMessage, + "!setup", + ) + } + + public async onMessageEvent(ev: MatrixEvent) { + // Just check if the user has enough PL to change state + if (!await this.as.botClient.userHasPowerLevelFor(ev.sender, this.roomId, "", true)) { + throw new CommandError("not-configured", "You must be able to set state in a room ('Change settings') in order to setup new integrations."); + } + return super.onMessageEvent(ev); + } + + @botCommand("github repo", "Create a connection for a GitHub repository. (You must be logged in with GitHub to do this)", ["url"], [], true) + public async onGitHubRepo(userId: string, url: string) { + if (!this.githubInstance) { + throw new CommandError("not-configured", "The bridge is not configured to support GitHub"); + } + if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { + throw new CommandError("Bot lacks power level to set room state", "I do not have permission to setup a bridge in this room. Please promote me to an Admin/Moderator"); + } + const octokit = await this.tokenStore.getOctokitForUser(userId); + if (!octokit) { + throw new CommandError("User not logged in", "You are not logged into GitHub. Start a DM with this bot and use the command `github login`."); + } + const res = /^https:\/\/github\.com\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/.exec(url.trim().toLowerCase()); + if (!res) { + throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid"); + } + const [_, org, repo] = res; + let resultRepo + try { + resultRepo = await octokit.repos.get({owner: org, repo}); + } catch (ex) { + throw new CommandError("Invalid GitHub repo", "Could not find the requested GitHub repo. Do you have permission to view it?"); + } + // Check if we have a webhook for this repo + try { + await this.githubInstance.getOctokitForRepo(org, repo); + } catch (ex) { + log.warn(`No app instance for new git connection:`, ex); + // We might be able to do it via a personal access token + await this.as.botClient.sendNotice(this.roomId, `Note: There doesn't appear to be a GitHub App install that covers this repository so webhooks won't work.`) + } + await this.as.botClient.sendStateEvent(this.roomId, GitHubRepoConnection.CanonicalEventType, url, { + org, + repo, + } as GitHubRepoConnectionState); + await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${resultRepo.data.full_name}`); + } + + @botCommand("jira project", "Create a connection for a JIRA project. (You must be logged in with JIRA to do this)", ["url"], [], true) + public async onJiraProject(userId: string, url: string) { + if (!this.jiraEnabled) { + throw new CommandError("not-configured", "The bridge is not configured to support Jira"); + } + if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { + throw new CommandError("Bot lacks power level to set room state", "I do not have permission to setup a bridge in this room. Please promote me to an Admin/Moderator"); + } + const jiraClient = await this.tokenStore.getJiraForUser(userId); + if (!jiraClient) { + throw new CommandError("User not logged in", "You are not logged into Jira. Start a DM with this bot and use the command `jira login`."); + } + const res = /^https:\/\/([A-z.\-_]+)\/.+\/projects\/(\w+)\/?(\w+\/?)*$/.exec(url.trim().toLowerCase()); + if (!res) { + throw new CommandError("Invalid Jira url", "The JIRA project url you entered was not valid. It should be in the format of `https://jira-instance/.../projects/PROJECTKEY/...`"); + } + const [, origin, projectKey] = res; + const safeUrl = `https://${origin}/projects/${projectKey}`; + const jiraOriginClient = await jiraClient.getClientForUrl(new URL(safeUrl)); + if (!jiraOriginClient) { + throw new CommandError("User does not have permission to access this JIRA instance", "You do not have access to this JIRA instance. You may need to log into Jira again to provide access"); + } + let jiraProject: JiraProject; + try { + jiraProject = await jiraOriginClient.getProject(projectKey.toUpperCase()); + } catch (ex) { + log.warn(`Failed to get jira project:`, ex); + throw new CommandError("Missing or invalid JIRA project", "Could not find the requested JIRA project. Do you have permission to view it?"); + } + await this.as.botClient.sendStateEvent(this.roomId, JiraProjectConnection.CanonicalEventType, safeUrl, { + url: safeUrl, + } as JiraProjectConnectionState); + await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge Jira project '${jiraProject.name}' (${jiraProject.key})`); + } + + @botCommand("webhook", "Create a inbound webhook") + public async onWebhook() { + if (!this.webhooksConfig?.enabled) { + throw new CommandError("not-configured", "The bridge is not configured to support webhooks"); + } + if (!await this.as.botClient.userHasPowerLevelFor(this.as.botUserId, this.roomId, GitHubRepoConnection.CanonicalEventType, true)) { + throw new CommandError("Bot lacks power level to set room state", "I do not have permission to setup a bridge in this room. Please promote me to an Admin/Moderator"); + } + const hookId = uuid(); + const url = `${this.webhooksConfig.urlPrefix}${this.webhooksConfig.urlPrefix.endsWith('/') ? '' : '/'}${hookId}`; + await this.as.botClient.setRoomAccountData(this.roomId, GenericHookConnection.CanonicalEventType, {hookId}); + await this.as.botClient.sendStateEvent(this.roomId, GenericHookConnection.CanonicalEventType, hookId, {hookId}); + return this.as.botClient.sendHtmlNotice(this.roomId, md.renderInline(`Room configured to bridge webhooks. Please configure your webhook source to use \`${url}\``)); + } +} + +// Typescript doesn't understand Prototypes very well yet. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const res = compileBotCommands(SetupConnection.prototype as any, CommandConnection.prototype as any); +SetupConnection.helpMessage = res.helpMessage; +SetupConnection.botCommands = res.botCommands; \ No newline at end of file From 51c8be14228097560d6d6f7e4bb461341b640bc3 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 19:33:29 +0000 Subject: [PATCH 03/11] Update required bot config --- config.sample.yml | 1 + src/Bridge.ts | 51 ++++++++++++++++++++++++++++++------------ src/Config/Config.ts | 3 ++- src/Config/Defaults.ts | 1 + 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/config.sample.yml b/config.sample.yml index b716d752..226cea19 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -41,6 +41,7 @@ generic: # (Optional) Support for generic webhook events. `allowJsTransformationFunctions` will allow users to write short transformation snippets in code, and thus is unsafe in untrusted environments # enabled: false + urlPrefix: https://example.com/mywebhookspath/ allowJsTransformationFunctions: false webhook: # HTTP webhook listener options diff --git a/src/Bridge.ts b/src/Bridge.ts index 432e5679..b54a1a0e 100644 --- a/src/Bridge.ts +++ b/src/Bridge.ts @@ -30,6 +30,7 @@ import * as GitHubWebhookTypes from "@octokit/webhooks-types"; import LogWrapper from "./LogWrapper"; import { OAuthRequest } from "./WebhookTypes"; import { promises as fs } from "fs"; +import { SetupConnection } from "./Connections/SetupConnection"; const log = new LogWrapper("Bridge"); export class Bridge { @@ -264,15 +265,9 @@ export class Bridge { this.bindHandlerToQueue( "gitlab.merge_request.merge", (data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace), - (c, data) => c.onMergeRequestReviewed(data), + (c, data) => c.onMergeRequestMerged(data), ); - - this.bindHandlerToQueue( - "gitlab.merge_request.merge", - (data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace), - (c, data) => c.onMergeRequestReviewed(data), - ); - + this.bindHandlerToQueue( "gitlab.merge_request.approved", (data) => connManager.getConnectionsForGitLabRepo(data.project.path_with_namespace), @@ -561,7 +556,6 @@ export class Bridge { BRIDGE_ROOM_TYPE, roomId, room.accountData, ); } - // This is a group room, don't add the admin settings and just sit in the room. } private async onRoomMessage(roomId: string, event: MatrixEvent) { @@ -596,7 +590,7 @@ export class Bridge { // Divert to the setup room code if we didn't match any of these try { await ( - new SetupConnection(roomId, this.as, this.tokenStore, this.github, !!this.config.jira) + new SetupConnection(roomId, this.as, this.tokenStore, this.github, !!this.config.jira, this.config.generic) ).onMessageEvent(event); } catch (ex) { log.warn(`Setup connection failed to handle:`, ex); @@ -605,15 +599,44 @@ export class Bridge { return; } - for (const connection of this.connectionManager.getAllConnectionsForRoom(roomId)) { + if (adminRoom.userId !== event.sender) { + return; + } + + const replyProcessor = new RichRepliesPreprocessor(true); + const processedReply = await replyProcessor.processEvent(event, this.as.botClient); + + if (processedReply) { + const metadata: IRichReplyMetadata = processedReply.mx_richreply; + log.info(`Handling reply to ${metadata.parentEventId} for ${adminRoom.userId}`); + // This might be a reply to a notification try { - if (connection.onMessageEvent) { - await connection.onMessageEvent(event); + const ev = metadata.realEvent; + const splitParts: string[] = ev.content["uk.half-shot.matrix-hookshot.github.repo"]?.name.split("/"); + const issueNumber = ev.content["uk.half-shot.matrix-hookshot.github.issue"]?.number; + if (splitParts && issueNumber) { + log.info(`Handling reply for ${splitParts}${issueNumber}`); + const connections = this.connectionManager.getConnectionsForGithubIssue(splitParts[0], splitParts[1], issueNumber); + await Promise.all(connections.map(async c => { + if (c instanceof GitHubIssueConnection) { + return c.onMatrixIssueComment(processedReply); + } + })); + } else { + log.info("Missing parts!:", splitParts, issueNumber); } } catch (ex) { - log.warn(`Connection ${connection.toString()} failed to handle message:`, ex); + await adminRoom.sendNotice("Failed to handle repy. You may not be authenticated to do that."); + log.error("Reply event could not be handled:", ex); } + return; } + + const command = event.content.body; + if (command) { + await adminRoom.handleCommand(event.event_id, command); + } + } private async onRoomJoin(roomId: string, matrixEvent: MatrixEvent) { diff --git a/src/Config/Config.ts b/src/Config/Config.ts index 166cefbd..9e7f1e1a 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -52,8 +52,9 @@ export interface BridgeConfigJira { }; } -interface BridgeGenericWebhooksConfig { +export interface BridgeGenericWebhooksConfig { enabled: boolean; + urlPrefix: string; allowJsTransformationFunctions?: boolean; } diff --git a/src/Config/Defaults.ts b/src/Config/Defaults.ts index cb989039..46a515e8 100644 --- a/src/Config/Defaults.ts +++ b/src/Config/Defaults.ts @@ -70,6 +70,7 @@ export const DefaultConfig = new BridgeConfig({ }, generic: { enabled: false, + urlPrefix: "https://example.com/mywebhookspath/", allowJsTransformationFunctions: false, } }, {}); From 7802233f2f5bf5208fbdb9909390aa5e0e770735 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 19:33:49 +0000 Subject: [PATCH 04/11] Cleanup bot commands --- src/ConnectionManager.ts | 3 +-- src/Connections/GitlabRepo.ts | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index d6d9e5de..9500ccbe 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -60,7 +60,7 @@ export class ConnectionManager { if (!this.github) { throw Error('GitHub is not configured'); } - return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey); + return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey, this.github); } if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { @@ -129,7 +129,6 @@ export class ConnectionManager { } if (JiraProjectConnection.EventTypes.includes(state.type)) { - console.log("WOOF", state); if (!this.config.jira) { throw Error('JIRA is not configured'); } diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts index 0294b2d3..bb431eaa 100644 --- a/src/Connections/GitlabRepo.ts +++ b/src/Connections/GitlabRepo.ts @@ -46,7 +46,7 @@ export class GitLabRepoConnection extends CommandConnection { as.botClient, GitLabRepoConnection.botCommands, GitLabRepoConnection.helpMessage, - "!gl" + state.commandPrefix || "!gl" ) if (!state.path || !state.instance) { throw Error('Invalid state, missing `path` or `instance`'); @@ -66,7 +66,7 @@ export class GitLabRepoConnection extends CommandConnection { return GitLabRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey; } - @botCommand("gl create", "Create an issue for this repo", ["title"], ["description", "labels"], true) + @botCommand("create", "Create an issue for this repo", ["title"], ["description", "labels"], true) public async onCreateIssue(userId: string, title: string, description?: string, labels?: string) { const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url); if (!client) { @@ -89,7 +89,7 @@ export class GitLabRepoConnection extends CommandConnection { }); } - @botCommand("gl close", "Close an issue", ["number"], ["comment"], true) + @botCommand("close", "Close an issue", ["number"], ["comment"], true) public async onClose(userId: string, number: string) { const client = await this.tokenStore.getGitLabForUser(userId, this.instance.url); if (!client) { From 8ab5505a0ffd8a50416b109207b34f0397376b63 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 19:34:15 +0000 Subject: [PATCH 05/11] Add support for showing an issue room link when enabled --- src/Connections/CommandConnection.ts | 5 +++-- src/Connections/GithubDiscussion.ts | 3 ++- src/Connections/GithubDiscussionSpace.ts | 1 - src/Connections/GithubIssue.ts | 8 +++++++- src/Connections/GithubRepo.ts | 19 +++++++++++++++++-- src/Github/GithubInstance.ts | 13 +++++++++++-- 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Connections/CommandConnection.ts b/src/Connections/CommandConnection.ts index 263814f9..ecbb127b 100644 --- a/src/Connections/CommandConnection.ts +++ b/src/Connections/CommandConnection.ts @@ -21,7 +21,7 @@ export abstract class CommandConnection { const { error, handled, humanError } = await handleCommand(ev.sender, ev.content.body, this.botCommands, this, this.commandPrefix); if (!handled) { // Not for us. - return; + return false; } if (error) { await this.botClient.sendEvent(this.roomId, "m.reaction", { @@ -36,7 +36,7 @@ export abstract class CommandConnection { body: humanError ? `Failed to handle command: ${humanError}` : "Failed to handle command", }); log.warn(`Failed to handle command:`, error); - return; + return true; } await this.botClient.sendEvent(this.roomId, "m.reaction", { "m.relates_to": { @@ -45,6 +45,7 @@ export abstract class CommandConnection { key: "✅", } }); + return true; } @botCommand("help", "This help text") diff --git a/src/Connections/GithubDiscussion.ts b/src/Connections/GithubDiscussion.ts index 8bbaa37c..f13570c6 100644 --- a/src/Connections/GithubDiscussion.ts +++ b/src/Connections/GithubDiscussion.ts @@ -100,12 +100,13 @@ export class GitHubDiscussionConnection implements IConnection { if (octokit === null) { // TODO: Use Reply - Also mention user. await this.as.botClient.sendNotice(this.roomId, `${ev.sender}: Cannot send comment, you are not logged into GitHub`); - return; + return true; } const qlClient = new GithubGraphQLClient(octokit); const commentId = await qlClient.addDiscussionComment(this.state.internalId, ev.content.body); log.info(`Sent ${commentId} for ${ev.event_id} (${ev.sender})`); this.sentEvents.add(commentId); + return true; } public get discussionNumber() { diff --git a/src/Connections/GithubDiscussionSpace.ts b/src/Connections/GithubDiscussionSpace.ts index 5d3b8b62..8ce7590d 100644 --- a/src/Connections/GithubDiscussionSpace.ts +++ b/src/Connections/GithubDiscussionSpace.ts @@ -1,7 +1,6 @@ import { IConnection } from "./IConnection"; import { Appservice, Space } from "matrix-bot-sdk"; import LogWrapper from "../LogWrapper"; -import { Octokit } from "@octokit/rest"; import { ReposGetResponseData } from "../Github/Types"; import axios from "axios"; import { GitHubDiscussionConnection } from "./GithubDiscussion"; diff --git a/src/Connections/GithubIssue.ts b/src/Connections/GithubIssue.ts index 2d7b61f2..4b93151a 100644 --- a/src/Connections/GithubIssue.ts +++ b/src/Connections/GithubIssue.ts @@ -47,6 +47,10 @@ export class GitHubIssueConnection implements IConnection { static readonly QueryRoomRegex = /#github_(.+)_(.+)_(\d+):.*/; + static generateAliasLocalpart(org: string, repo: string, issueNo: string|number) { + return `github_${org}_${repo}_${issueNo}`; + } + static async onQueryRoom(result: RegExpExecArray, opts: IQueryRoomOpts): Promise { const parts = result?.slice(1); if (!parts) { @@ -322,9 +326,11 @@ export class GitHubIssueConnection implements IConnection { public async onMessageEvent(ev: MatrixEvent) { if (ev.content.body === '!sync') { // Sync data. - return this.syncIssueState(); + await this.syncIssueState(); + return true; } await this.onMatrixIssueComment(ev); + return true; } public toString() { diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts index 9d22d2bd..aedfe085 100644 --- a/src/Connections/GithubRepo.ts +++ b/src/Connections/GithubRepo.ts @@ -16,6 +16,7 @@ import LogWrapper from "../LogWrapper"; import markdown from "markdown-it"; import { CommandConnection } from "./CommandConnection"; import { GithubInstance } from "../Github/GithubInstance"; +import { GitHubIssueConnection } from "."; const log = new LogWrapper("GitHubRepoConnection"); const md = new markdown(); @@ -32,6 +33,7 @@ export interface GitHubRepoConnectionState { repo: string; ignoreHooks?: string[], commandPrefix?: string; + showIssueRoomLink?: boolean; } const GITHUB_REACTION_CONTENT: {[emoji: string]: string} = { @@ -151,7 +153,8 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti private readonly as: Appservice, private state: GitHubRepoConnectionState, private readonly tokenStore: UserTokenStore, - private readonly stateKey: string) { + private readonly stateKey: string, + private readonly githubInstance: GithubInstance) { super( roomId, as.botClient, @@ -165,6 +168,10 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti return this.state.org.toLowerCase(); } + private get showIssueRoomLink() { + return this.state.showIssueRoomLink === false ? false : true; + } + public get repo() { return this.state.repo.toLowerCase(); } @@ -311,7 +318,15 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti const orgRepoName = event.repository.full_name; let message = `${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`; - message = message + (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : ''); + message += (event.issue.assignee ? ` assigned to ${event.issue.assignee.login}` : ''); + if (this.showIssueRoomLink) { + const appInstance = await this.githubInstance.getSafeOctokitForRepo(this.org, this.repo); + if (appInstance) { + message += ` [Issue Room](https://matrix.to/#/${this.as.getAlias(GitHubIssueConnection.generateAliasLocalpart(this.org, this.repo, event.issue.number))})`; + } else { + log.warn(`Cannot show issue room link, no app install for ${orgRepoName}`); + } + } const content = emoji.emojify(message); const labels = FormatUtil.formatLabels(event.issue.labels?.map(l => ({ name: l.name, description: l.description || undefined, color: l.color || undefined }))); await this.as.botIntent.sendEvent(this.roomId, { diff --git a/src/Github/GithubInstance.ts b/src/Github/GithubInstance.ts index 131b19a1..d7ac8a0b 100644 --- a/src/Github/GithubInstance.ts +++ b/src/Github/GithubInstance.ts @@ -39,15 +39,24 @@ export class GithubInstance { }); } - public getOctokitForRepo(orgName: string, repoName?: string) { + public getSafeOctokitForRepo(orgName: string, repoName?: string) { const targetName = (repoName ? `${orgName}/${repoName}` : orgName).toLowerCase(); + console.log([...this.installationsCache.values()]); for (const install of this.installationsCache.values()) { if (install.matchesRepository.includes(targetName) || install.matchesRepository.includes(`${targetName.split('/')[0]}/*`)) { return this.createOctokitForInstallation(install.id); } } + return null; + } + + public getOctokitForRepo(orgName: string, repoName?: string) { + const res = this.getSafeOctokitForRepo(orgName, repoName); + if (res) { + return res; + } // TODO: Refresh cache? - throw Error(`No installation found to handle ${targetName}`); + throw Error(`No installation found to handle ${orgName}/${repoName}`); } private createOctokitForInstallation(installationId: number) { From 945514bcbc25f65661bfe8ba454669c18c381c8a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 30 Nov 2021 19:34:46 +0000 Subject: [PATCH 06/11] Sneaky jira tweaks --- src/Connections/GitlabIssue.ts | 2 ++ src/Connections/IConnection.ts | 5 +++-- src/Connections/JiraProject.ts | 17 ++++++++++------- src/Jira/Client.ts | 25 +++++++++++++++---------- src/Jira/Types.ts | 2 +- src/UserTokenStore.ts | 1 - 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Connections/GitlabIssue.ts b/src/Connections/GitlabIssue.ts index 99173156..947e639c 100644 --- a/src/Connections/GitlabIssue.ts +++ b/src/Connections/GitlabIssue.ts @@ -179,8 +179,10 @@ export class GitLabIssueConnection implements IConnection { if (ev.content.body === '!sync') { // Sync data. // return this.syncIssueState(); + return true; } await this.onMatrixIssueComment(ev); + return true; } public toString() { diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts index d8579b81..93516477 100644 --- a/src/Connections/IConnection.ts +++ b/src/Connections/IConnection.ts @@ -13,9 +13,10 @@ export interface IConnection { onEvent?: (ev: MatrixEvent) => Promise; /** - * When a room gets a message event + * When a room gets a message event. + * @returns Was the message handled */ - onMessageEvent?: (ev: MatrixEvent) => Promise; + onMessageEvent?: (ev: MatrixEvent) => Promise; onIssueCreated?: (ev: IssuesOpenedEvent) => Promise; diff --git a/src/Connections/JiraProject.ts b/src/Connections/JiraProject.ts index d349d716..fa50b8cd 100644 --- a/src/Connections/JiraProject.ts +++ b/src/Connections/JiraProject.ts @@ -7,13 +7,13 @@ import { JiraIssueEvent, JiraIssueUpdatedEvent } from "../Jira/WebhookTypes"; import { FormatUtil } from "../FormatUtil"; import markdownit from "markdown-it"; import { generateJiraWebLinkFromIssue } from "../Jira"; -import { JiraIssue, JiraProject } from "../Jira/Types"; +import { JiraProject } from "../Jira/Types"; import { botCommand, BotCommands, compileBotCommands } from "../BotCommands"; import { MatrixMessageContent } from "../MatrixEvent"; import { CommandConnection } from "./CommandConnection"; -import { start } from "repl"; import { UserTokenStore } from "../UserTokenStore"; import { CommandError, NotLoggedInError } from "../errors"; +import JiraApi from "jira-client"; type JiraAllowedEventsNames = "issue.created"; const JiraAllowedEvents: JiraAllowedEventsNames[] = ["issue.created"]; @@ -159,11 +159,14 @@ export class JiraProjectConnection extends CommandConnection implements IConnect if (!jiraClient) { throw new NotLoggedInError(); } - const resource = (await jiraClient.getAccessibleResources()).find((r) => new URL(r.url).origin === this.instanceOrigin); - if (!resource) { - throw new CommandError("No-resource", "You do not have permission to create issues for this JIRA org"); + if (!this.projectUrl) { + throw new CommandError("No-resource-origin", "Room is configured with an ID and not a URL, cannot determine correct JIRA client"); } - return jiraClient.getClientForResource(resource); + const jiraProjectClient = await jiraClient.getClientForUrl(this.projectUrl); + if (!jiraProjectClient) { + throw new CommandError("No-resource", "You do not have permission to manage issues for this JIRA org"); + } + return jiraProjectClient; } @botCommand("create", "Create an issue for this project", ["type", "title"], ["description", "labels"], true) @@ -183,7 +186,7 @@ export class JiraProjectConnection extends CommandConnection implements IConnect throw new CommandError("invalid-issuetype", `You must specify a valid issue type (one of ${content}). E.g. ${this.commandPrefix} create ${project.issueTypes[0].name}`); } log.info(`Creating new issue on behalf of ${userId}`); - let result: any; + let result: JiraApi.JsonResponse; try { result = await api.addNewIssue({ //update: {}, diff --git a/src/Jira/Client.ts b/src/Jira/Client.ts index 9c078ead..75c4e87d 100644 --- a/src/Jira/Client.ts +++ b/src/Jira/Client.ts @@ -4,7 +4,9 @@ import JiraApi, { SearchUserOptions } from 'jira-client'; import QuickLRU from "@alloc/quick-lru"; import { JiraAccount, JiraAPIAccessibleResource, JiraIssue, JiraOAuthResult, JiraProject } from './Types'; import { BridgeConfigJira } from '../Config/Config'; +import LogWrapper from '../LogWrapper'; +const log = new LogWrapper("JiraClient"); const ACCESSIBLE_RESOURCE_CACHE_LIMIT = 100; const ACCESSIBLE_RESOURCE_CACHE_TTL_MS = 60000; @@ -13,11 +15,11 @@ export class HookshotJiraApi extends JiraApi { super(options); } - async getProject(projectIdOrKey: string) { + async getProject(projectIdOrKey: string): Promise { return await super.getProject(projectIdOrKey) as JiraProject; } - async getIssue(issueIdOrKey: string) { + async getIssue(issueIdOrKey: string): Promise { const res = await axios.get(`https://api.atlassian.com/${this.options.base}/rest/api/3/issue/${issueIdOrKey}`, { headers: { Authorization: `Bearer ${this.options.bearer}` @@ -60,6 +62,7 @@ export class JiraClient { // Existing failed promise, break out and try again. JiraClient.resourceCache.delete(this.bearer); } + await this.checkTokenAge(); const promise = (async () => { const res = await axios.get(`https://api.atlassian.com/oauth/token/accessible-resources`, { headers: { @@ -74,26 +77,28 @@ export class JiraClient { } async checkTokenAge() { + console.log("checkTokenAge:", this.oauth2State); if (this.oauth2State.expires_in + 60000 > Date.now()) { return; } + log.info(`Refreshing oauth token`); // Refresh the token - const res = await axios.post(`https://api.atlassian.com/oauth/token`, { + const res = await axios.post(`https://api.atlassian.com/oauth/token`, { grant_type: "refresh_token", client_id: this.config.oauth.client_id, client_secret: this.config.oauth.client_secret, refresh_token: this.oauth2State.refresh_token, }); - res.expires_in += Date.now() + (res.expires_in * 1000); - this.oauth2State = res; + const data = res.data as JiraOAuthResult; + data.expires_in += Date.now() + (data.expires_in * 1000); + this.oauth2State = data; + this.onTokenRefreshed(this.oauth2State); } - async getClientForName(name: string) { - const resources = await this.getAccessibleResources(); - const resource = resources.find((res) => res.name === name); - await this.checkTokenAge(); + async getClientForUrl(url: URL) { + const resource = (await this.getAccessibleResources()).find((r) => new URL(r.url).origin === url.origin); if (!resource) { - throw Error('User does not have access to this resource'); + return null; } return this.getClientForResource(resource); } diff --git a/src/Jira/Types.ts b/src/Jira/Types.ts index cad3a24f..3b7cb445 100644 --- a/src/Jira/Types.ts +++ b/src/Jira/Types.ts @@ -69,7 +69,7 @@ export interface JiraIssue { } export interface JiraOAuthResult { - state: string; + state?: string; access_token: string; refresh_token: string; expires_in: number; diff --git a/src/UserTokenStore.ts b/src/UserTokenStore.ts index 12a399a3..f4024671 100644 --- a/src/UserTokenStore.ts +++ b/src/UserTokenStore.ts @@ -3,7 +3,6 @@ import { GitLabClient } from "./Gitlab/Client"; import { Intent } from "matrix-bot-sdk"; import { promises as fs } from "fs"; import { publicEncrypt, privateDecrypt } from "crypto"; -import JiraApi from 'jira-client'; import LogWrapper from "./LogWrapper"; import { JiraClient } from "./Jira/Client"; import { JiraOAuthResult } from "./Jira/Types"; From 998ae730f824de79defab826d9f1b96a5f26bf3d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 1 Dec 2021 10:33:36 +0000 Subject: [PATCH 07/11] Add no-console rule, linting --- .eslintrc.js | 1 + src/App/BridgeApp.ts | 2 +- src/Config/Defaults.ts | 3 +++ src/Connections/SetupConnection.ts | 2 +- src/Github/GithubInstance.ts | 2 -- src/Jira/Client.ts | 1 - src/Notifications/GitLabWatcher.ts | 1 - 7 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 63af3380..4d3234c6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,6 +12,7 @@ module.exports = { rules: { "@typescript-eslint/explicit-module-boundary-types": "off", "camelcase": ["error", { "properties": "never", "ignoreDestructuring": true }], + "no-console": "error" }, "env": { "node": true, diff --git a/src/App/BridgeApp.ts b/src/App/BridgeApp.ts index d8b1fb11..1e40d7f2 100644 --- a/src/App/BridgeApp.ts +++ b/src/App/BridgeApp.ts @@ -5,7 +5,7 @@ import { BridgeConfig, parseRegistrationFile } from "../Config/Config"; import { Webhooks } from "../Webhooks"; import { MatrixSender } from "../MatrixSender"; import { UserNotificationWatcher } from "../Notifications/UserNotificationWatcher"; -import { LogLevel, LogService } from "matrix-bot-sdk"; + LogWrapper.configureLogging("debug"); const log = new LogWrapper("App"); diff --git a/src/Config/Defaults.ts b/src/Config/Defaults.ts index 46a515e8..6122faa9 100644 --- a/src/Config/Defaults.ts +++ b/src/Config/Defaults.ts @@ -141,6 +141,7 @@ async function renderRegistrationFile(configPath?: string) { rooms: [], }, }; + // eslint-disable-next-line no-console console.log(YAML.stringify(obj)); } @@ -148,9 +149,11 @@ async function renderRegistrationFile(configPath?: string) { // Can be called directly if (require.main === module) { if (process.argv[2] === '--config') { + // eslint-disable-next-line no-console console.log(renderDefaultConfig()); } else if (process.argv[2] === '--registration') { renderRegistrationFile(process.argv[3]).catch(ex => { + // eslint-disable-next-line no-console console.error(ex); process.exit(1); }); diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts index 7bb8488d..3b43a9f8 100644 --- a/src/Connections/SetupConnection.ts +++ b/src/Connections/SetupConnection.ts @@ -64,7 +64,7 @@ export class SetupConnection extends CommandConnection { if (!res) { throw new CommandError("Invalid GitHub url", "The GitHub url you entered was not valid"); } - const [_, org, repo] = res; + const [org, repo] = res; let resultRepo try { resultRepo = await octokit.repos.get({owner: org, repo}); diff --git a/src/Github/GithubInstance.ts b/src/Github/GithubInstance.ts index d7ac8a0b..2850ee83 100644 --- a/src/Github/GithubInstance.ts +++ b/src/Github/GithubInstance.ts @@ -5,7 +5,6 @@ import LogWrapper from "../LogWrapper"; import { DiscussionQLResponse, DiscussionQL } from "./Discussion"; import * as GitHubWebhookTypes from "@octokit/webhooks-types"; import { InstallationDataType } from "./Types"; -import e from "express"; const log = new LogWrapper("GithubInstance"); @@ -41,7 +40,6 @@ export class GithubInstance { public getSafeOctokitForRepo(orgName: string, repoName?: string) { const targetName = (repoName ? `${orgName}/${repoName}` : orgName).toLowerCase(); - console.log([...this.installationsCache.values()]); for (const install of this.installationsCache.values()) { if (install.matchesRepository.includes(targetName) || install.matchesRepository.includes(`${targetName.split('/')[0]}/*`)) { return this.createOctokitForInstallation(install.id); diff --git a/src/Jira/Client.ts b/src/Jira/Client.ts index 75c4e87d..16ca99ed 100644 --- a/src/Jira/Client.ts +++ b/src/Jira/Client.ts @@ -77,7 +77,6 @@ export class JiraClient { } async checkTokenAge() { - console.log("checkTokenAge:", this.oauth2State); if (this.oauth2State.expires_in + 60000 > Date.now()) { return; } diff --git a/src/Notifications/GitLabWatcher.ts b/src/Notifications/GitLabWatcher.ts index f27da33a..e14aa7f5 100644 --- a/src/Notifications/GitLabWatcher.ts +++ b/src/Notifications/GitLabWatcher.ts @@ -32,6 +32,5 @@ export class GitLabWatcher extends EventEmitter implements NotificationWatcherTa const events = await this.client.getEvents({ after: new Date(this.since) }); - console.log(events); } } \ No newline at end of file From 91ed232ebad8e389cb4a6ed5847e4d40ed85aacd Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 1 Dec 2021 10:41:24 +0000 Subject: [PATCH 08/11] Add config option for showIssueRoomLink --- src/Config/Config.ts | 43 +++++++++++++++++++++++++++++++---- src/Config/Defaults.ts | 3 +++ src/Connections/GithubRepo.ts | 7 ++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/Config/Config.ts b/src/Config/Config.ts index 9e7f1e1a..b5400e31 100644 --- a/src/Config/Config.ts +++ b/src/Config/Config.ts @@ -4,15 +4,15 @@ import { IAppserviceRegistration } from "matrix-bot-sdk"; import * as assert from "assert"; import { configKey } from "./Decorators"; -export interface BridgeConfigGitHub { +interface BridgeConfigGitHubYAML { auth: { id: number|string; privateKeyFile: string; }; webhook: { secret: string; - }, - oauth: { + }; + oauth?: { // eslint-disable-next-line camelcase client_id: string; // eslint-disable-next-line camelcase @@ -20,6 +20,41 @@ export interface BridgeConfigGitHub { // eslint-disable-next-line camelcase redirect_uri: string; }; + defaultOptions?: { + showIssueRoomLink: false; + } +} + +export class BridgeConfigGitHub { + @configKey("Authentication for the GitHub App.", false) + auth: { + id: number|string; + privateKeyFile: string; + }; + @configKey("Webhook settings for the GitHub app.", false) + webhook: { + secret: string; + }; + @configKey("Settings for allowing users to sign in via OAuth.", true) + oauth?: { + // eslint-disable-next-line camelcase + client_id: string; + // eslint-disable-next-line camelcase + client_secret: string; + // eslint-disable-next-line camelcase + redirect_uri: string; + }; + @configKey("Default options for GitHub connections.", true) + defaultOptions?: { + showIssueRoomLink: false; + }; + + constructor(yaml: BridgeConfigGitHubYAML) { + this.auth = yaml.auth; + this.webhook = yaml.webhook; + this.oauth = yaml.oauth; + this.defaultOptions = yaml.defaultOptions; + } } export interface GitLabInstance { @@ -140,7 +175,7 @@ export class BridgeConfig { constructor(configData: BridgeConfigRoot, env: {[key: string]: string|undefined}) { this.bridge = configData.bridge; assert.ok(this.bridge); - this.github = configData.github; + this.github = configData.github && new BridgeConfigGitHub(configData.github); if (this.github?.auth && env["GITHUB_PRIVATE_KEY_FILE"]) { this.github.auth.privateKeyFile = env["GITHUB_PRIVATE_KEY_FILE"]; } diff --git a/src/Config/Defaults.ts b/src/Config/Defaults.ts index 6122faa9..f07f618a 100644 --- a/src/Config/Defaults.ts +++ b/src/Config/Defaults.ts @@ -47,6 +47,9 @@ export const DefaultConfig = new BridgeConfig({ webhook: { secret: "secrettoken", }, + defaultOptions: { + showIssueRoomLink: false, + } }, gitlab: { instances: { diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts index aedfe085..b18ef799 100644 --- a/src/Connections/GithubRepo.ts +++ b/src/Connections/GithubRepo.ts @@ -17,6 +17,7 @@ import markdown from "markdown-it"; import { CommandConnection } from "./CommandConnection"; import { GithubInstance } from "../Github/GithubInstance"; import { GitHubIssueConnection } from "."; +import { BridgeConfigGitHub } from "../Config/Config"; const log = new LogWrapper("GitHubRepoConnection"); const md = new markdown(); @@ -154,7 +155,9 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti private state: GitHubRepoConnectionState, private readonly tokenStore: UserTokenStore, private readonly stateKey: string, - private readonly githubInstance: GithubInstance) { + private readonly githubInstance: GithubInstance, + private readonly config: BridgeConfigGitHub, + ) { super( roomId, as.botClient, @@ -169,7 +172,7 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti } private get showIssueRoomLink() { - return this.state.showIssueRoomLink === false ? false : true; + return this.state.showIssueRoomLink === undefined ? (this.config.defaultOptions?.showIssueRoomLink || false) : this.state.showIssueRoomLink; } public get repo() { From 0d2773a4e4129fa055e544c05ad8a7242608cecb Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 1 Dec 2021 10:45:02 +0000 Subject: [PATCH 09/11] Change command from !setup to !hookshot to help with namespace clashes --- src/Connections/SetupConnection.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts index 3b43a9f8..e243e716 100644 --- a/src/Connections/SetupConnection.ts +++ b/src/Connections/SetupConnection.ts @@ -1,5 +1,4 @@ // We need to instantiate some functions which are not directly called, which confuses typescript. -/* eslint-disable @typescript-eslint/ban-ts-comment */ import { Appservice } from "matrix-bot-sdk"; import { BotCommands, botCommand, compileBotCommands } from "../BotCommands"; import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; @@ -18,7 +17,8 @@ const md = new markdown(); const log = new LogWrapper("SetupConnection"); /** - * Handles setting up a room + * Handles setting up a room with connections. This connection is "virtual" in that it has + * no state, and is only invoked when messages from other clients fall through. */ export class SetupConnection extends CommandConnection { @@ -36,7 +36,7 @@ export class SetupConnection extends CommandConnection { as.botClient, SetupConnection.botCommands, SetupConnection.helpMessage, - "!setup", + "!hookshot", ) } From c75977a91fb81b9d0b65ab33f22c3c24569d9dcc Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 1 Dec 2021 10:51:49 +0000 Subject: [PATCH 10/11] Fixup some undefineds --- src/AdminRoom.ts | 3 +++ src/ConnectionManager.ts | 4 ++-- src/Webhooks.ts | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/AdminRoom.ts b/src/AdminRoom.ts index e81e6868..1f84965f 100644 --- a/src/AdminRoom.ts +++ b/src/AdminRoom.ts @@ -173,6 +173,9 @@ export class AdminRoom extends AdminRoomCommandHandler { if (!this.config.github) { throw new CommandError("no-github-support", "The bridge is not configured with GitHub support"); } + if (!this.config.github.oauth) { + throw new CommandError("no-github-support", "The bridge is not configured with GitHub OAuth support"); + } // If this is already set, calling this command will invalidate the previous session. this.pendingOAuthState = uuid(); const q = qs.stringify({ diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 9500ccbe..bf19909e 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -57,10 +57,10 @@ export class ConnectionManager { } if (GitHubRepoConnection.EventTypes.includes(state.type)) { - if (!this.github) { + if (!this.github || !this.config.github) { throw Error('GitHub is not configured'); } - return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey, this.github); + return new GitHubRepoConnection(roomId, this.as, state.content, this.tokenStore, state.stateKey, this.github, this.config.github); } if (GitHubDiscussionConnection.EventTypes.includes(state.type)) { diff --git a/src/Webhooks.ts b/src/Webhooks.ts index 66e9fc7a..c96695e5 100644 --- a/src/Webhooks.ts +++ b/src/Webhooks.ts @@ -205,7 +205,8 @@ export class Webhooks extends EventEmitter { public async onGitHubGetOauth(req: Request, res: Response) { log.info("Got new oauth request"); try { - if (!this.config.github) { + if (!this.config.github || !this.config.github.oauth) { + res.status(500).send(`

Bridge is not configured with OAuth support

`); throw Error("Got GitHub oauth request but github was not configured!"); } const exists = await this.queue.pushWait({ From f599618563649377c9a0840801f90f244079bd7e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 1 Dec 2021 10:54:47 +0000 Subject: [PATCH 11/11] Update sample config --- config.sample.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/config.sample.yml b/config.sample.yml index 226cea19..0698b788 100644 --- a/config.sample.yml +++ b/config.sample.yml @@ -12,14 +12,24 @@ github: # (Optional) Configure this to enable GitHub support # auth: + # Authentication for the GitHub App. + # id: 123 privateKeyFile: github-key.pem + webhook: + # Webhook settings for the GitHub app. + # + secret: secrettoken oauth: + # (Optional) Settings for allowing users to sign in via OAuth. + # client_id: foo client_secret: bar redirect_uri: https://example.com/bridge_oauth/ - webhook: - secret: secrettoken + defaultOptions: + # (Optional) Default options for GitHub connections. + # + showIssueRoomLink: false gitlab: # (Optional) Configure this to enable GitLab support #