diff --git a/github/GithubApp.ts b/github/GithubApp.ts index d9765c9..8e27a6e 100644 --- a/github/GithubApp.ts +++ b/github/GithubApp.ts @@ -4,6 +4,7 @@ import { IConfigurationExtend, IHttp, ILogger, + IMessageExtender, IModify, IPersistence, IRead, @@ -45,12 +46,40 @@ import { IJobContext } from "@rocket.chat/apps-engine/definition/scheduler"; import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; import { clearInteractionRoomData, getInteractionRoomData } from "./persistance/roomInteraction"; import { GHCommand } from "./commands/GhCommand"; +import { IPreMessageSentExtend, IMessage } from "@rocket.chat/apps-engine/definition/messages"; +import { handleGitHubCodeSegmentLink } from "./handlers/GitHubCodeSegmentHandler"; +import { isGithubLink, hasGitHubCodeSegmentLink } from "./helpers/checkLinks"; -export class GithubApp extends App { +export class GithubApp extends App implements IPreMessageSentExtend { constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } + public async checkPreMessageSentExtend( + message: IMessage, + read: IRead, + http: IHttp + ): Promise { + if (await isGithubLink(message)) { + return true; + } + return false; + } + + public async executePreMessageSentExtend( + message: IMessage, + extend: IMessageExtender, + read: IRead, + http: IHttp, + persistence: IPersistence + ): Promise { + + if (await hasGitHubCodeSegmentLink(message)) { + await handleGitHubCodeSegmentLink(message, read, http, message.sender, message.room, extend); + } + return extend.getMessage(); + } + public async authorizationCallback( token: IAuthData, user: IUser, diff --git a/github/app.json b/github/app.json index 58af157..0d2452e 100644 --- a/github/app.json +++ b/github/app.json @@ -12,5 +12,7 @@ "nameSlug": "github", "classFile": "GithubApp.ts", "description": "The ultimate app extending Rocket.Chat for all developers collaborating on Github", - "implements": [] + "implements": [ + "IPreMessageSentExtend" + ] } \ No newline at end of file diff --git a/github/enum/GitHubURL.ts b/github/enum/GitHubURL.ts new file mode 100644 index 0000000..d71f95d --- /dev/null +++ b/github/enum/GitHubURL.ts @@ -0,0 +1,5 @@ +export enum GitHubURLEnum { + PREFIX = "blob/", + HOST = "github.com", + RAW_HOST = "raw.githubusercontent.com", +} diff --git a/github/handlers/GitHubCodeSegmentHandler.ts b/github/handlers/GitHubCodeSegmentHandler.ts new file mode 100644 index 0000000..7f8bddc --- /dev/null +++ b/github/handlers/GitHubCodeSegmentHandler.ts @@ -0,0 +1,69 @@ +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { IHttp, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IMessage, IMessageAttachment } from "@rocket.chat/apps-engine/definition/messages"; +import { IRoom } from "@rocket.chat/apps-engine/definition/rooms"; +import { IMessageExtender } from "@rocket.chat/apps-engine/definition/accessors"; +import { TextObjectType } from "@rocket.chat/apps-engine/definition/uikit"; +import { GitHubURLEnum } from "../enum/GitHubURL"; + +async function extractCodeSnippetFromURL(content: string, url: string): Promise { + const lineRangeRegex: RegExp = /(?:L(\d+)+-L(\d+)|L(\d+))/; + const lineRangeMatch: RegExpMatchArray | null = url.match(lineRangeRegex); + + if (lineRangeMatch) { + return extractCodeSnippetByLineRange(content, lineRangeMatch); + } + + return ""; +} + +function extractCodeSnippetByLineRange(content: string, lineRangeMatch: RegExpMatchArray): string { + const [_, startLine, endLine, singleLine] = lineRangeMatch; + + const lineOffset = singleLine ? parseInt(singleLine) : parseInt(startLine) - 1; + const lineCount = singleLine ? 1 : parseInt(endLine) - parseInt(startLine) + 1; + + const linesRegex = `(?:.*\n){${lineOffset}}(.*(?:\n.*){${lineCount}})`; + const lines = new RegExp(linesRegex); + const match = content.match(lines); + + return match?.[1] ?? ""; +} + +async function fetchGitHubContent(http: IHttp, modifiedUrl: string): Promise { + const response: any = await http.get(modifiedUrl); + const { content } = response; + return content; +} + +function buildCodeSnippetAttachment(codeSnippet: string, url: string): IMessageAttachment { + const attachment: IMessageAttachment = { + text: `\`\`\`\n${codeSnippet}\n\`\`\` \n[Show more...](${url})`, + type: TextObjectType.MARKDOWN, + }; + return attachment; +} + +export async function handleGitHubCodeSegmentLink( + message: IMessage, + read: IRead, + http: IHttp, + user: IUser, + room: IRoom, + extend: IMessageExtender +) { + const urlRegex: RegExp = /\bhttps?:\/\/github\.com\/\S+\b/; + const messageText: string = message.text!; + const urlMatch: RegExpMatchArray | null = messageText.match(urlRegex); + const url: string | undefined = urlMatch?.[0]; + let modifiedUrl: string = url?.replace(GitHubURLEnum.PREFIX, "")!; + modifiedUrl = modifiedUrl.replace(GitHubURLEnum.HOST, GitHubURLEnum.RAW_HOST); + + const content: string = await fetchGitHubContent(http, modifiedUrl); + const codeSnippet = await extractCodeSnippetFromURL(content, modifiedUrl); + + if (codeSnippet) { + const attachment: IMessageAttachment = buildCodeSnippetAttachment(codeSnippet, url!); + extend.addAttachment(attachment); + } +} diff --git a/github/helpers/checkLinks.ts b/github/helpers/checkLinks.ts new file mode 100644 index 0000000..dd1fb69 --- /dev/null +++ b/github/helpers/checkLinks.ts @@ -0,0 +1,19 @@ +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; + +export async function hasGitHubCodeSegmentLink(message: IMessage): Promise { + let lineNo: RegExp = + /https?:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+\/blob\/[A-Za-z0-9_-]+\/.+/; + + if (lineNo.test(message.text!)) { + return true; + } + return false; +} + +export async function isGithubLink(message: IMessage) { + let githubLink: RegExp = /(?:https?:\/\/)?(?:www\.)?github\.com\//; + if (githubLink.test(message.text!)) { + return true; + } + return false; +} \ No newline at end of file