mirror of
https://github.com/matrix-org/matrix-hookshot.git
synced 2025-03-10 21:19:13 +00:00
A bunch of new features for GitHub Repo
This commit is contained in:
parent
144b7b840f
commit
433c906c7b
@ -49,7 +49,7 @@ export interface AdminAccountData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AdminRoom extends EventEmitter {
|
export class AdminRoom extends EventEmitter {
|
||||||
public static helpMessage: MatrixMessageContent;
|
public static helpMessage: () => MatrixMessageContent;
|
||||||
private widgetAccessToken = `abcdef`;
|
private widgetAccessToken = `abcdef`;
|
||||||
static botCommands: BotCommands;
|
static botCommands: BotCommands;
|
||||||
|
|
||||||
@ -149,7 +149,7 @@ export class AdminRoom extends EventEmitter {
|
|||||||
|
|
||||||
@botCommand("help", "This help text")
|
@botCommand("help", "This help text")
|
||||||
public async helpCommand() {
|
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'])
|
@botCommand("github setpersonaltoken", "Set your personal access token for GitHub", ['accessToken'])
|
||||||
|
@ -24,7 +24,7 @@ export type BotCommands = {[prefix: string]: {
|
|||||||
includeUserId: boolean,
|
includeUserId: boolean,
|
||||||
}};
|
}};
|
||||||
|
|
||||||
export function compileBotCommands(prototype: Record<string, BotCommandFunction>): {helpMessage: MatrixMessageContent, botCommands: BotCommands} {
|
export function compileBotCommands(prototype: Record<string, BotCommandFunction>): {helpMessage: (cmdPrefix?: string) => MatrixMessageContent, botCommands: BotCommands} {
|
||||||
let content = "Commands:\n";
|
let content = "Commands:\n";
|
||||||
const botCommands: BotCommands = {};
|
const botCommands: BotCommands = {};
|
||||||
Object.getOwnPropertyNames(prototype).forEach(propetyKey => {
|
Object.getOwnPropertyNames(prototype).forEach(propetyKey => {
|
||||||
@ -32,7 +32,7 @@ export function compileBotCommands(prototype: Record<string, BotCommandFunction>
|
|||||||
if (b) {
|
if (b) {
|
||||||
const requiredArgs = b.requiredArgs.join(" ");
|
const requiredArgs = b.requiredArgs.join(" ");
|
||||||
const optionalArgs = b.optionalArgs.map((arg: string) => `[${arg}]`).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.
|
// We know that this is safe.
|
||||||
botCommands[b.prefix as string] = {
|
botCommands[b.prefix as string] = {
|
||||||
fn: prototype[propetyKey],
|
fn: prototype[propetyKey],
|
||||||
@ -43,17 +43,23 @@ export function compileBotCommands(prototype: Record<string, BotCommandFunction>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
helpMessage: {
|
helpMessage: (cmdPrefix?: string) => ({
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content,
|
body: content,
|
||||||
formatted_body: md.render(content),
|
formatted_body: md.render(content).replace(/££PREFIX££/g, cmdPrefix || ""),
|
||||||
format: "org.matrix.custom.html"
|
format: "org.matrix.custom.html"
|
||||||
},
|
}),
|
||||||
botCommands,
|
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);
|
const parts = stringArgv(command);
|
||||||
for (let i = parts.length; i > 0; i--) {
|
for (let i = parts.length; i > 0; i--) {
|
||||||
const prefix = parts.slice(0, i).join(" ").toLowerCase();
|
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);
|
await botCommands[prefix].fn.apply(obj, args);
|
||||||
return {handled: true};
|
return {handled: true};
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return {handled: true, error: ex.message};
|
return {handled: true, error: ex.message, humanError: ex.humanError};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,11 +146,11 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get org() {
|
public get org() {
|
||||||
return this.state.org;
|
return this.state.org.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get repo() {
|
public get repo() {
|
||||||
return this.state.repo;
|
return this.state.repo.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueCommentCreated(event: IssueCommentCreatedEvent) {
|
public async onIssueCommentCreated(event: IssueCommentCreatedEvent) {
|
||||||
@ -316,7 +316,7 @@ export class GitHubIssueConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onIssueStateChange() {
|
public onIssueStateChange(data?: any) {
|
||||||
return this.syncIssueState();
|
return this.syncIssueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,8 +12,9 @@ import { FormatUtil } from "../FormatUtil";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { BotCommands, handleCommand, botCommand, compileBotCommands } from "../BotCommands";
|
import { BotCommands, handleCommand, botCommand, compileBotCommands } from "../BotCommands";
|
||||||
import { ReposGetResponseData } from "../Github/Types";
|
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 emoji from "node-emoji";
|
||||||
|
import { NotLoggedInError } from "../errors";
|
||||||
const log = new LogWrapper("GitHubRepoConnection");
|
const log = new LogWrapper("GitHubRepoConnection");
|
||||||
const md = new markdown();
|
const md = new markdown();
|
||||||
|
|
||||||
@ -28,6 +29,8 @@ interface IQueryRoomOpts {
|
|||||||
export interface GitHubRepoConnectionState {
|
export interface GitHubRepoConnectionState {
|
||||||
org: string;
|
org: string;
|
||||||
repo: string;
|
repo: string;
|
||||||
|
ignoreHooks?: string[],
|
||||||
|
commandPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GITHUB_REACTION_CONTENT: {[emoji: string]: 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;
|
static botCommands: BotCommands;
|
||||||
|
|
||||||
constructor(public readonly roomId: string,
|
constructor(public readonly roomId: string,
|
||||||
@ -158,20 +161,31 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
return this.state.repo.toLowerCase();
|
return this.state.repo.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get commandPrefix() {
|
||||||
|
return (this.state.commandPrefix || "gh") + " ";
|
||||||
|
}
|
||||||
|
|
||||||
public isInterestedInStateEvent() {
|
public isInterestedInStateEvent() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>) {
|
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>) {
|
||||||
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) {
|
if (!handled) {
|
||||||
// Not for us.
|
// Not for us.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (error) {
|
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,{
|
await this.as.botIntent.sendEvent(this.roomId,{
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: "Failed to handle command",
|
body: humanError ? `Failed to handle command: ${humanError}` : "Failed to handle command",
|
||||||
});
|
});
|
||||||
return;
|
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
|
// @ts-ignore
|
||||||
private async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
|
private async onCreateIssue(userId: string, title: string, description?: string, labels?: string) {
|
||||||
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
||||||
if (!octokit) {
|
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 labelsNames = labels?.split(",");
|
||||||
const res = await octokit.issues.create({
|
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
|
// @ts-ignore
|
||||||
private async onAssign(userId: string, number: string, ...users: string[]) {
|
private async onAssign(userId: string, number: string, ...users: string[]) {
|
||||||
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
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
|
// @ts-ignore
|
||||||
private async onClose(userId: string, number: string, comment?: string) {
|
private async onClose(userId: string, number: string, comment?: string) {
|
||||||
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
const octokit = await this.tokenStore.getOctokitForUser(userId);
|
||||||
@ -255,6 +274,9 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueCreated(event: IssuesOpenedEvent) {
|
public async onIssueCreated(event: IssuesOpenedEvent) {
|
||||||
|
if (this.shouldSkipHook('issue.created', 'issue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
log.info(`onIssueCreated ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||||
if (!event.issue) {
|
if (!event.issue) {
|
||||||
throw Error('No issue content!');
|
throw Error('No issue content!');
|
||||||
@ -262,20 +284,13 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
if (!event.repository) {
|
if (!event.repository) {
|
||||||
throw Error('No repository content!');
|
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 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) =>
|
const { labelsHtml, labelsStr } = FormatUtil.formatLabels(event.issue.labels);
|
||||||
typeof(label) === "string" ?
|
|
||||||
`<span>${label}</span>` :
|
|
||||||
`<span title="${label.description}" data-mx-color="#CCCCCC" data-mx-bg-color="#${label.color}">${label.name}</span>`
|
|
||||||
).join(" ") || "";
|
|
||||||
const labels = (event.issue?.labels || []).map((label: {name?: string}|string) =>
|
|
||||||
typeof(label) === "string" ? label : label.name
|
|
||||||
).join(", ") || "";
|
|
||||||
await this.as.botIntent.sendEvent(this.roomId, {
|
await this.as.botIntent.sendEvent(this.roomId, {
|
||||||
msgtype: "m.notice",
|
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}`: ""),
|
formatted_body: md.renderInline(content) + (labelsHtml.length > 0 ? ` with labels ${labelsHtml}`: ""),
|
||||||
format: "org.matrix.custom.html",
|
format: "org.matrix.custom.html",
|
||||||
// TODO: Fix types.
|
// TODO: Fix types.
|
||||||
@ -284,6 +299,9 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async onIssueStateChange(event: IssuesEditedEvent) {
|
public async onIssueStateChange(event: IssuesEditedEvent) {
|
||||||
|
if (this.shouldSkipHook('issue.changed', 'issue')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
log.info(`onIssueStateChange ${this.roomId} ${this.org}/${this.repo} #${event.issue?.number}`);
|
||||||
if (!event.issue) {
|
if (!event.issue) {
|
||||||
throw Error('No issue content!');
|
throw Error('No issue content!');
|
||||||
@ -291,9 +309,9 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
if (!event.repository) {
|
if (!event.repository) {
|
||||||
throw Error('No repository content!');
|
throw Error('No repository content!');
|
||||||
}
|
}
|
||||||
if (event.issue.state === "closed" && event.sender) {
|
const state = event.issue.state === 'open' ? 'reopened' : 'closed';
|
||||||
const orgRepoName = event.issue.repository_url.substr("https://api.github.com/repos/".length);
|
const orgRepoName = event.repository.full_name;
|
||||||
const content = `**@${event.sender.login}** closed issue [${orgRepoName}#${event.issue.number}](${event.issue.html_url}): "${emoji.emojify(event.issue.title)}"`;
|
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, {
|
await this.as.botIntent.sendEvent(this.roomId, {
|
||||||
msgtype: "m.notice",
|
msgtype: "m.notice",
|
||||||
body: content,
|
body: content,
|
||||||
@ -303,6 +321,146 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
...FormatUtil.getPartialBodyForIssue(event.repository, event.issue as any),
|
...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<unknown>) {
|
public async onEvent(evt: MatrixEvent<unknown>) {
|
||||||
@ -316,6 +474,7 @@ export class GitHubRepoConnection implements IConnection {
|
|||||||
const ev = await this.as.botClient.getEvent(this.roomId, event_id);
|
const ev = await this.as.botClient.getEvent(this.roomId, event_id);
|
||||||
const issueContent = ev.content["uk.half-shot.matrix-github.issue"];
|
const issueContent = ev.content["uk.half-shot.matrix-github.issue"];
|
||||||
if (!issueContent) {
|
if (!issueContent) {
|
||||||
|
log.debug('Reaction to event did not pertain to a issue');
|
||||||
return; // Not our event.
|
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({
|
await octokit.issues.update({
|
||||||
state: "closed",
|
state: "closed",
|
||||||
owner: this.org,
|
owner: this.org,
|
||||||
repo: this.repo,
|
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({
|
await octokit.issues.update({
|
||||||
state: "open",
|
state: "open",
|
||||||
owner: this.org,
|
owner: this.org,
|
||||||
repo: this.repo,
|
repo: this.repo,
|
||||||
issue_number: ev.number,
|
issue_number: issueContent.number,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString() {
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,6 @@ export class GitLabRepoConnection implements IConnection {
|
|||||||
GitLabRepoConnection.CanonicalEventType, // Legacy event, with an awful name.
|
GitLabRepoConnection.CanonicalEventType, // Legacy event, with an awful name.
|
||||||
];
|
];
|
||||||
|
|
||||||
static helpMessage: MatrixMessageContent;
|
|
||||||
static botCommands: BotCommands;
|
static botCommands: BotCommands;
|
||||||
|
|
||||||
constructor(public readonly roomId: string,
|
constructor(public readonly roomId: string,
|
||||||
@ -131,5 +130,4 @@ export class GitLabRepoConnection implements IConnection {
|
|||||||
// Typescript doesn't understand Prototypes very well yet.
|
// Typescript doesn't understand Prototypes very well yet.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const res = compileBotCommands(GitLabRepoConnection.prototype as any);
|
const res = compileBotCommands(GitLabRepoConnection.prototype as any);
|
||||||
GitLabRepoConnection.helpMessage = res.helpMessage;
|
|
||||||
GitLabRepoConnection.botCommands = res.botCommands;
|
GitLabRepoConnection.botCommands = res.botCommands;
|
@ -79,4 +79,19 @@ export class FormatUtil {
|
|||||||
}
|
}
|
||||||
return f;
|
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" ?
|
||||||
|
`<span>${label}</span>` :
|
||||||
|
`<span title="${label.description}" data-mx-color="#CCCCCC" data-mx-bg-color="#${label.color}">${label.name}</span>`
|
||||||
|
).join(" ") || "";
|
||||||
|
const labelsStr = labels.map((label: {name?: string}|string) =>
|
||||||
|
typeof(label) === "string" ? label : label.name
|
||||||
|
).join(", ") || "";
|
||||||
|
return {
|
||||||
|
labelsStr,
|
||||||
|
labelsHtml,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,8 @@ export class GithubBridge {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getConnectionsForGithubIssue(org: string, repo: string, issueNumber: number): (GitHubIssueConnection|GitLabRepoConnection)[] {
|
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) ||
|
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)[];
|
(c instanceof GitHubRepoConnection && c.org === org && c.repo === repo)) as (GitHubIssueConnection|GitLabRepoConnection)[];
|
||||||
}
|
}
|
||||||
@ -313,7 +315,7 @@ export class GithubBridge {
|
|||||||
if (c instanceof GitHubIssueConnection)
|
if (c instanceof GitHubIssueConnection)
|
||||||
await c.onIssueCommentCreated(data);
|
await c.onIssueCommentCreated(data);
|
||||||
} catch (ex) {
|
} 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 {
|
try {
|
||||||
await c.onIssueCreated(data);
|
await c.onIssueCreated(data);
|
||||||
} catch (ex) {
|
} 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*/)
|
if (c instanceof GitHubIssueConnection /* || c instanceof GitHubRepoConnection*/)
|
||||||
await c.onIssueEdited(data);
|
await c.onIssueEdited(data);
|
||||||
} catch (ex) {
|
} 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) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
||||||
await c.onIssueStateChange();
|
await c.onIssueStateChange(data);
|
||||||
} catch (ex) {
|
} 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) => {
|
connections.map(async (c) => {
|
||||||
try {
|
try {
|
||||||
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
if (c instanceof GitHubIssueConnection || c instanceof GitHubRepoConnection)
|
||||||
await c.onIssueStateChange();
|
await c.onIssueStateChange(data);
|
||||||
} catch (ex) {
|
} 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<GitHubWebhookTypes.IssuesEditedEvent>("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<GitHubWebhookTypes.PullRequestOpenedEvent>("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<GitHubWebhookTypes.PullRequestClosedEvent>("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<GitHubWebhookTypes.PullRequestReadyForReviewEvent>("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<GitHubWebhookTypes.PullRequestReviewSubmittedEvent>("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<GitHubWebhookTypes.ReleaseCreatedEvent>("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 */
|
/* We ignore messages from our users */
|
||||||
return;
|
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}`);
|
log.info(`Got message roomId=${roomId} type=${event.type} from=${event.sender}`);
|
||||||
console.log(event);
|
console.log(event);
|
||||||
log.debug("Content:", JSON.stringify(event));
|
log.debug("Content:", JSON.stringify(event));
|
||||||
@ -699,15 +772,22 @@ export class GithubBridge {
|
|||||||
this.connections.push(connection);
|
this.connections.push(connection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return;
|
||||||
}
|
}
|
||||||
if (event.sender === this.as.botUserId) {
|
if (event.sender === this.as.botUserId) {
|
||||||
// It's us
|
// It's us
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alas, it's just an event.
|
for (const connection of this.connections.filter((c) => c.roomId === roomId)) {
|
||||||
return this.connections.filter((c) => c.roomId === roomId).map((c) => c.onEvent ? c.onEvent(event) : undefined);
|
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) {
|
private async onQueryRoom(roomAlias: string) {
|
||||||
|
@ -96,6 +96,7 @@ export class Webhooks extends EventEmitter {
|
|||||||
private async onGitHubPayload({id, name, payload}: EmitterWebhookEvent) {
|
private async onGitHubPayload({id, name, payload}: EmitterWebhookEvent) {
|
||||||
log.info(`Got GitHub webhook event ${id} ${name}`);
|
log.info(`Got GitHub webhook event ${id} ${name}`);
|
||||||
console.log(payload);
|
console.log(payload);
|
||||||
|
log.debug("Payload:", payload);
|
||||||
const action = (payload as unknown as {action: string|undefined}).action;
|
const action = (payload as unknown as {action: string|undefined}).action;
|
||||||
try {
|
try {
|
||||||
await this.queue.push({
|
await this.queue.push({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user