Support hotlinking github issues and PRs (#277)

* Support hotlinking github issues and PRs

* changelog

* update sample config

* Update github_repo.md
This commit is contained in:
Will Hunt 2022-04-06 18:46:04 +01:00 committed by GitHub
parent 7df772cda5
commit 14abb011b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 123 additions and 43 deletions

1
changelog.d/277.feature Normal file
View File

@ -0,0 +1 @@
Automatically link GitHub issues and pull requests when an issue number is mentioned (by default, using the # prefix).

View File

@ -30,6 +30,8 @@ github:
# (Optional) Default options for GitHub connections.
#
showIssueRoomLink: false
hotlinkIssues:
prefix: "#"
gitlab:
# (Optional) Configure this to enable GitLab support
#

View File

@ -33,6 +33,7 @@ This connection supports a few options which can be defined in the room state:
|prDiff|Show a diff in the room when a PR is created, subject to limits|`{enabled: boolean, maxLines: number}`|`{enabled: false}`|
|includingLabels|Only notify on issues matching these label names|Array of: String matching a label name|*empty*|
|excludingLabels|Never notify on issues matching these label names|Array of: String matching a label name|*empty*|
|hotlinkIssues|Send a link to an issue/PR in the room when a user mentions a prefix followed by a number|` { prefix: string }`|`{prefix: "#"}`|
### Supported event types

View File

@ -54,6 +54,9 @@ export const DefaultConfig = new BridgeConfig({
},
defaultOptions: {
showIssueRoomLink: false,
hotlinkIssues: {
prefix: "#"
}
}
},
gitlab: {

View File

@ -20,6 +20,7 @@ import { GitHubIssueConnection } from "./GithubIssue";
import { BridgeConfigGitHub } from "../Config/Config";
import { ApiError, ErrCode } from "../provisioning/api";
import { PermissionCheckFn } from ".";
import { MinimalGitHubIssue, MinimalGitHubRepo } from "../libRs";
const log = new LogWrapper("GitHubRepoConnection");
const md = new markdown();
@ -41,6 +42,9 @@ export interface GitHubRepoConnectionOptions {
},
includingLabels?: string[];
excludingLabels?: string[];
hotlinkIssues?: boolean|{
prefix: string;
}
}
export interface GitHubRepoConnectionState extends GitHubRepoConnectionOptions{
org: string;
@ -102,6 +106,7 @@ const AllowedEvents: AllowedEventsNames[] = [
const LABELED_DEBOUNCE_MS = 5000;
const CREATED_GRACE_PERIOD_MS = 6000;
const DEFAULT_HOTLINK_PREFIX = "#";
function compareEmojiStrings(e0: string, e1: string, e0Index = 0) {
return e0.codePointAt(e0Index) === e1.codePointAt(0);
@ -289,6 +294,19 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
);
}
public get hotlinkIssues() {
const cfg = this.config.defaultOptions?.hotlinkIssues || this.state.hotlinkIssues;
if (cfg === false) {
return false;
}
if (cfg === true || cfg === undefined || cfg.prefix === undefined) {
return {
prefix: DEFAULT_HOTLINK_PREFIX,
}
}
return cfg;
}
public get org() {
return this.state.org.toLowerCase();
}
@ -309,15 +327,68 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
return GitHubRepoConnection.EventTypes.includes(eventType) && this.stateKey === stateKey;
}
public async handleIssueHotlink(ev: MatrixEvent<MatrixMessageContent>): Promise<boolean> {
if (ev.content.msgtype !== "m.text" && ev.content.msgtype !== "m.emote" || this.hotlinkIssues === false) {
return false;
}
const octokit = this.githubInstance.getSafeOctokitForRepo(this.org, this.repo);
if (!octokit) {
// No octokit for this repo, ignoring
return false;
}
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>, checkPermission: PermissionCheckFn, reply?: IRichReplyMetadata) {
let eventBody = ev.content.body.trim();
if (!eventBody) {
return false;
}
// Strip code blocks
eventBody = eventBody.replace(/(?:```|`)[^`]+(?:```|`)/g, "");
// Strip quotes
eventBody = eventBody.replace(/>.+/g, "");
const prefix = this.hotlinkIssues.prefix;
// Simple text search
const regex = new RegExp(`(?:^|\\s)${prefix}(\\d+)(?:$|\\s)`, "gm");
const result = regex.exec(eventBody);
const issueNumber = result?.[1];
if (issueNumber) {
let issue: MinimalGitHubIssue & { repository?: MinimalGitHubRepo, pull_request?: unknown, state: string };
try {
issue = (await octokit.issues.get({
repo: this.state.repo,
owner: this.state.org,
issue_number: parseInt(issueNumber),
})).data;
} catch (ex) {
// Failed to fetch the issue, don't handle.
return false;
}
let message = `${issue.pull_request ? "Pull Request" : "Issue"} [#${issue.number}](${issue.html_url}): ${issue.title} (${issue.state})`;
if (this.showIssueRoomLink) {
message += ` [Issue Room](https://matrix.to/#/${this.as.getAlias(GitHubIssueConnection.generateAliasLocalpart(this.org, this.repo, issue.number))})`;
}
const content = emoji.emojify(message);
await this.as.botIntent.sendEvent(this.roomId, {
msgtype: "m.notice",
body: content ,
formatted_body: md.renderInline(content),
format: "org.matrix.custom.html",
...(issue.repository ? FormatUtil.getPartialBodyForGithubIssue(issue.repository, issue) : {}),
});
return true;
}
return false;
}
public async onMessageEvent(ev: MatrixEvent<MatrixMessageContent>, checkPermission: PermissionCheckFn, reply?: IRichReplyMetadata): Promise<boolean> {
if (await super.onMessageEvent(ev, checkPermission)) {
return true;
}
if (!reply) {
return false;
}
const body = ev.content.body?.trim();
if (reply) {
const repoInfo = reply.realEvent.content["uk.half-shot.matrix-hookshot.github.repo"];
const pullRequestId = reply.realEvent.content["uk.half-shot.matrix-hookshot.github.pull_request"]?.number;
// Emojis can be multi-byte, so make sure we split properly
@ -354,9 +425,11 @@ export class GitHubRepoConnection extends CommandConnection implements IConnecti
body: `Failed to submit review: ${ex.message}`,
});
}
}
return true;
}
}
// We might want to do a hotlink.
return await this.handleIssueHotlink(ev);
}
@botCommand("create", "Create an issue for this repo", ["title"], ["description", "labels"], true)

View File

@ -25,7 +25,7 @@ export interface ILabel {
export class FormatUtil {
public static formatIssueRoomName(issue: MinimalGitHubIssue & {repository_url: string}) {
const orgRepoName = issue.repository_url.substr("https://api.github.com/repos/".length);
const orgRepoName = issue.repository_url.slice("https://api.github.com/repos/".length);
return emoji.emojify(`${orgRepoName}#${issue.number}: ${issue.title}`);
}

View File

@ -25,7 +25,7 @@ export interface MatrixMessageContent extends MatrixEventContent {
body: string;
formatted_body?: string;
format?: string;
msgtype: "m.text"|"m.notice"|"m.image"|"m.video"|"m.audio";
msgtype: "m.text"|"m.notice"|"m.image"|"m.video"|"m.audio"|"m.emote";
"m.relates_to"?: {
"m.in_reply_to"?: {
event_id: string;