From 4b973fc281d8b6b6164f422d8177a5efd514079f Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 11:05:24 +0900 Subject: [PATCH 01/14] feat: incentiveForPullRequest reviewers --- package.json | 3 +- src/handlers/payout/post.ts | 144 +++++++++++++++++++++++++++++++++++- src/handlers/processors.ts | 4 +- 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index bf2367eb1..fa7b7a601 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@sinclair/typebox": "^0.25.9", "@supabase/supabase-js": "^2.4.0", "@types/ms": "^0.7.31", + "@types/parse5": "^7.0.0", "@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/parser": "^5.59.11", "@uniswap/permit2-sdk": "^1.2.0", @@ -41,8 +42,8 @@ "ajv": "^8.11.2", "ajv-formats": "^2.1.1", "axios": "^1.3.2", - "decimal.js": "^10.4.3", "copyfiles": "^2.4.1", + "decimal.js": "^10.4.3", "ethers": "^5.7.2", "husky": "^8.0.2", "jimp": "^0.22.4", diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 17c8eb4ad..4a9171520 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -1,6 +1,16 @@ import { getWalletAddress } from "../../adapters/supabase"; import { getBotConfig, getBotContext, getLogger } from "../../bindings"; -import { addCommentToIssue, generatePermit2Signature, getAllIssueComments, getIssueDescription, getTokenSymbol, parseComments } from "../../helpers"; +import { + addCommentToIssue, + generatePermit2Signature, + getAllIssueComments, + getAllPullRequestReviews, + getAllPullRequests, + getIssueDescription, + getTokenSymbol, + parseComments, +} from "../../helpers"; +import { gitLinkedIssueParser } from "../../helpers/parser"; import { Incentives, Payload, StateReason, UserType } from "../../types"; import { commentParser } from "../comment"; import Decimal from "decimal.js"; @@ -121,6 +131,138 @@ export const incentivizeComments = async () => { await addCommentToIssue(comment, issue.number); }; +export const incentivizePullRequestReviews = async () => { + const logger = getLogger(); + const { + mode: { incentiveMode, paymentPermitMaxPrice }, + price: { baseMultiplier, incentives }, + payout: { paymentToken, rpc }, + } = getBotConfig(); + if (!incentiveMode) { + logger.info(`No incentive mode. skipping to process`); + // return; + } + const context = getBotContext(); + const payload = context.payload as Payload; + const issue = payload.issue; + if (!issue) { + logger.info(`Incomplete payload. issue: ${issue}`); + return; + } + + if (issue.state_reason !== StateReason.COMPLETED) { + logger.info("incentivizeComments: comment incentives skipped because the issue was not closed as completed"); + // return; + } + + if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) { + logger.info(`incentivizeComments: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); + // return; + } + + const issueDetailed = bountyInfo(issue); + if (!issueDetailed.isBounty) { + logger.info(`incentivizeComments: its not a bounty`); + // return; + } + + const pulls = await getAllPullRequests(context, "closed"); + if (pulls.length === 0) { + logger.debug(`No pull requests found at this time`); + return; + } + + let linkedPull; + + for (const pull of pulls) { + const pullRequestLinked = await gitLinkedIssueParser({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: pull.number, + }); + const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); + if (linkedIssueNumber === issue.number.toString()) { + linkedPull = pull; + break; + } + } + + if (!linkedPull) { + logger.debug(`No linked pull requests found`); + return; + } + + const comments = await getAllIssueComments(issue.number); + const permitComments = comments.filter( + (content) => content.body.includes("Reviewer Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot + ); + if (permitComments.length > 0) { + logger.info(`incentivizePullRequestReviews: skip to generate a permit url because it has been already posted`); + return; + } + + const assignees = issue?.assignees ?? []; + const assignee = assignees.length > 0 ? assignees[0] : undefined; + if (!assignee) { + logger.info("incentivizePullRequestReviews: skipping payment permit generation because `assignee` is `undefined`."); + return; + } + + const prReviews = await getAllPullRequestReviews(context, linkedPull.number); + logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); + const prReviewsByUser: Record = {}; + for (const review of prReviews) { + const user = review.user; + if (!user) continue; + if (user.type == UserType.Bot || user.login == assignee) continue; + if (!review.body) { + logger.info(`Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`); + continue; + } + if (!prReviewsByUser[user.login]) { + prReviewsByUser[user.login] = []; + } + prReviewsByUser[user.login].push(review.body); + } + const tokenSymbol = await getTokenSymbol(paymentToken, rpc); + logger.info(`Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); + + // The mapping between gh handle and comment with a permit url + const reward: Record = {}; + + // The mapping between gh handle and amount in ETH + const fallbackReward: Record = {}; + let comment = `#### Reviewer Rewards\n`; + for (const user of Object.keys(prReviewsByUser)) { + const comments = prReviewsByUser[user]; + const commentsByNode = await parseComments(comments, ItemsToExclude); + const rewardValue = calculateRewardValue(commentsByNode, incentives); + if (rewardValue.equals(0)) { + logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`); + continue; + } + logger.info(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + const account = await getWalletAddress(user); + const amountInETH = rewardValue.mul(baseMultiplier).div(1000); + if (amountInETH.gt(paymentPermitMaxPrice)) { + logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`); + continue; + } + if (account) { + const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id); + comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; + reward[user] = payoutUrl; + } else { + fallbackReward[user] = amountInETH; + } + } + + logger.info(`Permit url generated for contributors. reward: ${JSON.stringify(reward)}`); + logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); + + await addCommentToIssue(comment, issue.number); +}; + export const incentivizeCreatorComment = async () => { const logger = getLogger(); const { diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts index d64e07eaa..1c274db02 100644 --- a/src/handlers/processors.ts +++ b/src/handlers/processors.ts @@ -7,7 +7,7 @@ import { handleComment, issueClosedCallback, issueReopenedCallback } from "./com import { checkPullRequests } from "./assign/auto"; import { createDevPoolPR } from "./pull-request"; import { runOnPush } from "./push"; -import { incentivizeComments, incentivizeCreatorComment } from "./payout"; +import { incentivizeComments, incentivizeCreatorComment, incentivizePullRequestReviews } from "./payout"; export const processors: Record = { [GithubEvent.ISSUES_OPENED]: { @@ -53,7 +53,7 @@ export const processors: Record = { [GithubEvent.ISSUES_CLOSED]: { pre: [nullHandler], action: [issueClosedCallback], - post: [incentivizeCreatorComment, incentivizeComments], + post: [incentivizeCreatorComment, incentivizeComments, incentivizePullRequestReviews], }, [GithubEvent.PULL_REQUEST_OPENED]: { pre: [nullHandler], From aa84e7d641a89ea5c50d8734ef1b980079febc80 Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 11:26:28 +0900 Subject: [PATCH 02/14] feat: minor fix --- src/handlers/payout/post.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 4a9171520..89095d55e 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -151,24 +151,24 @@ export const incentivizePullRequestReviews = async () => { } if (issue.state_reason !== StateReason.COMPLETED) { - logger.info("incentivizeComments: comment incentives skipped because the issue was not closed as completed"); + logger.info("incentivizePullRequestReviews: comment incentives skipped because the issue was not closed as completed"); // return; } if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) { - logger.info(`incentivizeComments: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); + logger.info(`incentivizePullRequestReviews: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); // return; } const issueDetailed = bountyInfo(issue); if (!issueDetailed.isBounty) { - logger.info(`incentivizeComments: its not a bounty`); + logger.info(`incentivizePullRequestReviews: its not a bounty`); // return; } const pulls = await getAllPullRequests(context, "closed"); if (pulls.length === 0) { - logger.debug(`No pull requests found at this time`); + logger.debug(`incentivizePullRequestReviews: No pull requests found at this time`); return; } @@ -188,7 +188,7 @@ export const incentivizePullRequestReviews = async () => { } if (!linkedPull) { - logger.debug(`No linked pull requests found`); + logger.debug(`incentivizePullRequestReviews: No linked pull requests found`); return; } @@ -216,7 +216,7 @@ export const incentivizePullRequestReviews = async () => { if (!user) continue; if (user.type == UserType.Bot || user.login == assignee) continue; if (!review.body) { - logger.info(`Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`); + logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`); continue; } if (!prReviewsByUser[user.login]) { @@ -225,7 +225,7 @@ export const incentivizePullRequestReviews = async () => { prReviewsByUser[user.login].push(review.body); } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); - logger.info(`Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); + logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); // The mapping between gh handle and comment with a permit url const reward: Record = {}; @@ -238,14 +238,14 @@ export const incentivizePullRequestReviews = async () => { const commentsByNode = await parseComments(comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, incentives); if (rewardValue.equals(0)) { - logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`); + logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url because the reward value is 0. user: ${user}`); continue; } - logger.info(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + logger.info(`incentivizePullRequestReviews: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); const amountInETH = rewardValue.mul(baseMultiplier).div(1000); if (amountInETH.gt(paymentPermitMaxPrice)) { - logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`); + logger.info(`incentivizePullRequestReviews: Skipping comment reward for user ${user} because reward is higher than payment permit max price`); continue; } if (account) { @@ -257,8 +257,8 @@ export const incentivizePullRequestReviews = async () => { } } - logger.info(`Permit url generated for contributors. reward: ${JSON.stringify(reward)}`); - logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); + logger.info(`incentivizePullRequestReviews: Permit url generated for pull request reviewers. reward: ${JSON.stringify(reward)}`); + logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); await addCommentToIssue(comment, issue.number); }; From 109758e19bbef1309fe6dff5e2d97105f9233d0b Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 11:36:09 +0900 Subject: [PATCH 03/14] feat: slight change --- src/handlers/payout/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 89095d55e..be3104fb7 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -131,7 +131,7 @@ export const incentivizeComments = async () => { await addCommentToIssue(comment, issue.number); }; -export const incentivizePullRequestReviews = async () => { +export async function incentivizePullRequestReviews() { const logger = getLogger(); const { mode: { incentiveMode, paymentPermitMaxPrice }, @@ -261,7 +261,7 @@ export const incentivizePullRequestReviews = async () => { logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); await addCommentToIssue(comment, issue.number); -}; +} export const incentivizeCreatorComment = async () => { const logger = getLogger(); From 12bccaeea35636665f31e859fe011527801d8004 Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 11:37:01 +0900 Subject: [PATCH 04/14] feat: fix function declaration --- src/handlers/payout/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index be3104fb7..ac423fda7 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -21,7 +21,7 @@ const ItemsToExclude: string[] = ["blockquote"]; * Incentivize the contributors based on their contribution. * The default formula has been defined in https://github.com/ubiquity/ubiquibot/issues/272 */ -export const incentivizeComments = async () => { +export async function incentivizeComments() { const logger = getLogger(); const { mode: { incentiveMode, paymentPermitMaxPrice }, @@ -129,7 +129,7 @@ export const incentivizeComments = async () => { logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); await addCommentToIssue(comment, issue.number); -}; +} export async function incentivizePullRequestReviews() { const logger = getLogger(); From 25c4da5e4eacb131963949f7739ec2c2003301ed Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 18:25:33 +0900 Subject: [PATCH 05/14] feat: renamed some variables --- src/helpers/parser.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index b05411fee..fed365efd 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -25,19 +25,19 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): } }; -export const gitLinkedIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise => { +export const gitLinkedIssueParser = async ({ owner, repo, issue_number: pull_number }: GitParser): Promise => { try { - const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${issue_number}`); + const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${pull_number}`); const dom = parse(data); const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; - const linkedPRs = devForm.querySelectorAll(".my-1"); + const linkedIssues = devForm.querySelectorAll(".my-1"); - if (linkedPRs.length === 0) { + if (linkedIssues.length === 0) { return ""; } - const prUrl = linkedPRs[0].querySelector("a")?.attrs?.href || ""; - return prUrl; + const issueUrl = linkedIssues[0].querySelector("a")?.attrs?.href || ""; + return issueUrl; } catch (error) { return ""; } From efb941d8e933676384195f90024aea14c2e5ddd8 Mon Sep 17 00:00:00 2001 From: HARALD Date: Wed, 23 Aug 2023 18:31:28 +0900 Subject: [PATCH 06/14] feat: get comments as markdown --- src/helpers/issue.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 5127c1c6c..7e86f3fb1 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -506,13 +506,13 @@ export const closePullRequest = async (pull_number: number) => { } }; -export const getAllPullRequestReviews = async (context: Context, pull_number: number) => { +export const getAllPullRequestReviews = async (context: Context, pull_number: number, format: "raw" | "html" | "text" | "full" = "raw") => { const prArr = []; let fetchDone = false; const perPage = 30; let curPage = 1; while (!fetchDone) { - const prs = await getPullRequestReviews(context, pull_number, perPage, curPage); + const prs = await getPullRequestReviews(context, pull_number, perPage, curPage, format); // push the objects to array prArr.push(...prs); @@ -523,7 +523,13 @@ export const getAllPullRequestReviews = async (context: Context, pull_number: nu return prArr; }; -export const getPullRequestReviews = async (context: Context, pull_number: number, per_page: number, page: number) => { +export const getPullRequestReviews = async ( + context: Context, + pull_number: number, + per_page: number, + page: number, + format: "raw" | "html" | "text" | "full" = "raw" +) => { const logger = getLogger(); const payload = context.payload as Payload; try { @@ -533,6 +539,9 @@ export const getPullRequestReviews = async (context: Context, pull_number: numbe pull_number, per_page, page, + mediaType: { + format, + }, }); return reviews; } catch (e: unknown) { From 06d688a84d443012b9e180892d7ce8c0583bb1b0 Mon Sep 17 00:00:00 2001 From: HARALD Date: Thu, 24 Aug 2023 17:14:50 +0900 Subject: [PATCH 07/14] feat: revert changing function names --- src/handlers/payout/post.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index ac423fda7..89095d55e 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -21,7 +21,7 @@ const ItemsToExclude: string[] = ["blockquote"]; * Incentivize the contributors based on their contribution. * The default formula has been defined in https://github.com/ubiquity/ubiquibot/issues/272 */ -export async function incentivizeComments() { +export const incentivizeComments = async () => { const logger = getLogger(); const { mode: { incentiveMode, paymentPermitMaxPrice }, @@ -129,9 +129,9 @@ export async function incentivizeComments() { logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); await addCommentToIssue(comment, issue.number); -} +}; -export async function incentivizePullRequestReviews() { +export const incentivizePullRequestReviews = async () => { const logger = getLogger(); const { mode: { incentiveMode, paymentPermitMaxPrice }, @@ -261,7 +261,7 @@ export async function incentivizePullRequestReviews() { logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`); await addCommentToIssue(comment, issue.number); -} +}; export const incentivizeCreatorComment = async () => { const logger = getLogger(); From 07b2275e03f84fa649013f3dc39ae7d067c67ee8 Mon Sep 17 00:00:00 2001 From: HARALD Date: Thu, 24 Aug 2023 21:03:43 +0900 Subject: [PATCH 08/14] feat: minor fix --- src/handlers/payout/post.ts | 4 ++-- src/helpers/parser.ts | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index bcd502c32..348f12a47 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -215,14 +215,14 @@ export const incentivizePullRequestReviews = async () => { const user = review.user; if (!user) continue; if (user.type == UserType.Bot || user.login == assignee) continue; - if (!review.body) { + if (!review.body_html) { logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`); continue; } if (!prReviewsByUser[user.login]) { prReviewsByUser[user.login] = []; } - prReviewsByUser[user.login].push(review.body); + prReviewsByUser[user.login].push(review.body_html); } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index fed365efd..ceb08fbcf 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -4,7 +4,8 @@ import { HTMLElement, parse } from "node-html-parser"; interface GitParser { owner: string; repo: string; - issue_number: number; + issue_number?: number; + pull_number?: number; } export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise => { @@ -25,7 +26,7 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): } }; -export const gitLinkedIssueParser = async ({ owner, repo, issue_number: pull_number }: GitParser): Promise => { +export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitParser): Promise => { try { const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${pull_number}`); const dom = parse(data); From 7104dd39b2e8da61e171b48513fee3b3d609f8b4 Mon Sep 17 00:00:00 2001 From: HARALD Date: Thu, 24 Aug 2023 21:29:44 +0900 Subject: [PATCH 09/14] feat: minor fix --- src/handlers/payout/post.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 348f12a47..57e472893 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -178,7 +178,7 @@ export const incentivizePullRequestReviews = async () => { const pullRequestLinked = await gitLinkedIssueParser({ owner: payload.repository.owner.login, repo: payload.repository.name, - issue_number: pull.number, + pull_number: pull.number, }); const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); if (linkedIssueNumber === issue.number.toString()) { @@ -208,7 +208,7 @@ export const incentivizePullRequestReviews = async () => { return; } - const prReviews = await getAllPullRequestReviews(context, linkedPull.number); + const prReviews = await getAllPullRequestReviews(context, linkedPull.number, "full"); logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); const prReviewsByUser: Record = {}; for (const review of prReviews) { From 66d2d7b2d7a61eee271123a77afc1d6bff09e567 Mon Sep 17 00:00:00 2001 From: HARALD Date: Sat, 26 Aug 2023 00:27:02 +0900 Subject: [PATCH 10/14] feat: slight change --- src/handlers/payout/post.ts | 38 ++++++++++--------------------------- src/helpers/parser.ts | 13 +++++++++++++ 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 95b17caea..ffeaee275 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -5,12 +5,11 @@ import { generatePermit2Signature, getAllIssueComments, getAllPullRequestReviews, - getAllPullRequests, getIssueDescription, getTokenSymbol, parseComments, } from "../../helpers"; -import { gitLinkedIssueParser } from "../../helpers/parser"; +import { gitLinkedPrParser } from "../../helpers/parser"; import { Incentives, MarkdownItem, Payload, StateReason, UserType } from "../../types"; import { commentParser } from "../comment"; import Decimal from "decimal.js"; @@ -140,7 +139,7 @@ export const incentivizePullRequestReviews = async () => { } = getBotConfig(); if (!incentiveMode) { logger.info(`No incentive mode. skipping to process`); - // return; + return; } const context = getBotContext(); const payload = context.payload as Payload; @@ -152,46 +151,29 @@ export const incentivizePullRequestReviews = async () => { if (issue.state_reason !== StateReason.COMPLETED) { logger.info("incentivizePullRequestReviews: comment incentives skipped because the issue was not closed as completed"); - // return; + return; } if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) { logger.info(`incentivizePullRequestReviews: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); - // return; + return; } const issueDetailed = bountyInfo(issue); if (!issueDetailed.isBounty) { logger.info(`incentivizePullRequestReviews: its not a bounty`); - // return; - } - - const pulls = await getAllPullRequests(context, "closed"); - if (pulls.length === 0) { - logger.debug(`incentivizePullRequestReviews: No pull requests found at this time`); return; } - let linkedPull; + const pullRequestLinked = await gitLinkedPrParser({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue.number }); - for (const pull of pulls) { - const pullRequestLinked = await gitLinkedIssueParser({ - owner: payload.repository.owner.login, - repo: payload.repository.name, - pull_number: pull.number, - }); - const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); - if (linkedIssueNumber === issue.number.toString()) { - linkedPull = pull; - break; - } - } - - if (!linkedPull) { + if (pullRequestLinked === "") { logger.debug(`incentivizePullRequestReviews: No linked pull requests found`); return; } + const linkedPullNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); + const comments = await getAllIssueComments(issue.number); const permitComments = comments.filter( (content) => content.body.includes("Reviewer Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot @@ -208,7 +190,7 @@ export const incentivizePullRequestReviews = async () => { return; } - const prReviews = await getAllPullRequestReviews(context, linkedPull.number, "full"); + const prReviews = await getAllPullRequestReviews(context, Number(linkedPullNumber), "full"); logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); const prReviewsByUser: Record = {}; for (const review of prReviews) { @@ -243,7 +225,7 @@ export const incentivizePullRequestReviews = async () => { } logger.info(`incentivizePullRequestReviews: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const amountInETH = rewardValue.mul(baseMultiplier).div(1000); + const amountInETH = rewardValue.mul(baseMultiplier); if (amountInETH.gt(paymentPermitMaxPrice)) { logger.info(`incentivizePullRequestReviews: Skipping comment reward for user ${user} because reward is higher than payment permit max price`); continue; diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index ceb08fbcf..b7080f9a7 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -43,3 +43,16 @@ export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitPars return ""; } }; + +export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser): Promise => { + try { + const { data } = await axios.get(`https://github.com/${owner}/${repo}/issues/${issue_number}`); + const dom = parse(data); + const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; + const linkedPRs = devForm.querySelectorAll(".my-1"); + if (linkedPRs.length === 0) return ""; + return linkedPRs[linkedPRs.length - 1].querySelector("a")?.attrs?.href || ""; + } catch (error) { + return ""; + } +}; From 5ec2d6abc39646f36c388ddc90a1b09e6df15148 Mon Sep 17 00:00:00 2001 From: HARALD Date: Tue, 29 Aug 2023 00:59:49 +0900 Subject: [PATCH 11/14] feat: get correct linked pull request --- src/handlers/payout/post.ts | 8 +++----- src/helpers/parser.ts | 20 ++++++++++++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index ffeaee275..dd61d8b23 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -165,15 +165,13 @@ export const incentivizePullRequestReviews = async () => { return; } - const pullRequestLinked = await gitLinkedPrParser({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue.number }); + const linkedPullRequest = await gitLinkedPrParser({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue.number }); - if (pullRequestLinked === "") { + if (!linkedPullRequest) { logger.debug(`incentivizePullRequestReviews: No linked pull requests found`); return; } - const linkedPullNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); - const comments = await getAllIssueComments(issue.number); const permitComments = comments.filter( (content) => content.body.includes("Reviewer Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot @@ -190,7 +188,7 @@ export const incentivizePullRequestReviews = async () => { return; } - const prReviews = await getAllPullRequestReviews(context, Number(linkedPullNumber), "full"); + const prReviews = await getAllPullRequestReviews(context, linkedPullRequest.number, "full"); logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); const prReviewsByUser: Record = {}; for (const review of prReviews) { diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index b7080f9a7..9bd668b7f 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -1,5 +1,7 @@ import axios from "axios"; import { HTMLElement, parse } from "node-html-parser"; +import { getPullByNumber } from "./issue"; +import { getBotContext } from "../bindings"; interface GitParser { owner: string; @@ -44,14 +46,28 @@ export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitPars } }; -export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser): Promise => { +export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser) => { try { const { data } = await axios.get(`https://github.com/${owner}/${repo}/issues/${issue_number}`); const dom = parse(data); const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; const linkedPRs = devForm.querySelectorAll(".my-1"); if (linkedPRs.length === 0) return ""; - return linkedPRs[linkedPRs.length - 1].querySelector("a")?.attrs?.href || ""; + let linkedPullRequest; + for (const linkedPr of linkedPRs) { + const prHref = linkedPr.querySelector("a")?.attrs?.href || ""; + const prNumber = prHref.substring(prHref.lastIndexOf("/") + 1); + const pr = await getPullByNumber(getBotContext(), Number(prNumber)); + if (!pr || !pr.merged) continue; + + if (!linkedPullRequest) linkedPullRequest = pr; + else if (linkedPullRequest.merged_at && pr.merged_at && new Date(linkedPullRequest.merged_at) < new Date(pr.merged_at)) { + linkedPullRequest = pr; + } + } + console.log("---------------------------"); + console.log(linkedPullRequest); + return linkedPullRequest; } catch (error) { return ""; } From d0f1b0611eecb96250307c56bac8c52ef5acce21 Mon Sep 17 00:00:00 2001 From: HARALD Date: Tue, 29 Aug 2023 01:20:55 +0900 Subject: [PATCH 12/14] feat: check the organization and repo name --- src/helpers/parser.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index 9bd668b7f..82563657c 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -2,6 +2,7 @@ import axios from "axios"; import { HTMLElement, parse } from "node-html-parser"; import { getPullByNumber } from "./issue"; import { getBotContext } from "../bindings"; +import { Payload } from "../types"; interface GitParser { owner: string; @@ -49,6 +50,8 @@ export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitPars export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser) => { try { const { data } = await axios.get(`https://github.com/${owner}/${repo}/issues/${issue_number}`); + const context = getBotContext(); + const payload = context.payload as Payload; const dom = parse(data); const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; const linkedPRs = devForm.querySelectorAll(".my-1"); @@ -56,8 +59,14 @@ export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser let linkedPullRequest; for (const linkedPr of linkedPRs) { const prHref = linkedPr.querySelector("a")?.attrs?.href || ""; + const parts = prHref.split("/"); + // extract the organization name and repo name from the link:(e.g. "https://github.com/wannacfuture/ubiquibot/pull/5";) + const organization = parts[parts.length - 4]; + const repository = parts[parts.length - 3]; + + if (`${organization}/${repository}` !== payload.repository.full_name) continue; const prNumber = prHref.substring(prHref.lastIndexOf("/") + 1); - const pr = await getPullByNumber(getBotContext(), Number(prNumber)); + const pr = await getPullByNumber(context, Number(prNumber)); if (!pr || !pr.merged) continue; if (!linkedPullRequest) linkedPullRequest = pr; @@ -65,8 +74,6 @@ export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser linkedPullRequest = pr; } } - console.log("---------------------------"); - console.log(linkedPullRequest); return linkedPullRequest; } catch (error) { return ""; From 8ce9053747e6e80278b68c77f9fbfdaedcb21340 Mon Sep 17 00:00:00 2001 From: HARALD Date: Mon, 4 Sep 2023 18:01:11 +0900 Subject: [PATCH 13/14] feat: fix for nonse in incentive pr review --- src/handlers/payout/post.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 92969028e..00326fe18 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -192,7 +192,7 @@ export const incentivizePullRequestReviews = async () => { const prReviews = await getAllPullRequestReviews(context, linkedPullRequest.number, "full"); logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); - const prReviewsByUser: Record = {}; + const prReviewsByUser: Record = {}; for (const review of prReviews) { const user = review.user; if (!user) continue; @@ -202,9 +202,9 @@ export const incentivizePullRequestReviews = async () => { continue; } if (!prReviewsByUser[user.login]) { - prReviewsByUser[user.login] = []; + prReviewsByUser[user.login] = { id: user.node_id, comments: [] }; } - prReviewsByUser[user.login].push(review.body_html); + prReviewsByUser[user.login].comments.push(review.body_html); } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); @@ -216,8 +216,8 @@ export const incentivizePullRequestReviews = async () => { const fallbackReward: Record = {}; let comment = `#### Reviewer Rewards\n`; for (const user of Object.keys(prReviewsByUser)) { - const comments = prReviewsByUser[user]; - const commentsByNode = await parseComments(comments, ItemsToExclude); + const commentByUser = prReviewsByUser[user]; + const commentsByNode = await parseComments(commentByUser.comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, incentives); if (rewardValue.equals(0)) { logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url because the reward value is 0. user: ${user}`); @@ -231,7 +231,7 @@ export const incentivizePullRequestReviews = async () => { continue; } if (account) { - const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id); + const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id, commentByUser.id, "ISSUE_COMMENTER"); comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; reward[user] = payoutUrl; } else { From c6b6709b5af1447dddcb5a9ab231f00fcd16d33e Mon Sep 17 00:00:00 2001 From: HARALD Date: Mon, 11 Sep 2023 22:48:51 -0400 Subject: [PATCH 14/14] feat: include comments on pr --- src/handlers/assign/auto.ts | 8 ++++---- src/handlers/payout/post.ts | 15 +++++++++++++++ src/helpers/parser.ts | 21 +++++++++++++-------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts index 7c648b5e7..380b59983 100644 --- a/src/handlers/assign/auto.ts +++ b/src/handlers/assign/auto.ts @@ -18,14 +18,14 @@ export const checkPullRequests = async () => { // Loop through the pull requests and assign them to their respective issues if needed for (const pull of pulls) { - const pullRequestLinked = await gitLinkedIssueParser({ + const linkedIssue = await gitLinkedIssueParser({ owner: payload.repository.owner.login, repo: payload.repository.name, - issue_number: pull.number, + pull_number: pull.number, }); // if pullRequestLinked is empty, continue - if (pullRequestLinked == "" || !pull.user) { + if (linkedIssue == "" || !pull.user || !linkedIssue) { continue; } @@ -37,7 +37,7 @@ export const checkPullRequests = async () => { continue; } - const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); + const linkedIssueNumber = linkedIssue.substring(linkedIssue.lastIndexOf("/") + 1); // Check if the pull request opener is assigned to the issue const opener = pull.user.login; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index de2f1d3aa..e43f9d4c5 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -191,6 +191,7 @@ export const incentivizePullRequestReviews = async () => { } const prReviews = await getAllPullRequestReviews(context, linkedPullRequest.number, "full"); + const prComments = await getAllIssueComments(linkedPullRequest.number, "full"); logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`); const prReviewsByUser: Record = {}; for (const review of prReviews) { @@ -206,6 +207,20 @@ export const incentivizePullRequestReviews = async () => { } prReviewsByUser[user.login].comments.push(review.body_html); } + + for (const comment of prComments) { + const user = comment.user; + if (!user) continue; + if (user.type == UserType.Bot || user.login == assignee) continue; + if (!comment.body_html) { + logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(comment)}`); + continue; + } + if (!prReviewsByUser[user.login]) { + prReviewsByUser[user.login] = { id: user.node_id, comments: [] }; + } + prReviewsByUser[user.login].comments.push(comment.body_html); + } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); diff --git a/src/helpers/parser.ts b/src/helpers/parser.ts index 82563657c..93d249404 100644 --- a/src/helpers/parser.ts +++ b/src/helpers/parser.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { HTMLElement, parse } from "node-html-parser"; import { getPullByNumber } from "./issue"; -import { getBotContext } from "../bindings"; +import { getBotContext, getLogger } from "../bindings"; import { Payload } from "../types"; interface GitParser { @@ -29,7 +29,8 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): } }; -export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitParser): Promise => { +export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitParser) => { + const logger = getLogger(); try { const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${pull_number}`); const dom = parse(data); @@ -37,17 +38,19 @@ export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitPars const linkedIssues = devForm.querySelectorAll(".my-1"); if (linkedIssues.length === 0) { - return ""; + return null; } const issueUrl = linkedIssues[0].querySelector("a")?.attrs?.href || ""; return issueUrl; } catch (error) { - return ""; + logger.error(`${JSON.stringify(error)}`); + return null; } }; export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser) => { + const logger = getLogger(); try { const { data } = await axios.get(`https://github.com/${owner}/${repo}/issues/${issue_number}`); const context = getBotContext(); @@ -55,8 +58,8 @@ export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser const dom = parse(data); const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement; const linkedPRs = devForm.querySelectorAll(".my-1"); - if (linkedPRs.length === 0) return ""; - let linkedPullRequest; + if (linkedPRs.length === 0) return null; + let linkedPullRequest = null; for (const linkedPr of linkedPRs) { const prHref = linkedPr.querySelector("a")?.attrs?.href || ""; const parts = prHref.split("/"); @@ -65,7 +68,8 @@ export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser const repository = parts[parts.length - 3]; if (`${organization}/${repository}` !== payload.repository.full_name) continue; - const prNumber = prHref.substring(prHref.lastIndexOf("/") + 1); + const prNumber = parts[parts.length - 1]; + if (Number.isNaN(Number(prNumber))) return null; const pr = await getPullByNumber(context, Number(prNumber)); if (!pr || !pr.merged) continue; @@ -76,6 +80,7 @@ export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser } return linkedPullRequest; } catch (error) { - return ""; + logger.error(`${JSON.stringify(error)}`); + return null; } };