diff --git a/src/AdminRoom.ts b/src/AdminRoom.ts index 07e2d01b..a779d986 100644 --- a/src/AdminRoom.ts +++ b/src/AdminRoom.ts @@ -49,7 +49,7 @@ export interface AdminAccountData { } export class AdminRoom extends EventEmitter { - public static helpMessage: MatrixMessageContent; + public static helpMessage: () => MatrixMessageContent; private widgetAccessToken = `abcdef`; static botCommands: BotCommands; @@ -149,7 +149,7 @@ export class AdminRoom extends EventEmitter { @botCommand("help", "This help text") public async helpCommand() { - return this.botIntent.sendEvent(this.roomId, AdminRoom.helpMessage); + return this.botIntent.sendEvent(this.roomId, AdminRoom.helpMessage()); } @botCommand("github setpersonaltoken", "Set your personal access token for GitHub", ['accessToken']) diff --git a/src/BotCommands.ts b/src/BotCommands.ts index 17e083bc..e3f251d2 100644 --- a/src/BotCommands.ts +++ b/src/BotCommands.ts @@ -24,7 +24,7 @@ export type BotCommands = {[prefix: string]: { includeUserId: boolean, }}; -export function compileBotCommands(prototype: Record): {helpMessage: MatrixMessageContent, botCommands: BotCommands} { +export function compileBotCommands(prototype: Record): {helpMessage: (cmdPrefix?: string) => MatrixMessageContent, botCommands: BotCommands} { let content = "Commands:\n"; const botCommands: BotCommands = {}; Object.getOwnPropertyNames(prototype).forEach(propetyKey => { @@ -32,7 +32,7 @@ export function compileBotCommands(prototype: Record if (b) { const requiredArgs = b.requiredArgs.join(" "); const optionalArgs = b.optionalArgs.map((arg: string) => `[${arg}]`).join(" "); - content += ` - \`${b.prefix}\` ${requiredArgs} ${optionalArgs} - ${b.help}\n`; + content += ` - \`££PREFIX££${b.prefix}\` ${requiredArgs} ${optionalArgs} - ${b.help}\n`; // We know that this is safe. botCommands[b.prefix as string] = { fn: prototype[propetyKey], @@ -43,17 +43,23 @@ export function compileBotCommands(prototype: Record } }); return { - helpMessage: { + helpMessage: (cmdPrefix?: string) => ({ msgtype: "m.notice", body: content, - formatted_body: md.render(content), + formatted_body: md.render(content).replace(/££PREFIX££/g, cmdPrefix || ""), format: "org.matrix.custom.html" - }, + }), botCommands, } } -export async function handleCommand(userId: string, command: string, botCommands: BotCommands, obj: unknown): Promise<{error?: string, handled?: boolean}> { +export async function handleCommand(userId: string, command: string, botCommands: BotCommands, obj: unknown, prefix?: string): Promise<{error?: string, handled?: boolean, humanError?: string}> { + if (prefix) { + if (!command.startsWith(prefix)) { + return {handled: false}; + } + command = command.substring(prefix.length); + } const parts = stringArgv(command); for (let i = parts.length; i > 0; i--) { const prefix = parts.slice(0, i).join(" ").toLowerCase(); @@ -71,7 +77,7 @@ export async function handleCommand(userId: string, command: string, botCommands await botCommands[prefix].fn.apply(obj, args); return {handled: true}; } catch (ex) { - return {handled: true, error: ex.message}; + return {handled: true, error: ex.message, humanError: ex.humanError}; } } } diff --git a/src/Connections/GithubIssue.ts b/src/Connections/GithubIssue.ts index 905c45ff..b56135fc 100644 --- a/src/Connections/GithubIssue.ts +++ b/src/Connections/GithubIssue.ts @@ -146,11 +146,11 @@ export class GitHubIssueConnection implements IConnection { } public get org() { - return this.state.org; + return this.state.org.toLowerCase(); } public get repo() { - return this.state.repo; + return this.state.repo.toLowerCase(); } public async onIssueCommentCreated(event: IssueCommentCreatedEvent) { @@ -316,7 +316,7 @@ export class GitHubIssueConnection implements IConnection { } } - public onIssueStateChange() { + public onIssueStateChange(data?: any) { return this.syncIssueState(); } diff --git a/src/Connections/GithubRepo.ts b/src/Connections/GithubRepo.ts index 08d65b72..7bb1f9c3 100644 --- a/src/Connections/GithubRepo.ts +++ b/src/Connections/GithubRepo.ts @@ -12,8 +12,9 @@ import { FormatUtil } from "../FormatUtil"; import axios from "axios"; import { BotCommands, handleCommand, botCommand, compileBotCommands } from "../BotCommands"; import { ReposGetResponseData } from "../Github/Types"; -import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types"; +import { IssuesOpenedEvent, IssuesEditedEvent, PullRequestOpenedEvent, PullRequestClosedEvent, PullRequestReadyForReviewEvent, PullRequestReviewSubmittedEvent, ReleaseCreatedEvent } from "@octokit/webhooks-types"; import emoji from "node-emoji"; +import { NotLoggedInError } from "../errors"; const log = new LogWrapper("GitHubRepoConnection"); const md = new markdown(); @@ -28,6 +29,8 @@ interface IQueryRoomOpts { export interface GitHubRepoConnectionState { org: string; repo: string; + ignoreHooks?: string[], + commandPrefix?: string; } const GITHUB_REACTION_CONTENT: {[emoji: string]: string} = { @@ -140,7 +143,7 @@ export class GitHubRepoConnection implements IConnection { }; } - static helpMessage: MatrixMessageContent; + static helpMessage: (cmdPrefix: string) => MatrixMessageContent; static botCommands: BotCommands; constructor(public readonly roomId: string, @@ -158,20 +161,31 @@ export class GitHubRepoConnection implements IConnection { return this.state.repo.toLowerCase(); } + private get commandPrefix() { + return (this.state.commandPrefix || "gh") + " "; + } + public isInterestedInStateEvent() { return false; } public async onMessageEvent(ev: MatrixEvent) { - const { error, handled } = await handleCommand(ev.sender, ev.content.body, GitHubRepoConnection.botCommands, this); + const { error, handled, humanError } = await handleCommand(ev.sender, ev.content.body, GitHubRepoConnection.botCommands, this, this.commandPrefix); if (!handled) { // Not for us. return; } if (error) { + await this.as.botClient.sendEvent(this.roomId, "m.reaction", { + "m.relates_to": { + rel_type: "m.annotation", + event_id: ev.event_id, + key: "⛔", + } + }); await this.as.botIntent.sendEvent(this.roomId,{ msgtype: "m.notice", - body: "Failed to handle command", + body: humanError ? `Failed to handle command: ${humanError}` : "Failed to handle command", }); return; } @@ -184,12 +198,17 @@ export class GitHubRepoConnection implements IConnection { }); } - @botCommand("gh create", "Create an issue for this repo", ["title"], ["description", "labels"], true) + @botCommand("help", "This help text") + public async helpCommand() { + return this.as.botIntent.sendEvent(this.roomId, GitHubRepoConnection.helpMessage(this.commandPrefix)); + } + + @botCommand("create", "Create an issue for this repo", ["title"], ["description", "labels"], true) // @ts-ignore private async onCreateIssue(userId: string, title: string, description?: string, labels?: string) { const octokit = await this.tokenStore.getOctokitForUser(userId); if (!octokit) { - return this.as.botIntent.sendText(this.roomId, "You must login to create an issue", "m.notice"); + throw new NotLoggedInError(); } const labelsNames = labels?.split(","); const res = await octokit.issues.create({ @@ -209,7 +228,7 @@ export class GitHubRepoConnection implements IConnection { }); } - @botCommand("gh assign", "Assign an issue to a user", ["number", "...users"], [], true) + @botCommand("assign", "Assign an issue to a user", ["number", "...users"], [], true) // @ts-ignore private async onAssign(userId: string, number: string, ...users: string[]) { const octokit = await this.tokenStore.getOctokitForUser(userId); @@ -229,7 +248,7 @@ export class GitHubRepoConnection implements IConnection { }); } - @botCommand("gh close", "Close an issue", ["number"], ["comment"], true) + @botCommand("close", "Close an issue", ["number"], ["comment"], true) // @ts-ignore private async onClose(userId: string, number: string, comment?: string) { const octokit = await this.tokenStore.getOctokitForUser(userId); @@ -255,6 +274,9 @@ export class GitHubRepoConnection implements IConnection { } public async onIssueCreated(event: IssuesOpenedEvent) { + if (this.shouldSkipHook('issue.created', 'issue')) { + return; + } log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`); if (!event.issue) { throw Error('No issue content!'); @@ -262,20 +284,13 @@ export class GitHubRepoConnection implements IConnection { if (!event.repository) { throw Error('No repository content!'); } - const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length); + const orgRepoName = event.repository.full_name; const content = emoji.emojify(`${event.issue.user?.login} created new issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${event.issue.title}"`); - const labelsHtml = (event.issue.labels || []).map((label: {color?: string|null, name?: string, description?: string|null}|string) => - typeof(label) === "string" ? - `${label}` : - `${label.name}` - ).join(" ") || ""; - const labels = (event.issue?.labels || []).map((label: {name?: string}|string) => - typeof(label) === "string" ? label : label.name - ).join(", ") || ""; + const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.issue.labels); await this.as.botIntent.sendEvent(this.roomId, { msgtype: "m.notice", - body: content + (labels.length > 0 ? ` with labels ${labels}`: ""), + body: content + (labelsStr.length > 0 ? ` with labels ${labelsStr}`: ""), formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""), format: "org.matrix.custom.html", // TODO: Fix types. @@ -284,6 +299,9 @@ export class GitHubRepoConnection implements IConnection { } public async onIssueStateChange(event: IssuesEditedEvent) { + if (this.shouldSkipHook('issue.changed', 'issue')) { + return; + } log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`); if (!event.issue) { throw Error('No issue content!'); @@ -291,18 +309,158 @@ export class GitHubRepoConnection implements IConnection { if (!event.repository) { throw Error('No repository content!'); } - if (event.issue.state === "closed" && event.sender) { - const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length); - const content = `**@${event.sender.login}** closed issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`; - await this.as.botIntent.sendEvent(this.roomId, { - msgtype: "m.notice", - body: content, - formatted_body: md.renderInline(content), - format: "org.matrix.custom.html", - // TODO: Fix types - ...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any), - }); + const state = event.issue.state === 'open' ? 'reopened' : 'closed'; + const orgRepoName = event.repository.full_name; + const content = `**${event.sender.login}** ${state} issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`; + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + // TODO: Fix types + ...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any), + }); + } + + public async onIssueEdited(event: IssuesEditedEvent) { + if (this.shouldSkipHook('issue.edited', 'issue')) { + return; } + if (!event.issue) { + throw Error('No issue content!'); + } + log.info(`onIssueEdited ${this.roomId} ${this.org}/${this.repo} #${event.issue.number}`); + const orgRepoName = event.repository.full_name; + const content = `**${event.sender.login}** edited issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`; + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + // TODO: Fix types + ...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any), + }); + } + + public async onPROpened(event: PullRequestOpenedEvent) { + if (this.shouldSkipHook('pull_request.opened', 'pull_request')) { + return; + } + log.info(`onPROpened ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`); + if (!event.pull_request) { + throw Error('No pull_request content!'); + } + if (!event.repository) { + throw Error('No repository content!'); + } + const orgRepoName = event.repository.full_name; + const verb = event.pull_request.draft ? 'drafted' : 'opened'; + const content = emoji.emojify(`**${event.pull_request.user?.login}** ${verb} a new PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"`); + const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.pull_request.labels); + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content + (labelsStr.length > 0 ? ` with labels ${labelsStr}`: ""), + formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""), + format: "org.matrix.custom.html", + // TODO: Fix types. + ...FormatUtil.getPartialBodyForIssue(event.repository, event.pull_request as any), + }); + } + + public async onPRReadyForReview(event: PullRequestReadyForReviewEvent) { + if (this.shouldSkipHook('pull_request.ready_for_review', 'pull_request')) { + return; + } + log.info(`onPRReadyForReview ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`); + if (!event.pull_request) { + throw Error('No pull_request content!'); + } + if (!event.repository) { + throw Error('No repository content!'); + } + const orgRepoName = event.repository.full_name; + const content = emoji.emojify(`**${event.pull_request.user?.login}** has marked [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) as ready to review (${event.pull_request.title})`); + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + // TODO: Fix types. + ...FormatUtil.getPartialBodyForIssue(event.repository, event.pull_request as any), + }); + } + + public async onPRReviewed(event: PullRequestReviewSubmittedEvent) { + if (this.shouldSkipHook('pull_request.reviewed', 'pull_request')) { + return; + } + log.info(`onPRReadyForReview ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`); + if (!event.pull_request) { + throw Error('No pull_request content!'); + } + if (!event.repository) { + throw Error('No repository content!'); + } + const orgRepoName = event.repository.full_name; + const emojiForReview = {'approved': '✅', 'commented': '🗨️', 'changes_requested': '🔴'}[event.review.state.toLowerCase()]; + if (!emojiForReview) { + // We don't recongnise this state, run away! + return; + } + const content = emoji.emojify(`**${event.review.user?.login}** ${emojiForReview} ${event.review.state.toLowerCase()} [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}) (${event.pull_request.title})`); + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + // TODO: Fix types. + ...FormatUtil.getPartialBodyForIssue(event.repository, event.pull_request as any), + }); + } + + public async onPRClosed(event: PullRequestClosedEvent) { + if (this.shouldSkipHook('pull_request.closed', 'pull_request')) { + return; + } + log.info(`onPRClosed ${this.roomId} ${this.org}/${this.repo} #${event.pull_request.number}`); + if (!event.pull_request) { + throw Error('No pull_request content!'); + } + if (!event.repository) { + throw Error('No repository content!'); + } + const orgRepoName = event.repository.full_name; + const verb = event.pull_request.merged ? 'merged' : 'closed'; + const content = emoji.emojify(`**${event.pull_request.user?.login}** ${verb} PR [${orgRepoName}#${event.pull_request.number}](${event.pull_request.html_url}): "${event.pull_request.title}"`); + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + // TODO: Fix types. + ...FormatUtil.getPartialBodyForIssue(event.repository, event.pull_request as any), + }); + } + + public async onReleaseCreated(event: ReleaseCreatedEvent) { + if (this.shouldSkipHook('release', 'release.created')) { + return; + } + log.info(`onReleaseCreated ${this.roomId} ${this.org}/${this.repo} #${event.release.tag_name}`); + if (!event.release) { + throw Error('No release content!'); + } + if (!event.repository) { + throw Error('No repository content!'); + } + const orgRepoName = event.repository.full_name; + const content = `🪄 **${event.release.author?.login}** released [${event.release.name}]${event.release.html_url} for ${orgRepoName}`; + await this.as.botIntent.sendEvent(this.roomId, { + msgtype: "m.notice", + body: content, + formatted_body: md.renderInline(content), + format: "org.matrix.custom.html", + }); } public async onEvent(evt: MatrixEvent) { @@ -316,6 +474,7 @@ export class GitHubRepoConnection implements IConnection { const ev = await this.as.botClient.getEvent(this.roomId, event_id); const issueContent = ev.content["uk.half-shot.matrix-github.issue"]; if (!issueContent) { + log.debug('Reaction to event did not pertain to a issue'); return; // Not our event. } @@ -335,27 +494,37 @@ export class GitHubRepoConnection implements IConnection { ] } }); - } else if (action && action[1] === "close") { + } else if (action && action === "close") { await octokit.issues.update({ state: "closed", owner: this.org, repo: this.repo, - issue_number: ev.number, + issue_number: issueContent.number, }); - } else if (action && action[1] === "open") { + } else if (action && action === "open") { await octokit.issues.update({ state: "open", owner: this.org, repo: this.repo, - issue_number: ev.number, + issue_number: issueContent.number, }); } - return; } } public toString() { - return `GitHubRepo`; + return `GitHubRepo ${this.org}/${this.repo}`; + } + + private shouldSkipHook(...hookName: string[]) { + if (this.state.ignoreHooks) { + for (const name of hookName) { + if (this.state.ignoreHooks?.includes(name)) { + return true; + } + } + } + return false; } } diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts index da92e90f..1bf3238b 100644 --- a/src/Connections/GitlabRepo.ts +++ b/src/Connections/GitlabRepo.ts @@ -29,7 +29,6 @@ export class GitLabRepoConnection implements IConnection { GitLabRepoConnection.CanonicalEventType, // Legacy event, with an awful name. ]; - static helpMessage: MatrixMessageContent; static botCommands: BotCommands; constructor(public readonly roomId: string, @@ -131,5 +130,4 @@ export class GitLabRepoConnection implements IConnection { // Typescript doesn't understand Prototypes very well yet. // eslint-disable-next-line @typescript-eslint/no-explicit-any const res = compileBotCommands(GitLabRepoConnection.prototype as any); -GitLabRepoConnection.helpMessage = res.helpMessage; GitLabRepoConnection.botCommands = res.botCommands; \ No newline at end of file diff --git a/src/FormatUtil.ts b/src/FormatUtil.ts index 5877b646..f333d938 100644 --- a/src/FormatUtil.ts +++ b/src/FormatUtil.ts @@ -79,4 +79,19 @@ export class FormatUtil { } return f; } + + public static formatLabels(labels: Array<{color?: string|null, name?: string, description?: string|null}|string> = []) { + const labelsHtml = labels.map((label: {color?: string|null, name?: string, description?: string|null}|string) => + typeof(label) === "string" ? + `${label}` : + `${label.name}` + ).join(" ") || ""; + const labelsStr = labels.map((label: {name?: string}|string) => + typeof(label) === "string" ? label : label.name + ).join(", ") || ""; + return { + labelsStr, + labelsHtml, + } + } } diff --git a/src/GithubBridge.ts b/src/GithubBridge.ts index 97e430e4..c5049489 100644 --- a/src/GithubBridge.ts +++ b/src/GithubBridge.ts @@ -141,6 +141,8 @@ export class GithubBridge { } private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] { + org = org.toLowerCase(); + repo = repo.toLowerCase(); return this.connections.filter((c) => (c instanceof GitHubIssueConnection && c.org === org && c.repo === repo && c.issueNumber === issueNumber) || (c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as (GitHubIssueConnection|GitLabRepoConnection)[]; } @@ -313,7 +315,7 @@ export class GithubBridge { if (c instanceof GitHubIssueConnection) await c.onIssueCommentCreated(data); } catch (ex) { - log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex); + log.warn(`Connection ${c.toString()} failed to handle github.issue_comment.created:`, ex); } }) }); @@ -325,7 +327,7 @@ export class GithubBridge { try { await c.onIssueCreated(data); } catch (ex) { - log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex); + log.warn(`Connection ${c.toString()} failed to handle github.issues.opened:`, ex); } }) }); @@ -339,7 +341,7 @@ export class GithubBridge { if (c instanceof GitHubIssueConnection /* || c instanceof GitHubRepoConnection*/) await c.onIssueEdited(data); } catch (ex) { - log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex); + log.warn(`Connection ${c.toString()} failed to handle github.issues.edited:`, ex); } }) }); @@ -350,9 +352,9 @@ export class GithubBridge { connections.map(async (c) => { try { if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection) - await c.onIssueStateChange(); + await c.onIssueStateChange(data); } catch (ex) { - log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex); + log.warn(`Connection ${c.toString()} failed to handle github.issues.closed:`, ex); } }) }); @@ -363,9 +365,76 @@ export class GithubBridge { connections.map(async (c) => { try { if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection) - await c.onIssueStateChange(); + await c.onIssueStateChange(data); } catch (ex) { - log.warn(`Connection ${c.toString()} failed to handle comment.created:`, ex); + log.warn(`Connection ${c.toString()} failed to handle github.issues.reopened:`, ex); + } + }) + }); + + this.queue.on("github.issues.edited", async ({ data }) => { + const { repository, issue, owner } = validateRepoIssue(data); + const connections = this.getConnectionsForGithubRepo(owner, repository.name); + connections.map(async (c) => { + try { + await c.onIssueEdited(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.issues.edited:`, ex); + } + }) + }); + + this.queue.on("github.pull_request.opened", async ({ data }) => { + const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + connections.map(async (c) => { + try { + await c.onPROpened(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.pull_request.opened:`, ex); + } + }) + }); + + this.queue.on("github.pull_request.closed", async ({ data }) => { + const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + connections.map(async (c) => { + try { + await c.onPRClosed(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.pull_request.closed:`, ex); + } + }) + }); + + this.queue.on("github.pull_request.ready_for_review", async ({ data }) => { + const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + connections.map(async (c) => { + try { + await c.onPRReadyForReview(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.pull_request.closed:`, ex); + } + }) + }); + + this.queue.on("github.pull_request_review.submitted", async ({ data }) => { + const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + connections.map(async (c) => { + try { + await c.onPRReviewed(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.pull_request.closed:`, ex); + } + }) + }); + + this.queue.on("github.release.created", async ({ data }) => { + const connections = this.getConnectionsForGithubRepo(data.repository.owner.login, data.repository.name); + connections.map(async (c) => { + try { + await c.onReleaseCreated(data); + } catch (ex) { + log.warn(`Connection ${c.toString()} failed to handle github.pull_request.closed:`, ex); } }) }); @@ -614,6 +683,10 @@ export class GithubBridge { /* We ignore messages from our users */ return; } + if (Date.now() - event.origin_server_ts > 30000) { + /* We ignore old messages too */ + return; + } log.info(`Got message roomId=${roomId} type=${event.type} from=${event.sender}`); console.log(event); log.debug("Content:", JSON.stringify(event)); @@ -699,15 +772,22 @@ export class GithubBridge { this.connections.push(connection); } } - return null; + return; } if (event.sender === this.as.botUserId) { // It's us return; } - // Alas, it's just an event. - return this.connections.filter((c) => c.roomId === roomId).map((c) => c.onEvent ? c.onEvent(event) : undefined); + for (const connection of this.connections.filter((c) => c.roomId === roomId)) { + try { + if (connection.onEvent) { + await connection.onEvent(event); + } + } catch (ex) { + log.warn(`Connection ${connection.toString()} failed to handle event:`, ex); + } + } } private async onQueryRoom(roomAlias: string) { diff --git a/src/Webhooks.ts b/src/Webhooks.ts index f9d10544..1161b108 100644 --- a/src/Webhooks.ts +++ b/src/Webhooks.ts @@ -96,6 +96,7 @@ export class Webhooks extends EventEmitter { private async onGitHubPayload({id, name, payload}: EmitterWebhookEvent) { log.info(`Got GitHub webhook event ${id} ${name}`); console.log(payload); + log.debug("Payload:", payload); const action = (payload as unknown as {action: string|undefined}).action; try { await this.queue.push({