diff --git a/github/GithubApp.ts b/github/GithubApp.ts index 3ae3913..90e925f 100644 --- a/github/GithubApp.ts +++ b/github/GithubApp.ts @@ -5,6 +5,7 @@ import { IConfigurationModify, IHttp, ILogger, + IMessageBuilder, IMessageExtender, IModify, IPersistence, @@ -14,6 +15,7 @@ import { App } from "@rocket.chat/apps-engine/definition/App"; import { IAppInfo } from "@rocket.chat/apps-engine/definition/metadata"; import { GithubCommand } from "./commands/GithubCommand"; import { + ButtonStyle, IUIKitResponse, UIKitBlockInteractionContext, UIKitViewCloseInteractionContext, @@ -32,6 +34,7 @@ import { createOAuth2Client } from "@rocket.chat/apps-engine/definition/oauth2/O import { sendDirectMessage, sendDirectMessageOnInstall, + sendMessage, sendNotification, } from "./lib/message"; import { deleteOathToken } from "./processors/deleteOAthToken"; @@ -45,17 +48,31 @@ import { IJobContext, StartupType } from "@rocket.chat/apps-engine/definition/sc 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 { IPreMessageSentExtend, IMessage,IPreMessageSentModify, IPostMessageSent } from "@rocket.chat/apps-engine/definition/messages"; import { handleGitHubCodeSegmentLink } from "./handlers/GitHubCodeSegmentHandler"; -import { isGithubLink, hasGitHubCodeSegmentLink } from "./helpers/checkLinks"; +import { isGithubLink, hasGitHubCodeSegmentLink, hasGithubPRLink } from "./helpers/checkLinks"; import { SendReminder } from "./handlers/SendReminder"; import { AppSettings, settings } from "./settings/settings"; -import { ISetting } from "@rocket.chat/apps-engine/definition/settings"; -export class GithubApp extends App implements IPreMessageSentExtend { +import { ISetting } from "@rocket.chat/apps-engine/definition/settings";import { handleGithubPRLink } from "./handlers/GithubPRlinkHandler"; + +export class GithubApp extends App implements IPreMessageSentExtend,IPostMessageSent{ constructor(info: IAppInfo, logger: ILogger, accessors: IAppAccessors) { super(info, logger, accessors); } + async checkPostMessageSent?(message: IMessage, read: IRead, http: IHttp): Promise { + if (await hasGithubPRLink(message)){ + return true + } + return false; + } + + async executePostMessageSent(message: IMessage, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise { + + await handleGithubPRLink(message,read,http,persistence,modify) + + } + public async checkPreMessageSentExtend( message: IMessage, read: IRead, @@ -78,9 +95,10 @@ export class GithubApp extends App implements IPreMessageSentExtend { 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 0d2452e..baeab48 100644 --- a/github/app.json +++ b/github/app.json @@ -13,6 +13,7 @@ "classFile": "GithubApp.ts", "description": "The ultimate app extending Rocket.Chat for all developers collaborating on Github", "implements": [ - "IPreMessageSentExtend" + "IPreMessageSentExtend", + "IPostMessageSent" ] } \ No newline at end of file diff --git a/github/enum/App.ts b/github/enum/App.ts index a813e21..9af9325 100644 --- a/github/enum/App.ts +++ b/github/enum/App.ts @@ -3,4 +3,5 @@ export enum AppEnum { USERNAME_ALIAS = 'GitHub', EMOJI_AVATAR = '', USER_MESSAGED_BOT = 'You have messaged the bot user. This has no effect.', + APP_ID='826f0d95-9e25-48a6-a781-a32f147230a5' } \ No newline at end of file diff --git a/github/enum/Modals.ts b/github/enum/Modals.ts index 0570c9a..c3dc5b0 100644 --- a/github/enum/Modals.ts +++ b/github/enum/Modals.ts @@ -34,6 +34,7 @@ export enum ModalsEnum { PULL_VIEW_LABEL = 'Pull Request', MERGE_PULL_REQUEST_ACTION = 'merge-pull-request', MERGE_PULL_REQUEST_LABEL = 'Merge', + APPROVE_PULL_REQUEST_ACTION = 'approve-pull-request', COMMENT_PR_ACTION = 'comment-pull-request', COMMENT_PR_LABEL = 'Add Comment', COMMENT_ISSUE_ACTION = 'comment-issue', diff --git a/github/handlers/ExecuteBlockActionHandler.ts b/github/handlers/ExecuteBlockActionHandler.ts index f9b274a..ecf8576 100644 --- a/github/handlers/ExecuteBlockActionHandler.ts +++ b/github/handlers/ExecuteBlockActionHandler.ts @@ -18,7 +18,7 @@ import { } from "@rocket.chat/apps-engine/definition/uikit"; import { AddSubscriptionModal } from "../modals/addSubscriptionsModal"; import { deleteSubscriptionsModal } from "../modals/deleteSubscriptions"; -import { deleteSubscription, updateSubscription, getIssueTemplateCode, getPullRequestComments, getPullRequestData, getRepositoryIssues, getBasicUserInfo, getIssueData, getIssuesComments } from "../helpers/githubSDK"; +import { deleteSubscription, updateSubscription, getIssueTemplateCode, getPullRequestComments, getPullRequestData, getRepositoryIssues, getBasicUserInfo, getIssueData, getIssuesComments, approvePullRequest } from "../helpers/githubSDK"; import { Subscription } from "../persistance/subscriptions"; import { getAccessTokenForUser } from "../persistance/auth"; import { GithubApp } from "../GithubApp"; @@ -734,7 +734,50 @@ export class ExecuteBlockActionHandler { return context.getInteractionResponder().openModalViewResponse(shareProfileMod); } + case ModalsEnum.APPROVE_PULL_REQUEST_ACTION:{ + + let value: string = context.getInteractionData().value as string; + let splittedValues = value?.split(" "); + let { user } = await context.getInteractionData(); + let { room} = await context.getInteractionData(); + let accessToken = await getAccessTokenForUser(this.read, user, this.app.oauth2Config) as IAuthData; + + if(splittedValues.length==2 && accessToken?.token){ + let data={ + "repo" : splittedValues[0], + "pullNumber": splittedValues[1] + } + let repoDetails = await getRepoData(this.http,splittedValues[0],accessToken.token); + + if(repoDetails?.permissions?.admin || repoDetails?.permissions?.push || repoDetails?.permissions?.maintain ){ + const response = await approvePullRequest(this.http,data.repo,accessToken.token,data.pullNumber); + let message = `🤖 Pull Request successfully Approved ✔️ : https://github.com/${data.repo}/pull/${data.pullNumber}` + + if(response.state == "APPROVED" && room ){ + sendMessage(this.modify,room,user,message) + } + + if(response.errors && room){ + sendNotification(this.read,this.modify,user,room,response.errors[0]); + } + + }else{ + const unauthorizedMessageModal = await messageModal({ + message:"Unauthorized Action 🤖 You dont have push rights ⚠️", + modify: this.modify, + read: this.read, + persistence: this.persistence, + http: this.http, + uikitcontext: context + }) + return context + .getInteractionResponder() + .openModalViewResponse(unauthorizedMessageModal); + } + } + break; + } case ModalsEnum.ISSUE_COMMENT_LIST_ACTION:{ let value: string = context.getInteractionData().value as string; let splittedValues = value?.split(" "); diff --git a/github/handlers/GithubPRlinkHandler.ts b/github/handlers/GithubPRlinkHandler.ts new file mode 100644 index 0000000..3c5a21d --- /dev/null +++ b/github/handlers/GithubPRlinkHandler.ts @@ -0,0 +1,62 @@ +import { IUser } from "@rocket.chat/apps-engine/definition/users"; +import { IHttp, IMessageBuilder, IModify, IPersistence, IRead } from "@rocket.chat/apps-engine/definition/accessors"; +import { IMessage } from "@rocket.chat/apps-engine/definition/messages"; +import { BlockBuilder, ButtonStyle, IBlock, TextObjectType } from "@rocket.chat/apps-engine/definition/uikit"; +import { ModalsEnum } from "../enum/Modals"; + + +export async function handleGithubPRLink(message: IMessage, read: IRead, http: IHttp, persistence: IPersistence, modify: IModify): Promise { + try { + const githubPRLinkRegex = /\bhttps?:\/\/github\.com\/\S+\/pull\/\d+\b/; + const text = message.text!; + const prLinkMatch = text.match(githubPRLinkRegex); + const prLink = prLinkMatch?.[0]; + const githubLinkPartsRegex = /(?:https?:\/\/github\.com\/)(\S+)\/(\S+)\/pull\/(\d+)/; + const linkPartsMatch = prLink?.match(githubLinkPartsRegex); + const username = linkPartsMatch?.[1]; + const repositoryName = linkPartsMatch?.[2]; + const pullNumber = linkPartsMatch?.[3]; + + if (!username || !repositoryName || !pullNumber) { + throw new Error("Invalid GitHub PR link"); + } + + const messageBuilder = await modify.getCreator().startMessage() + .setRoom(message.room) + .setSender(message.sender) + .setGroupable(true); + + const block = modify.getCreator().getBlockBuilder(); + + block.addActionsBlock({ + blockId: "githubdata", + elements: [ + block.newButtonElement({ + actionId: ModalsEnum.MERGE_PULL_REQUEST_ACTION, + text: block.newPlainTextObject("Merge"), + value: `${username}/${repositoryName} ${pullNumber}`, + style: ButtonStyle.PRIMARY + }), + block.newButtonElement({ + actionId: ModalsEnum.PR_COMMENT_LIST_ACTION, + text: block.newPlainTextObject("Comment"), + value: `${username}/${repositoryName} ${pullNumber}`, + style: ButtonStyle.PRIMARY + }), + block.newButtonElement({ + actionId: ModalsEnum.APPROVE_PULL_REQUEST_ACTION, + text: block.newPlainTextObject("Approve"), + value: `${username}/${repositoryName} ${pullNumber}`, + style: ButtonStyle.PRIMARY + }) + ] + }) + + messageBuilder.setBlocks(block); + + return await modify.getCreator().finish(messageBuilder); + } catch (error) { + console.error("Error in handleGithubPRLink:", error); + return "Error: Unable to process the GitHub PR link."; + } +} diff --git a/github/helpers/checkLinks.ts b/github/helpers/checkLinks.ts index dd1fb69..7d24628 100644 --- a/github/helpers/checkLinks.ts +++ b/github/helpers/checkLinks.ts @@ -9,7 +9,13 @@ export async function hasGitHubCodeSegmentLink(message: IMessage): Promise { + let prLink: RegExp = /https?:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+\/pull\/[0-9]+/; + if (prLink.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!)) { diff --git a/github/helpers/githubSDK.ts b/github/helpers/githubSDK.ts index 36d5e59..2574a72 100644 --- a/github/helpers/githubSDK.ts +++ b/github/helpers/githubSDK.ts @@ -1,6 +1,7 @@ import { IHttp, HttpStatusCode } from "@rocket.chat/apps-engine/definition/accessors"; import { IGitHubIssue } from "../definitions/githubIssue"; import { ModalsEnum } from "../enum/Modals"; +import { IAuthData } from "@rocket.chat/apps-engine/definition/oauth2/IOAuth2"; const BaseHost = "https://github.com/"; const BaseApiHost = "https://api.github.com/"; @@ -432,6 +433,36 @@ export async function mergePullRequest( return JSONResponse; } +export async function approvePullRequest( + http: IHttp, + repoName: string, + access_token: string, + pullRequestNumber: string | number, + ){ + const response = await http.post( + `https://api.github.com/repos/${repoName}/pulls/${pullRequestNumber}/reviews`, + { + headers: { + Authorization: `token ${access_token}`, + "Content-Type": "application/json", + }, + data:{ + 'event':"APPROVE" + } + } + ); + + // If it isn't a 2xx code, something wrong happened + let JSONResponse = JSON.parse(response.content || "{}"); + + if (!response.statusCode.toString().startsWith("2")) { + JSONResponse["serverError"] = true; + } else { + JSONResponse["serverError"] = false; + } + + return JSONResponse; +} export async function getBasicUserInfo( http: IHttp, access_token: String,